Packages

tui

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

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.

main.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)
Click the code to edit it

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.

main.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") ++ "]"
Click the code to edit it

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.

main.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)
Click the code to edit it

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.

main.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)
Click the code to edit it

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.

main.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)
Click the code to edit it

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.

main.osl
import "std:tui"

log "[" ++ tui.center("title", {width: 15}) ++ "]"
log tui.width() tui.height()
Click the code to edit it

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.

main.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")
Click the code to edit it

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.

main.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"))
Click the code to edit it

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

main.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"
  )
})
Click the code to edit it

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.

main.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 )
Click the code to edit it

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.

main.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()
Click the code to edit it

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.

main.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"
Click the code to edit it

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.

main.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())
Click the code to edit it

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.

main.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()
Click the code to edit it
Quick check

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

Your turn

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.

main.osl
Ctrl Enter runs
Expected output
+-------+------+
| Task  | Done |
+-------+------+
| write | yes  |
| test  | no   |
+-------+------+
[█████░░░░░] 50%

API reference#

53 members, generated from the std:tui source of OSL 9.3.0.

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.