# `tui` The `tui` global is for simple terminal interactivity: asking the user questions (**prompts**), producing colored, styled output (**`tui.styled`**), and printing ready-made **message blocks** (`tui.success`, `tui.outlineError`, …). Prompts render an interactive one-line UI on the terminal — a yes/no question, a text field, a filterable option list, a calendar — and return the answer as a plain Lua value. It is always available as the global `tui`. ```lua local name = tui.promptText("Your name?") local lang = tui.promptSelect("Favorite language?", "lua", { options = { "lua", "rust", "c" } }) if tui.confirm("Save results?", true) then print(tui.styled("saved for " .. name .. "")) end ``` ## Prompts All prompts share one shape: ```lua tui.promptXxx(text, default, opts) ``` - `text` — the question shown to the user (required string). - `default` — the prompt's default value; `nil` means none. (Exceptions: `promptSelect` matches the default against the options *by value*; `promptSecret` has no default parameter at all.) - `opts` — an optional table of extended options, listed per prompt below. Every prompt accepts `help` (a hint line shown below the prompt). Unknown keys are an error, so typos fail loudly. Shared behavior: - **Esc returns `nil`.** Declining a prompt is a normal, checkable outcome — this holds for `tui.confirm` too, since `false` ("answered no") is distinct from `nil` ("didn't answer"). - **Ctrl-C raises an error** (`tui.: interrupted`), stopping the script unless trapped with `pcall`/`utils.try`. - **A terminal is required.** Prompting with stdin/stdout redirected raises `tui.: not a terminal`. - **Prompts are async.** Like `os.sleep` or `http`, a prompt suspends only the calling coroutine (see [Concurrency](concurrency.md)). Prompts from concurrent `task.join` branches are serialized — one owns the terminal at a time — but `print`/`log` output from sibling coroutines can still interleave visually with a live prompt. - **Validation options re-prompt inline.** Constraint options like `min`, `maxLen` or `minSelected` are enforced by the prompt UI itself (an inline message, then the user tries again) — they do not raise Lua errors. Malformed *arguments* (a default that is not among the options, `min > max`, a bad date string) raise immediately, before the terminal is touched. ### `tui.confirm(text, default, opts)` → `boolean | nil` Yes/no question, answered with `y`/`n`. ```lua local ok = tui.confirm("Deploy to production?", false, { help = "y/N" }) if ok == nil then return end -- Esc: user backed out ``` - `default` — boolean returned when the user just presses Enter; with `nil` the user must type an answer. - opts: `help`, `placeholder`. ### `tui.promptText(text, default, opts)` → `string | nil` One-line text input. ```lua local host = tui.promptText("Host?", "localhost", { placeholder = "hostname or IP", suggestions = { "localhost", "db.internal", "cache.internal" }, }) ``` - `default` — pre-filled **editable** text (the user edits it in place). - opts: - `placeholder` — dim hint shown while the input is empty. - `default` — string returned when the input is submitted empty (shown as a hint; distinct from the pre-filled positional default, and both can be used together). - `minLen`, `maxLen` — length bounds (characters), enforced inline. - `suggestions` — array of strings offered as autocompletion (case-insensitive substring match; Tab completes the highlighted one). - `pageSize` — how many suggestions are visible at once. ### `tui.promptLongText(text, default, opts)` → `string | nil` Multi-line input via an external editor: opens `$VISUAL`/`$EDITOR` (falling back to a platform default) on a temp file and returns its contents on save. ```lua local body = tui.promptLongText("Release notes", "## Changes\n\n", { extension = ".md" }) ``` - `default` — text the editor buffer starts with. - opts: - `extension` — temp-file extension (drives editor syntax highlighting); a missing leading dot is added, default `.txt`. - `editor` — editor command to use instead of `$VISUAL`/`$EDITOR`. ### `tui.promptSelect(text, default, opts)` → `string | string[] | nil` Pick from a list. The third argument is **required** — it carries the options. The list is filterable by typing (fuzzy match); arrows or `vimMode` keys move the cursor. ```lua local color = tui.promptSelect("Color?", "green", { options = { "red", "green", "blue" } }) local picks = tui.promptSelect("Toppings?", { "cheese" }, { options = { "cheese", "ham", "mushrooms", "olives" }, multiple = true, minSelected = 1, }) ``` - `default` — matched against the options **by value**; a value that is not among the options raises an error (a stale default is a script bug). Single mode: a string (the cursor starts there). Multiple mode: a string or an array of strings (pre-checked). - opts: - `options` — non-empty array of strings (**required**). - `multiple` — `true` switches to checkbox-style multi-select (Space toggles, Enter submits) and the result becomes an array of the chosen strings (possibly empty). - `pageSize` — visible rows (default 7). - `vimMode` — `hjkl` navigation. - `filter` — set `false` to disable the type-to-filter input. - multiple-only: `minSelected` / `maxSelected` (selection-count bounds, enforced inline), `allSelected` (start with everything checked). ### `tui.promptSecret(text, opts)` → `string | nil` Secret input. Deliberately has **no default parameter**. ```lua local secret = tui.promptSecret("API token", { confirm = true, minLen = 8 }) ``` - opts: - `confirm` — ask twice and require both entries to match (default `false`). - `display` — `"masked"` (default, `*` per character), `"hidden"` (no echo at all), or `"full"` (plain text). - `toggle` — allow Ctrl-R to toggle revealing the input. - `minLen` — minimum length, enforced inline. ### `tui.promptNumber(text, default, opts)` → `number | nil` ### `tui.promptInt(text, default, opts)` → `integer | nil` Numeric input; the entry is parsed and re-prompted until it is a valid number (`promptNumber`) or whole number (`promptInt`). ```lua local ratio = tui.promptNumber("Ratio?", 0.5, { min = 0, max = 1 }) local port = tui.promptInt("Port?", 8080, { min = 1, max = 65535 }) ``` - `default` — returned when the input is submitted empty. `promptInt` rejects a fractional default (`1.5`) at call time. - opts: `placeholder`, `min`, `max` (bounds enforced inline; `min > max` is a call-time error). ### `tui.promptDate(text, default, opts)` → `string | nil` Date picker: an interactive calendar navigated with the arrow keys. Dates cross the API as `"YYYY-MM-DD"` strings in both directions. ```lua local day = tui.promptDate("Start date?", "2026-07-06", { min = "2026-01-01", max = "2026-12-31", weekStart = "monday", }) ``` - `default` — the initially selected date, `"YYYY-MM-DD"`. - opts: - `min`, `max` — selectable range, `"YYYY-MM-DD"`. - `weekStart` — first day of the calendar week: a weekday name like `"monday"` or `"sun"` (default Sunday). ## `tui.styled(markup)` → `string` Renders text with HTML-like style tags into a string with ANSI escape sequences, ready to `print`. The tag syntax follows [Symfony console formatting](https://symfony.com/doc/current/console/coloring.html). ```lua print(tui.styled(" FAIL expected 42, see " .. "the docs for details")) ``` ### Built-in styles | Tag | Rendering | |-----|-----------| | `` | bold green text | | `` | white on red | | `` | **bold** | | `` | *italic* | | `` | underscore | | `` | ~~strikethrough~~ | (Symfony's `` and `` are deliberately not built in — add them with `tui.addPreset` if you want them.) ### Inline styles ``, `` and `` can be combined in one tag, separated by `;`: ```lua tui.styled("ok careful brick red") ``` - Colors: `black`, `red`, `green`, `yellow`, `blue`, `magenta`, `cyan`, `white`, `gray`, their `bright-*` variants, `default` (the terminal's own color — as an *inner* tag it resets an inherited color), and hex `#rrggbb` / `#rgb` (truecolor). - Options (comma-separated): `bold`, `italic`, `underscore`, `blink`, `reverse`, `conceal`, `strikethrough`. - The syntax is strict: no spaces around `=` or `;`. ### Shorthand Bare tokens work without the `fg=`/`options=` keys: a color names the foreground, `on-` the background, and option names apply directly. Shorthand mixes freely with `key=value` parts and built-in names: ```lua tui.styled("alarm <#f0a;u>pink underline bold green") ``` ### Custom presets — `tui.addPreset(name, style)` Register your own tag name for any style spec. The spec uses the same syntax as tags (attributes, shorthand, or another preset/built-in name); the name must be identifier-like (start with a letter; letters, digits, `-`, `_`). ```lua tui.addPreset("keyword", "fg=white;bg=magenta") tui.addPreset("em", "i;bold") print(tui.styled("the local keyword is important")) tui.note("see the docs") -- NOT styled: block text is plain tui.block("styled block", { style = "keyword" }) -- but block styles resolve presets ``` Re-registering a name overwrites it, and a preset may shadow a built-in name or shorthand token (e.g. `addPreset("info", "fg=blue")` restyles ``). ### Closing tags and nesting `` closes the innermost open style; named forms (``, ``) work too and do the same thing. Styles nest — an inner tag overrides only what it sets and inherits the rest: ```lua tui.styled("fail in step 2 of job") -- "step 2" is bold white-on-red; the rest of the sentence plain white-on-red ``` ### Hyperlinks `` emits an [OSC-8 hyperlink](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda); supporting terminals show clickable text, others show just the text: ```lua tui.styled("see the manual") ``` ### Escaping and invalid tags A backslash escapes a literal bracket: `"\\"` prints ``. Anything that does not parse as a tag — ``, `` (tags are case-sensitive), a lone `<`, a bad color — is passed through as literal text, never an error. ### Color detection When output is piped or `NO_COLOR` is set, `tui.styled` returns the plain text: tags are still consumed, but no escape codes (colors or hyperlinks) are emitted. Detection follows the usual conventions (`NO_COLOR`, `CLICOLOR`, `CLICOLOR_FORCE`, tty check on stdout). ## Message blocks Ready-made status messages in the style of Symfony's console: a labeled, word-wrapped block, padded to the terminal width (capped at 120 columns) and painted with a background color. They **print directly to stdout**, surrounded by blank lines. When output is piped (or `NO_COLOR` is set) they degrade to plain aligned text. ```lua tui.success("Deployment finished") tui.warning({ "Disk usage above 80%", "Consider pruning old artifacts" }) tui.error("Build failed") ``` | Function | Label | Style | |---|---|---| | `tui.success(message, opts)` | `[OK]` | black on green | | `tui.error(message, opts)` | `[ERROR]` | white on red | | `tui.warning(message, opts)` | `[WARNING]` | black on yellow | | `tui.caution(message, opts)` | `[CAUTION]` | white on red, `!` prefix | | `tui.info(message, opts)` | `[INFO]` | green text | | `tui.note(message, opts)` | `[NOTE]` | yellow text, `!` prefix | | `tui.comment(message, opts)` | — | plain, `//` prefix | | `tui.block(message, opts)` | — | generic: everything from opts | - `message` — a string or an array of strings (rendered into one block, separated by a blank line). Treated as **plain text**: markup tags are not interpreted here (compose with `tui.styled` on your own lines instead), and lines wrap by word to fit the width. - `opts` (each overrides the preset's default): - `label` — the `[LABEL]` text; continuation lines align under it. - `style` — same syntax as a `styled` tag body: a preset or built-in name (`"error"`, anything from `tui.addPreset`), a spec (`"fg=black;bg=cyan"`), or shorthand (`"black;on-cyan"`). An invalid style is an error. - `prefix` — string prepended to every line (default `" "`). - `padding` — blank styled line above and below the text (only applies when colors are on; without a background it would just be empty lines). ### Outline blocks The same messages as a bordered box — colored **borders** instead of colored backgrounds, which stays readable on any terminal color scheme: ``` ┌─ Success ──────────────────────────┐ │ │ │ All tests passed │ │ │ └────────────────────────────────────┘ ``` ```lua tui.outlineSuccess("All tests passed") tui.outlineError("Build failed", { title = "Compile error" }) tui.outlineBlock("Anything", { title = "Stats", style = "fg=magenta" }) ``` | Function | Fallback title | Border style | |---|---|---| | `tui.outlineSuccess` | Success | green | | `tui.outlineError` | Error | red | | `tui.outlineWarning` | Warning | yellow | | `tui.outlineNote` | Note | blue | | `tui.outlineInfo` | Info | green | | `tui.outlineCaution` | Caution | red | | `tui.outlineBlock` | — | — (generic) | - `opts`: `title` (shown in the top border; overrides the preset's fallback), `style` (border color, tag-body syntax), `padding` (blank line above/below the content **inside** the box, default `true`). ## Low-level output ### `tui.raw(...)` Writes its arguments to stdout with **no newline and no separator** (each is converted with `tostring`), then flushes. This is the building block for progress-style output that redraws one line: ```lua for i = 1, 100 do tui.raw("\rprocessing ", i, "/100") -- ... work ... end tui.raw("\n") ``` ### `tui.screenInfo()` → `table` Facts about the terminal, for scripts that render their own UI (progress bars, right-aligned text, …): | Field | Meaning | |---|---| | `width`, `height` | terminal size in cells; `nil` when there is no terminal to measure | | `tty` | whether stdout is a terminal (`false` when piped) | | `colors` | whether styled output will actually emit colors (same detection as `tui.styled`) | ## Notes - **Always loaded.** No `require`; `tui` is a global in every script (`require "tui"` also works and returns the same table). - **Prompts need a terminal.** In pipes, cron, CI etc. every prompt raises `not a terminal`; `tui.styled` and the blocks still work and degrade to plain text. - **More styling tags are planned** (screen clearing, cursor movement); the markup engine is built to grow them. Progress bars can be built today from `tui.raw` + `tui.screenInfo`.