# tui

> Build terminal output as strings (tables, boxes, bars, trees), colour and style text, and read keys and answers from the user.

This is the Markdown version of https://osl-lang.org/packages/tui. Every page of the OSL docs has one: add .md to its path. https://osl-lang.org/llms.txt lists them all.

```osl
import "std:tui"
```

`tui` is for programs that live in a terminal. It has two halves. The first half returns plain strings you can print or test: tables, boxes, progress bars, bar charts, trees, dividers and padded text. The second half talks to a real terminal: colours, prompts, single key presses, cursor movement and a redrawable screen.

Reach for the string builders in any command-line tool that prints results. Reach for prompts and screens when the program is interactive.

```osl
import "std:tui"
```

The first sections build strings, so they run here with their output shown. Colour codes are left out of the recorded output on this page. Prompts and screens need a real terminal, so those examples are compiled but not run.

## Tables

`tui.table(headers, rows)` draws a table. Each column is as wide as its widest cell.

```osl
import "std:tui"

auto headers = ["Name", "Language", "Stars"]
auto rows = [
  ["osl", "Go", 120],
  ["compiler", "OSL", 48],
  ["docs", "Markdown", 7]
]
log tui.table(headers, rows)
```

Output:

```text
+----------+----------+-------+
| Name     | Language | Stars |
+----------+----------+-------+
| osl      | Go       | 120   |
| compiler | OSL      | 48    |
| docs     | Markdown | 7     |
+----------+----------+-------+
```

Cells can be any value, and cells beyond the last header are left out. A row that isn't an array is a `TypeError`.

Try it: add a fourth column to the headers and a value to each row.

## Pad and align text

`tui.pad(text, width, align)` makes text a fixed width, aligned `"left"`, `"right"` or `"center"`. It is the right tool for columns you lay out yourself, such as a receipt.

```osl
import "std:tui"

for item in [["tea", "2.50"], ["coffee", "3.10"], ["cake", "12.00"]] (
  log tui.pad(item[1], 10, "left") ++ tui.pad(item[2], 8, "right")
)
log "[" ++ tui.pad("mid", 9, "center") ++ "]"
```

Output:

```text
tea           2.50
coffee        3.10
cake         12.00
[   mid   ]
```

## Progress and bar charts

`tui.progress(current, total, width)` returns a progress bar string, and `tui.barChart(data, width, colored)` a horizontal chart whose items are `[label, value]` arrays or `{label, value}` objects. The largest value fills the width.

```osl
import "std:tui"

log tui.progress(0, 100, 20)
log tui.progress(35, 100, 20)
log tui.progress(100, 100, 20)

log tui.barChart([["mon", 3], ["tue", 8], ["wed", 5]], 24, false)
```

Output:

```text
[░░░░░░░░░░░░░░░░░░░░] 0%
[███████░░░░░░░░░░░░░] 35%
[████████████████████] 100%
████████████████████████
mon             █████████ 3.00
tue             ████████████████████████ 8.00
wed             ███████████████ 5.00
```

Try it: change the second chart value to 20 and watch the bars rescale.

## Boxes, frames and dividers

`tui.box(title, content)` draws a box around text, and `tui.frame(text, width)` a double-line frame at least `width` columns wide. `tui.divider(char, width, title)` and `tui.horizontal(width)` draw rules.

```osl
import "std:tui"

log tui.box("Status", "all systems go\nuptime: 3 days")
log tui.frame("Welcome", 20)
log tui.divider("=", 30, "Section")
log tui.divider("", 12, "")
log tui.horizontal(8)
```

Output:

```text
┌─ Status ───────┐
│ all systems go │
│ uptime: 3 days │
└────────────────┘
╔════════════════════╗
║ Welcome            ║
╚════════════════════╝
========== Section ===========
────────────
────────
```

## Trees and grids

`tui.tree(items, prefix)` draws nested items with connectors. An item is a value, or an object with a `name` and `children`. `tui.grid(items, columns)` lays items out in equal columns.

```osl
import "std:tui"

auto files = [
  "README.md",
  {name: "src", children: ["main.osl", {name: "util", children: ["strings.osl"]}]},
  "osl.json"
]
log tui.tree(files, "")
log tui.grid(["a", "bb", "ccc", "d", "e"], 3)
```

Output:

