You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
395 lines
15 KiB
395 lines
15 KiB
# `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("<info>saved</info> for <options=bold>" .. 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.<fn>: interrupted`), stopping the script
|
|
unless trapped with `pcall`/`utils.try`.
|
|
- **A terminal is required.** Prompting with stdin/stdout redirected raises
|
|
`tui.<fn>: 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("<error> FAIL </error> expected <info>42</info>, see " ..
|
|
"<href=https://example.com/docs>the docs</> for <fg=#8888ff;options=bold>details</>"))
|
|
```
|
|
|
|
### Built-in styles
|
|
|
|
| Tag | Rendering |
|
|
|-----|-----------|
|
|
| `<info>` | bold green text |
|
|
| `<error>` | white on red |
|
|
| `<b>` | **bold** |
|
|
| `<i>` | *italic* |
|
|
| `<u>` | underscore |
|
|
| `<s>` | ~~strikethrough~~ |
|
|
|
|
(Symfony's `<comment>` and `<question>` are deliberately not built in — add
|
|
them with `tui.addPreset` if you want them.)
|
|
|
|
### Inline styles
|
|
|
|
`<fg=COLOR>`, `<bg=COLOR>` and `<options=...>` can be combined in one tag,
|
|
separated by `;`:
|
|
|
|
```lua
|
|
tui.styled("<fg=green>ok</> <bg=yellow;options=bold>careful</> <fg=#c0392b>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-<color>` the background, and option names apply directly.
|
|
Shorthand mixes freely with `key=value` parts and built-in names:
|
|
|
|
```lua
|
|
tui.styled("<red;on-white;bold>alarm</> <#f0a;u>pink underline</> <info;b>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 <keyword>local</keyword> keyword is <em>important</em>"))
|
|
tui.note("see the <keyword>docs</keyword>") -- 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 `<info>`).
|
|
|
|
### Closing tags and nesting
|
|
|
|
`</>` closes the innermost open style; named forms (`</info>`,
|
|
`</fg=green>`) work too and do the same thing. Styles nest — an inner tag
|
|
overrides only what it sets and inherits the rest:
|
|
|
|
```lua
|
|
tui.styled("<error>fail in <options=bold>step 2</> of job</error>")
|
|
-- "step 2" is bold white-on-red; the rest of the sentence plain white-on-red
|
|
```
|
|
|
|
### Hyperlinks
|
|
|
|
`<href=URL>` 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 <href=https://example.com>the manual</>")
|
|
```
|
|
|
|
### Escaping and invalid tags
|
|
|
|
A backslash escapes a literal bracket: `"\\<info>"` prints `<info>`. Anything
|
|
that does not parse as a tag — `<unknown>`, `<Info>` (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`.
|
|
|