Standard library · System and tools

tui

Full-screen terminal programs: widgets, layout, focus, keys, mouse and resizing, drawn into a buffer and repainted only where something changed.

import tui

tui builds full-screen terminal programs: file managers, dashboards, installers, editors, anything that takes over the window instead of printing lines. It is retained mode: you build a tree of long-lived widgets once, change their state when something happens, and the Screen turns key presses, clicks and resizes into events, and widget drawings into frames. It repaints only the cells that changed, so a large screen stays fast over a slow connection. It sits on top of term and uses os.signal to learn about window resizes.

The library started as basalam by Parsa and is now part of the standard library.

The pieces:

  • Screen owns the terminal, the widget tree, the focus and the event loop. NewTerminalScreen sizes it to the terminal; NewScreen gives a fixed size and never touches the terminal, which is how you test.
  • Widgets are structs that embed BaseWidget and override what they need: render to draw, handle_event to react, focusable to take keys. Ready-made ones: Text, Input, Button, Checkbox, List, Progress, Separator, Table, TableView, Panel and Modal.
  • Layout containers (Row, Column, Grid, Stack) give their children positions, so you never compute coordinates by hand.
  • Events go first to the focused widget and bubble up to its parents until one consumes them; whatever nobody consumes reaches the optional Keymap and then your loop.

A complete program

A widget, a screen, and the event loop. start switches the terminal to raw mode and the alternate screen; the defer screen.close() right after it puts the terminal back on every exit path, so a crash never leaves the user's shell broken. next_event waits for the next key, click, tick or resize; dispatch hands it to the widgets; present draws what changed. A key the widgets did not consume is yours: here q quits. This program needs a real terminal, so the page only compiles it.

tui-guide-hello.salam
import term
import tui

struct Hello:
    pub use tui.BaseWidget
    pub presses: int = 0

    pub func render(buf: tui.Buffer, bounds: tui.Rect):
        buf.put_str(bounds.x + 2, bounds.y + 1, "Hello from tui!", tui.Style { bold = true, fg = tui.Cyan })
        buf.put_str(bounds.x + 2, bounds.y + 3, "Enter pressed " + this.presses + " times", tui.StyleDefault)
        buf.put_str(bounds.x + 2, bounds.bottom() - 1, "q quits", tui.Style { dim = true })
    end

    pub func handle_event(event: tui.Event): bool:
        if event.is_key(term.KeyEnter):
            this.presses++
            ret true
        end
        ret false
    end
end

func main:
    if not term.IsTerminal():
        printerrln "run me in a terminal"
        ret 1
    end
    mut screen := tui.NewTerminalScreen(Hello {})
    screen.start()
    defer screen.Close()
    screen.present()
    mut ev := tui.Event {}
    until screen.next_event(ev):
        consumed := screen.dispatch(ev)
        if not consumed and (ev.is_char("q") or ev.is_ctrl("c")):
            screen.stop()
        end
        screen.present()
    end
    ret 0
end

See a frame without a terminal

A screen made with NewScreen never touches the terminal. You can still send it events with dispatch (or post and next_event), draw with draw, and read every row back with buffer.row_text. That is how the examples on this page produce their output, and how you write tests for your own screens.

tui-guide-preview.salam
import term
import tui

struct Hello:
    pub use tui.BaseWidget
    pub presses: int = 0

    pub func render(buf: tui.Buffer, bounds: tui.Rect):
        tui.DrawBorder(buf, bounds, tui.BorderStyle.Rounded, tui.StyleDefault, "hello")
        buf.put_str(bounds.x + 2, bounds.y + 1, "Hello from tui!", tui.Style { bold = true })
        buf.put_str(bounds.x + 2, bounds.y + 2, "Enter pressed " + this.presses + " times", tui.StyleDefault)
    end

    pub func handle_event(event: tui.Event): bool:
        if event.is_key(term.KeyEnter):
            this.presses++
            ret true
        end
        ret false
    end
end

func show(s: tui.Screen):
    s.draw()
    repeat s.buffer.height in y:
        println s.buffer.row_text(y)
    end
end

func main:
    mut s := tui.NewScreen(Hello {}, 30, 4)
    defer s.Close()
    show(s)
    enter := tui.Event { kind = tui.KeyEvent, key = term.Key { code = term.KeyEnter } }
    _ := s.dispatch(enter)
    _ := s.dispatch(enter)
    show(s)
end
Output╭─ hello ────────────────────╮ │ Hello from tui! │ │ Enter pressed 0 times │ ╰────────────────────────────╯ ╭─ hello ────────────────────╮ │ Hello from tui! │ │ Enter pressed 2 times │ ╰────────────────────────────╯

Lay out a screen

Hand widgets to a container and the container places them. A Column stacks its children; set_height with a LayoutSize chooses each share: Fixed cells, a Fill weight of what is left, or Pref for the widget's own preferred size. Make the container the root of the screen, and add each child to the screen too: the container decides where, the screen decides what is drawn and focused. On a resize the screen gives the root the new size and the layout runs again.

tui-guide-layout.salam
import tui

func fixed(n: int): tui.LayoutSize:
    ret tui.LayoutSize { kind = tui.SizeKind.Fixed, cells = n }
end

func main:
    title := tui.NewText("Downloads") as dyn tui.Widget
    rule := tui.NewSeparator() as dyn tui.Widget
    mut bar := tui.NewProgress()
    bar.set_progress(40)
    progress := bar as dyn tui.Widget
    mut body := tui.NewText("salam-0.6.3.tar.gz is 40% done. The rest of this line wraps inside the column.")
    body.set_wrap(tui.WrapMode.Word)
    text := body as dyn tui.Widget

    mut col := tui.NewColumn()
    col.set_padding(tui.UniformInsets(1))
    i := col.Add(title)
    j := col.Add(rule)
    k := col.Add(progress)
    _ := col.Add(text)
    col.set_height(i, fixed(1))
    col.set_height(j, fixed(1))
    col.set_height(k, fixed(1))

    mut s := tui.NewScreen(col as dyn tui.Widget, 32, 8)
    defer s.Close()
    _ := s.Add(tui.RootId, title)
    _ := s.Add(tui.RootId, rule)
    _ := s.Add(tui.RootId, progress)
    _ := s.Add(tui.RootId, text)
    s.draw()
    repeat s.buffer.height in y:
        println "|" + s.buffer.row_text(y) + "|"
    end
end
Output| | | Downloads | | ────────────────────────────── | | ##########--------------- 40% | | salam-0.6.3.tar.gz is 40% | | done. The rest of this line | | wraps inside the column. | | |

A form: focus, typing and callbacks

Focusable widgets take turns receiving keys. The first focusable widget added gets the focus; with NewDefaultKeymap installed, Tab and the arrow keys move it. Values come back through callbacks: on_submit for an Input when Enter is pressed, on_toggle for a Checkbox, on_click for a Button. Callbacks are lambdas and cannot capture local variables, so keep the state they change in globals or in your own widget.

tui-guide-form.salam
import term
import tui

mut name := ""
mut subscribed := false
mut saved := 0

func rect(x: int, y: int, w: int): tui.Rect:
    ret tui.Rect { x = x, y = y, width = w, height = 1 }
end

func key(seq: str): tui.Event:
    ret tui.Event { kind = tui.KeyEvent, key = term.Decode(seq) }
end

func typing(s &: tui.Screen, text: str):
    repeat text.Len() in i:
        _ := s.dispatch(key(text.Substr(i, 1)))
    end
end

func main:
    mut s := tui.NewScreen(tui.BaseWidget {}, 30, 4)
    defer s.Close()
    s.set_keymap(tui.NewDefaultKeymap())

    label := s.Add(tui.RootId, tui.NewText("Name:") as dyn tui.Widget)
    s.widget(label).SetBounds(rect(1, 0, 6))
    mut entry := tui.NewInput()
    entry.set_placeholder("your name")
    entry.on_submit = (value: str):
        name = value
        ret true
    end
    field := s.Add(tui.RootId, entry as dyn tui.Widget)
    s.widget(field).SetBounds(rect(7, 0, 20))
    mut box := tui.NewCheckbox("Send me news", false)
    box.on_toggle = (enabled: bool): subscribed = enabled end
    news := s.Add(tui.RootId, box as dyn tui.Widget)
    s.widget(news).SetBounds(rect(1, 1, 20))
    mut button := tui.NewButton("Save")
    button.on_click = (): saved++ end
    save := s.Add(tui.RootId, button as dyn tui.Widget)
    s.widget(save).SetBounds(rect(1, 2, 10))

    println "focused first:", s.focused_id() == field
    typing(s, "Sara")
    _ := s.dispatch(key("\r"))
    _ := s.dispatch(key("\t"))
    _ := s.dispatch(key(" "))
    _ := s.dispatch(key("\t"))
    _ := s.dispatch(key("\r"))
    println "name:", name, "news:", subscribed, "saved:", saved

    s.draw()
    repeat s.buffer.height in y:
        println "|" + s.buffer.row_text(y) + "|"
    end
end
Outputfocused first: true name: Sara news: true saved: 1 | Name: Sara | | [x] Send me news | | [ Save ] | | |

Lists and tables

List shows one selectable item per row and moves with j and k (the arrow keys stay free for focus moves); Enter calls on_select. TableView is a scrolling table with a selected row; Up and Down move it and Enter calls activate with the data row. Columns take a LayoutSize width, an alignment and what to do with text that does not fit.

tui-guide-lists.salam
import term
import tui

mut opened := -1

func row(a: str, b: str): Vector<str>:
    mut v := Vector {} as Vector<str>
    v.Push(a)
    v.Push(b)
    ret v
end

func column(width: int, align: tui.Alignment): tui.TableColumn:
    ret tui.TableColumn { width = tui.LayoutSize { kind = tui.SizeKind.Fixed, cells = width }, align = align }
end

func down(): tui.Event:
    ret tui.Event { kind = tui.KeyEvent, key = term.Key { code = term.KeyDown } }
end

func main:
    mut s := tui.NewScreen(tui.BaseWidget {}, 34, 6)
    defer s.Close()

    mut files := Vector {} as Vector<str>
    files.Push("main.salam")
    files.Push("util.salam")
    files.Push("README.md")
    list := s.Add(tui.RootId, tui.NewList(files) as dyn tui.Widget)
    s.widget(list).SetBounds(tui.Rect { x = 0, y = 0, width = 12, height = 3 })

    mut t := tui.NewTableView()
    _ := t.add_column(column(8, tui.Alignment.Left))
    _ := t.add_column(column(7, tui.Alignment.Right))
    t.header = true
    _ := t.add_row(row("Name", "Lines"))
    _ := t.add_row(row("tui", "8956"))
    _ := t.add_row(row("signal", "976"))
    _ := t.add_row(row("term", "1944"))
    t.activate = (index: int):
        opened = index
        ret true
    end
    table := s.Add(tui.RootId, t as dyn tui.Widget)
    s.widget(table).SetBounds(tui.Rect { x = 13, y = 0, width = 20, height = 5 })

    _ := s.focus(list)
    _ := s.dispatch(tui.Event { kind = tui.KeyEvent, key = term.Decode("j") })
    _ := s.focus(table)
    _ := s.dispatch(down())
    _ := s.dispatch(tui.Event { kind = tui.KeyEvent, key = term.Key { code = term.KeyEnter } })
    names := ["tui", "signal", "term"]
    println "opened:", names[opened]
    s.draw()
    repeat s.buffer.height in y:
        println "|" + s.buffer.row_text(y) + "|"
    end
end
Outputopened: signal |main.salam Name Lines | |util.salam ─────────────── | |README.md tui 8956 | | signal 976 | | term 1944 | | |
NeedUse
Draw text, fill or a frameBuffer.put_str, fill, DrawBorder
Colors and attributesStyle with fg, bg, bold... and the color constants or Color.Rgb
Place widgetsNewColumn, NewRow, NewGrid, NewStack
Move the focusfocus, focus_next, focus_left... or set_keymap(NewDefaultKeymap())
Your own keys and sequencesKeymap.bind, bind_seq, NewVimKeymap
A dialog over the screenModal with push_focus and raise_subtree
Something on a timertick_ms and run_with_tick
Clicksset mouse before start; Event.mouse_point
Test a screenNewScreen, dispatch, draw, buffer.row_text
Only colors or a progress bar on plain outputterm

What to keep in mind:

  • Always defer screen.close() on the line after start().
  • A child given to a container or a Panel must also be added to the screen, or it is placed but never drawn.
  • A dyn Widget cannot be turned back into its struct, so read values through callbacks or keep them in your own state.
  • Draw only inside render; change state in handle_event or in your loop, then the next present shows it. After changing state the widgets cannot see, set screen.dirty = true.
  • Glyphs fall back to ASCII on terminals without UTF-8; set SALAM_TUI_GLYPHS to ascii or unicode to choose. Colors follow NO_COLOR, COLORTERM and TERM.
  • On Windows resizes are noticed by polling the size about every 20 milliseconds instead of by a signal.

Types

Widget

interface Widget
The interface every widget satisfies. Store widgets as dyn Widget.
tui-widget.salam
import term
import tui

struct Counter:
    pub use tui.BaseWidget
    pub count: int = 0

    pub func focusable(): bool:
        ret true
    end

    pub func handle_event(event: tui.Event): bool:
        if event.is_char("+"):
            this.count++
            ret true
        end
        ret false
    end

    pub func render(buf: tui.Buffer, bounds: tui.Rect):
        mut style := tui.StyleDefault
        if buf.focused:
            style = tui.Style { reverse = true }
        end
        buf.put_str(bounds.x, bounds.y, "count " + this.count, style)
    end
end

func main:
    base := tui.BaseWidget {}
    println "defaults:", base.Focusable(), base.Hits(), base.Visible(), base.PreferredSize(10).width
    m := base.Measure(tui.Axis.X, 10)
    println "measures:", m.ok

    mut tree := tui.NewTree(tui.BaseWidget {} as dyn tui.Widget)
    id := tree.Add(tui.RootId, Counter {} as dyn tui.Widget)
    println "tree:", tree.count(), tree.valid(id), tree.parent(id) == tui.RootId, tree.valid(tui.NoWidget)
    plus := tui.Event { kind = tui.KeyEvent, key = term.Decode("+") }
    println "handled by:", tree.dispatch(id, plus) == id
    w := tree.Get(id)
    w.SetBounds(tui.Rect { width = 10, height = 1 })
    mut buf := tui.NewBuffer(10, 1)
    w.Render(buf, w.Bounds())
    println buf.row_text(0)
    buf.Free()
    tree.Free()
    _ := base.rect
end
Outputdefaults: false true true -1 measures: false tree: 2 true true false handled by: true count 1

BaseWidget

struct BaseWidget
Default implementations of every Widget method. Embed it with pub use tui.BaseWidget and override what you need. On its own it is a widget that paints nothing, is unfocusable, reports itself visible and ignores every event, which also makes it a useful container or placeholder.
NameTypeDescription
rectRectStorage for bounds() and set_bounds(). Embedding widgets can read it directly as this.rect.

The example for this item is shown with the ones above: tui-widget.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Draws nothing.

The example for this item is shown with the ones above: tui-widget.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool
Consumes nothing: returns false so the event bubbles on.

The example for this item is shown with the ones above: tui-widget.salam

Focusable

func Focusable(): bool

focusable

func focusable(): bool
Returns false. Override it to return true for interactive widgets.

The example for this item is shown with the ones above: tui-widget.salam

Hits

func Hits(): bool

hits

func hits(): bool
Returns true. Override it to return false when the widget may paint and still refuse hits.

The example for this item is shown with the ones above: tui-widget.salam

Visible

func Visible(): bool

visible

func visible(): bool
Returns true: a BaseWidget is on screen even though it paints nothing. Override it to hide a whole subtree.

The example for this item is shown with the ones above: tui-widget.salam

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect
Returns the rect stored by the last set_bounds call.

The example for this item is shown with the ones above: tui-widget.salam

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)
Stores the rect.

The example for this item is shown with the ones above: tui-widget.salam

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect
Cannot measure: returns the negative sentinel (width and height -1), which the layout engine maps to the available length. Override it to report a real size.

The example for this item is shown with the ones above: tui-widget.salam

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure
Cannot measure: ok is false and extent is 0. The layout engine then reads preferred_size. Containers that can report a natural extent override it.

The example for this item is shown with the ones above: tui-widget.salam

Tree

struct Tree
The widgets of one screen and their parent links. Build it with NewTree; a tree always holds at least its root.
NameTypeDescription
widgetsVectorThe widgets, indexed by ID. Copies of a dyn value share the widget, so widgets.get(id) reaches the same widget the caller added.
parentsVectorparents[id] is the parent's ID; NoWidget for the root.

The example for this item is shown with the ones above: tui-widget.salam

Count

func Count(): int

count

func count(): int
The number of widgets, root included.

The example for this item is shown with the ones above: tui-widget.salam

Valid

func Valid(id: int): bool

valid

func valid(id: int): bool
True when id names a widget in this tree.

The example for this item is shown with the ones above: tui-widget.salam

Add

func Add(parent: int, w: dyn Widget): int

add

func add(parent: int, w: dyn Widget): int
Adds w as the last child of parent and returns its new ID. An invalid parent ID attaches the widget to the root instead.

The example for this item is shown with the ones above: tui-widget.salam

Get

func Get(id: int): dyn Widget

get

func get(id: int): dyn Widget
The widget with this ID. An invalid ID returns the root, so the call can never fail.

The example for this item is shown with the ones above: tui-widget.salam

Parent

func Parent(id: int): int

parent

func parent(id: int): int
The parent's ID, or NoWidget for the root and for invalid IDs.

The example for this item is shown with the ones above: tui-widget.salam

Dispatch

func Dispatch(from: int, event: Event): int

dispatch

func dispatch(from: int, event: Event): int
Offers event to the widget from, then to each of its ancestors up to the root, until one consumes it. Returns the consumer's ID, or NoWidget when nobody did. An invalid from starts at the root.

The example for this item is shown with the ones above: tui-widget.salam

Free

func Free()

free

func free()
Releases the tree's own storage. The widgets themselves are left alone: the caller created them and may still hold copies.

The example for this item is shown with the ones above: tui-widget.salam

Alignment

enum Alignment
Where each rendered line sits horizontally inside the widget's box.
NameTypeDescription
LeftLines start at the box's left edge (the default).
CenterLines are centred; when the slack is odd the extra cell is on the right.
RightLines end at the box's right edge.
tui-text.salam
import tui

