Skip to content
heff

std.terminal.widget

What a program shows on a terminal, and what is typed at it, as values: the keys a terminal’s input is read as (decode), the rows a screen is drawn as (Frame) and the text that draws them (paint), and a few widgets, each a value and what a key makes of it. Pure, the same on every backend: import it by a name, import std.terminal.widget as Widget.

An app (App s r) is a state at the start (start), what a key makes of a state (step: Continue a new state, or Done a result), and how a state is shown on a terminal of a Size (view). std.terminal’s interact runs one on the terminal; to test one without a terminal, replay app keys gives where it ends, and shownAfter app size keys the rows it shows.

A Key is Char a rune, Enter, Escape, Tab, BackTab, Backspace, Delete, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, PageUp, PageDown, Control a letter, or an unknown Sequence; write them without Widget..

A Frame is rows, each a list of Spans, and where the cursor is, if shown; a Span is text with a Style (plain, bold, dim and inverse make one). width, cut, pad and wrap measure and lay out text by columns, an East Asian wide rune two.

WidgetKey functionWhat it is
MenumenuKey, menuRowsan item picked from a list
FieldfieldKeytext typed, with a shell’s editing keys
ChoicechoiceKeyone of some values
statusa status line

Key

data Key
= Char Rune
| Enter
| Escape
| Tab
| BackTab
| Backspace
| Delete
| ArrowUp
| ArrowDown
| ArrowLeft
| ArrowRight
| Home
| End
| PageUp
| PageDown
| Control Rune
| Sequence String

A key typed: a rune, or a key of its own, or a control key (Control 'a', Ctrl-A); an escape sequence not known is Sequence, as it came.

Decoded

type Decoded = { key : Key, next : Int }

decode

decode : String -> List Key

The keys a terminal’s input is: its runes, UTF-8, and its control bytes and escape sequences (xterm’s, VT100’s) as the keys they are.

>>> decode "j\r\u{1b}[A\u{1b}[3~\u{1b}OB\t\u{1b}[Z\u{7f}\u{1}é"
[Char 'j', Enter, ArrowUp, Delete, ArrowDown, Tab, BackTab, Backspace, Control 'a', Char 'é']
>>> decode "\u{1b}\u{1b}[1;5C\u{1b}[200~"
[Escape, ArrowRight, Sequence "\u{1b}[200~"]

columns

columns : Rune -> Int

How many columns a rune takes on a terminal: two, an East Asian wide one’s or an emoji’s, else one.

>>> List.map columns ['a', 'é', '中', '😀']
[1, 1, 2, 2]

width

width : String -> Int

How many columns a text takes.

cut

cut : Int -> String -> String

As much of a text as fits in w columns.

>>> cut 3 "中文字"
"中"

pad

pad : Int -> String -> String

A text cut to w columns, and spaces after it up to them.

>>> pad 5 "ab"
"ab "

wrap

wrap : Int -> String -> List String

A text’s words, put on lines of w columns at most.

>>> wrap 10 "the words of a text, wrapped"
["the words", "of a text,", "wrapped"]

Style

data Style = Plain | Bold | Dim | Inverse | Underline

How a span of text is drawn.

Span

type Span = { text : String, style : Style }

A span of a row: its text, and how it is drawn.

Row

type Row = List Span

A row of the screen: its spans, left to right.

Frame

type Frame = { rows : List Row, cursor : Maybe (Int, Int) }

A screen: its rows, from the top, and where the cursor is shown, a column and a row from 0 (none: hidden).

Size

type Size = { columns : Int, rows : Int }

The size of a terminal, in columns and rows.

plain

plain : String -> Span

A span drawn plainly, and the others.

bold

bold : String -> Span

A span drawn bold.

dim

dim : String -> Span

A span drawn dim.

inverse

inverse : String -> Span

A span drawn inverse, its colours swapped.

rowText

rowText : Row -> String

A row’s text, its styles left out.

texts

texts : Frame -> List String

A screen’s rows’ texts: what it shows, its styles left out (for a test).

fill

fill : Int -> List Row -> List Row

The rows, as many as h: those given, and empty ones after them.

status

status : Int -> String -> Row

A row of the width, inverse: a status line.

>>> rowText (status 6 "ok")
"ok "

paint

paint : Size -> Frame -> String