```text
├── README.md
├── src
│   ├── main.osl
│   └── util
│       └── strings.osl
└── osl.json
│ a   │ bb  │ ccc 
│ d   │ e   
```

## Centre text

`tui.center(text, options)` adds spaces to centre each line. Without a width it uses the terminal's, or 80 when output isn't a terminal. Pass `width` to be exact.

```osl
import "std:tui"

log "[" ++ tui.center("title", {width: 15}) ++ "]"
log tui.width() tui.height()
```

Output:

```text
[     title]
80 24
```

## Status lines

`tui.status(kind, message)` puts a symbol in front of a message: `success`, `error`, `warning`, `info` or `loading`. Anything else gets a plain bullet.

```osl
import "std:tui"

log tui.status("success", "build finished")
log tui.status("error", "3 tests failed")
log tui.status("warn", "disk almost full")
log tui.status("info", "using cache")
log tui.status("custom", "something else")
```

Output:

```text
✓ build finished
✗ 3 tests failed
⚠ disk almost full
ℹ using cache
• something else
```

## Colour and style

`tui.color(color, text)` colours text with a name (`red`, `green`, `brightBlue`, `grey`...), a hex string or an `{r, g, b}` object. `tui.bgColor` sets the background and `tui.style` applies `bold`, `dim`, `underline`, `reverse`, `strikethrough` and others. They return strings you can join. An unknown name is a `ValueError`.

The recorded output on this page strips the codes, so the text looks plain here, but it appears coloured in a terminal.

```osl
import "std:tui"

log tui.color("red", "error:") tui.style("bold", "disk full")
log tui.color("#ff8800", "orange") tui.bgColor("blue", " on blue ")
log tui.color({r: 10, g: 200, b: 120}, "rgb")
log tui.style("underline", tui.color("cyan", "link"))
```

Output:

```text
error: disk full
orange  on blue 
rgb
link
```

Pass `cellColor` to `table` to colour each cell by its value:

```osl
import "std:tui"

log tui.table(["test", "result"], [["parse", "pass"], ["render", "fail"]], {
  cellColor: def(any cell) string (
    if cell == "fail" return "red"
    return "green"
  )
})
```

Output:

```text
+--------+--------+
| test   | result |
+--------+--------+
| parse  | pass   |
| render | fail   |
+--------+--------+
```

Try it: use `"brightBlue"` or `{r: 255, g: 0, b: 128}`.

## Ask the user

Prompts print a question and wait. They are results, because standard input may be closed (`IOError`). They read from the terminal, so these examples are compiled but not run here.

```osl
import "std:tui"

string name = tui.input("Your name: ").unwrapOr("stranger")
bool ok = tui.confirm("Create " ++ name ++ "'s folder").unwrapOr(false)
string secret = tui.password("Token: ").unwrap()
log ok secret.len
string? pick = tui.choose("Pick a colour", ["red", "green", "blue"]).unwrap()
if pick == null ( log "cancelled" ) else ( log "chose" pick )
```

`confirm` adds `[y/N]` and treats an empty line as no. `choose` shows a menu moved with the arrow keys, and returns `null` when the user cancels with Esc. When input is piped instead of typed, the same functions read plain lines, so scripts can feed them.

## Read single keys

`tui.readKey()` waits for one key press and returns its name: `"a"`, `"enter"`, `"up"`, `"ctrl+c"`, `"f5"` and so on. `tui.keyPressed()` checks without waiting, so you can keep animating while you wait for input.

```osl
import "std:tui"

tui.hideCursor()
log "Press q to quit, or any other key."
while true (
  string key = tui.readKey().unwrap()
  if key == "q" break
  log "you pressed" key
)
tui.showCursor()
```

> **Note**
>
> In key mode Ctrl+C is read as a key, not as an interrupt, so always give the user a key that quits.

## Cursor movement and spinners

`moveTo(x, y)` jumps to a cell (1, 1 is the top-left corner), and `moveUp`, `moveDown`, `moveLeft`, `moveRight`, `saveCursor`, `restoreCursor`, `clearLine` and `clearLines` redraw part of the output in place. `spinner` returns the frame to show, which changes every 100 milliseconds.

```osl
import "std:tui"
import "std:thread"

for i in 1 to 5 (
  tui.clearLine()
  tui.write("\r" ++ tui.spinner(false) ++ " working " ++ tui.progress(i, 5, 10))
  thread.sleep(200)
)
tui.clearLine()
log tui.spinner(true) "done"
```