func main:
    lines := tui.WrapText("terminal user interfaces in Salam", 12)
    each line in lines:
        println "[" + line + "]"
    end
    size := tui.MeasureText("two\nlines", 20)
    println "measured", size.width, "x", size.height

    mut buf := tui.NewBuffer(20, 4)
    mut title := tui.NewText("centered")
    title.set_align(tui.Alignment.Center)
    title.set_style(tui.Style { bold = true })
    title.SetBounds(tui.Rect { x = 0, y = 0, width = 20, height = 1 })
    title.Render(buf, title.Bounds())
    mut rule := tui.NewSeparator()
    rule.SetBounds(tui.Rect { x = 0, y = 1, width = 20, height = 1 })
    rule.Render(buf, rule.Bounds())
    mut bar := tui.NewProgress()
    bar.set_max(200)
    bar.set_progress(50)
    bar.SetBounds(tui.Rect { x = 0, y = 2, width = 20, height = 1 })
    bar.Render(buf, bar.Bounds())
    mut long := tui.NewText("no wrap here at all")
    long.set_wrap(tui.WrapMode.None)
    long.SetBounds(tui.Rect { x = 0, y = 3, width = 10, height = 1 })
    buf.clip = long.Bounds()
    long.Render(buf, long.Bounds())
    buf.clip = buf.area()
    repeat buf.height in y:
        println "|" + buf.row_text(y) + "|"
    end
    println "progress", bar.progress(), "preferred width", title.preferred_width()
    buf.Free()
end
Output[terminal] [user] [interfaces] [in Salam] measured 5 x 2 | centered | |────────────────────| |###------------ 25%| |no wrap he | progress 50 preferred width 8

WrapMode

enum WrapMode
How Text turns its content into lines at render time.
NameTypeDescription
WordGreedy word-boundary wrapping at the box width (the default).
NoneEach explicit line is drawn as-is; the box edge clips it.

The example for this item is shown with the ones above: tui-text.salam

Text

struct Text
A widget that draws a string: set once or updated later, aligned left, centre, or right, wrapped to its box (or clipped), in one style. Its preferred size comes from the content. Embed it or construct it with NewText.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
contentstr = ""The text to draw. An explicit "\n" starts a new line (see WrapText).
alignAlignmentHorizontal placement of each line. Defaults to Alignment.Left.
styleStyleHow the content is drawn. The background fills the whole box; the foreground and attributes apply to the glyph cells only, interior spaces included, so an underline stays continuous. StyleDefault draws with the terminal's defaults. The buffer emits it at its tier.
wrapWrapModeHow the content becomes lines at render time. Defaults to WrapMode.Word.

The example for this item is shown with the ones above: tui-text.salam

SetText

func SetText(content: str)

set_text

func set_text(content: str)
Replaces the content and clears the source; the last setter wins.

The example for this item is shown with the ones above: tui-text.salam

SetSource

func SetSource(src: func(): str)

set_source

func set_source(src: func(): str)
Reads src at render and measure time; the last setter wins. Keep its value stable within a frame. Closures capture by value, so read module state or a shared holder. Set the source before Screen.add, which copies the widget. Changes do not repaint or relayout automatically: call invalidate(id), register a tick, or call set_bounds again.

The example for this item is shown with the ones above: tui-text.salam

ClearSource

func ClearSource()

clear_source

func clear_source()
Clears the source and returns to the last pushed content.

The example for this item is shown with the ones above: tui-text.salam

SetAlign

func SetAlign(align: Alignment)

set_align

func set_align(align: Alignment)
Sets the horizontal alignment.

The example for this item is shown with the ones above: tui-text.salam

SetStyle

func SetStyle(style: Style)

set_style

func set_style(style: Style)
Sets the style the content is drawn with.

The example for this item is shown with the ones above: tui-text.salam

SetWrap

func SetWrap(wrap: WrapMode)

set_wrap

func set_wrap(wrap: WrapMode)
Sets the wrap mode.

The example for this item is shown with the ones above: tui-text.salam

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect
The size the content wants at max_width, as a rect at (0, 0): width is the longest wrapped line in cells, height the number of wrapped lines. max_width <= 0 means unbounded, so the explicit lines are measured.

The example for this item is shown with the ones above: tui-text.salam

PreferredWidth

func PreferredWidth(): int

preferred_width

func preferred_width(): int
The longest explicit line of the content, in cells (the unbounded width).

The example for this item is shown with the ones above: tui-text.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Draws the content into bounds. The box is first filled with the pad style (the widget style's background only) so a background colour reads full-bleed while nothing else reaches the padding. Each line is then placed with the alignment offset and written as one run in the full style, interior spaces included, so an underline never breaks; at most bounds.height lines are drawn. A degenerate box draws nothing and never panics.

The example for this item is shown with the ones above: tui-text.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool

Focusable

func Focusable(): bool

focusable

func focusable(): bool

Hits

func Hits(): bool

hits

func hits(): bool

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure

CellOverflow

enum CellOverflow
What happens to cell text wider than its text area.
NameTypeDescription
ClipText past the edge is cut off (the default).
EllipsisThe last visible cell is replaced by the ellipsis character.
tui-table.salam
import term
import tui

func row(a: str, b: str): Vector<str>:
    mut v := Vector {} as Vector<str>
    v.Push(a)
    v.Push(b)
    ret v
end

func main:
    mut t := tui.NewTable()
    _ := t.add_column(tui.TableColumn {})
    _ := t.add_column(tui.TableColumn {})
    t.set_column_width(0, tui.LayoutSize { kind = tui.SizeKind.Fixed, cells = 9 })
    t.set_column_align(1, tui.Alignment.Right)
    t.set_column_overflow(0, tui.CellOverflow.Ellipsis)
    t.set_pad_x(1)
    t.header = true
    t.rule = true
    t.header_style = tui.Style { bold = true }
    _ := t.add_row(row("Package", "Size"))
    _ := t.add_row(row("tui", "8956"))
    _ := t.add_row(row("os.signal", "976"))
    _ := t.add_row(row("encoding.json", "3100"))
    t.set_cell_align(3, 1, tui.Alignment.Left)
    pref := t.PreferredSize(40)
    println "preferred height", pref.height
    mut buf := tui.NewBuffer(20, pref.height)
    t.SetBounds(buf.area())
    t.Render(buf, buf.area())
    repeat buf.height in y:
        println "|" + buf.row_text(y) + "|"
    end
    println "columns", t.columns.Len(), "rows", t.rows.Len(), "focusable", t.Focusable()
    buf.Free()

    mut v := tui.NewTableView()
    _ := v.add_column(tui.TableColumn { width = tui.LayoutSize { kind = tui.SizeKind.Fill, weight = 1 }, overflow = tui.CellOverflow.Clip })
    repeat 10 in i:
        mut one := Vector {} as Vector<str>
        one.Push("line " + i)
        _ := v.add_row(one)
    end
    v.SetBounds(tui.Rect { width = 12, height = 3 })
    down := tui.Event { kind = tui.KeyEvent, key = term.Key { code = term.KeyDown } }
    repeat 4:
        _ := v.HandleEvent(down)
    end
    println "selected", v.selected, "first visible", v.offset
end
Outputpreferred height 5 | Package Size | |────────────────────| | tui 8956 | | os.sig… 976 | | encodi… 3100 | columns 2 rows 4 focusable false selected 4 first visible 2

TableColumn

struct TableColumn
One column of a Table. width is read as a LayoutSize; min and max are read only when width is Pref. max = -1 means unbounded. When min and max conflict, min wins.
NameTypeDescription
widthLayoutSizeFixed exact cells, Pref the content width, Fill a star share.
minint = 0Lower bound for a Pref column, in cells.
maxint = -1Upper bound for a Pref column, in cells. -1 means unbounded.
alignAlignmentDefault alignment of this column's cells and header.
overflowCellOverflowHow text wider than the column is shown.

The example for this item is shown with the ones above: tui-table.salam

Table

struct Table
A non-focusable grid of single-line rows. Add columns and rows, then render it. When header is on, row 0 is the header: it is painted in header_style and each cell takes its column's alignment. rule draws a line of ─ under the header across the table's width. pad_x is the blank space on each side of a cell; a negative value counts as 0.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
columnsVectorThe column definitions, left to right.
rowsVector>The cell text by row, then column. Row 0 is the header when header is on.
headerbool = falseWhen true, row 0 is the header.
rulebool = trueDraws a rule under the header. Only meaningful when header is on.
pad_xint = 1Blank cells on each side of every cell's text.
styleStyleStyle of the data rows.
header_styleStyleStyle of the header row. It replaces style there, rather than merging.

The example for this item is shown with the ones above: tui-table.salam

Focusable

func Focusable(): bool

focusable

func focusable(): bool
Returns false: a table takes no keyboard focus.

The example for this item is shown with the ones above: tui-table.salam

Hits

func Hits(): bool

hits

func hits(): bool
Returns false: a click on the table falls through to whatever is beneath.

The example for this item is shown with the ones above: tui-table.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool
Consumes nothing and changes nothing.

The example for this item is shown with the ones above: tui-table.salam

AddColumn

func AddColumn(col: TableColumn): int

add_column

func add_column(col: TableColumn): int
Appends a column and returns its index.

The example for this item is shown with the ones above: tui-table.salam

AddRow

func AddRow(cells: Vector<str>): int

add_row

func add_row(cells: Vector<str>): int
Appends a row and returns its index. Missing cells read as blank.

The example for this item is shown with the ones above: tui-table.salam

SetColumnWidth

func SetColumnWidth(i: int, w: LayoutSize)

set_column_width

func set_column_width(i: int, w: LayoutSize)
Sets a column's width rule. An index out of range does nothing.

The example for this item is shown with the ones above: tui-table.salam

SetColumnAlign

func SetColumnAlign(i: int, a: Alignment)

set_column_align

func set_column_align(i: int, a: Alignment)
Sets a column's default alignment. An index out of range does nothing.

The example for this item is shown with the ones above: tui-table.salam

SetColumnOverflow

func SetColumnOverflow(i: int, o: CellOverflow)

set_column_overflow

func set_column_overflow(i: int, o: CellOverflow)
Sets a column's overflow. An index out of range does nothing.

The example for this item is shown with the ones above: tui-table.salam

SetCellAlign

func SetCellAlign(row: int, col: int, a: Alignment)

set_cell_align

func set_cell_align(row: int, col: int, a: Alignment)
Overrides the alignment of one cell. It beats the column's alignment, which beats Left. Row is an index into rows, so 0 is the header when header is on.

The example for this item is shown with the ones above: tui-table.salam

SetPadX

func SetPadX(n: int)

set_pad_x

func set_pad_x(n: int)
Sets the blank space on each side of a cell. A negative value counts as 0.

The example for this item is shown with the ones above: tui-table.salam

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect
The table's natural size: the resolved widths with star columns at 0, clamped to max_width when it is positive. The height is the header, the rule and the data rows, so a table with no rows is 0 high. A table with no columns is 0 by 0.

The example for this item is shown with the ones above: tui-table.salam

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure
Reports the preferred size along axis. ok is always true, so the layout engine reads a real answer.

The example for this item is shown with the ones above: tui-table.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Draws the table into bounds. Rows past bounds.height are not drawn. Nothing is drawn for an empty table or an empty rect.

The example for this item is shown with the ones above: tui-table.salam

ResolveWidths

func ResolveWidths(w: int): Vector<int>

PaintRow

func PaintRow(buf: Buffer, bounds: Rect, ws: Vector<int>, r: int, y: int, style: Style)

PaintRule

func PaintRule(buf: Buffer, bounds: Rect, ws: Vector<int>, y: int, style: Style)

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)

TableView

struct TableView
A focusable, vertically scrollable Table. It embeds Table, so columns, rows, header, rule, styles and every setter are Table's. The header and rule stay pinned; the data rows scroll under them in a window that starts at offset. selected is the cursor row, an index into the data rows, or -1 when there are none. Place a TableView in a Fill slot: preferred_size is Table's full height, so any other slot shows every row and never scrolls. Up, Down, PageUp, PageDown, Home and End move the cursor and are always consumed; Enter calls activate. When rows outnumber the window, the last column of cells holds a scrollbar.
NameTypeDescription
TableTableThe embedded Table: columns, rows, header and styles, drawn by the view with scrolling and a selected row.
offsetint = 0Index of the first visible data row.
selectedint = 0The selected data row, or -1 when there are no data rows.
activatefunc(int): boolCalled with the selected row when Enter is pressed on a non-empty table. Its result is the consumed flag: the default returns false, so Enter bubbles on.

The example for this item is shown with the ones above: tui-table.salam

Focusable

func Focusable(): bool

focusable

func focusable(): bool
Returns true: the screen routes keys to a TableView while it has focus.

The example for this item is shown with the ones above: tui-table.salam

Hits

func Hits(): bool

hits

func hits(): bool
Returns true. A click does not focus the table; the wheel is not decoded by std/term yet, so nothing scrolls by mouse.

The example for this item is shown with the ones above: tui-table.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool
Up and Down move the cursor by one row, PageUp and PageDown by the window height, Home and End to the first and last row. The window scrolls by the least amount that keeps the cursor visible, and a page key moves the window by a page. All six are consumed, even with no rows. Enter returns activate(selected), and is not consumed when there are no rows. Everything else returns false.

The example for this item is shown with the ones above: tui-table.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Draws the pinned header and rule, then data rows offset up to offset + window height, each painted only inside bounds. The selected row is reversed while the buffer is focused and bold while it is not; no row is marked when the selection lies outside the window. The scrollbar uses │ and █, or | and # under the ASCII glyph policy.

The example for this item is shown with the ones above: tui-table.salam

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect
Same as Table.preferred_size, on the table inside the view.

The example for this item is shown with the ones above: tui-table.salam

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure
Same as Table.measure, on the table inside the view.

The example for this item is shown with the ones above: tui-table.salam

AddColumn

func AddColumn(col: TableColumn): int

add_column

func add_column(col: TableColumn): int
Same as Table.add_column, on the table inside the view.

The example for this item is shown with the ones above: tui-table.salam

AddRow

func AddRow(cells: Vector<str>): int

add_row

func add_row(cells: Vector<str>): int
Same as Table.add_row, on the table inside the view.

The example for this item is shown with the ones above: tui-table.salam

SetColumnWidth

func SetColumnWidth(i: int, w: LayoutSize)

set_column_width

func set_column_width(i: int, w: LayoutSize)
Same as Table.set_column_width, on the table inside the view.

The example for this item is shown with the ones above: tui-table.salam

SetColumnAlign

func SetColumnAlign(i: int, a: Alignment)

set_column_align

func set_column_align(i: int, a: Alignment)
Same as Table.set_column_align, on the table inside the view.

The example for this item is shown with the ones above: tui-table.salam

SetColumnOverflow

func SetColumnOverflow(i: int, o: CellOverflow)

set_column_overflow

func set_column_overflow(i: int, o: CellOverflow)
Same as Table.set_column_overflow, on the table inside the view.

The example for this item is shown with the ones above: tui-table.salam

SetCellAlign

func SetCellAlign(row: int, col: int, a: Alignment)

set_cell_align

func set_cell_align(row: int, col: int, a: Alignment)
Same as Table.set_cell_align, on the table inside the view.

The example for this item is shown with the ones above: tui-table.salam

SetPadX

func SetPadX(n: int)

set_pad_x

func set_pad_x(n: int)
Same as Table.set_pad_x, on the table inside the view.

The example for this item is shown with the ones above: tui-table.salam

ResolveWidths

func ResolveWidths(w: int): Vector<int>

PaintRow

func PaintRow(buf: Buffer, bounds: Rect, ws: Vector<int>, r: int, y: int, style: Style)

PaintRule

func PaintRule(buf: Buffer, bounds: Rect, ws: Vector<int>, y: int, style: Style)

BorderStyle

enum BorderStyle
The kind of border a container draws: none, or a one-cell ring drawn with light lines, double lines, or light lines and arc corners. A value only: drawing the ring (its code points, corners and titles) is CUR-220. It is a sibling of Style, never a field of it, so a border is a per-widget choice and is not inherited or merged.
NameTypeDescription
NoneNo border; the content area is the full area.
SingleA one-cell ring of light lines with square corners.
DoubleA one-cell ring of double lines.
RoundedA one-cell ring of light lines with arc corners.
tui-style.salam
import str
import tui

func show(seq: str): str:
    ret str.Replace(seq, "\x1b", "ESC")
end

func main:
    println show(tui.FgSeq(tui.Red)), show(tui.BgSeq(tui.Blue))
    orange := tui.Color.Rgb(255, 128, 0)
    println "truecolor:", show(tui.FgSeqAt(orange, tui.ColorTier.TrueColor))
    println "256 colors:", show(tui.FgSeqAt(orange, tui.ColorTier.Indexed256))
    println "16 colors:", show(tui.BgSeqAt(orange, tui.ColorTier.Ansi16))
    println "no color:", show(tui.FgSeqAt(orange, tui.ColorTier.Off))
    println "lowered:", tui.Lower(tui.Color.Indexed(196), tui.ColorTier.Ansi16) == tui.BrightRed
    println "palette:", tui.Black != tui.White, tui.Green != tui.BrightGreen, tui.Yellow != tui.Magenta, tui.Cyan != tui.BrightCyan
    println "bright:", tui.BrightBlack != tui.BrightYellow, tui.BrightBlue != tui.BrightMagenta, tui.BrightWhite != tui.Color.Default
    println "terminal tier known:", tui.DetectTier() == tui.DetectTier()

    base := tui.Style { fg = tui.Green, bold = true }
    over := tui.Style { bg = tui.Black, underline = true }
    both := tui.StyleMerge(base, over)
    println "merged keeps bold:", both.bold, "and underline:", both.underline
    println "equal to default:", tui.StyleEq(tui.StyleDefault, tui.Style {})
    println "on:", show(tui.StyleSgr(both, tui.ColorTier.Ansi16)), "off:", show(tui.StyleSgrOff(both, tui.ColorTier.Ansi16))
    println "frame inset:", tui.BorderInset(tui.BorderStyle.Single), tui.BorderInset(tui.BorderStyle.None)
    println "styles built:", tui.SgrBuilds() > 0
    _ := tui.Style { dim = true, italic = true, reverse = true }