What draws the screen on a terminal of the size, in place of what was there: from its top left, each row cut to its width and the rest of its line cleared, as many rows as it has, what is below cleared, and the cursor put where it is shown (else hidden) — as one update, the terminal told to show it whole (?2026).

>>> paint { columns = 4, rows = 2 } { rows = [[bold "hello"], [plain "a"], [plain "b"]], cursor = Just (MkPair 1 1) }
"\u{1b}[?2026h\u{1b}[?25l\u{1b}[H\u{1b}[0;1mhell\u{1b}[0m\u{1b}[K\r\n\u{1b}[0ma\u{1b}[0m\u{1b}[K\u{1b}[0m\u{1b}[J\u{1b}[2;2H\u{1b}[?25h\u{1b}[?2026l"
type Menu = { at : Int }

A list’s selection: which of its items is picked, from 0.

menu : Menu

Nothing moved yet: the first.

moveBy

moveBy : Int -> Int -> Menu -> Menu

The selection moved by d among n items, no further than the ends.

menuKey : Int -> Key -> Menu -> Menu

The selection, after a key, among n items: up and down, a page (10) up and down, to the first and the last; another key moves nothing.

>>> (menuKey 5 End (menuKey 5 ArrowDown menu)).at
4
>>> (menuKey 5 PageDown { at = 1 }).at
4
menuRows : Int -> Menu -> List Row -> List Row

The items shown in h rows, the picked one marked and inverse, scrolled so that it is among them.

>>> List.map rowText (menuRows 2 { at = 2 } [[plain "a"], [plain "b"], [plain "c"]])
[" b", "> c"]

Field

type Field = { text : List Rune, at : Int }

A field’s text, as runes, and where its cursor is among them.

field

field : String -> Field

A field of the text, its cursor at its end.

fieldText

fieldText : Field -> String

A field’s text.

fieldCursor

fieldCursor : Field -> Int

The column the cursor is at, from the field’s start.

fieldKey

fieldKey : Key -> Field -> Field

The field, after a key: a rune typed at the cursor, a backspace, a delete, the cursor moved (left, right, to the start, to the end), or a control key’s edit; another key changes nothing.

>>> fieldText (List.foldl (\f k -> fieldKey k f) (field "") (decode "ac\u{1b}[Db\u{1b}[F!"))
"abc!"
>>> fieldText (List.foldl (\f k -> fieldKey k f) (field "one two") (decode "\u{17}x"))
"one x"

Choice

type Choice = { options : List String, at : Int }

A choice: the values, and which of them is chosen, from 0.

choice

choice : List String -> Choice

A choice of the values, the first chosen.

chosen

chosen : Choice -> String

The value chosen ("" where there is none).

choiceKey

choiceKey : Key -> Choice -> Choice

The choice, after a key: the next value (right, space, l), the one before (left, h), round; another key changes nothing.

>>> chosen (choiceKey ArrowLeft (choice ["esm", "page", "c"]))
"c"

choiceRow

choiceRow : Bool -> (String -> String) -> Choice -> Row

The values side by side, each as shown writes it, the chosen one inverse where the choice is the one being set (focused), else bold.

>>> rowText (choiceRow True (\o -> o) (choice ["a", "b"]))
"a b"

Step

data Step s r = Continue s | Done r

What a key makes of an app: its state, to go on with, or what it came to, r, where it is done.

App

type App s r =
{ start : s, step : Key -> s -> Step s r, view : Size -> s -> Frame }

An app: its state at the start, what a key makes of a state, and how a state is shown on a terminal of a size.

stepAll

stepAll : {s r : Type}
-> (Key -> s -> Step s r)
-> Step s r
-> List Key
-> Step s r

Each key stepped, in turn, until one is done.

advance

advance : {s r : Type}
-> (Key -> s -> Step s r)
-> s
-> Maybe String
-> Step s (Maybe r)

What is read from a terminal, stepped: each of its keys in turn, until one is done; at the end of the input (Nothing), done, with nothing.

replay

replay : {s r : Type} -> App s r -> List Key -> Either s r

The app, run on the keys: what it came to (Right), or the state it is in after them (Left).

shownAfter

shownAfter : {s r : Type} -> App s r -> Size -> List Key -> List String

What the app shows after the keys (none, where it is done), its rows’ texts.