## Screens

For full-screen programs, `tui.newScreen()` returns a buffer the size of the terminal. Set cells, then call `render()` to draw everything in one write, which avoids flicker. `drawPanel` draws a titled box at a position.

```osl
import "std:tui"

auto screen = tui.newScreen()
tui.clear()
screen.writeCenter(2, "Dashboard")
screen.set(2, 4, "CPU  " ++ tui.progress(42, 100, 20))
screen.set(2, 5, "Disk " ++ tui.progress(87, 100, 20))
screen.render()
tui.drawPanel({x: 2, y: 8, width: 30, height: 5}, {title: "Log"})
tui.moveTo(1, screen.height())
```

A screen created with `{width: 40, height: 10}` has that size instead, which is also how you can test your layout code without a terminal.

```osl
import "std:tui"

auto s = tui.newScreen({width: 40, height: 10})
s.set(1, 1, "hello")
s.writeCenter(3, "centred")
log s.width() s.height()
```

Output:

```text
40 10
```

**Quiz.** What does `tui.table(headers, rows)` return?

- [ ] Nothing: it prints the table.
- [x] A string you print or test.
- [ ] A `Screen`.

Answer: The string builders return text, so you can log it, test it or save it.

### Exercise: A tiny report

Print a table with the headers `Task` and `Done` and two rows, then a progress bar showing 1 of 2 done, 10 wide.

Starting code:

```osl
import "std:tui"

log tui.progress(0, 2, 10)
```

It should print:

```text
+-------+------+
| Task  | Done |
+-------+------+
| write | yes  |
| test  | no   |
+-------+------+
[█████░░░░░] 50%
```

Hint 1: `tui.table(["Task", "Done"], [[...], [...]])`

Hint 2: `tui.progress(1, 2, 10)`

A solution:

```osl
import "std:tui"

log tui.table(["Task", "Done"], [["write", "yes"], ["test", "no"]])
log tui.progress(1, 2, 10)
```

## API reference

## API reference

Every member of `std:tui`, generated from the source of OSL.

### Functions

#### `tui.Select(any prompt, array options) any`

**Deprecated.** Deprecated: use tui.choose(prompt, choices) instead, which returns null when the user cancels and a result that fails with IOError.

Select clears the screen, lists options with numbers and returns the one
whose number is typed, asking again after anything else; "" when options
is empty. Throws IOError when standard input is closed first.

#### `tui.barChart(array data, int width, boolean colored) string`

barChart returns a horizontal bar chart of data, whose items are
{label, value} objects or [label, value] arrays: a scale line width
columns long, then one line per item with its label, a bar scaled so the
largest value fills width, and the value. colored draws the bars in
green. Throws TypeError for any other item.

#### `tui.bgColor(any color, string text) string`

bgColor returns text on a background of color, which is a name or an OSL
colour as for tui.color. Throws ValueError for an unknown name or an
invalid colour.

#### `tui.box(string title, string content) string`

box returns content, which may have several lines, inside a box drawn
with line characters, with title in the top border when it isn't empty.

#### `tui.center(string text, object? options) string`

