Documentation
¶
Overview ¶
Package tui is the terminal-I/O layer around github.com/go-widgets/painter's CellPainter. It renders a widget tree into an ANSI cell stream sized to the current terminal, so the same widget code that produces pixels for a WUI or GUI back-end also produces text for a terminal — no widget changes required.
The package is deliberately minimal: no raw mode, no keyboard event loop, no alt-screen management. It ships a "snapshot" model — render one frame to a writer and return — that composes cleanly with either a caller-managed event loop or a CLI that prints once and exits. Higher-level facilities (an App runner with input, resize, and cleanup handling) live in a follow-up cycle.
Sizing follows the caller's preference:
- RenderOnceSized takes explicit (cols, rows) — the reliable form used by tests, size-aware callers, and headless renderers.
- RenderOnce queries the size from environment variables (EnvSize) and falls back to [DefaultCols]x[DefaultRows] when the environment does not report a size. This keeps the package stdlib-only — no ioctl, no cgo, no dependency on golang.org/x/term.
Both variants write a self-contained ANSI stream to the caller's writer via painter.CellPainter.WriteANSI; the caller is responsible for the surrounding terminal state (raw mode, alt screen, cursor visibility) if any.
Index ¶
- Constants
- func EnvSize() (cols, rows int, ok bool)
- func GutterWidth(lineCount int) int
- func LineNumberInk(theme *toolkit.Theme) toolkit.RGBA
- func RenderOnce(w io.Writer, widgets []painter.Widget, theme *painter.Theme) error
- func RenderOnceSized(w io.Writer, cols, rows int, widgets []painter.Widget, theme *painter.Theme) error
- func RenderToolkit(w io.Writer, widgets []toolkit.Widget, theme *toolkit.Theme) error
- func RenderToolkitSized(w io.Writer, cols, rows int, widgets []toolkit.Widget, theme *toolkit.Theme) error
- func SizeOrDefault() (cols, rows int)
- func SyntaxInk(k syntax.Kind, theme *toolkit.Theme) toolkit.RGBA
- type App
- type Cell
- type Color
- type InputParser
- type TTY
- type TermGrid
- type TextEditor
Constants ¶
const ( DefaultCols = 80 DefaultRows = 24 )
DefaultCols and DefaultRows are the terminal dimensions used when neither the environment nor the caller supplies a size — the classic VT100 defaults every terminal emulator honours as a floor.
const EventTick toolkit.EventKind = 100
EventTick is the toolkit.EventKind the App emits at a caller- configured tick rate. Widgets that animate (Toast, Spinner) match on this kind to advance a frame counter and repaint. The value is deliberately placed above the toolkit's own iota range so it can never collide with a widget-produced Click / KeyDown / Char event.
Variables ¶
This section is empty.
Functions ¶
func EnvSize ¶
EnvSize reads the terminal size from the COLUMNS and LINES environment variables. It returns (cols, rows, true) when both parse cleanly and hold positive integers; otherwise it returns (0, 0, false) and the caller should substitute defaults.
The env-vars-only strategy is a deliberate trade-off: it keeps the package stdlib-only (no TIOCGWINSZ ioctl per platform, no cgo, no golang.org/x/term dependency). Interactive shells that need dynamic sizes should export COLUMNS / LINES from their prompt hook, or pass an explicit size to RenderOnceSized.
func GutterWidth ¶ added in v0.8.0
GutterWidth returns the cell width of a line-number gutter for a code pane of lineCount lines: the number of digits in the largest line number (a minimum of 2) plus one cell of separation before the code. Shared by tui-explorer + tui-editor so their gutters line up.
func LineNumberInk ¶ added in v0.8.0
LineNumberInk is the muted ink for a line-number gutter: the theme's Border colour, which reads as a dim separator tone against any pane background.
func RenderOnce ¶
RenderOnce paints the widget tree into a fresh CellPainter sized to SizeOrDefault, fills the background with theme.Background, calls each widget's Draw, and writes the resulting ANSI stream to w. It is the snapshot form: no raw mode, no event loop; one frame then return.
The theme argument may be nil, in which case painter.LightTheme is used. This keeps CLI callers ergonomic (`tui.RenderOnce(os.Stdout, widgets, nil)`) without forcing them to import painter just to name the default theme.
func RenderOnceSized ¶
func RenderOnceSized(w io.Writer, cols, rows int, widgets []painter.Widget, theme *painter.Theme) error
RenderOnceSized is like RenderOnce but uses the explicit (cols, rows) dimensions instead of querying the environment. Cols and rows must both be positive; a non-positive value falls back to the corresponding default so callers cannot accidentally produce an empty render.
func RenderToolkit ¶ added in v0.2.0
RenderToolkit paints a toolkit.Widget tree into a fresh CellPainter sized to SizeOrDefault, fills the background with theme.Background, calls each widget's Draw, and writes the resulting ANSI stream to w.
It is the toolkit-widget counterpart of RenderOnce (which renders painter.Widget). The two exist side-by-side because toolkit.Theme carries fields (SurfaceAlt, OnBackground, Extra) that painter.Theme does not, so a toolkit widget cannot be rendered through RenderOnce without losing theme surface.
The theme argument may be nil, in which case toolkit.DefaultLight is used.
func RenderToolkitSized ¶ added in v0.2.0
func RenderToolkitSized(w io.Writer, cols, rows int, widgets []toolkit.Widget, theme *toolkit.Theme) error
RenderToolkitSized is like RenderToolkit but uses the explicit (cols, rows) dimensions instead of querying the environment. Cols and rows must both be positive; a non-positive value falls back to the corresponding default so callers cannot accidentally produce an empty render.
func SizeOrDefault ¶
func SizeOrDefault() (cols, rows int)
SizeOrDefault returns EnvSize when the environment reports a size, otherwise [DefaultCols]x[DefaultRows]. This is what RenderOnce consumes.
func SyntaxInk ¶ added in v0.5.0
SyntaxInk maps a syntax.Kind to a terminal ink for the given theme, choosing a dark- or light-appropriate One Dark / One Light-style palette from the theme's background luminance. Plain + Punct fall back to the theme foreground. Shared by tui-explorer + tui-editor so their code panes colour identically.
Types ¶
type App ¶ added in v0.3.0
type App struct {
// Root is the widget tree the App draws and dispatches events
// to. Callers wire the composition once and mutate its fields
// during OnKey callbacks; the next repaint reflects the state.
Root toolkit.Widget
// Theme cascades through the widget tree on every draw. Defaults
// to [toolkit.DefaultLight] if left nil.
Theme *toolkit.Theme
// Keys is a map from a key Code (matching InputParser output —
// "q", "Ctrl+C", "Up", "Enter", …) to a handler. Handlers can
// mutate App state (e.g. call [App.Quit]) or the widget tree.
// Global handlers run BEFORE the event reaches Root.OnEvent — the
// App always dispatches to Root afterwards.
Keys map[string]func(*App)
// TickHz sets the auto-tick frequency in Hz. 0 disables ticks. A
// widget subscribing to EventTick needs TickHz > 0 to animate.
TickHz int
// contains filtered or unexported fields
}
App is an interactive TUI runner. Instantiate one, set App.Root to your widget tree, optionally register keybindings + a tick rate, and call App.Run. The App handles alt-screen + raw mode, parses stdin into toolkit.Event values, dispatches them to the widget tree, repaints on demand, and cleans up on exit — including on panic, via a deferred TTY.Leave.
func NewApp ¶ added in v0.3.0
func NewApp() *App
NewApp returns an App wired to the real terminal: OpenTTY, os.Stdin, os.Stdout, real Unix signal handling, and time.NewTicker for the tick channel. Tests build an App by hand and swap the seams instead.
func (*App) Consume ¶ added in v0.3.1
func (a *App) Consume()
Consume tells the event loop that the current Keys handler fully handled the event and it must NOT propagate to Root.OnEvent. Without Consume, every key that matches a Keys handler also reaches Root — which is fine for global shortcuts (Ctrl+C to quit still lets the Root repaint from its own state), but wrong for mode-switching editors where pressing 'i' to enter edit mode would otherwise ALSO insert 'i' into the underlying TextView.
Idempotent within a single event dispatch. The event loop clears the flag at the top of every event so a Consume from a previous event never affects the next one.
func (*App) IsQuitting ¶ added in v0.3.0
IsQuitting reports whether Quit has been called (whether the loop is on its way to exit). Non-blocking. Useful for consumer tests that verify a key handler triggered a quit without spinning the event loop.
func (*App) Quit ¶ added in v0.3.0
func (a *App) Quit()
Quit signals the event loop to exit on the next iteration. Safe to call from a key handler or a goroutine, and idempotent: a second call is a no-op (the underlying quit channel is closed exactly once via sync.Once).
func (*App) Refresh ¶ added in v0.3.0
func (a *App) Refresh()
Refresh marks the frame dirty so the next iteration repaints. It also nudges the wake channel so a loop currently blocked in select unblocks — enabling async I/O goroutines to schedule a redraw without racing an unrelated event.
func (*App) Run ¶ added in v0.3.0
Run blocks, driving the event loop until Quit is called or an interrupt signal arrives. Returns the exit code (0 by default; 1 if TTY setup fails; 2 if a key handler or widget panics). Terminal state is guaranteed to be restored on exit — even on panic — via a deferred TTY.Leave.
The return is named (exitCode) so a panic recovered by the deferred recover func can overwrite it; without the named return the panic path would still yield the pre-panic value.
func (*App) SetOpenTTYFn ¶ added in v0.3.0
SetOpenTTYFn overrides the TTY factory the event loop uses at Run time. Consumer tests inject a fake TTY (or an error) so they can drive Run() without needing a real controlling terminal. Passing a factory that returns an error is a valid way to force Run() to exit with a non-zero code before any raw-mode side effect.
type Cell ¶ added in v0.3.6
Cell is one grid position after ANSI-decoding a terminal frame: the rune actually written, plus the foreground and background colors that were active when it was written. All zero-value colors mean "default" (nothing set). Cell exists so consumer tests can assert on the visual output at cell-precision — the previous text-strip protocol dropped color information and let bugs like "highlight rendered as `█` characters" ship undetected.
type Color ¶ added in v0.3.6
Color carries the 24-bit RGB values decoded from a CSI SGR "38;2" (foreground) or "48;2" (background) sequence. Zero value means "default" — SGR 0 or no color set.
type InputParser ¶ added in v0.3.0
type InputParser struct {
// contains filtered or unexported fields
}
InputParser reads bytes from a terminal stdin stream and emits toolkit.Event values. A single call to InputParser.Feed may produce zero, one, or many events: typing "abc" yields three toolkit.EventChar events, pasting a control sequence like "\x1b[A" yields one arrow-up event but consumes three bytes.
The toolkit exposes toolkit.EventKeyDown for named-key presses (Enter, Tab, arrows, Escape, …) and toolkit.EventChar for printable characters — the parser routes accordingly.
The pending buffer holds the incomplete tail of the last Feed call: a lone ESC (0x1B) whose next byte has not arrived, an ESC [ … partial CSI sequence still waiting for its final byte (0x40..0x7E), or a partial UTF-8 rune (1–3 bytes of a multi-byte codepoint split across two Reads). It is consumed automatically on the next Feed call, or drained by InputParser.Flush on an input-idle deadline.
func NewInputParser ¶ added in v0.3.0
func NewInputParser() *InputParser
NewInputParser returns a fresh parser with an empty buffer.
func (*InputParser) Feed ¶ added in v0.3.0
func (p *InputParser) Feed(b []byte) []toolkit.Event
Feed hands b bytes to the parser and returns any events that completed as a result. Bytes belonging to an incomplete sequence (a lone ESC, a partial CSI, a partial UTF-8 rune) are buffered internally and consumed on the next Feed call.
The classic Escape-vs-CSI ambiguity is resolved as follows: a lone ESC at the end of the buffer is held pending; on the next Feed a leading '[' starts a CSI sequence, any other byte causes the held ESC to be emitted as "Escape" and the following byte is then processed as a fresh input. Callers waiting on an idle deadline flush the held ESC via InputParser.Flush.
func (*InputParser) Flush ¶ added in v0.3.0
func (p *InputParser) Flush() []toolkit.Event
Flush emits any pending ESC (or partial CSI) as an "Escape" event and clears the buffer. Callers invoke it on an input-idle deadline to resolve the Escape-vs-CSI ambiguity in favour of a plain Escape. A partial UTF-8 rune is discarded silently — its tail bytes would be indistinguishable from an unrelated new keystroke by the time an idle timeout fires.
type TTY ¶ added in v0.3.0
type TTY interface {
// Enter puts the terminal in raw mode, saves the current
// termios state, switches to the alt-screen, and hides the
// cursor. Idempotent — a second call while entered is a no-op.
Enter() error
// Leave reverses Enter: restores the alt-screen, shows the
// cursor, and restores the saved termios state. Always safe to
// call, including from a defer or a panic recovery.
Leave() error
// Size returns the current terminal dimensions in cells.
Size() (cols, rows int, err error)
}
TTY is the abstraction the App runner uses to talk to a real terminal. Implementations set raw mode, hide the cursor, switch to the alt-screen, and reverse everything on Close. The mock implementation used in tests satisfies the same interface without touching a real terminal.
type TermGrid ¶ added in v0.3.6
TermGrid is a 2D cell buffer produced by DecodeANSI. Rows and Cols are the grid dimensions; Cells is row-major (Cells[y*Cols+x]).
func DecodeANSI ¶ added in v0.3.6
DecodeANSI parses stream as a sequence of CSI SGR + printable runes + cursor-positioning + line breaks, and lays every printable rune into a Cell at the current cursor position with the current foreground / background color. Only the subset of CSI a painter.CellPainter emits is honored:
- CSI H → cursor to (1, 1)
- CSI r ; c H → cursor to (c, r), 1-indexed
- CSI 0 m → reset colors
- CSI 38 ; 2 ; R ; G ; B m → foreground truecolor
- CSI 48 ; 2 ; R ; G ; B m → background truecolor
- CSI ?1049 h / l → alt-screen enter/leave (recognised, no-op)
- CSI ?25 h / l → cursor show/hide (recognised, no-op)
Unknown CSI sequences consume their bytes and are ignored so a caller can freely pass a frame that also contains cursor styling or bracketed paste. \n and \r reset the cursor to column 0 and advance the row (LF) or stay on the same row (CR).
func (*TermGrid) At ¶ added in v0.3.6
At returns the cell at (x, y). Out-of-bounds coordinates return the zero Cell — callers that care about bounds should check dims.
func (*TermGrid) Row ¶ added in v0.3.6
Row returns the y-th row as a Cell slice. Panics on out-of-bounds y — mirrors slice indexing conventions.
type TextEditor ¶ added in v0.9.0
type TextEditor struct {
toolkit.Base
Lines []string
CursorLine int
CursorCol int
Focused bool
Filename string
ReadOnly bool
ShowGutter bool
// contains filtered or unexported fields
}
TextEditor is a cell-native, read-write multi-line text editor widget with syntax highlighting and an optional line-number gutter. It is a toolkit.Widget: give it a bounds, set Focused, and forward events to OnEvent.
- One cell per glyph, one row per line (no soft wrap).
- Filename's extension drives the syntax language (see the syntax package); "" leaves the text plain. Highlighting is recomputed on every change so it tracks live edits.
- Set ReadOnly to use it as a code viewer: insert / delete / newline events are ignored, but the caret still navigates (arrows, click).
- ShowGutter draws a right-aligned line-number gutter (NewTextEditor enables it).
func NewTextEditor ¶ added in v0.9.0
func NewTextEditor() *TextEditor
NewTextEditor returns an empty editor with the line-number gutter enabled.
func (*TextEditor) Draw ¶ added in v0.9.0
func (t *TextEditor) Draw(pnt painter.Painter, theme *toolkit.Theme)
Draw paints the pane background, the (optional) line-number gutter, the syntax-highlighted text, and the caret.
func (*TextEditor) OnEvent ¶ added in v0.9.0
func (t *TextEditor) OnEvent(ev toolkit.Event)
OnEvent applies one input event: character insert, Backspace, Enter, Ctrl+Z (undo) / Ctrl+Y (redo) -- all no-ops when ReadOnly -- the arrow keys, or a click (which positions the caret past the gutter). Re-highlights after any change.
func (*TextEditor) SetText ¶ added in v0.9.0
func (t *TextEditor) SetText(s string)
SetText replaces the buffer (split on "\n"), resets the caret to the top, and re-highlights.
func (*TextEditor) Text ¶ added in v0.9.0
func (t *TextEditor) Text() string
Text returns the buffer joined by newlines (no trailing newline).
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
tui-catalogue
command
tui-catalogue renders the complete go-widgets/toolkit widget catalogue (every kind with a public constructor) into a scrollable cell-grid frame written to stdout as an ANSI stream.
|
tui-catalogue renders the complete go-widgets/toolkit widget catalogue (every kind with a public constructor) into a scrollable cell-grid frame written to stdout as an ANSI stream. |
|
tui-editor
command
tui-editor is the reference loom-style interactive demo for tui.App: a modal (vi-inspired) text editor built on top of the existing toolkit widgets — TextView for the buffer, Statusbar for the mode + cursor + filename indicator, MenuBar for the chrome, and a Popover-wrapped SearchEntry for the command palette.
|
tui-editor is the reference loom-style interactive demo for tui.App: a modal (vi-inspired) text editor built on top of the existing toolkit widgets — TextView for the buffer, Statusbar for the mode + cursor + filename indicator, MenuBar for the chrome, and a Popover-wrapped SearchEntry for the command palette. |
|
tui-explorer
command
tui-explorer is the reference interactive demo for tui.App: a small k9s-style file-browser mockup composed from toolkit widgets.
|
tui-explorer is the reference interactive demo for tui.App: a small k9s-style file-browser mockup composed from toolkit widgets. |
|
tui-snapshot
command
tui-snapshot renders a sample widget tree to stdout as an ANSI stream.
|
tui-snapshot renders a sample widget tree to stdout as an ANSI stream. |
|
Package syntax is a tiny, dependency-free syntax highlighter for the TUI previews.
|
Package syntax is a tiny, dependency-free syntax highlighter for the TUI previews. |