end
OutputESC[31m ESC[44m truecolor: ESC[38;2;255;128;0m 256 colors: ESC[38;5;208m 16 colors: ESC[43m no color: ESC[39m lowered: true palette: true true true true bright: true true true terminal tier known: true merged keeps bold: true and underline: true equal to default: true on: ESC[1mESC[4mESC[32mESC[40m off: ESC[22mESC[24mESC[39mESC[49m frame inset: 1 0 styles built: true

Style

struct Style
How a cell is drawn: a foreground and a background color and five attributes. Colors are Color, not Option<Color> (Default already means "no color chosen") and a false attribute is one that is off. Reverse is stored rather than composed at emission so StyleEq sees it and a reverse-only change reaches the buffer diff. The set stays closed at these seven: no strikethrough, blink or hidden.
NameTypeDescription
fgColorGlyph foreground; Color.Default leaves the terminal's own foreground.
bgColorCell background; Color.Default leaves the terminal's own background.
boldbool = falseBold weight only; never brightens the foreground slot.
dimbool = falseReduced intensity.
italicbool = falseSlanted glyphs.
underlinebool = falseA single underline.
reversebool = falseSwaps the foreground and background the terminal draws with.

The example for this item is shown with the ones above: tui-style.salam

Orientation

enum Orientation
The direction of a Separator's rule.
NameTypeDescription
HorizontalA rule across row 0 of the rect, full width. The default.
VerticalA rule down column 0 of the rect, full height.

The example for this item is shown with the ones above: tui-text.salam

Separator

struct Separator
A one-line rule that draws one rune repeated along the rect's first row (horizontal) or first column (vertical). Set the direction with set_orientation, the rune with set_ch and its style with set_style.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
orientationOrientationWhich line is drawn. Horizontal by default.
chstr = ""The rune, one cell wide. Empty means the orientation's default rune.
styleStyleThe rule's style. Its foreground colours the rune cells. A set background fills the whole rect before the rune is drawn.

The example for this item is shown with the ones above: tui-text.salam

Focusable

func Focusable(): bool

focusable

func focusable(): bool
Returns false: a separator never takes focus.

The example for this item is shown with the ones above: tui-text.salam

Hits

func Hits(): bool

hits

func hits(): bool
Returns false: a click on the rule falls through to the widget beneath.

The example for this item is shown with the ones above: tui-text.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool
Consumes nothing: returns false so the event bubbles on.

The example for this item is shown with the ones above: tui-text.salam

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect
Horizontal: unknown width (the layout fills the available width) and one row. Vertical: one column and unknown height.

The example for this item is shown with the ones above: tui-text.salam

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure
Always measurable. The cross axis is one cell. The main axis takes max_width, the same fill the preferred_size sentinel asks for. A separator never reports ok = false, so the layout never falls back to a stub.

The example for this item is shown with the ones above: tui-text.salam

SetOrientation

func SetOrientation(o: Orientation)

set_orientation

func set_orientation(o: Orientation)
Sets the direction of the rule.

The example for this item is shown with the ones above: tui-text.salam

SetCh

func SetCh(ch: str): bool

set_ch

func set_ch(ch: str): bool
Sets the rune. The rune must be exactly one cell wide, by display width. An empty string resets to the orientation's default rune. Any other string that is wide or zero-width is refused: nothing changes, the previous rune stays, and the call returns false. The rule is a bool, not an error, because the library's constructors and methods are infallible.

The example for this item is shown with the ones above: tui-text.salam

SetStyle

func SetStyle(s: Style)

set_style

func set_style(s: Style)
Sets the style of the rule and its background.

The example for this item is shown with the ones above: tui-text.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Draws the rule inside bounds. Nothing is drawn when the rect has no width or no height. A set background fills the whole rect first. The rune then repeats along row 0 (horizontal) or column 0 (vertical) of the rect, full length. Every write goes through set, which clips to the buffer.

The example for this item is shown with the ones above: tui-text.salam

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)

Screen

struct Screen
The terminal session: widget tree, back buffer, focus, and the event loop. Create it with NewScreen.
NameTypeDescription
eventschan.ChanThe channel the input thread sends events on and next_event reads.
rootdyn WidgetThe root widget (ID 0). It always covers the whole screen.
focuseddyn WidgetThe widget that currently receives events first.
bufferBufferThe back buffer widgets draw into.
glyphsGlyphPolicyThe glyph policy seeded into every frame's buffer copy. Unicode by default; start() replaces it with DetectGlyphPolicy() unless the app called set_glyphs.
runningbool = falseTrue from start() (or the first run) until stop().
treeTreeThe widget registry. Prefer add() and widget() to editing it.
tick_msint = 0Interval of TickEvents in milliseconds; 0 (the default) sends none. Set it before start().
mousebool = falseWhen true, start() enables click reporting: presses and releases arrive as MouseEvents. Set it before start().
mouse_motionbool = falseWhen true, start() enables all-motion reporting and every pointer movement, press, and release arrives as a MouseEvent. Includes clicks, so it does not need mouse, and reports are frequent. Set it before start().
keymapKeymapThe opt-in key bindings, consulted as a dispatch fallback for KeyEvents no widget consumed. Empty by default, so an app that never touches it behaves exactly as before. Bind through this field to extend it in place, or install a whole map with set_keymap. Never assign a Keymap directly: it would leak the current map's buffers.
dirtybool = trueTrue when the next present() must repaint the whole frame: a resize, a tick, an added widget, raise/unraise, a keymap-consumed key, or invalidate of an unknown widget. A precise change is a region.
focus_stackVectorWidget IDs; the top (last) entry is the focused widget, and an empty stack means nothing is focused. Change it through focus, push_focus, pop_focus, and the navigation methods, which keep focused in sync and queue the old and new bounds.
tui-screen.salam
import term
import tui

func at(x: int, y: int): tui.Rect:
    ret tui.Rect { x = x, y = y, width = 8, height = 1 }
end

func key(code: int): tui.Event:
    ret tui.Event { kind = tui.KeyEvent, key = term.Key { code = code } }
end

func main:
    mut s := tui.NewScreen(tui.BaseWidget {}, 20, 3)
    defer s.Close()
    a := s.Add(tui.RootId, tui.NewButton("A") as dyn tui.Widget)
    b := s.Add(tui.RootId, tui.NewButton("B") as dyn tui.Widget)
    c := s.Add(tui.RootId, tui.NewButton("C") as dyn tui.Widget)
    s.widget(a).SetBounds(at(0, 0))
    s.widget(b).SetBounds(at(10, 0))
    s.widget(c).SetBounds(at(0, 2))
    println "nothing focused yet:", s.focused_id() == tui.NoWidget
    _ := s.focus(a)
    println "focused a:", s.focused_id() == a, "can focus b:", s.can_focus(b)
    _ := s.focus_right()
    println "right:", s.focused_id() == b
    _ := s.focus_left()
    _ := s.focus_down()
    println "down:", s.focused_id() == c
    _ := s.focus_up()
    _ := s.focus_next()
    _ := s.focus_prev()
    println "after next and prev:", s.focused_id() == a
    _ := s.push_focus(c)
    _ := s.pop_focus()
    println "pop goes back:", s.focused_id() == a, "stack:", s.focus_stack.Len()
    println "hit at 11,0:", s.hit_test(11, 0) == b, "nothing at 19,2:", s.hit_test(19, 2) == tui.RootId

    mut layer := Vector {} as Vector<int>
    layer.Push(c)
    s.raise(layer)
    println "raised takes focus:", s.focused_id() == c
    s.unraise(layer)
    s.raise_subtree(b)
    s.unraise_subtree(b)

    s.set_keymap(tui.NewDefaultKeymap())
    println "posted:", s.post(key(term.KeyTab))
    mut ev := tui.Event {}
    if s.next_event(ev):
        println "consumed:", s.dispatch(ev)
    end
    s.invalidate(a)
    s.invalidate_subtree(tui.RootId)
    s.resize(22, 3)
    s.fit_root()
    s.draw()
    s.present()
    println "|" + s.buffer.row_text(0) + "|"
    println "|" + s.buffer.row_text(2) + "|"
    println "widgets:", s.tree.count(), "dirty:", s.dirty, "focused bounds width:", s.focused.Bounds().width
    println "root covers", s.root.Bounds().width, "x", s.root.Bounds().height, "glyphs unicode:", s.glyphs == tui.GlyphPolicy.Unicode
    _ := s.events
    _ := s.keymap
    s.stop()
end
Outputnothing focused yet: true focused a: true can focus b: true right: true down: true after next and prev: true pop goes back: true stack: 1 hit at 11,0: true nothing at 19,2: true raised takes focus: true posted: true consumed: true |[ A ] [ B ] | |[ C ] | widgets: 4 dirty: false focused bounds width: 8 root covers 22 x 3 glyphs unicode: true

Add

func Add(parent: int, w: dyn Widget): int

add

func add(parent: int, w: dyn Widget): int
Adds w under parent (an ID; RootId for top level) and returns the new widget's ID. Unraised widgets paint in ID order, which is the order they were added. The first reachable focusable widget added while nothing has focus receives it; a child of a hidden container is not reachable, so it is skipped.

The example for this item is shown with the ones above: tui-screen.salam

Raise

func Raise(ids: Vector<int>)

raise

func raise(ids: Vector<int>)
Assigns every id one fresh stamp, shared by this whole call, so the ids form a single layer above every widget raised earlier and above every unraised widget. Within the layer, paint order stays ID order. When the resulting focus order is non-empty and the current focus is outside it, saves that focus and moves to the order's first id. Otherwise leaves the focus and the saved id unchanged. An id that is not in the tree is recorded and does not crash; it changes painting only once that id is registered.

The example for this item is shown with the ones above: tui-screen.salam

Unraise

func Unraise(ids: Vector<int>)

unraise

func unraise(ids: Vector<int>)
Clears the stamp of every id to 0, so those widgets return to plain ID order. Idempotent: an id that was not raised, or that is not in the tree, is left at 0. Does not rewind the stamp counter. When nothing remains raised, focuses the saved id if it is still focusable outside a hidden subtree, otherwise the first focusable widget outside a hidden subtree, and clears the saved id. While a layer remains, focuses the first id of the new focus order when the current focus is outside it.

The example for this item is shown with the ones above: tui-screen.salam

RaiseSubtree

func RaiseSubtree(id: int)

raise_subtree

func raise_subtree(id: int)
Raises id and every descendant of id as one layer: one fresh stamp for the whole subtree, so it paints above every other widget. Inside the layer, ascending ID order keeps each parent before its children. Focus follows raise: when the current focus is outside the layer's focus order, it moves to that order's first id and the old focus is saved for unraise_subtree. An id that is not in the tree, including NoWidget, does nothing. The saved focus is one level deep: a second raise overwrites it, so nest layers with push_focus and pop_focus. Rejected: a public subtree query followed by raise(), which is two calls on every modal open. Also rejected: a blocking-owner field that dispatch and hit_test consult, which duplicates the geometric block and can fall out of step with it.

The example for this item is shown with the ones above: tui-screen.salam

UnraiseSubtree

func UnraiseSubtree(id: int)

unraise_subtree

func unraise_subtree(id: int)
Clears the stamp of id and every descendant to 0, returning them to plain ID order, and restores focus exactly as unraise does. An id that is not in the tree, including NoWidget, does nothing.

The example for this item is shown with the ones above: tui-screen.salam

Widget

func Widget(id: int): dyn Widget

widget

func widget(id: int): dyn Widget
The widget with this ID (the root for an invalid ID).

The example for this item is shown with the ones above: tui-screen.salam

FocusedId

func FocusedId(): int

focused_id

func focused_id(): int
The ID of the focused widget, or NoWidget when nothing has focus (events then start at the root).

The example for this item is shown with the ones above: tui-screen.salam

CanFocus

func CanFocus(id: int): bool

can_focus

func can_focus(id: int): bool
True when id names a widget that is currently focusable and no widget on its path to the root reports itself hidden.

The example for this item is shown with the ones above: tui-screen.salam

Focus

func Focus(id: int): bool

focus

func focus(id: int): bool
Moves the focus to id, replacing the top of the focus stack (or starting it). Returns false, changing nothing, when id is not a focusable widget outside a hidden subtree.

The example for this item is shown with the ones above: tui-screen.salam

PushFocus

func PushFocus(id: int): bool

push_focus

func push_focus(id: int): bool
Focuses id and remembers the previous focus underneath it, for example while a dialog is open. pop_focus returns to it. Returns false, changing nothing, when id is not a focusable widget outside a hidden subtree.

The example for this item is shown with the ones above: tui-screen.salam

PopFocus

func PopFocus(): bool

pop_focus

func pop_focus(): bool
Drops the top of the focus stack, returning the focus to whatever was focused before the matching push_focus. Returns false when the stack was already empty.

The example for this item is shown with the ones above: tui-screen.salam

FocusNext

func FocusNext(): bool

focus_next

func focus_next(): bool
Moves the focus to the next widget in focus order, wrapping from the last to the first. Focus order is the focusable widgets outside hidden subtrees in the topmost raised layer, or every such widget in ID order when that layer has none. A current focus outside the order lands on the first entry. With no such widget, does nothing. Returns true when the focus moved.

The example for this item is shown with the ones above: tui-screen.salam

FocusPrev

func FocusPrev(): bool

focus_prev

func focus_prev(): bool
Moves the focus to the previous widget in focus order, wrapping from the first to the last. A current focus outside the order lands on the last entry. With no such widget, does nothing. Returns true when the focus moved.

The example for this item is shown with the ones above: tui-screen.salam

FocusUp

func FocusUp(): bool

focus_up

func focus_up(): bool
Moves the focus to the nearest widget above, chosen from the focus order (see Nearest). Returns true when the focus moved.

The example for this item is shown with the ones above: tui-screen.salam

FocusDown

func FocusDown(): bool

focus_down

func focus_down(): bool
Moves the focus to the nearest widget below, from the focus order.

The example for this item is shown with the ones above: tui-screen.salam

FocusLeft

func FocusLeft(): bool

focus_left

func focus_left(): bool
Moves the focus to the nearest widget to the left, from the focus order.

The example for this item is shown with the ones above: tui-screen.salam

FocusRight

func FocusRight(): bool

focus_right

func focus_right(): bool
Moves the focus to the nearest widget to the right, from the focus order.

The example for this item is shown with the ones above: tui-screen.salam

FitRoot

func FitRoot()

fit_root

func fit_root()
Gives the root widget the whole buffer: (0, 0, width, height).

The example for this item is shown with the ones above: tui-screen.salam

Resize

func Resize(width: int, height: int)

resize

func resize(width: int, height: int)
Resizes the back buffer to width x height and refits the root.

The example for this item is shown with the ones above: tui-screen.salam

HitTest

func HitTest(x: int, y: int): int

hit_test

func hit_test(x: int, y: int): int
The ID of the widget drawn at cell (x, y), both 0-based, or NoWidget when no widget covers it. The scan is reverse paint order: the first widget whose rect contains the point, whose hits() is true, and whose ancestor chain reports visible wins. A widget may paint and still refuse hits, and a widget inside a hidden subtree never wins; the scan then continues. Widgets with empty bounds never match.

The example for this item is shown with the ones above: tui-screen.salam

Dispatch

func Dispatch(event: Event): bool

dispatch

func dispatch(event: Event): bool
Handles one event synchronously: a ResizeEvent first resizes the screen. A decoded MouseEvent then goes to the widget under the cursor (see hit_test); every other event, and a mouse event with no widget under it, goes to the focused widget. From that first widget the event bubbles up the tree. An unconsumed KeyEvent is then offered to the keymap fallback. Returns true when a widget or the keymap consumed it.

The example for this item is shown with the ones above: tui-screen.salam

SetKeymap

func SetKeymap(km: Keymap)

set_keymap

func set_keymap(km: Keymap)
Replaces the screen's keymap with km. Frees the current map's buffers first, so this is the only safe way to swap a Keymap (assigning to the field directly leaks the old one). The new map starts with no pending sequence. Ownership of km transfers to the screen: the caller must not free or use it afterward. Intended for mode switches and for installing a freshly built map.

The example for this item is shown with the ones above: tui-screen.salam

Invalidate

func Invalidate(id: int)

invalidate

func invalidate(id: int)
Queues a repaint of widget id's bounds. An unknown id, or a widget whose rect is empty, marks the whole screen instead. Legal before start(): it only marks, and the flush still waits until the screen is attached.

The example for this item is shown with the ones above: tui-screen.salam

InvalidateSubtree

func InvalidateSubtree(id: int)

invalidate_subtree

func invalidate_subtree(id: int)
Queues one repaint region covering widget id and every descendant, instead of only the widget's own bounds as invalidate does. Use it when a widget's look depends on a child, or when a child can extend past the widget's own rect. The region is the bounding union, so a spread-out subtree also repaints the gaps between its rects, and one over half the screen escalates to a full repaint. An unknown id, including NoWidget, or a subtree whose rects are all empty marks the whole screen instead. Legal before start(): it only marks, and the flush still waits until the screen is attached.

The example for this item is shown with the ones above: tui-screen.salam

Draw

func Draw()

draw

func draw()
Repaints the back buffer. A set dirty flag, more than RegionLimit regions, or regions covering more than half the screen repaints every widget. Otherwise only the pending regions are cleared and repainted. Does nothing when neither is set. Paint order is ascending (stamp, id). Each widget gets a buffer copy clipped to its visible bounds, with focused set if it holds the focus.

The example for this item is shown with the ones above: tui-screen.salam

Present

func Present()

present

func present()
Draws and, once start() has attached the terminal, writes the changed cells out. Returns immediately when nothing is pending. Clears dirty and the pending regions.

The example for this item is shown with the ones above: tui-screen.salam

Post

func Post(event: Event): bool

post

func post(event: Event): bool
Queues an event as if the input thread had produced it. Safe from any thread. Returns false once the screen is stopped.

The example for this item is shown with the ones above: tui-screen.salam

NextEvent

func NextEvent(event &: Event): bool

next_event

func next_event(event &: Event): bool
Waits for the next event and stores it in event. Returns false once the screen has been stopped and every queued event was delivered. The intended loop is: until screen.next_event(ev): ... end

The example for this item is shown with the ones above: tui-screen.salam

SetGlyphs

func SetGlyphs(policy: GlyphPolicy)

set_glyphs

func set_glyphs(policy: GlyphPolicy)
Fixes the glyph policy and marks it explicit, so start() does not detect one from the environment. May be called before or after start().
tui-terminal.salam
import term
import tui

mut seconds := 0

struct Clock:
    pub use tui.BaseWidget

    pub func render(buf: tui.Buffer, bounds: tui.Rect):
        buf.put_str(bounds.x + 1, bounds.y, "running for " + seconds + "s, q quits", tui.StyleDefault)
    end

    pub func handle_event(event: tui.Event): bool:
        if event.is_char("q"):
            ret false
        end
        ret event.kind == tui.MouseEvent
    end
end

func main:
    if not term.IsTerminal():
        printerrln "needs a terminal"
        ret 1
    end
    mut screen := tui.NewTerminalScreen(Clock {})
    screen.tick_ms = 1000
    screen.mouse = true
    screen.mouse_motion = false
    screen.set_glyphs(tui.GlyphPolicy.Unicode)
    screen.start()
    defer screen.Close()
    screen.run_with_tick((): seconds++ end)
    println "polled every", tui.PollMs, "ms, queue of", tui.EventCapacity, "running:", screen.running
    ret 0
end

Start

func Start()

start

func start()
Takes over the terminal: raw mode, alternate screen, hidden cursor, all-motion reporting if mouse_motion is set, click reporting otherwise if mouse is set, a buffer sized to the terminal, and the input thread. Call defer screen.close() right after it.

The example for this item is shown with the ones above: tui-terminal.salam

Stop

func Stop()

stop

func stop()
Ends the event loop: next_event returns false once the queue is drained. Safe to call more than once and from a widget-free context such as a tick callback.

The example for this item is shown with the ones above: tui-screen.salam

Close

func Close()

close

func close()
Stops the loop, restores the terminal exactly as start() found it, waits for the input and winch threads, and frees the screen's storage. Safe to call more than once; meant for defer.

The example for this item is shown with the ones above: tui-screen.salam

Run

func Run()

run

func run()
Runs the loop until stop(): present, then for every event dispatch it and present again if anything changed. An unconsumed Ctrl+C stops the loop, so a raw-mode program can always be left.

The example for this item is shown with the ones above: tui-terminal.salam

RunWithTick

func RunWithTick(on_tick: func())

run_with_tick

func run_with_tick(on_tick: func())
Like run, and also calls on_tick on the main thread for every TickEvent, before the tick is dispatched, then redraws. Ticks arrive every tick_ms milliseconds; set tick_ms before start().

The example for this item is shown with the ones above: tui-terminal.salam

Point

struct Point
A single cell position: x is the column, y the row, both 0-based.
NameTypeDescription
xint = 0Column, counted from 0 at the left edge.
yint = 0Row, counted from 0 at the top edge.
tui-geometry.salam
import tui

func main:
    r := tui.Rect { x = 2, y = 1, width = 10, height = 4 }
    println "right", r.right(), "bottom", r.bottom(), "empty", r.IsEmpty()
    println "contains 5,2:", r.Contains(tui.Point { x = 5, y = 2 })
    other := tui.Rect { x = 8, y = 3, width = 6, height = 6 }
    println "intersects:", r.intersects(other)
    both := r.intersection(other)
    println "overlap", both.x, both.y, both.width, both.height
    mid := r.center()
    println "center", mid.x, mid.y

    mut boxes := Vector {} as Vector<tui.Rect>
    boxes.Push(tui.Rect { x = 0, y = 10, width = 4, height = 1 })
    boxes.Push(tui.Rect { x = 20, y = 1, width = 4, height = 1 })
    println "right:", tui.Nearest(r, boxes, tui.DirRight), "down:", tui.Nearest(r, boxes, tui.DirDown)
    println "up:", tui.Nearest(r, boxes, tui.DirUp), "left:", tui.Nearest(r, boxes, tui.DirLeft)
end
Outputright 12 bottom 5 empty false contains 5,2: true intersects: true overlap 8 3 4 2 center 7 3 right: 1 down: 0 up: -1 left: -1

Rect

struct Rect
An axis-aligned, half-open rectangle of terminal cells. Covers columns x .. x + width - 1 and rows y .. y + height - 1.
NameTypeDescription
xint = 0Column of the left edge.
yint = 0Row of the top edge.
widthint = 0Number of columns. Zero or negative means the rect is empty.
heightint = 0Number of rows. Zero or negative means the rect is empty.

The example for this item is shown with the ones above: tui-geometry.salam

IsEmpty

func IsEmpty(): bool

is_empty

func is_empty(): bool
True when the rect covers no cells (width or height <= 0).

The example for this item is shown with the ones above: tui-geometry.salam

Right

func Right(): int

right

func right(): int
The first column to the right of the rect (x + width), exclusive.

The example for this item is shown with the ones above: tui-geometry.salam

Bottom

func Bottom(): int

bottom

func bottom(): int
The first row below the rect (y + height), exclusive.

The example for this item is shown with the ones above: tui-geometry.salam

Contains

func Contains(p: Point): bool

contains

func contains(p: Point): bool
True when the cell p lies inside the rect. Uses the half-open rule, so the right and bottom edges are outside, and an empty rect contains nothing.

The example for this item is shown with the ones above: tui-geometry.salam

Intersects

func Intersects(other: Rect): bool

intersects

func intersects(other: Rect): bool
True when the two rects share at least one cell. Rects that only touch along an edge do not intersect, and an empty rect intersects nothing.

The example for this item is shown with the ones above: tui-geometry.salam

Intersection

func Intersection(other: Rect): Rect

intersection

func intersection(other: Rect): Rect
The cells shared by both rects. When they do not intersect the result is an empty Rect at the origin.

The example for this item is shown with the ones above: tui-geometry.salam

Center

func Center(): Point

center

func center(): Point
The center cell: (x + width / 2, y + height / 2) with integer division. An odd size has an exact middle cell (3x3 at the origin gives (1, 1)). An even size has none, so the cell just right of and below the geometric middle is used (4x4 at the origin gives (2, 2)). The result is not meaningful for an empty rect.

The example for this item is shown with the ones above: tui-geometry.salam

Progress

struct Progress
A one-row progress bar. It fills left to right from the value, which is a percentage of max (100 by default). Set the value with set_progress and the maximum with set_max. Style the filled cells, the empty cells and the percent tail separately with the fill_style, empty_style and text_style fields.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
fill_styleStyleStyle of the filled cells. StyleDefault uses the terminal's defaults.
empty_styleStyleStyle of the empty cells.
text_styleStyleStyle of the five-cell percent tail.

The example for this item is shown with the ones above: tui-text.salam

Hits

func Hits(): bool

hits

func hits(): bool
Returns false: a click on the bar falls through to the widget painted beneath it.

The example for this item is shown with the ones above: tui-text.salam

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect
Unknown width (the layout fills the available width) and one row.

The example for this item is shown with the ones above: tui-text.salam

Progress

func Progress(): int

progress

func progress(): int
The stored value, already clamped to 0..max.

The example for this item is shown with the ones above: tui-text.salam

SetProgress

func SetProgress(percent: int)

set_progress

func set_progress(percent: int)
Sets the value, clamped to 0..max. Values above max show as full, and values below 0 show as empty.

The example for this item is shown with the ones above: tui-text.salam

SetMax

func SetMax(limit: int)

set_max

func set_max(limit: int)
Sets the value that means 100%, and clamps the stored value to the new range. A maximum of 0 or less makes the bar empty.

The example for this item is shown with the ones above: tui-text.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Draws one row at bounds.y, from bounds.x across bounds.width cells. The bar takes the width less the tail, when the tail is shown. Nothing is drawn when the width is zero or less. Every cell is written through set, which clips to the buffer.

The example for this item is shown with the ones above: tui-text.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool

Focusable

func Focusable(): bool

focusable

func focusable(): bool

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure

Panel

struct Panel
A single-child decorator that draws a border ring around its child. The child is positioned inside the ring: Panel insets it by BorderInset of border on each side and passes that rect to the child's set_bounds. A Panel with border None draws nothing and insets nothing, so it behaves like its child. Add the child with add; it must also be added to the Screen's tree to be painted, like any other child. Panel reports the child's natural size plus BorderInset of border on each side, so the ring it draws is part of that size. It is not focusable unless takes_focus is set.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
borderBorderStyleThe ring drawn around the child. None draws nothing and insets nothing.
border_styleStyleThe frame's style. Only its foreground is used, for every ring cell and the title; attributes are ignored.
titlestr = ""Text on the top edge. Empty or whitespace-only means no title.
takes_focusbool = falseWhen true the Panel is focusable: it takes focus and passes every event to its child. The child is not focusable and gets no focus of its own. False by default, so a Panel is never a focus stop.
focus_styleStyleThe frame's style while the Panel has focus. StyleDefault, the default, means no focus style: the frame keeps border_style. Only the foreground is used, as for border_style.
slotVectorThe child, held as the one layout slot. Empty until add is called.
tui-panels.salam
import term
import tui

mut closed := 0

func show(s: tui.Screen):
    s.draw()
    repeat s.buffer.height in y:
        println "|" + s.buffer.row_text(y) + "|"
    end
end

func main:
    mut s := tui.NewScreen(tui.BaseWidget {}, 24, 6)
    defer s.Close()
    area := tui.Rect { width = 24, height = 6 }

    body := tui.NewText("inside the frame") as dyn tui.Widget
    mut frame := tui.Panel { border = tui.BorderStyle.Single, title = "Panel", takes_focus = true, focus_style = tui.Style { bold = true } }
    frame.border_style = tui.StyleDefault
    frame.Add(body)
    pid := s.Add(tui.RootId, frame as dyn tui.Widget)
    _ := s.Add(pid, body)
    s.widget(pid).SetBounds(area)
    println "panel focusable:", s.widget(pid).Focusable(), "child slot:", frame.slot.Len()

    mut dialog := tui.NewModal()
    dialog.set_title("Quit?")
    dialog.set_size(tui.ModalSize.Fixed(14, 3))
    dialog.set_closable(true)
    dialog.border_style = tui.StyleDefault
    dialog.on_close = (): closed++ end
    answer := tui.NewText("y / n") as dyn tui.Widget
    dialog.Add(answer)
    dialog.SetBounds(area)
    mid := s.Add(tui.RootId, dialog as dyn tui.Widget)
    _ := s.Add(mid, answer)
    dialog.open()
    _ := s.push_focus(mid)
    s.raise_subtree(mid)
    show(s)

    _ := s.dispatch(tui.Event { kind = tui.KeyEvent, key = term.Key { code = term.KeyEscape } })
    s.unraise_subtree(mid)
    _ := s.pop_focus()
    println "closed:", closed, "focus back on panel:", s.focused_id() == pid
    show(s)
    _ := dialog.takes_focus
    _ := dialog.state
    dialog.Close()
    _ := tui.ModalSize.Percent(50, 50)
    _ := tui.Weight.Heavy
end
Outputpanel focusable: true child slot: 1 |┌─ Panel ──────────────┐| |│insi┌─ Quit? ────┐ │| |│ │y / n │ │| |│ └────────────┘ │| |│ │| |└──────────────────────┘| closed: 1 focus back on panel: true |┌─ Panel ──────────────┐| |│inside the frame │| |│ │| |│ │| |│ │| |└──────────────────────┘|

Add

func Add(child: dyn Widget)

add

func add(child: dyn Widget)
Sets the child. A second call replaces the first, so a Panel has at most one child. The child is positioned in the Panel's content area on the next set_bounds.

The example for this item is shown with the ones above: tui-panels.salam

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)
Stores the Panel's rect, then sets the child's rect to the Panel's rect inset by the border's cost on each side (BorderInset).

The example for this item is shown with the ones above: tui-panels.salam

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect
The child's natural size plus BorderInset of border on each side. Each axis is measured at max_width minus that inset on both sides, floored at 1; a non-positive max_width is forwarded unchanged as unbounded. A child that cannot measure falls back to its preferred_size. A negative component means no preference on that axis and stays -1. An empty Panel reports no preference.

The example for this item is shown with the ones above: tui-panels.salam

Focusable

func Focusable(): bool

focusable

func focusable(): bool
True only when takes_focus is set.

The example for this item is shown with the ones above: tui-panels.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool
Passes the event to the child and returns what the child returns. Only a focused Panel receives events, so this is the child's key path.

The example for this item is shown with the ones above: tui-panels.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Paints the frame over the Panel's rect. While the Panel has focus and focus_style is set, the frame uses focus_style. The child paints itself: the Screen draws every widget in its tree.

The example for this item is shown with the ones above: tui-panels.salam

Hits

func Hits(): bool

hits

func hits(): bool

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure

ModalSize

enum ModalSize:
    Percent(w: int, h: int)
    Fixed(w: int, h: int)
end
How big the dialog is inside the Modal's bounds.
NameTypeDescription
Percent(w: int, h: int)A share of the bounds, in percent (0-100) on each axis. The product is floored, then clamped to at least 20 by 5 and at most the bounds. When the bounds are smaller than 20 by 5 the dialog is the bounds.
Fixed(w: int, h: int)Exactly w by h cells, clamped to the bounds and nothing more: no 20 by 5 floor applies to an explicit size.

The example for this item is shown with the ones above: tui-panels.salam

struct Modal
An overlay dialog: a Single-line ring with a title, centred in the Modal's bounds over a DIM scrim. The child is inset one cell inside the ring. A hidden Modal paints nothing, takes no focus and hits nothing. Build it with NewModal. Its state is shared by every copy, so the methods below work before and after Screen.add.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
border_styleStyleThe ring's colour. Only its foreground is used; attributes are ignored, as for Panel.
takes_focusbool = trueWhen true the Modal is a focus stop while open. Its one focus stop keeps Tab inside its layer.
on_closefunc()Called after Esc closes the Modal. close does not call it.
stateVector<_ModalState>The shared state holder, one element. NewModal builds it; every copy shares it. Read and write the Modal through its methods.

The example for this item is shown with the ones above: tui-panels.salam

Add

func Add(child: dyn Widget)

add

func add(child: dyn Widget)
Sets the child. A second call replaces the first. The child must also be added to the Screen's tree under the Modal's id to be painted and to receive events. It is placed on the next set_bounds.

The example for this item is shown with the ones above: tui-panels.salam

Open

func Open()

open

func open()
Shows the Modal. State only: it does not raise the Modal or move focus. The app does both with Screen.push_focus, then raise_subtree. Works on the local value or on the copy Screen.add stored.

The example for this item is shown with the ones above: tui-panels.salam

Close

func Close()

close

func close()
Hides the Modal. State only: it does not call on_close, unraise or restore focus. The app does those with unraise_subtree, then pop_focus. Works on the local value or on the copy Screen.add stored.

The example for this item is shown with the ones above: tui-panels.salam

SetTitle

func SetTitle(t: str)

set_title

func set_title(t: str)
Sets the text on the top edge of the ring. Works before or after Screen.add.

The example for this item is shown with the ones above: tui-panels.salam

SetSize

func SetSize(sz: ModalSize)

set_size

func set_size(sz: ModalSize)
Sets the dialog size and re-places the child for the current bounds. Works before or after Screen.add.

The example for this item is shown with the ones above: tui-panels.salam

SetClosable

func SetClosable(b: bool)

set_closable

func set_closable(b: bool)
Sets whether Esc closes the Modal. Works before or after Screen.add.

The example for this item is shown with the ones above: tui-panels.salam

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)
Stores the Modal's rect in the shared state, then places the child in the dialog rect inset by one cell for the ring. Works before or after Screen.add.

The example for this item is shown with the ones above: tui-panels.salam

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect
The rect stored by the last set_bounds, read from the shared state so the Screen sees the same rect on the stored copy.

The example for this item is shown with the ones above: tui-panels.salam

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect
A Fixed size reports its exact w by h. A Percent size depends on the bounds it is given, so it reports the no-preference sentinel.

The example for this item is shown with the ones above: tui-panels.salam

Focusable

func Focusable(): bool

focusable

func focusable(): bool
True only while open and when takes_focus is set.

The example for this item is shown with the ones above: tui-panels.salam

Hits

func Hits(): bool

hits

func hits(): bool
True only while open. A hidden Modal lets a click fall through.

The example for this item is shown with the ones above: tui-panels.salam

Visible

func Visible(): bool

visible

func visible(): bool
True only while open. A hidden Modal also hides its child from focus order, painting, and hit testing, because the screen checks every widget on the path to the root.

The example for this item is shown with the ones above: tui-panels.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool
Offers the event to the child first and returns true if it consumed it. Then Esc closes the Modal when closable, calls on_close and returns true. Then a mouse event inside the bounds but outside the dialog returns true, so it never reaches a widget below. Anything else returns false. A hidden Modal returns false for everything.

The example for this item is shown with the ones above: tui-panels.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Paints nothing while hidden. Otherwise dims every cell of bounds outside the dialog rect, then draws the ring around the dialog rect when it is at least 2 by 2. The child paints itself.

The example for this item is shown with the ones above: tui-panels.salam

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure

List

struct List
A single-selection scrollable list of strings. It is focusable, so keyboard events reach it through the screen's focus dispatch: j moves the selection down and k up (clamped, no wrap), and enter calls on_select with the selected index. The selected row is drawn full width in the selection style, and the window scrolls by the minimum needed to keep it visible.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
styleStyleThe style of every unselected row. StyleDefault draws with the terminal's defaults.
selected_styleStyleThe style of the selected row while focused; it replaces style on that row. Defaults to reverse video.
selected_unfocused_styleStyleThe style of the selected row while unfocused; it replaces style on that row. Defaults to a bright-black background.
on_selectfunc(int)Called with the selected index when enter is pressed on a non-empty list. The default does nothing.
tui-controls.salam
import str
import term
import tui

mut submitted := ""
mut picked := -1
mut pressed := 0
mut toggled := false

func key(seq: str): tui.Event:
    ret tui.Event { kind = tui.KeyEvent, key = term.Decode(seq) }
end

func main:
    mut buf := tui.NewBuffer(24, 5)

    mut pass := tui.NewInput()
    pass.masked = true
    pass.mask_glyph = "*"
    pass.max_length = 8
    pass.filter = (candidate: str): ret not str.Contains(candidate, " ") end
    pass.on_submit = (v: str):
        submitted = v
        ret true
    end
    pass.set_placeholder("password")
    pass.set_value("")
    pass.style = tui.StyleDefault
    pass.cursor_style = tui.Style { reverse = true }
    pass.placeholder_style = tui.Style { dim = true }
    pass.SetBounds(tui.Rect { x = 0, y = 0, width = 12, height = 1 })
    each c in ["s", " ", "e", "c"]:
        _ := pass.HandleEvent(key(c))
    end
    _ := pass.HandleEvent(key("\r"))
    println "input focusable:", pass.Focusable(), "value:", pass.Value(), "submitted:", submitted

    mut ok := tui.NewButton("OK")
    ok.on_click = (): pressed++ end
    ok.focus_style = tui.Style { bold = true }
    ok.style = tui.StyleDefault
    ok.disabled = false
    ok.SetBounds(tui.Rect { x = 0, y = 1, width = 8, height = 1 })
    _ := ok.HandleEvent(key(" "))
    println "button:", ok.label, ok.Focusable(), ok.Hits(), "pressed", pressed, "width", ok.PreferredSize(20).width

    mut wrap := tui.NewCheckbox("Wrap lines", false)
    wrap.on_toggle = (v: bool): toggled = v end
    wrap.box_style = tui.StyleDefault
    wrap.label_style = tui.StyleDefault
    wrap.disabled = false
    wrap.SetBounds(tui.Rect { x = 0, y = 2, width = 14, height = 1 })
    _ := wrap.HandleEvent(key(" "))
    wrap.set_checked(wrap.checked())
    println "checkbox:", wrap.label, wrap.checked(), toggled, wrap.Focusable(), wrap.Hits(), wrap.PreferredSize(20).width

    mut items := Vector {} as Vector<str>
    items.Push("red")
    items.Push("green")
    mut colors := tui.NewList(items)
    colors.on_select = (i: int): picked = i end
    colors.style = tui.StyleDefault
    colors.selected_style = tui.Style { reverse = true }
    colors.selected_unfocused_style = tui.Style { underline = true }
    colors.SetBounds(tui.Rect { x = 14, y = 0, width = 8, height = 2 })
    _ := colors.HandleEvent(key("j"))
    _ := colors.HandleEvent(key("\r"))
    println "list:", colors.selected_index(), colors.selected_item(), picked, colors.Focusable()

    pass.Render(buf, pass.Bounds())
    ok.Render(buf, ok.Bounds())
    wrap.Render(buf, wrap.Bounds())
    colors.Render(buf, colors.Bounds())
    repeat buf.height in y:
        println "|" + buf.row_text(y) + "|"
    end
    buf.Free()
end
Outputinput focusable: true value: sec submitted: sec button: OK true true pressed 1 width 4 checkbox: Wrap lines true true true true 14 list: 1 green 1 true |*** red | |[ OK ] green | |[x] Wrap lines | | | | |

Focusable

func Focusable(): bool

focusable

func focusable(): bool
Returns true: List is interactive, so the screen's focus dispatch routes keyboard events to it.

The example for this item is shown with the ones above: tui-controls.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool
Reacts to a key press. j moves the selection down and k up, each clamped to the ends (no wrap) and each always consumed even when the list is empty. Enter is consumed and, on a non-empty list, calls on_select with the selected index. Other events and keys are not consumed, so they bubble on.

The example for this item is shown with the ones above: tui-controls.salam

SelectedIndex

func SelectedIndex(): int

selected_index

func selected_index(): int
The selected index, or -1 when the list is empty.

The example for this item is shown with the ones above: tui-controls.salam

SelectedItem

func SelectedItem(): str

selected_item

func selected_item(): str
The selected string, or "" when the list is empty.

The example for this item is shown with the ones above: tui-controls.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Draws the visible window into bounds: each row is filled full width with the base style and then the item that occupies it is written, clipped to the width, with the selected row in the selection style (reverse video focused, a bright-black background unfocused). A degenerate box draws nothing and never panics.

The example for this item is shown with the ones above: tui-controls.salam

Hits

func Hits(): bool

hits

func hits(): bool

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure

Axis

enum Axis
The axis a measurement is taken on. X is horizontal, Y is vertical.
NameTypeDescription
XThe horizontal axis.
YThe vertical axis.
tui-layout.salam
import tui

struct Box:
    pub use tui.BaseWidget
    pub mark: str = "#"

    pub func render(buf: tui.Buffer, bounds: tui.Rect):
        buf.fill(bounds, this.mark, tui.StyleDefault)
    end
end

func box(mark: str): dyn tui.Widget:
    ret Box { mark = mark } as dyn tui.Widget
end

func size(kind: tui.SizeKind, cells: int, weight: int): tui.LayoutSize:
    ret tui.LayoutSize { kind = kind, cells = cells, weight = weight }
end

func main:
    a := box("a")
    b := box("b")
    c := box("c")
    d := box("d")
    e := box("e")
    f := box("f")

    mut top := tui.NewRow()
    top.set_gap(1)
    ia := top.Add(a)
    ib := top.Add(b)
    top.set_width(ia, size(tui.SizeKind.Fixed, 4, 0))
    top.set_width(ib, size(tui.SizeKind.Fill, 0, 1))
    toprow := top as dyn tui.Widget

    mut grid := tui.NewGrid(1, 2)
    ic := grid.Add(c)
    id := grid.Add(d)
    grid.set_cell(ic, 0, 0)
    grid.set_cell(id, 0, 1)
    gridw := grid as dyn tui.Widget

    mut stack := tui.NewStack()
    ie := stack.Add(e)
    if2 := stack.Add(f)
    stack.set_margin(if2, tui.NewInsets(0, 2, 0, 2))
    stack.set_align(ie, tui.CrossAlign.Stretch, tui.CrossAlign.Stretch)
    stackw := stack as dyn tui.Widget

    mut col := tui.NewColumn()
    col.set_padding(tui.UniformInsets(1))
    i1 := col.Add(toprow)
    i2 := col.Add(gridw)
    i3 := col.Add(stackw)
    col.set_height(i1, size(tui.SizeKind.Fixed, 1, 0))
    col.set_height(i2, size(tui.SizeKind.Fixed, 2, 0))
    col.set_height(i3, size(tui.SizeKind.Fill, 0, 1))

    mut s := tui.NewScreen(col as dyn tui.Widget, 20, 8)
    defer s.Close()
    each w in [toprow, a, b, gridw, c, d, stackw, e, f]:
        _ := s.Add(tui.RootId, w)
    end
    s.draw()
    repeat s.buffer.height in y:
        println "|" + s.buffer.row_text(y) + "|"
    end

    mut sizes := Vector {} as Vector<tui.LayoutSize>
    sizes.Push(size(tui.SizeKind.Fixed, 3, 0))
    sizes.Push(size(tui.SizeKind.Fill, 0, 2))
    sizes.Push(size(tui.SizeKind.Pref, 0, 1))
    split := tui.SplitMain(12, sizes, 0)
    println "split 12:", split.Get(0), split.Get(1), split.Get(2)
    tracks := tui.TrackSizes(10, 3)
    println "tracks 10/3:", tracks.Get(0), tracks.Get(1), tracks.Get(2)
    inner := tui.InsetRect(tui.Rect { width = 10, height = 4 }, tui.NewInsets(1, 2, 1, 2))
    println "inset:", inner.x, inner.y, inner.width, inner.height
    fit := tui.FitRect(tui.Rect { x = 8, y = 0, width = 6, height = 2 }, tui.Rect { width = 10, height = 4 })
    println "fit:", fit.x, fit.width
    println "place:", tui.PlaceAxisSize(10, tui.CrossAlign.Center, 4), tui.PlaceAxisOffset(10, tui.CrossAlign.End, 4)
    m := tui.Measure { ok = true, extent = 3 }
    println "measure:", m.ok, m.extent, tui.Axis.X != tui.Axis.Y
    slot := col.items.Get(1)
    println "slot span:", slot.row_span, slot.col_span, "rows/cols:", grid.rows, grid.cols, "gap:", top.gap, "pad:", col.pad.top
end
Output| | | aaaa bbbbbbbbbbbbb | | cccccccccddddddddd | | cccccccccddddddddd | | eeffffffffffffffee | | eeffffffffffffffee | | eeffffffffffffffee | | | split 12: 3 6 3 tracks 10/3: 4 3 3 inset: 2 1 6 2 fit: 8 2 place: 4 6 measure: true 3 true slot span: 1 1 rows/cols: 1 2 gap: 1 pad: 1

Measure

struct Measure
The result of Widget.measure. ok is false when the widget cannot measure. The layout engine then reads preferred_size.
NameTypeDescription
okbool = falseTrue when extent is a real measurement. False means cannot measure.
extentint = 0The measured length in cells when ok is true. The engine clamps a negative value to 0. Ignored when ok is false.

The example for this item is shown with the ones above: tui-layout.salam

CrossAlign

enum CrossAlign
How a child is placed on the container's cross axis, the axis the container does not split. Stretch (the default) fills the whole axis; the others place a child of natural size inside the free space.
NameTypeDescription
StartThe child starts at the box's leading edge; free space follows it.
CenterThe child is centred; when the free space is odd the extra cell goes right (Row) or bottom (Column).
EndThe child ends at the box's trailing edge; free space precedes it.
StretchThe child fills the whole axis (the default); any size is ignored.

The example for this item is shown with the ones above: tui-layout.salam

SizeKind

enum SizeKind
How a child resolves its size on one axis.
NameTypeDescription
FillA weighted share of whatever is left after the Fixed children (R5). On a cross axis, Fill behaves like Pref and the weight is ignored (R8).
FixedExactly cells long. A negative value clamps to 0.
PrefThe child's preferred size. On a main axis a measured Pref is reserved like Fixed; one that cannot measure is Fill weight 1. On a cross axis it is the measured natural size (R8).

The example for this item is shown with the ones above: tui-layout.salam

LayoutSize

struct LayoutSize
A child's requested size on one axis. cells is read only when kind is Fixed. weight is read only when a main-axis Fill or Pref joins the share.
NameTypeDescription
kindSizeKindHow the size is resolved. Defaults to Fill.
cellsint = 0The requested length, read only when kind is Fixed. Negative values clamp to 0 when used.
weightint = 1Share of the main-axis leftover for a Fill. Defaults to 1. A negative value clamps to 0 when used. Fixed ignores it, and so do Grid, Stack, and cross-axis alignment. SplitMain also reads it for a Pref that is still in the vector. Arrange resolves a measured Pref to Fixed first, and a Pref that cannot measure to Fill weight 1, so this value is not that slot's weight.

The example for this item is shown with the ones above: tui-layout.salam

Insets

struct Insets
Four side lengths in cells, written top, right, bottom, left. Used for both a container's padding and a child's margin. Negatives clamp to 0 at use (InsetRect); NewInsets and UniformInsets clamp on construction.
NameTypeDescription
topint = 0Cells added above the content (padding) or reserved above the child (margin).
rightint = 0Cells added to the right of the content or reserved right of the child.
bottomint = 0Cells added below the content or reserved below the child.
leftint = 0Cells added to the left of the content or reserved left of the child.

The example for this item is shown with the ones above: tui-layout.salam

LayoutSlot

struct LayoutSlot
One child's place in a container's ordered slot list. A slot is owned by the parent; a widget never sees it. margin insets the slot before the child is placed, width and height are the requested sizes on each axis, and align_x / align_y position a child that does not stretch. row, col, row_span and col_span are read by Grid only.
NameTypeDescription
childdyn WidgetThe child widget. It must be registered in the Screen's tree as well, or the Screen never paints it.
marginInsetsSpace reserved inside the slot, before alignment (R4). Zero by default.
widthLayoutSizeThe requested size on the x axis. Fill by default.
heightLayoutSizeThe requested size on the y axis. Fill by default.
align_xCrossAlignCross-axis alignment on x. Stretch by default.
align_yCrossAlignCross-axis alignment on y. Stretch by default.
rowint = 0The Grid row this child occupies. 0 by default.
colint = 0The Grid column this child occupies. 0 by default.
row_spanint = 1How many Grid rows this child covers from row. 1 by default. A non-positive value means 1, and Grid clamps it to the rows remaining.
col_spanint = 1How many Grid columns this child covers from col. 1 by default. A non-positive value means 1, and Grid clamps it to the columns remaining.

The example for this item is shown with the ones above: tui-layout.salam

Row

struct Row
A container that lays its children left to right along the x axis. Slots are packed from the content start; set_gap inserts that many cells between them. A measured main-axis Pref is reserved like Fixed. Fill then shares the content width left after that reservation and the gap budget (R5). A child fills the content height unless its align_y places it otherwise. Set padding with set_padding; a child's margin, size and alignment with the slot methods. Children are positioned, not painted, so add them to the Screen's tree too.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
padInsetsThe inset between the container's bounds and its content box (R3).
gapint = 0Cells between adjacent slots on the main axis (R5). Zero by default. A negative value clamps to 0.
itemsVectorThe children in placement order, one slot each.

The example for this item is shown with the ones above: tui-layout.salam

Add

func Add(child: dyn Widget): int

add

func add(child: dyn Widget): int
Appends child as the last slot and returns its index. This only orders the child for layout; add it to the Screen's tree to have it painted.

The example for this item is shown with the ones above: tui-layout.salam

SetMargin

func SetMargin(i: int, margin: Insets)

set_margin

func set_margin(i: int, margin: Insets)
Sets the margin of the slot at i (R4). An out-of-range index is ignored.

The example for this item is shown with the ones above: tui-layout.salam

SetWidth

func SetWidth(i: int, size: LayoutSize)

set_width

func set_width(i: int, size: LayoutSize)
Sets the x-axis size request of the slot at i (R5).

The example for this item is shown with the ones above: tui-layout.salam

SetHeight

func SetHeight(i: int, size: LayoutSize)

set_height

func set_height(i: int, size: LayoutSize)
Sets the y-axis size request of the slot at i (R7).

The example for this item is shown with the ones above: tui-layout.salam

SetAlign

func SetAlign(i: int, align_x: CrossAlign, align_y: CrossAlign)

set_align

func set_align(i: int, align_x: CrossAlign, align_y: CrossAlign)
Sets both cross-axis alignments of the slot at i (R7).

The example for this item is shown with the ones above: tui-layout.salam

SetPadding

func SetPadding(pad: Insets)

set_padding

func set_padding(pad: Insets)
Sets the container's padding (R3).

The example for this item is shown with the ones above: tui-layout.salam

SetGap

func SetGap(gap: int)

set_gap

func set_gap(gap: int)
Sets the main-axis gap between slots (R5). A negative value clamps to 0.

The example for this item is shown with the ones above: tui-layout.salam

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure
Natural extent on axis at max_width, without moving children. Main axis is padding, plus each Fixed size or measured Fill or Pref, plus the gap budget. Cross axis is padding plus the largest measured child. A child that cannot measure makes ok false.

The example for this item is shown with the ones above: tui-layout.salam

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)
Stores the container's own rect (normalised), measures, then lays out and moves every child in slot order (R1). Overrides the BaseWidget default.

The example for this item is shown with the ones above: tui-layout.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool

Focusable

func Focusable(): bool

focusable

func focusable(): bool

Hits

func Hits(): bool

hits

func hits(): bool

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect

Column

struct Column
A container that lays its children top to bottom along the y axis. The mirror of Row: a measured main-axis Pref is reserved like Fixed, then Fill shares the content height left after that reservation and the gap budget (R5). A child fills the content width unless its align_x places it otherwise. See Row for the slot, padding and gap methods.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
padInsetsThe inset between the container's bounds and its content box (R3).
gapint = 0Cells between adjacent slots on the main axis (R5). Zero by default. A negative value clamps to 0.
itemsVectorThe children in placement order, one slot each.

The example for this item is shown with the ones above: tui-layout.salam

Add

func Add(child: dyn Widget): int

add

func add(child: dyn Widget): int
Appends child as the last slot and returns its index. This only orders the child for layout; add it to the Screen's tree to have it painted.

The example for this item is shown with the ones above: tui-layout.salam

SetMargin

func SetMargin(i: int, margin: Insets)

set_margin

func set_margin(i: int, margin: Insets)
Sets the margin of the slot at i (R4). An out-of-range index is ignored.

The example for this item is shown with the ones above: tui-layout.salam

SetWidth

func SetWidth(i: int, size: LayoutSize)

set_width

func set_width(i: int, size: LayoutSize)
Sets the x-axis size request of the slot at i (R7).

The example for this item is shown with the ones above: tui-layout.salam

SetHeight

func SetHeight(i: int, size: LayoutSize)

set_height

func set_height(i: int, size: LayoutSize)
Sets the y-axis size request of the slot at i (R5).

The example for this item is shown with the ones above: tui-layout.salam

SetAlign

func SetAlign(i: int, align_x: CrossAlign, align_y: CrossAlign)

set_align

func set_align(i: int, align_x: CrossAlign, align_y: CrossAlign)
Sets both cross-axis alignments of the slot at i (R7).

The example for this item is shown with the ones above: tui-layout.salam

SetPadding

func SetPadding(pad: Insets)

set_padding

func set_padding(pad: Insets)
Sets the container's padding (R3).

The example for this item is shown with the ones above: tui-layout.salam

SetGap

func SetGap(gap: int)

set_gap

func set_gap(gap: int)
Sets the main-axis gap between slots (R5). A negative value clamps to 0.

The example for this item is shown with the ones above: tui-layout.salam

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure
Natural extent on axis at max_width, without moving children. The mirror of Row.measure: main axis is Y.

The example for this item is shown with the ones above: tui-layout.salam

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)
Stores the container's own rect (normalised), measures, then lays out and moves every child in slot order (R1). Overrides the BaseWidget default.

The example for this item is shown with the ones above: tui-layout.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool

Focusable

func Focusable(): bool

focusable

func focusable(): bool

Hits

func Hits(): bool

hits

func hits(): bool

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect

Grid

struct Grid
A container that divides its content box into a fixed rows x cols grid (R9) and places each child by its explicit (row, col) (R10). Columns and rows share the space equally, the remainder to the earliest; an out-of-range or negative cell is a 0x0 slot at the content origin, and the child is still visited. set_span extends a cell by whole tracks (R15). Two children in one cell share it and overlap.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
padInsetsThe inset between the container's bounds and its content box (R3).
itemsVectorThe children in placement order, one slot each.
rowsint = 0The number of grid rows. Set by NewGrid.
colsint = 0The number of grid columns. Set by NewGrid.

The example for this item is shown with the ones above: tui-layout.salam

Add

func Add(child: dyn Widget): int

add

func add(child: dyn Widget): int
Appends child as the last slot and returns its index. This only orders the child for layout; add it to the Screen's tree to have it painted.

The example for this item is shown with the ones above: tui-layout.salam

SetMargin

func SetMargin(i: int, margin: Insets)

set_margin

func set_margin(i: int, margin: Insets)
Sets the margin of the slot at i (R4). An out-of-range index is ignored.

The example for this item is shown with the ones above: tui-layout.salam

SetWidth

func SetWidth(i: int, size: LayoutSize)

set_width

func set_width(i: int, size: LayoutSize)
Sets the x-axis size request of the slot at i (R7).

The example for this item is shown with the ones above: tui-layout.salam

SetHeight

func SetHeight(i: int, size: LayoutSize)

set_height

func set_height(i: int, size: LayoutSize)
Sets the y-axis size request of the slot at i (R7).

The example for this item is shown with the ones above: tui-layout.salam

SetAlign

func SetAlign(i: int, align_x: CrossAlign, align_y: CrossAlign)

set_align

func set_align(i: int, align_x: CrossAlign, align_y: CrossAlign)
Sets both cross-axis alignments of the slot at i (R7).

The example for this item is shown with the ones above: tui-layout.salam

SetCell

func SetCell(i: int, row: int, col: int)

set_cell

func set_cell(i: int, row: int, col: int)
Sets the slot's grid cell (R10).

The example for this item is shown with the ones above: tui-layout.salam

SetSpan

func SetSpan(i: int, rows: int, cols: int)

set_span

func set_span(i: int, rows: int, cols: int)
Sets how many rows and columns the slot at i covers (R15). A non-positive count is stored as 1. An out-of-range index is ignored.

The example for this item is shown with the ones above: tui-layout.salam

SetPadding

func SetPadding(pad: Insets)

set_padding

func set_padding(pad: Insets)
Sets the container's padding (R3).

The example for this item is shown with the ones above: tui-layout.salam

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)
Stores the container's own rect (normalised), measures, then lays out and moves every child in slot order (R1). Overrides the BaseWidget default. Grid cannot measure; measure stays the BaseWidget default.

The example for this item is shown with the ones above: tui-layout.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool

Focusable

func Focusable(): bool

focusable

func focusable(): bool

Hits

func Hits(): bool

hits

func hits(): bool

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure

Stack

struct Stack
A container that gives every child the content box minus its own margin (R11), so children overlap exactly. Size and alignment fields are ignored. The container does not paint or raise; Screen's paint order decides which child draws on top. A zero-margin child is exactly the content box.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
padInsetsThe inset between the container's bounds and its content box (R3).
itemsVectorThe children in placement order, one slot each.

The example for this item is shown with the ones above: tui-layout.salam

Add

func Add(child: dyn Widget): int

add

func add(child: dyn Widget): int
Appends child as the last slot and returns its index. This only orders the child for layout; add it to the Screen's tree to have it painted.

The example for this item is shown with the ones above: tui-layout.salam

SetMargin

func SetMargin(i: int, margin: Insets)

set_margin

func set_margin(i: int, margin: Insets)
Sets the margin of the slot at i (R4). An out-of-range index is ignored.

The example for this item is shown with the ones above: tui-layout.salam

SetWidth

func SetWidth(i: int, size: LayoutSize)

set_width

func set_width(i: int, size: LayoutSize)
Sets the x-axis size request of the slot at i. Ignored by Stack, which accepts it for API symmetry.

The example for this item is shown with the ones above: tui-layout.salam

SetHeight

func SetHeight(i: int, size: LayoutSize)

set_height

func set_height(i: int, size: LayoutSize)
Sets the y-axis size request of the slot at i. Ignored by Stack, which accepts it for API symmetry.

The example for this item is shown with the ones above: tui-layout.salam

SetAlign

func SetAlign(i: int, align_x: CrossAlign, align_y: CrossAlign)

set_align

func set_align(i: int, align_x: CrossAlign, align_y: CrossAlign)
Sets both cross-axis alignments of the slot at i. Ignored by Stack, which accepts it for API symmetry.

The example for this item is shown with the ones above: tui-layout.salam

SetPadding

func SetPadding(pad: Insets)

set_padding

func set_padding(pad: Insets)
Sets the container's padding (R3).

The example for this item is shown with the ones above: tui-layout.salam

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)
Stores the container's own rect (normalised), measures, then lays out and moves every child in slot order (R1). Overrides the BaseWidget default. Stack cannot measure; measure stays the BaseWidget default.

The example for this item is shown with the ones above: tui-layout.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool

Focusable

func Focusable(): bool

focusable

func focusable(): bool

Hits

func Hits(): bool

hits

func hits(): bool

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure

Keymap

struct Keymap
A table of key and key-sequence bindings. Build it with NewKeymap, NewDefaultKeymap or NewVimKeymap (or empty with Keymap {}), fill it with bind and its variants, feed events to handle, and release it with free. Never copy a Keymap value.
NameTypeDescription
pending_timeout_msint = 0How long a pending key sequence may wait for its next key, in milliseconds. 0 (the default) never expires a prefix, which is the behavior without a timeout. The timeout is evaluated lazily, on the next KeyEvent only; non-key events never expire a prefix. Set it directly (keymap.pending_timeout_ms = 700); there is no constructor.
pending_since_msint = 0The clock reading, in milliseconds, when the current pending sequence started waiting; it is refreshed on every prefix step and is 0 when nothing is pending. handle maintains it; an application sets pending_timeout_ms instead.
tui-events.salam
import term
import tui

mut quits := 0
mut tops := 0

func main:
    k := tui.Event { kind = tui.KeyEvent, key = term.Decode("q") }
    println "is q:", k.is_char("q"), "is Ctrl+C:", k.is_ctrl("c"), "is Enter:", k.is_key(term.KeyEnter)
    click := tui.Event { kind = tui.MouseEvent, key = term.Key { raw = "\x1b[<0;5;3M" }, mouse = term.DecodeMouse("\x1b[<0;5;3M") }
    p := click.mouse_point()
    println "click at", p.x, p.y
    resize := tui.Event { kind = tui.ResizeEvent, width = 100, height = 30 }
    println "kinds:", tui.NoEvent, tui.KeyEvent, tui.MouseEvent, resize.kind, tui.TickEvent, "size", resize.width, resize.height, "bubbled", resize.bubbled

    mut s := tui.NewScreen(tui.BaseWidget {}, 10, 2)
    defer s.Close()
    mut km := tui.NewKeymap()
    km.bind(term.Decode("q"), (_s: tui.Screen*, _e: tui.Event):
        quits++
        ret true
    end)
    km.bind_seq("gg", (_s: tui.Screen*, _e: tui.Event):
        tops++
        ret true
    end)
    km.bind_for(tui.RootId, term.Decode("x"), (_s: tui.Screen*, _e: tui.Event): ret true end)
    km.bind_for_seq(tui.RootId, "dd", (_s: tui.Screen*, _e: tui.Event): ret true end)
    km.pending_timeout_ms = 1000
    s.set_keymap(km)
    _ := s.dispatch(k)
    _ := s.dispatch(tui.Event { kind = tui.KeyEvent, key = term.Decode("g") })
    println "sequence timeout:", s.keymap.pending_timeout_ms, "ms"
    _ := s.keymap.pending_since_ms
    _ := s.dispatch(tui.Event { kind = tui.KeyEvent, key = term.Decode("g") })
    println "q pressed", quits, "gg pressed", tops
    s.keymap.unbind(term.Decode("q"))
    s.keymap.unbind_for(tui.RootId, term.Decode("x"))
    s.keymap.unbind_for_seq(tui.RootId, "dd")
    s.keymap.cancel_pending()
    println "after unbind:", s.dispatch(k)

    mut arrows := tui.NewDefaultKeymap()
    arrows.Free()
    mut vim := tui.NewVimKeymap((_s: tui.Screen*, _e: tui.Event): ret false end)
    vim.Free()
end
Outputis q: true is Ctrl+C: false is Enter: false click at 4 2 kinds: 0 1 2 3 4 size 100 30 bubbled false sequence timeout: 1000 ms q pressed 1 gg pressed 1 after unbind: false

Bind

func Bind(key: term.Key, action: func(Screen*, Event): bool)

bind

func bind(key: term.Key, action: func(Screen*, Event): bool)
Binds one key to an action in the global scope. A later bind of the same key replaces this one (last write wins).

The example for this item is shown with the ones above: tui-events.salam

BindFor

func BindFor(widget_id: int, key: term.Key, action: func(Screen*, Event): bool)

bind_for

func bind_for(widget_id: int, key: term.Key, action: func(Screen*, Event): bool)
Binds one key to an action for the widget with this ID: the action runs when that widget (or one of its descendants) has the focus. NoWidget binds globally.

The example for this item is shown with the ones above: tui-events.salam

BindSeq

func BindSeq(seq: str, action: func(Screen*, Event): bool)

bind_seq

func bind_seq(seq: str, action: func(Screen*, Event): bool)
Binds a key sequence to an action in the global scope. seq is decoded per rune; an empty string is ignored. A later bind of the same sequence replaces this one.

The example for this item is shown with the ones above: tui-events.salam

BindForSeq

func BindForSeq(widget_id: int, seq: str, action: func(Screen*, Event): bool)

bind_for_seq

func bind_for_seq(widget_id: int, seq: str, action: func(Screen*, Event): bool)
Binds a key sequence to an action for the widget with this ID. NoWidget binds globally. An empty string is ignored.

The example for this item is shown with the ones above: tui-events.salam

Unbind

func Unbind(key: term.Key)

unbind

func unbind(key: term.Key)
Removes the global binding for one key. A no-op when there is none. Scoped bindings are not touched (use bind_for to replace one, or unbind_for to drop one). unbind_for(NoWidget, key) does the same work.

The example for this item is shown with the ones above: tui-events.salam

UnbindFor

func UnbindFor(widget_id: int, key: term.Key)

unbind_for

func unbind_for(widget_id: int, key: term.Key)
Removes the binding for one widget on one key: bind_for's inverse. Only that widget's override is removed, so the global binding and every other widget's override on the same key still apply, and no trie node is deleted. A no-op when the map is empty, the key was never bound, or the widget never bound it. NoWidget removes the global binding, so unbind_for(NoWidget, key) is unbind(key). Resets a pending sequence.

The example for this item is shown with the ones above: tui-events.salam

UnbindForSeq

func UnbindForSeq(widget_id: int, seq: str)

unbind_for_seq

func unbind_for_seq(widget_id: int, seq: str)
Removes one widget's binding for one key sequence: bind_for_seq's inverse. Only that widget's override is removed, so the global sequence and every other widget's override on the same sequence still apply. The trie is walked read-only, so no node is created or deleted and a later bind_for_seq on the same sequence still works. A no-op when the map is empty, the sequence was never bound, or the widget never bound it; NoWidget removes the global sequence binding, and an empty string is ignored. Resets a pending sequence.

The example for this item is shown with the ones above: tui-events.salam

Handle

func Handle(screen: Screen*, event: Event): bool

handle

func handle(screen: Screen*, event: Event): bool
Handles one event against the bindings. Only KeyEvents reach the trie; every other kind returns false and leaves pending untouched. Returns true when an action ran. Reads resolve the key before any action is invoked; nothing touches the trie after an invocation.

The example for this item is shown with the ones above: tui-events.salam

Free

func Free()

free

func free()
Releases every node's children map and binding vector, then the node storage itself. The actions and their captured values are the caller's; only the trie is freed. Call once.

The example for this item is shown with the ones above: tui-events.salam

CancelPending

func CancelPending()

cancel_pending

func cancel_pending()
Forgets a key sequence that was half typed, for example the first g of gg. The screen calls it when a widget consumes a key, so keys that were not typed in a row never complete a sequence.

The example for this item is shown with the ones above: tui-events.salam

Input

struct Input
A single-line text input: value, caret, optional placeholder, and styles for the value, the cursor cell, and the placeholder. It is focusable, so keyboard events reach it through the screen's focus dispatch. Set the value with the field or set_value, read it with value, and set the placeholder with the field or set_placeholder.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
placeholderstr = ""Text shown (dim) while the value is empty. "" means no placeholder.
styleStyleThe base style of the value and the blank background. StyleDefault draws with the terminal's defaults.
cursor_styleStyleThe style merged over style for the cursor cell. Defaults to reverse video.
placeholder_styleStyleThe style merged over style for the placeholder. Defaults to a bright-black foreground.
max_lengthint = 0Maximum value length in glyphs (codepoints), not display cells. 0 means unlimited. Typing at the limit is consumed and changes nothing; set_value truncates a longer value; deleting is always allowed.
filterfunc(str): boolCalled with the whole candidate value after a proposed insertion (one typed glyph), once the max_length check has passed. Returning false rejects it: the key is consumed and nothing changes. Not called for Backspace, Delete or set_value. The default accepts everything.
maskedbool = falseWhen true every value glyph is painted as mask_glyph. Display only: value, caret and window are unchanged. The placeholder is never masked.
mask_glyphstr = "*"The glyph painted for each value glyph while masked. Only its first codepoint is used; "" means "*". Under GlyphPolicy.ASCII a non-ASCII mask glyph paints as "*".
on_submitfunc(str): boolCalled synchronously on Enter with the real value, never the mask. Returning true consumes the key; false lets it bubble to the parent. The default returns true. After it returns, the caret and window are clamped to the value, in case it changed the value.

The example for this item is shown with the ones above: tui-controls.salam

Focusable

func Focusable(): bool

focusable

func focusable(): bool
Returns true: Input is interactive, so the screen's focus dispatch routes keyboard events to it.

The example for this item is shown with the ones above: tui-controls.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool
Reacts to a key press. A printable character or space is inserted at the caret; Left, Right, Home and End move it (all consumed, even when it cannot move); backspace drops the codepoint before the caret and delete the one at it (nothing at the start or end); enter calls on_submit and returns its result. Other events and keys are not consumed, so they bubble on.

The example for this item is shown with the ones above: tui-controls.salam

Value

func Value(): str

value

func value(): str
The current text, without styles.

The example for this item is shown with the ones above: tui-controls.salam

SetValue

func SetValue(v: str)

set_value

func set_value(v: str)
Replaces the value, cut to max_length glyphs when that is set, without calling filter; the caret moves to its end.

The example for this item is shown with the ones above: tui-controls.salam

SetPlaceholder

func SetPlaceholder(p: str)

set_placeholder

func set_placeholder(p: str)
Sets the text shown while the value is empty.

The example for this item is shown with the ones above: tui-controls.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Draws the value into bounds: the box is painted with style, the window of cells that holds the caret is written (the placeholder, dim, when the value is empty), and (while the buffer reports focus) one reverse-video cell marks the caret column. The window start is kept between frames and moves only when the caret leaves it. A degenerate box draws nothing and never panics.

The example for this item is shown with the ones above: tui-controls.salam

Hits

func Hits(): bool

hits

func hits(): bool

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure

Event

struct Event
One input event. kind says which of the payload fields are meaningful.
NameTypeDescription
kindint = 0One of NoEvent, KeyEvent, MouseEvent, ResizeEvent, TickEvent.
keyterm.KeyThe key for KeyEvent; the raw sequence (in key.raw) for MouseEvent.
mouseterm.MouseEventThe decoded report for MouseEvent.
widthint = 0The new terminal width in columns, for ResizeEvent.
heightint = 0The new terminal height in rows, for ResizeEvent.
bubbledbool = falseTrue once the event has moved from the widget it started at to an ancestor. Containers such as Panel and Modal use it so they do not hand the same event to their child a second time.

The example for this item is shown with the ones above: tui-events.salam

MousePoint

func MousePoint(): Point

mouse_point

func mouse_point(): Point
The cell a MouseEvent happened on, as a 0-based Point like every other coordinate in tui. term.DecodeMouse keeps the terminal's 1-based column and row, so this subtracts 1 from each. Only meaningful for a MouseEvent whose mouse.ok is true.

The example for this item is shown with the ones above: tui-events.salam

IsKey

func IsKey(code: int): bool

is_key

func is_key(code: int): bool
True for a KeyEvent whose key code is code (a term.Key* constant, for example term.KeyEnter or term.KeyUp). Modifiers are ignored.

The example for this item is shown with the ones above: tui-events.salam

IsChar

func IsChar(ch: str): bool

is_char

func is_char(ch: str): bool
True for a KeyEvent that typed exactly ch with neither Ctrl nor Alt held, so is_char("q") does not match Ctrl+Q or Alt+Q.

The example for this item is shown with the ones above: tui-events.salam

IsCtrl

func IsCtrl(ch: str): bool

is_ctrl

func is_ctrl(ch: str): bool
True for Ctrl plus the letter ch, for example is_ctrl("c") for Ctrl+C. In raw mode Ctrl+C arrives as this key, not as a signal.

The example for this item is shown with the ones above: tui-events.salam

Color

enum Color:
    Default
    Ansi(n: int)
    Indexed(n: int)
    Rgb(r: int, g: int, b: int)
end
One terminal color, as the four kinds the wire format distinguishes. Equality is native: two Colors are equal when their kind and payload are equal, so different kinds are never equal.
NameTypeDescription
DefaultThe terminal's own default foreground or background (SGR 39 / 49), not an entry in any palette.
Ansi(n: int)A 16-color palette index, 0-15: 0-7 the normal colors, 8-15 their bright variants. No validation; the documented range is 0-15.
Indexed(n: int)A 256-color palette index, 0-255 (SGR 38;5;n / 48;5;n). An index in 0-15 stays Indexed on the wire, never rewritten to the ANSI form.
Rgb(r: int, g: int, b: int)A 24-bit truecolor triple, each channel 0-255 (SGR 38;2;r;g;b).

The example for this item is shown with the ones above: tui-style.salam

ColorTier

enum ColorTier
How much color the terminal can show. Off is no color, Ansi16 the 16-color palette, Indexed256 the 256-color palette, TrueColor 24-bit RGB.
NameTypeDescription
OffNo color: every Color lowers to Default.
Ansi16The 16-color palette only.
Indexed256The 256-color palette.
TrueColor24-bit truecolor.

The example for this item is shown with the ones above: tui-style.salam

Checkbox

struct Checkbox
A one-row boolean toggle drawn as "[x] label" when checked and "[ ] label" when not. It is focusable, so keyboard events reach it through the screen's focus dispatch: Space toggles it and is consumed, and every other key bubbles on. A left press inside its bounds toggles it whether or not it has focus. Read the state with checked, and change it with set_checked or by the user. Set disabled to stop it from taking input.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
labelstr = ""Text drawn after the mark. "" draws the mark alone.
box_styleStyleStyle of the three mark cells, the "[", the state glyph and the "]".
label_styleStyleStyle of the spacer, the label, and the trailing fill of the row.
disabledbool = falseWhen true the checkbox is not focusable, ignores every event and does not take hits, and draws its row dim.
on_togglefunc(bool)Called with the new checked value after every toggle. The default does nothing.

The example for this item is shown with the ones above: tui-controls.salam

Focusable

func Focusable(): bool

focusable

func focusable(): bool
True when enabled and at least one cell wide, so the checkbox can take keyboard focus.

The example for this item is shown with the ones above: tui-controls.salam

Hits

func Hits(): bool

hits

func hits(): bool
True when enabled, so a click on the row lands on the checkbox. A disabled checkbox lets the click fall through to what lies under it.

The example for this item is shown with the ones above: tui-controls.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool
Reacts to Space and to a left press inside bounds. Either one flips the checked state, calls on_toggle with the new value, and returns true. Every other event returns false, and a disabled checkbox returns false for all of them.

The example for this item is shown with the ones above: tui-controls.salam

Checked

func Checked(): bool

checked

func checked(): bool
The current checked state.

The example for this item is shown with the ones above: tui-controls.salam

SetChecked

func SetChecked(value: bool)

set_checked

func set_checked(value: bool)
Sets the checked state without calling on_toggle.

The example for this item is shown with the ones above: tui-controls.salam

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect
The label width plus the four cells before it (the three mark cells and the spacer), one row tall, clamped to max_width when max_width > 0.

The example for this item is shown with the ones above: tui-controls.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Draws one row at bounds.y: the spacer and label cells and the trailing fill in label_style, then the three mark cells in box_style. Focus (while enabled) overlays bold and disabled overlays dim and drops bold onto both styles. Each cell is clipped to the width, so a narrow row drops the label first and then the mark cells from the right. A degenerate box draws nothing.

The example for this item is shown with the ones above: tui-controls.salam

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure

Button

struct Button
A one-row action button drawn as "[ label ]". Enter and Space call on_click while the button has the focus, and a left click inside its bounds calls on_click whether or not it has the focus. A disabled button draws dim, takes no focus, and ignores every event.
NameTypeDescription
BaseWidgetBaseWidgetThe embedded BaseWidget: bounds and the default answers for every Widget method this type does not override.
labelstr = ""The text drawn between the brackets. Clipped to the width, never ellipsised.
styleStyleThe base style of every cell in the button.
focus_styleStyleThe style used instead of style while the button is focused and enabled. StyleDefault, the default, means bold on every cell.
disabledbool = falseWhen true the button draws dim, does not take focus or hits, and ignores every event.
on_clickfunc()Called when the button is activated. The default does nothing.

The example for this item is shown with the ones above: tui-controls.salam

Focusable

func Focusable(): bool

focusable

func focusable(): bool
True when the button is enabled and has a width, so keyboard focus can reach it. A button added before it has bounds is not focusable.

The example for this item is shown with the ones above: tui-controls.salam

Hits

func Hits(): bool

hits

func hits(): bool
True when the button is enabled, so a click on it lands here. A disabled button lets the click fall through to what is painted under it.

The example for this item is shown with the ones above: tui-controls.salam

PreferredSize

func PreferredSize(max_width: int): Rect

preferred_size

func preferred_size(max_width: int): Rect
The label width plus the two brackets, on one row, clamped to max_width when max_width > 0.

The example for this item is shown with the ones above: tui-controls.salam

HandleEvent

func HandleEvent(event: Event): bool

handle_event

func handle_event(event: Event): bool
Reacts to Enter or Space, and to a left press inside the bounds. A disabled button consumes nothing. Each activation calls on_click and returns true; every other event returns false.

The example for this item is shown with the ones above: tui-controls.salam

Render

func Render(buf: Buffer, bounds: Rect)

render

func render(buf: Buffer, bounds: Rect)
Draws the button into the first row of bounds: the row is filled with the effective style, "[" goes in the first column and "]" in the last (when the width allows), and the label is centred between them, clipped to fit. The effective style is style; dim and not bold when disabled; a focused, enabled button uses focus_style, or bold when focus_style is StyleDefault. A width of zero or less draws nothing.

The example for this item is shown with the ones above: tui-controls.salam

Visible

func Visible(): bool

visible

func visible(): bool

Bounds

func Bounds(): Rect

bounds

func bounds(): Rect

SetBounds

func SetBounds(bounds: Rect)

set_bounds

func set_bounds(bounds: Rect)

Measure

func Measure(axis: Axis, max_width: int): Measure

measure

func measure(axis: Axis, max_width: int): Measure

GlyphPolicy

enum GlyphPolicy
Which glyph repertoire structural glyphs (borders) may use. Unicode is the zero value and draws box-drawing glyphs; ASCII restricts them to codepoints up to 0x7F for terminals without box drawing.
NameTypeDescription
UnicodeBox-drawing glyphs.
ASCIIPlain ASCII glyphs only.
tui-buffer.salam
import tui

func main:
    mut buf := tui.NewBuffer(16, 5)
    buf.glyphs = tui.GlyphPolicy.Unicode
    area := buf.area()
    println "area", area.width, "x", area.height
    tui.DrawBorder(buf, area, tui.BorderStyle.Double, tui.StyleDefault, "box")
    buf.put_str(2, 1, "hi 中文", tui.Style { bold = true })
    buf.fill(tui.Rect { x = 2, y = 2, width = 5, height = 1 }, ".", tui.StyleDefault)
    buf.Set(9, 2, tui.Cell { ch = "*", style = tui.StyleDefault })
    tui.DrawBorderGlyphs(buf, tui.Rect { x = 10, y = 2, width = 5, height = 3 }, tui.GlyphsRounded(), tui.StyleDefault, "")
    repeat buf.height in y:
        println buf.row_text(y)
    end
    println "cell 2,1:", buf.Get(2, 1).ch, "wide head:", buf.Get(5, 1).ch
    println "frame bytes:", buf.ansi().Len() > 0, "changes:", buf.diff().Len() > 0
    buf.commit()
    println "after commit:", buf.diff().Len()
    buf.Clear()
    buf.resize(4, 1)
    println "resized", buf.width, buf.height, "full repaint:", buf.force_full
    println "glyph sets:", tui.GlyphsSingle().h, tui.GlyphsDouble().h, tui.GlyphsHeavy().h, tui.GlyphsASCII().h, tui.GlyphsMixed().tl
    println "ascii fallback:", tui.GlyphsFor(tui.BorderStyle.Rounded, tui.GlyphPolicy.ASCII).tl
    println "policy from env:", tui.DetectGlyphPolicy() == tui.DetectGlyphPolicy()
    _ := buf.focused
    _ := buf.clip
    buf.Free()
end
Outputarea 16 x 5 ╔═ box ════════╗ ║ hi 中文 ║ ║ ..... *╭───╮║ ║ │ │║ ╚═════════╰───╯╝ cell 2,1: h wide head: 中 frame bytes: true changes: true after commit: 0 resized 4 1 full repaint: true glyph sets: ─ ═ ━ - ┍ ascii fallback: + policy from env: true

Cell

struct Cell
One display cell and the style it is drawn with.
NameTypeDescription
chstr = " "A one-cell-wide glyph, or "" for the continuation of the wide glyph in the cell to its left. A blank cell holds " ". put_str writes wide glyphs; set blanks any ch that is not one cell wide.
styleStyleThe resolved style this cell is drawn with. StyleDefault asks for the terminal's default style. The buffer, not the widget, turns it into bytes.

The example for this item is shown with the ones above: tui-buffer.salam

Buffer

struct Buffer
A grid of cells that widgets draw into. Writes outside clip (or outside the grid) are silently ignored, so drawing is infallible.
NameTypeDescription
widthint = 0Number of columns.
heightint = 0Number of rows.
cellsVectorRow-major cell storage: the cell at (x, y) is cells[y * width + x]. Prefer get and set, which bounds-check and clip. Write through set / fill / put_str / clear. Direct writes to the backing vector are outside the contract and are not tracked.
frontVectorThe front grid: what the terminal currently shows. Same length and layout as cells. Widgets never write it; only commit() advances it.
clipRectWrites outside this rect are dropped. NewBuffer sets it to the whole grid; the screen narrows it to each widget's bounds while that widget renders.
focusedbool = falseRender context: true while the screen renders the focused widget, so the widget can draw a focus indicator. Always false outside a screen-driven render.
tierColorTierRender context: the colour capability of the terminal this buffer is drawn for. The buffer lowers every cell's Style to this tier when it emits a frame; widgets and DrawBorder only store styles. Screen.start() sets it to DetectTier() once; a buffer built by hand keeps TrueColor, so headless renders stay true colour whatever the environment says. resize() preserves it.
glyphsGlyphPolicyRender context: the glyph policy structural glyphs follow. DrawBorder reads it. Screen seeds it on every frame copy; a buffer built by hand keeps Unicode. resize() preserves it.
force_fullbool = trueTrue when the next diff() must serialize the whole frame instead of only the changed cells. NewBuffer and resize set it; commit() clears it once the front grid has caught up with the back grid.

The example for this item is shown with the ones above: tui-buffer.salam

Area

func Area(): Rect

area

func area(): Rect
The rect covering the whole grid: (0, 0, width, height).

The example for this item is shown with the ones above: tui-buffer.salam

Set

func Set(x: int, y: int, cell: Cell)

set

func set(x: int, y: int, cell: Cell)
Writes one cell. Does nothing when (x, y) is outside the grid or outside clip. A non-empty ch that is not one cell wide becomes blank; "" is accepted only after a wide glyph.

The example for this item is shown with the ones above: tui-buffer.salam

Get

func Get(x: int, y: int): Cell

get

func get(x: int, y: int): Cell
Reads one cell. Returns a blank default Cell when (x, y) is outside the grid. Reading ignores clip.

The example for this item is shown with the ones above: tui-buffer.salam

PutStr

func PutStr(x: int, y: int, text: str, style: Style)

put_str

func put_str(x: int, y: int, text: str, style: Style)
Writes text left to right, advancing one column per narrow codepoint and two per wide one. A wide glyph that cannot fit both its columns inside the grid and the clip writes a blank. Zero-width codepoints are dropped. Text is cut off at the clip; it never wraps.

The example for this item is shown with the ones above: tui-buffer.salam

Fill

func Fill(r: Rect, ch: str, style: Style)

fill

func fill(r: Rect, ch: str, style: Style)
Sets every cell of r (clipped) to the same character and style. A multi-cell or zero-width ch becomes blank through set.

The example for this item is shown with the ones above: tui-buffer.salam

Clear

func Clear()

clear

func clear()
Resets every cell of the grid to a blank default Cell, ignoring clip. The next diff() and commit() treat every cell as changed.

The example for this item is shown with the ones above: tui-buffer.salam

RowText

func RowText(y: int): str

row_text

func row_text(y: int): str
The glyphs of row y joined into one string, without styles. Continuation cells contribute nothing, so display width equals the buffer width. Returns "" for a row outside the grid. Meant for tests and debugging.

The example for this item is shown with the ones above: tui-buffer.salam

Resize

func Resize(width: int, height: int)

resize

func resize(width: int, height: int)
Replaces both grids with blank width x height ones and resets clip to cover them, forcing a full repaint on the next diff(). Only the owner of the original Buffer may call this: it frees the old storage that copies were sharing.

The example for this item is shown with the ones above: tui-buffer.salam

Ansi

func Ansi(): str

ansi

func ansi(): str
The whole grid as one string of terminal output: for each row, a cursor move to its first column, then its cells, switching style only where it changes, and closing with the last style turned off. This is the entire frame; nothing is diffed against the previous one.

The example for this item is shown with the ones above: tui-buffer.salam

Diff

func Diff(): str

diff

func diff(): str
The bytes flush() would write: the whole frame (see ansi) while force_full, otherwise only the cells that differ from the front grid, row by row. Each maximal run of changed cells is prefixed by one cursor move; styles switch only where they change and are carried across runs; a non-empty diff ends by turning the last style off. Returns "" when the grids match.

The example for this item is shown with the ones above: tui-buffer.salam

Commit

func Commit()

commit

func commit()
Copies every cell when force_full is set or clear() touched the whole grid, and only marked cells otherwise, then clears the marks and force_full, so a later diff() reports only subsequent changes.

The example for this item is shown with the ones above: tui-buffer.salam

Flush

func Flush()

flush

func flush()
Writes the changed cells (see diff) to the terminal, then commits so the front grid matches the back grid. Nothing is written when the diff is empty.

The example for this item is shown with the ones above: tui-buffer.salam

Free

func Free()

free

func free()
Releases both grids. Call it once, on the original Buffer, when you are done with it; copies share the storage.

The example for this item is shown with the ones above: tui-buffer.salam

Weight

enum Weight
The stroke weight of a border run. Light is the zero value; Heavy and Double describe the other weights of the glyph table. A BorderGlyphs records the weight of its horizontal and vertical runs for Screen's post-paint junction pass.
NameTypeDescription
LightThin single line.
HeavyThick single line.
DoubleDouble line.

The example for this item is shown with the ones above: tui-buffer.salam

BorderGlyphs

struct BorderGlyphs
The glyphs of one border ring: edges, corners, the four T junctions and the cross, plus the weight of the horizontal and vertical runs. Every glyph is one codepoint. DrawBorderGlyphs draws the edges and corners; Screen's post-paint pass selects junction glyphs from painted arms.
NameTypeDescription
hstr = ""Horizontal run, drawn on the top and bottom edges.
vstr = ""Vertical run, drawn on the left and right edges.
tlstr = ""Top-left corner.
trstr = ""Top-right corner.
blstr = ""Bottom-left corner.
brstr = ""Bottom-right corner.
t_downstr = ""T junction with its stem pointing down.
t_upstr = ""T junction with its stem pointing up.
t_rightstr = ""T junction with its stem pointing right.
t_leftstr = ""T junction with its stem pointing left.
crossstr = ""Four-way junction.
hwWeightWeight of the horizontal run.
vwWeightWeight of the vertical run.

The example for this item is shown with the ones above: tui-buffer.salam

Constants and variables

NoWidget

const NoWidget: i32 = -1
The ID meaning "no widget": the root's parent, and the result of a dispatch nobody consumed.

The example for this item is shown with the ones above: tui-widget.salam

RootId

const RootId: i32 = 0
The ID of the root widget in every tree.

The example for this item is shown with the ones above: tui-widget.salam

StyleDefault

const StyleDefault: tui_Style
The style of a cell that asks for nothing: both colors the terminal's default and all five attributes off. It is the zero value of Style and the identity of StyleMerge.

The example for this item is shown with the ones above: tui-style.salam

PollMs

const PollMs: i32 = 20
How often, at most, the input thread wakes when no key arrives, in milliseconds. Bounds the latency of stop() and, on Windows, of resize detection (on POSIX a resize arrives as SIGWINCH instead).

The example for this item is shown with the ones above: tui-terminal.salam

EventCapacity

const EventCapacity: i32 = 256
How many events the channel buffers before the input thread waits.

The example for this item is shown with the ones above: tui-terminal.salam

Directions Dir…

The four directions Nearest and the spatial focus moves (focus_up, focus_down, focus_left, focus_right) take.
NameValue
DirUp0
DirDown1
DirLeft2
DirRight3

The example for this item is shown with the ones above: tui-geometry.salam

NoEvent

const NoEvent: i32 = 0
Kind of a zero-valued Event that carries nothing.

The example for this item is shown with the ones above: tui-events.salam

KeyEvent

const KeyEvent: i32 = 1
Kind of a key press. The key field holds the decoded key.

The example for this item is shown with the ones above: tui-events.salam

MouseEvent

const MouseEvent: i32 = 2
Kind of a mouse report. The mouse field holds the decoded report and key keeps the raw escape sequence it came from.

The example for this item is shown with the ones above: tui-events.salam

ResizeEvent

const ResizeEvent: i32 = 3
Kind of a terminal resize. width and height hold the new size in cells.

The example for this item is shown with the ones above: tui-events.salam

TickEvent

const TickEvent: i32 = 4
Kind of a periodic timer tick, for animation and background work.

The example for this item is shown with the ones above: tui-events.salam

Black

const Black: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 0.

The example for this item is shown with the ones above: tui-style.salam

Red

const Red: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 1.

The example for this item is shown with the ones above: tui-style.salam

Green

const Green: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 2.

The example for this item is shown with the ones above: tui-style.salam

Yellow

const Yellow: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 3.

The example for this item is shown with the ones above: tui-style.salam

Blue

const Blue: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 4.

The example for this item is shown with the ones above: tui-style.salam

Magenta

const Magenta: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 5.

The example for this item is shown with the ones above: tui-style.salam

Cyan

const Cyan: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 6.

The example for this item is shown with the ones above: tui-style.salam

White

const White: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 7.

The example for this item is shown with the ones above: tui-style.salam

BrightBlack

const BrightBlack: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 8, the bright variant of Black.

The example for this item is shown with the ones above: tui-style.salam

BrightRed

const BrightRed: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 9, the bright variant of Red.

The example for this item is shown with the ones above: tui-style.salam

BrightGreen

const BrightGreen: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 10, the bright variant of Green.

The example for this item is shown with the ones above: tui-style.salam

BrightYellow

const BrightYellow: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 11, the bright variant of Yellow.

The example for this item is shown with the ones above: tui-style.salam

BrightBlue

const BrightBlue: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 12, the bright variant of Blue.

The example for this item is shown with the ones above: tui-style.salam

BrightMagenta

const BrightMagenta: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 13, the bright variant of Magenta.

The example for this item is shown with the ones above: tui-style.salam

BrightCyan

const BrightCyan: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 14, the bright variant of Cyan.

The example for this item is shown with the ones above: tui-style.salam

BrightWhite

const BrightWhite: Variant<tui___Color__Default, tui___Color__Ansi, tui___Color__Indexed, tui___Color__Rgb>
Palette index 15, the bright variant of White.

The example for this item is shown with the ones above: tui-style.salam

Functions

NewTree

func NewTree(root: dyn Widget): Tree
Creates a tree whose root (ID 0) is root.

The example for this item is shown with the ones above: tui-widget.salam

WrapText

func WrapText(text: str, width: int): Vector<str>
Splits text into display lines of at most width cells, top to bottom. An explicit "\n" starts a new paragraph, each wrapped on its own; the space at a break is consumed. A word longer than width is cut on the character grid. A two-cell glyph can exceed a width of one. width <= 0 means unbounded, so each paragraph is returned unwrapped.

The example for this item is shown with the ones above: tui-text.salam

MeasureText

func MeasureText(text: str, max_width: int): Rect
The preferred size of text wrapped at max_width, as a rect at (0, 0): width is the longest wrapped line in cells, height the number of wrapped lines. max_width <= 0 means unbounded, so width is the longest explicit line and height is the explicit line count. Widths count display cells.

The example for this item is shown with the ones above: tui-text.salam

NewText

func NewText(content: str): Text
Creates a Text widget with the given content: left-aligned, unstyled, and word-wrapped.

The example for this item is shown with the ones above: tui-text.salam

NewTable

func NewTable(): Table
Creates an empty Table: no columns, no rows, no header, a rule, pad_x 1.

The example for this item is shown with the ones above: tui-table.salam

NewTableView

func NewTableView(): TableView
Creates an empty TableView: Table's defaults, offset 0, selected 0, and an activate that returns false.

The example for this item is shown with the ones above: tui-table.salam

BorderInset

func BorderInset(b: BorderStyle): int
The cells a border of this kind uses on each side: 0 for None, 1 for every other kind. A container shrinks its content area by this to get the area its children draw into.

The example for this item is shown with the ones above: tui-style.salam

StyleMerge

func StyleMerge(parent: Style, child: Style): Style
The style a child ends up with under a parent, by sentinel patch: a child's color is used unless it is Color.Default (then the parent's color is inherited), and each attribute is the OR of the two. So StyleMerge(StyleDefault, s) and StyleMerge(s, StyleDefault) both equal s, and the result of merging into StyleDefault is a fully resolved style. Pure: it returns a new value and never mutates its inputs. a child can add an attribute or override a color but cannot remove an inherited attribute (child false is indistinguishable from unset) or force the terminal-default color under a colored parent (child Default means inherit). Ceiling: no subtraction. Upgrade path if removal is ever needed: a separate StylePatch of Option fields that resolves into a Style, leaving Style, cells and equality unchanged.

The example for this item is shown with the ones above: tui-style.salam

StyleEq

func StyleEq(a: Style, b: Style): bool
True when two styles are equal field by field, over the seven stored fields: colors by kind and payload, attributes directly. It needs no tier. The contract with emission: equal styles emit identical bytes for every tier (sound), but unequal styles can emit the same bytes at tier Off, where every color is dropped (not complete). It therefore errs toward "different", so a cell is never wrongly skipped; a tier change invalidates every cell, since equality ignores the tier.

The example for this item is shown with the ones above: tui-style.salam

StyleSgr

func StyleSgr(style: Style, tier: ColorTier): str
The SGR sequence that takes a terminal from its default state to style at tier: attributes first in the fixed order bold, dim, italic, underline, reverse, then the foreground, then the background. Attributes ignore the tier (NO_COLOR asks for no color, not for no bold); each color is lowered to the tier first, and one that is Default after lowering is skipped. Returns "" when nothing is set, and never emits a reset: every sequence is built from term's builders and color.salam's encoders, none is written by hand.

The example for this item is shown with the ones above: tui-style.salam

StyleSgrOff

func StyleSgrOff(style: Style, tier: ColorTier): str
The mirror of StyleSgr: the SGR sequence that takes a terminal from style back to its default state at tier, in the fixed order a single 22 if bold or dim is set (22 clears both together), then 23 if italic, then 24 if underline, then 27 if reverse, then 39 if the foreground is non-default after lowering, then 49 if the background is. Returns "" when nothing is set and never emits a reset; the codes are built with term.Sgr.

The example for this item is shown with the ones above: tui-style.salam

SgrBuilds

func SgrBuilds(): int
Diagnostics: how many non-empty SGR sequences StyleSgr and StyleSgrOff have emitted this process. Nothing caches them: the buffer appends one per style change it writes, so a frame adds one per on or off sequence emitted. Read-only; not configuration.

The example for this item is shown with the ones above: tui-style.salam

NewSeparator

func NewSeparator(): Separator
Creates a horizontal Separator with the default rune and default style.

The example for this item is shown with the ones above: tui-text.salam

NewScreen

func NewScreen(root: dyn Widget, width: int, height: int): Screen
Creates a screen of width x height cells with root as its root widget (ID 0, sized to the whole screen). Nothing touches the terminal until start(), which resizes the screen to the real terminal.

The example for this item is shown with the ones above: tui-screen.salam

NewTerminalScreen

func NewTerminalScreen(root: dyn Widget): Screen
Creates a screen sized to the current terminal (term.Size()).

The example for this item is shown with the ones above: tui-terminal.salam

NewProgress

func NewProgress(): Progress
Creates a Progress at 0% with the maximum at 100 and default styles.

The example for this item is shown with the ones above: tui-text.salam

NewModal

func NewModal(): Modal
Creates a hidden Modal: Percent(50, 40), no title, no child. Its state holder is created here, so every copy made later shares it.

The example for this item is shown with the ones above: tui-panels.salam

NewList

func NewList(items: Vector<str>): List
Creates a List over items with the selection on the first item (or on nothing when items is empty) and the window at the top, using default styles and a no-op on_select.

The example for this item is shown with the ones above: tui-controls.salam

NewInsets

func NewInsets(top: int, right: int, bottom: int, left: int): Insets
Builds an Insets from the four sides in top, right, bottom, left order, clamping each negative value to 0.

The example for this item is shown with the ones above: tui-layout.salam

UniformInsets

func UniformInsets(v: int): Insets
Builds an Insets with the same value on all four sides, clamped to 0.

The example for this item is shown with the ones above: tui-layout.salam

FitRect

func FitRect(r: Rect, box: Rect): Rect
The cells shared by r and box (R2). When they do not intersect, a 0x0 rect at the nearest point of box to r: the result is never empty in the negative sense, so a rect that leaves this function is either non-empty or exactly 0x0.

The example for this item is shown with the ones above: tui-layout.salam

InsetRect

func InsetRect(r: Rect, i: Insets): Rect
Insets r by i, then clips the result back inside r (R3, R4). Each side is first clamped to 0, so a negative padding or margin adds nothing. Padding larger than r gives an empty content box anchored inside r; the returned rect is never negative.

The example for this item is shown with the ones above: tui-layout.salam

SplitMain

func SplitMain(length: int, sizes: Vector<LayoutSize>, gap: int): Vector<int>
Splits a main-axis length of length cells between sizes, in order (R5). Fixed takes max(0, cells) and ignores weight. gap is the cells between adjacent slots. A negative gap clamps to 0. The budget is gap * max(0, k - 1), where k is the slot count, including zero-size slots; k <= 1 charges nothing. Fill and Pref share the leftover R = max(0, length - sum of those Fixed sizes - budget). Arrange resolves a measured Pref to Fixed before this call, and a Pref that cannot measure to Fill weight 1. This function does not measure; a Pref still in the vector stays in the weight pool. Each pool entry's weight is max(0, weight). The share is floor(R * weight / sum of weights), then one extra cell goes to each remaining cell, given to the pool entries with the largest (R * weight) mod sum of weights. Ties go to the earliest pool slot. A weight sum of 0 gives every pool entry 0 and leaves R unused, with no division. Fixed overflow, or a budget that uses up the length, makes R 0, so every pool entry is 0 and Fixed is not shrunk. The gap is not part of the returned sizes and is not refunded. The vector has one entry per input. Weight 1 on every pool entry is an equal split, remainder to the earliest entries.

The example for this item is shown with the ones above: tui-layout.salam

TrackSizes

func TrackSizes(total: int, n: int): Vector<int>
Divides a total length into n grid tracks (R9). Assumes total >= 0 (content lengths are never negative; on a negative total the division truncates toward zero and the exact-sum claim below does not hold). Track k is floor(total/n) plus one for each of the earliest total mod n tracks, so the tracks sum to total exactly. A non-positive n gives an empty vector.

The example for this item is shown with the ones above: tui-layout.salam

PlaceAxisSize

func PlaceAxisSize(avail: int, align: CrossAlign, natural: int): int
The length a child takes on one axis (R7). Stretch, or a non-positive avail, gives max(0, avail); otherwise the natural size clamped into [0, avail].

The example for this item is shown with the ones above: tui-layout.salam

PlaceAxisOffset

func PlaceAxisOffset(avail: int, align: CrossAlign, natural: int): int
The offset of a child from the box start on one axis (R7). Stretch, or a non-positive avail, gives 0; otherwise the natural size is clamped into [0, avail] and the free space is split per align: Start 0, Center floor(free/2) with the odd cell trailing, End free.

The example for this item is shown with the ones above: tui-layout.salam

NewRow

func NewRow(): Row
Creates an empty Row with no padding.

The example for this item is shown with the ones above: tui-layout.salam

NewColumn

func NewColumn(): Column
Creates an empty Column with no padding.

The example for this item is shown with the ones above: tui-layout.salam

NewGrid

func NewGrid(rows: int, cols: int): Grid
Creates an empty Grid with rows x cols cells and no padding. A non-positive count leaves every child without a cell.

The example for this item is shown with the ones above: tui-layout.salam

NewStack

func NewStack(): Stack
Creates an empty Stack with no padding.

The example for this item is shown with the ones above: tui-layout.salam

NewKeymap

func NewKeymap(): Keymap
Creates an empty keymap. Feed it keys with bind and the bind_* variants and release it with free. The root node is created on the first bind.

The example for this item is shown with the ones above: tui-events.salam

NewDefaultKeymap

func NewDefaultKeymap(): Keymap
Creates an opt-in keymap with the library's default focus bindings: exactly five rows: Tab moves the focus to the next widget, and Up, Down, Left and Right move it in those directions. Nothing installs this map on its own; pass it to set_keymap (or bind through a screen's keymap field) to use it. Shift+Tab is deliberately not bound: Salam decodes the backtab sequence to KeyUnknown.

The example for this item is shown with the ones above: tui-events.salam

NewVimKeymap

func NewVimKeymap(on_search: func(Screen*, Event): bool): Keymap
Creates an opt-in vim-style preset. It is a superset of NewDefaultKeymap, so Tab and the four arrows keep working, and it adds h, j, k and l for the same four directions; "gg" and "G" move to the top and the bottom of the focused widget's column, by repeated directional moves, so the column is kept; Esc pops the focus stack, but only while a pushed focus remains, and is reported unconsumed at the root so the application still sees it; and "/" runs on_search, the application's own action, whose boolean is the dispatch result. Enter and Space are deliberately not bound: dispatch offers every key to the focused widget first, where Button, Checkbox, List and Modal already act on them, and Widget has no generic activation to bind. A focused Input consumes "/" before the keymap sees it. Install the map with set_keymap, which takes ownership: never copy or free it afterward.

The example for this item is shown with the ones above: tui-events.salam

NewInput

func NewInput(): Input
Creates an empty Input with no placeholder, unstyled values, and the default cursor and placeholder styles.

The example for this item is shown with the ones above: tui-controls.salam

Nearest

func Nearest(from: Rect, candidates: Vector<Rect>, dir: int): int
Finds the candidate nearest to from in direction dir (DirUp, DirDown, DirLeft, DirRight) and returns its index in candidates, or -1 when no candidate qualifies. The rules are spelled out in the comments of the function body. O(n): every candidate is looked at once.

The example for this item is shown with the ones above: tui-geometry.salam

FgSeq

func FgSeq(c: Color): str
The foreground SGR sequence for a color, at full fidelity: the exact escape the color's kind encodes to, with no tier downgrade. Default is SGR 39; Ansi 0-7 is 30+n and Ansi 8-15 is 90+(n-8); Indexed is 38;5;n; Rgb is 38;2;r;g;b. Built from term's sequence builders: no escape is written by hand.

The example for this item is shown with the ones above: tui-style.salam

BgSeq

func BgSeq(c: Color): str
The background SGR sequence for a color, at full fidelity: the exact escape the color's kind encodes to, with no tier downgrade. Default is SGR 49; Ansi 0-7 is 40+n and Ansi 8-15 is 100+(n-8); Indexed is 48;5;n; Rgb is 48;2;r;g;b. Built from term's sequence builders: no escape is written by hand.

The example for this item is shown with the ones above: tui-style.salam

DetectTier

func DetectTier(): ColorTier
The color tier the environment advertises, from NO_COLOR, TERM, COLORTERM and CLICOLOR_FORCE only: no tty probe and no caching, so it is pure and headless-testable. First match wins: NO_COLOR set and non-empty -> Off; TERM == "dumb" unless CLICOLOR_FORCE is set to a non-empty value other than "0" -> Off; COLORTERM == "truecolor" or "24bit" -> TrueColor; TERM containing "256color" or any other non-empty COLORTERM -> Indexed256; otherwise Ansi16. NO_COLOR beats CLICOLOR_FORCE.

The example for this item is shown with the ones above: tui-style.salam

Lower

func Lower(c: Color, tier: ColorTier): Color
The same color at a lower tier: the emission-time downgrade. Off makes every color Default; TrueColor is unchanged; Indexed256 lowers only Rgb to the nearest 256 index; Ansi16 lowers Rgb and Indexed 16-255 to the nearest of the 16 ANSI colors (Rgb directly, never through Indexed) and leaves Default and Ansi alone. Pure: it returns a new value and never mutates its input.

The example for this item is shown with the ones above: tui-style.salam

FgSeqAt

func FgSeqAt(c: Color, tier: ColorTier): str
The foreground SGR sequence for a color after lowering it to tier: FgSeq of Lower(c, tier), so the emitted escape matches the tier and no sequence is written by hand.

The example for this item is shown with the ones above: tui-style.salam

BgSeqAt

func BgSeqAt(c: Color, tier: ColorTier): str
The background SGR sequence for a color after lowering it to tier: BgSeq of Lower(c, tier), so the emitted escape matches the tier and no sequence is written by hand.

The example for this item is shown with the ones above: tui-style.salam

NewCheckbox

func NewCheckbox(label: str, checked: bool): Checkbox
Creates a Checkbox with the given label and initial checked state, enabled, with default styles and a do-nothing on_toggle.

The example for this item is shown with the ones above: tui-controls.salam

NewButton

func NewButton(label: str): Button
Creates an enabled Button with the given label, default styles, and a no-op on_click.

The example for this item is shown with the ones above: tui-controls.salam

DetectGlyphPolicy

func DetectGlyphPolicy(): GlyphPolicy
The glyph policy the environment advertises. Pure and uncached; the only environment reader for glyphs, called once by Screen.start. First match wins: SALAM_TUI_GLYPHS "ascii" -> ASCII, "unicode" -> Unicode; TERM == "dumb" -> ASCII; the first non-empty of LC_ALL, LC_CTYPE, LANG decides: not UTF-8 -> ASCII, UTF-8 -> Unicode; otherwise Unicode.

The example for this item is shown with the ones above: tui-buffer.salam

NewBuffer

func NewBuffer(width: int, height: int): Buffer
Creates a blank width x height buffer whose clip covers the whole grid and whose next diff() is a full frame. A negative size is treated as zero.

The example for this item is shown with the ones above: tui-buffer.salam

GlyphsSingle

func GlyphsSingle(): BorderGlyphs
Light lines with square corners; what BorderStyle.Single draws.

The example for this item is shown with the ones above: tui-buffer.salam

GlyphsDouble

func GlyphsDouble(): BorderGlyphs
Double lines; what BorderStyle.Double draws.

The example for this item is shown with the ones above: tui-buffer.salam

GlyphsRounded

func GlyphsRounded(): BorderGlyphs
Light runs with rounded corners; what BorderStyle.Rounded draws. Rounded has no rounded junction glyphs, so its T and cross fields are light.

The example for this item is shown with the ones above: tui-buffer.salam

GlyphsASCII

func GlyphsASCII(): BorderGlyphs
Plain ASCII: - and | runs and + for every corner and junction. Used for every border under GlyphPolicy.ASCII.

The example for this item is shown with the ones above: tui-buffer.salam

GlyphsHeavy

func GlyphsHeavy(): BorderGlyphs
Heavy lines with heavy corners.

The example for this item is shown with the ones above: tui-buffer.salam

GlyphsMixed

func GlyphsMixed(): BorderGlyphs
Heavy horizontal runs and light vertical runs, with the mixed corners.

The example for this item is shown with the ones above: tui-buffer.salam

GlyphsFor

func GlyphsFor(border: BorderStyle, policy: GlyphPolicy): BorderGlyphs
The glyph set for border under policy. BorderStyle.None gives the zero value (no glyphs). Under GlyphPolicy.ASCII every other style gives GlyphsASCII; otherwise Single, Double and Rounded give GlyphsSingle, GlyphsDouble and GlyphsRounded.

The example for this item is shown with the ones above: tui-buffer.salam

DrawBorder

func DrawBorder(buf: Buffer, rect: Rect, border: BorderStyle, style: Style, title: str)
Paints a one-cell ring of border around rect into buf, every cell in one foreground color taken from style.fg. Rules: - BorderStyle.None, or a rect with no area, writes nothing. - A one-row rect is a horizontal rule and a one-column rect is a vertical rule; neither has corners or a title. - Otherwise the four corners are drawn, then the horizontal edges, then the vertical edges. Edges run strictly between the corners. - title sits on the top edge starting at rect.x + 2, with one space before and after it. It is trimmed; an empty title draws nothing; it is cut to rect.width - 5 display cells on the right and needs a width of at least 6. - Only foreground color is used. Attributes in style are ignored, and a Color.Default fg writes an empty style (the terminal default). Writes go through Buffer.set, which clips to the grid and the clip region, so a rect partly outside the buffer paints only its inside part.

The example for this item is shown with the ones above: tui-buffer.salam

DrawBorderGlyphs

func DrawBorderGlyphs(buf: Buffer, rect: Rect, glyphs: BorderGlyphs, style: Style, title: str)
Paints a one-cell ring of glyphs around rect into buf, with every rule of DrawBorder (degenerate rects, title, one foreground color). Use it for a custom or non-default glyph set. When buf.glyphs is GlyphPolicy.ASCII the set is replaced by GlyphsASCII, so a custom set never emits a non-ASCII glyph under that policy.

The example for this item is shown with the ones above: tui-buffer.salam