center returns text, line by line, with spaces before each line to centre
it. Options: width (whole number, default the terminal's width, or 80
when output isn't a terminal) is the width to centre in. Before this
release it always centred in 80 columns, whatever the terminal's width;
pass {width: 80} to keep that.

#### `tui.choose(string prompt, string[] choices) result<string?, error>`

choose asks the user to pick one of choices and returns the one picked,
or null when they cancel. In a terminal it shows a menu moved with the
arrow keys (or number keys), picked with Enter and cancelled with Esc or
Ctrl+C; otherwise, such as when input is piped, it lists the choices with
numbers and reads the number of the one picked, where 0 cancels. Fails
with IOError when standard input is closed first. Throws ValueError when
choices is empty.

#### `tui.clear()`

clear clears the terminal and moves the cursor to the top-left corner.

#### `tui.clearLine()`

clearLine clears the line the cursor is on.

#### `tui.clearLines(int count)`

clearLines clears the count lines above the cursor, such as the last count
lines printed, and leaves the cursor at the start of the first of them.
Throws ValueError for a negative count.

#### `tui.color(any color, string text) string`

color returns text coloured with color: a name (black, red, green,
yellow, blue, magenta or purple, cyan, white, gray or grey, and each with
a "bright" prefix such as "brightBlue"; any case) or an OSL colour such as
"#ff8800" or {r, g, b}, drawn in 24-bit colour. Throws ValueError for an
unknown name or an invalid colour.

#### `tui.confirm(string prompt) result<boolean, error>`

confirm asks a yes-or-no question: it prints prompt (adding "?" when it
doesn't end with one) and " [y/N]: ", and returns true for y or yes and
false for n, no or an empty line, in any case, asking again after any
other answer. Fails with IOError when standard input is closed first.

#### `tui.divider(string char, int width, string title) string`

divider returns a line of char width columns long with title centred in
it when title isn't empty. char "" means "─" and a width of 0 or less
means 80. Throws ValueError when char isn't a single one-column
character.

#### `tui.drawPanel(object rect, object? options)`

drawPanel draws a box filling rect {x, y, width, height}, in terminal
cells with x and y its top-left cell counted from 1, 1, and blanks its
inside. The cursor stays where it was. Options: title (string) is shown
centred in the top border, cut to fit. Throws ValueError for a position
under 1, a width or height under 2, or a malformed rectangle.

#### `tui.frame(string text, int width) string`

frame returns text, which may have several lines, inside a double-line
frame at least width columns wide inside.

#### `tui.grid(array items, int columns) string`

grid returns items laid out in columns columns (2 when columns is 0 or
less), each cell as wide as the widest item.

#### `tui.height() int`

height returns the terminal's height in rows, or 24 when output isn't a
terminal.

#### `tui.hideCursor()`

hideCursor hides the cursor until showCursor.

#### `tui.horizontal(int width) string`

horizontal returns a horizontal line "───" width columns long; a width of
0 or less gives 80.

#### `tui.input(string prompt) result<string, error>`

input prints prompt and returns the line typed after it, without
surrounding whitespace. Fails with IOError when standard input is closed
before a line arrives.

#### `tui.interactiveSelect(any prompt, array options) any`

**Deprecated.** Deprecated: use tui.choose(prompt, choices) instead.

interactiveSelect asks the user to pick one of options like tui.choose and
returns it, or null when they cancel; "" when options is empty. Throws
IOError when standard input is closed first.

#### `tui.keyPressed() boolean`

keyPressed reports whether a key press is waiting to be read with
readKey, without waiting for one. When standard input isn't a terminal it
reports whether input is already buffered. Throws UnsupportedError on
terminals that can't be checked without waiting (Windows consoles).

#### `tui.menu(any title, array items) any`

**Deprecated.** Deprecated: use tui.choose(title, items) instead.

menu clears the screen, shows title and items numbered from 1 with
"[0] Exit", and returns the item whose number is typed, or null for 0 or
when standard input is closed; "" when items is empty.

#### `tui.moveDown(int n)`

moveDown moves the cursor down n rows; 0 doesn't move it. Throws
ValueError for a negative n.

#### `tui.moveLeft(int n)`

moveLeft moves the cursor left n columns; 0 doesn't move it. Throws
ValueError for a negative n.

#### `tui.moveRight(int n)`

moveRight moves the cursor right n columns; 0 doesn't move it. Throws
ValueError for a negative n.

#### `tui.moveTo(int x, int y)`

moveTo moves the cursor to column x and row y, counted from 1, 1 at the
top-left cell. Throws ValueError for a position under 1.

#### `tui.moveUp(int n)`

moveUp moves the cursor up n rows; 0 doesn't move it. Throws ValueError
for a negative n.

#### `tui.newScreen(object? options) *tui.Screen`

newScreen returns an empty screen buffer the size of the terminal (80 x
24 when output isn't a terminal). Options: width and height (whole
numbers of 1 or more) set its size instead. Throws ValueError for a size
under 1.

#### `tui.pad(string text, int width, string align) string`

pad returns text padded with spaces to width columns, aligned "left"
(also ""), "right" or "center"; text already that wide is returned as it
is. Throws ValueError for another align.

#### `tui.password(string prompt) result<string, error>`

password prints prompt and reads a line without showing what is typed
when standard input is a terminal; otherwise it reads the next line.
Returns the text without its line ending. Fails with IOError when
standard input is closed first.

#### `tui.progress(number current, number total, int width) string`

progress returns a progress bar such as "[█████░░░░░] 50%" for current out
of total, with a bar width columns wide. current is clamped to 0..total,
and a total of 0 or less shows 0%. Throws ValueError for a negative width.

#### `tui.readKey() result<string, error>`

readKey waits for one key press and returns its name: a character such
as "a", "A", "?" or "é" (" " for space), "enter", "tab", "backspace",
"escape", "up", "down", "left", "right", "home", "end", "insert",
"delete", "pageup", "pagedown", "f1" to "f12", "ctrl+a" to "ctrl+z"
(Ctrl+C is read as a key, not an interrupt), modified keys such as
"ctrl+up", "shift+tab" or "alt+x", or "unknown" for any other sequence.
When standard input isn't a terminal it reads the next key from it. Fails
with IOError when standard input is closed.

#### `tui.restoreCursor()`

restoreCursor moves the cursor back to where saveCursor remembered.

#### `tui.rgbBg(any r, any g, any b, any text) string`

**Deprecated.** Deprecated: use tui.bgColor({r: r, g: g, b: b}, text) or tui.bgColor("#rrggbb", text) instead.

rgbBg returns text on the 24-bit background colour r, g, b.

#### `tui.rgbColor(any r, any g, any b, any text) string`

**Deprecated.** Deprecated: use tui.color({r: r, g: g, b: b}, text) or tui.color("#rrggbb", text) instead.

rgbColor returns text in the 24-bit colour r, g, b.

#### `tui.saveCursor()`

saveCursor remembers the cursor position for restoreCursor.

#### `tui.showCursor()`

showCursor shows the cursor again after hideCursor.

#### `tui.size() array`

**Deprecated.** Deprecated: use tui.width() and tui.height() instead.

size returns [width, height] of the terminal, or [80, 24] when output
isn't a terminal.

#### `tui.spinner(boolean finished) string`

spinner returns the current frame of a spinner, which changes every 100
ms, or "✓" when finished is true. Print it over itself to animate it.

#### `tui.status(string status, string message) string`

status returns message after a coloured symbol for status: "success",
"ok" or "done" (✓), "error", "fail" or "failed" (✗), "warning" or "warn"
(⚠), "info" (ℹ) and "loading" or "pending" (⏳), in any case; any other
status gets a plain "•".

#### `tui.style(string name, string text) string`

style returns text in a style: "bold", "dim", "underline", "blink",
"reverse" (or "invert"), "hidden" or "strikethrough", in any case. Throws
ValueError for an unknown style.

#### `tui.table(array headers, array rows, object? options) string`

table returns headers and rows (an array of arrays of cells) as a table
drawn with +, - and |, each column as wide as its widest cell; cells past
the last header are left out. Options: cellColor (function) is called with
each body cell and returns its colour as for tui.color, and turns on cyan
borders and bold headers. Throws TypeError when a row isn't an array.

#### `tui.tableColored(array headers, array rows, any colorFn) string`

**Deprecated.** Deprecated: use tui.table(headers, rows, {cellColor: colorFn}) instead.

tableColored returns a table with cyan borders, bold headers and each
cell coloured with the colour name colorFn(cell) returns; an unknown name
leaves the cell uncoloured.

#### `tui.tree(array items, string prefix) string`

tree returns items drawn as a tree with ├── and └── connectors, each line
starting with prefix. An item is a value shown as text, or an object
{name, children} whose children array is drawn nested under it.

#### `tui.vertical(int height) string[]`

vertical returns height copies of "│", one per row; a height of 0 or less
gives 10.

#### `tui.width() int`

width returns the terminal's width in columns, or 80 when output isn't a
terminal.

#### `tui.write(any value)`

write prints value to standard output as text, without a newline.

### `*tui.Screen`

#### `screen.clear()`

clear blanks every cell of the buffer.

#### `screen.height() int`

height returns the screen's height in cells.

#### `screen.render()`

render draws the whole buffer on the terminal from the top-left corner in
one write.

#### `screen.set(int x, int y, string text)`

set writes text into the buffer starting at column x of row y, both
counted from 1, 1 at the top-left cell, one character per cell; the parts
that fall outside the screen, such as at column 0 or below, are cut off.
Call render to show it.

#### `screen.width() int`

width returns the screen's width in cells.

#### `screen.writeCenter(int y, string text)`

writeCenter writes text centred on row y (counted from 1); a row outside
the screen writes nothing.
