dart_tui 1.4.0
dart_tui: ^1.4.0 copied to clipboard
Elm-style terminal UI framework for Dart — Model–Update–View, async commands, 27+ components, Lipgloss-inspired styling, spring animation, and gum-style helpers.
Changelog #
1.4.0 #
Forms (huh-style) #
A composable, immutable, key-based forms subsystem — the equivalent of Charm's huh.
Form(forms/form.dart) — an immutableTeaModelyou can embed (exposessubmitted/cancelled/values), or run one-shot withawait form.run()(returnsFormValues?,nullif cancelled). Groups of fields render one page at a time as a wizard, with aGroup N/Mindicator.- Field types via the
Fieldfactory:input,password,text(multiline),file,select/selectOf<T>,multiSelect/multiSelectOf<T>,confirm,note. Text-ish fields reuseTextInputModel/TextAreaModel/FilePickerModel. - Key-based values —
FormValues.get<T>(key); hidden fields are excluded from the result. - Validation — per-field
validatewith inline errors; blocks advancing and submit. - Dynamic / conditional fields — any field or group takes
hidden: (FormValues) => bool; titles and select options can be computed from other fields' values (titleFor,optionsFor), recomputed on every change with selection clamping. - Navigation —
Tab/Enteradvance (Enter inserts a newline in multilinetext;Tab/Ctrl+Dadvance there),Shift+Tabback,Esc/Ctrl+Ccancel. FormStyles— Catppuccin Mocha defaults.- New
form.dartexample + VHS tape.
Fixes #
TextInputModel/TextAreaModel: typing a space inserted the literal string"space"(and F-keys inserted"f1", etc.) because the default branch inserted the keystroke name rather than the character. Now only unmodified printable runes are inserted, using the actual rune text — so text fields (and form inputs) accept spaces correctly.
Docs #
- New documentation website — a searchable gallery of every component, block and guide with live GIF previews and copyable source. Lives in
website/(excluded from the published package via.pubignore).
1.3.0 #
New components & helpers #
Spring(bubbles/spring.dart) — harmonica-style damped-harmonic-oscillator for smooth eased motion of any scalar (progress, scroll, cursor). Pure math, no terminal I/O:Spring(fps:, frequency:, damping:).update(pos, vel, target)plusfpsToDelta().FileLog(log.dart) — append timestamped diagnostics to a file. Because a runningProgramowns stdout,print()corrupts the UI; write here andtail -finstead.FileLog.none()discards.- gum-style one-shot helpers (
gum.dart) —filter()(interactive fuzzy picker),spin()(run a spinner while awaiting aFuture, returns its result), andpager()(scrollable viewer). Each takes aprogramOptionslist for headless testing. ListModelruntime mutation API —withItems,appendItem,insertItem,removeItemAt,setItemAt,select, andselectedIndex; all clamp the cursor.- Readline / emacs editing keys for
TextInputModelandTextAreaModel—ctrl+a/ctrl+e(home/end),ctrl+b/ctrl+f(char left/right),alt+←/alt+→(word motion), andctrl+w/alt+backspace(delete previous word).
Examples #
Five new runnable examples (each with a VHS tape): spring, gum (filter/spin/pager), list_mutation, readline, and file_log.
Performance #
Hot-path allocation and CPU reductions on the per-frame render and per-keystroke paths (all behaviour-preserving, covered by regression tests):
stripAnsifast-path (skip the regex when there is no escape) plus a single-grapheme width helper — the width/wrap/truncate/pad path no longer runs a regex per grapheme.ViewportModelreuses its wrapped lines across scrolling instead of re-wrapping every frame.AnsiRendererandCellRendererskip the grid rebuild / diff walk on identical frames; the redundant per-frame list copy is gone.ListModel.filteredItemsis memoised;TreeModelreuses its flattened node list on cursor-only changes.- The render throttle uses a precomputed frame budget and a monotonic
Stopwatch.
Fixes #
- Prompts no longer hang on cancel.
promptSelect/promptConfirm/promptInputsetfinishedon Esc/Ctrl+C but never quit, and raw mode delivers Ctrl+C as a byte (not SIGINT) — so cancelling hung forever. They now quit cleanly and returnnull. The three prompt helpers also gained aprogramOptionsparameter for headless/scripted use.
Quality #
- Test suite grown from 423 to 560+ cases; measured
lib/line coverage raised from ~67% to 92%. - New
make coverage [FLOOR=90]target and a CI coverage gate (Linux/macOS) that fails below the floor, honouring// coverage:ignore-*markers for genuinely-untestable code (Win32 FFI, raw-mode TTY, OS signals).
1.2.0 #
New components #
-
CursorModel(bubbles/cursor.dart): in-line blinking cursor widget with three display modes —CursorMode.block(█),CursorMode.underline(_), andCursorMode.bar(|). Toggles visibility on everyTickMsgwhenblink: true. Useful for building custom text editors, prompts, or any UI that needs a visible insertion point independent of the real terminal cursor. Supportsfocus()/blur()to pause blinking, andwithMode()/withBlink()builders. -
MultiSelectModel(bubbles/multi_select.dart): scrollable checkbox list supporting multiple concurrent selections. Navigate with↑↓ / jk, toggle withSpaceorx, select/deselect all witha, confirm withEnter. Features:wrap: bool— cursor wraps at list boundariesheight— viewport limitingshowStatusBar—"N/Total selected"footerselectedValuesgetter — returns customvalueor falls back tolabelMultiSelectStylesfor full per-element theming (Catppuccin Mocha defaults)
New ProgramOption functions #
Seven new fluent option functions complement the existing ProgramOptions struct:
withAltScreen() // enter alt-screen buffer at startup
withHideCursor([bool hide = true]) // hide/show terminal cursor at startup
withTickInterval(Duration interval) // emit TickMsg at a fixed interval
withMouseCellMotion() // enable button-event mouse tracking
withMouseAllMotion() // enable all-motion mouse tracking
withReportFocus() // emit FocusMsg / BlurMsg on window focus
withWindowSize(int width, int height) // inject fixed dimensions (useful in tests)
These compose with ProgramOptions and take precedence over it; defaultMouseMode and defaultReportFocus act as floor values so per-View overrides still work.
Style additions #
Border.normal— ASCII-art border (+,-,|) for environments without Unicode box-drawing support.- Per-side border flags —
Bordernow hasshowTop,showRight,showBottom,showLeft(all defaulttrue). UseStyle.withBorderSides({top, right, bottom, left})or the pre-built helpersBorder.topOnly,Border.bottomOnly,Border.sidesOnlyto draw partial borders. Border.copyWith()— produce modifiedBorderinstances without recreating all fields.tabWidth: intfield onStyle(default4) —\tcharacters are expanded to spaces before rendering. UseStyle.withTabWidth(n)fluent builder.marginBackground: RgbColor?field onStyle— fills the margin area with a solid ANSI background colour. UseStyle.withMarginBackground(color)fluent builder.getWidth(String)(public) — visible terminal column width after stripping ANSI and counting double-wide characters.getHeight(String)(public) — number of newline-delimited lines.truncate(String, int)(public) — drop trailing visible columns to fitmaxWidth; ANSI-safe.truncateLeft(String, int)(public) — drop leading visible columns; ANSI codes in the kept portion are preserved.
Component navigation & mouse improvements #
ListModel— addedpgup/ctrl+b,pgdown/ctrl+f,home/g,end/Gkey bindings;viewOffsetYfield for click-to-select mouse handling; mouse wheel scrolling.SelectListModel— addedwrap: bool(cursor wraps at list boundaries).TableModel— addedviewOffsetYand mouse handling (wheel up/down, left-click to select with header offset).TreeModel— addedviewOffsetYand mouse handling (wheel scroll, left-click to move cursor); fixed' 'space key mapping to'space'keystroke.
New examples #
| Example | What it shows |
|---|---|
cursor_model.dart |
CursorModel — all three blink modes side-by-side, toggle blink with b |
multi_select.dart |
MultiSelectModel — toggle, select-all, confirm, display result |
Tests #
10 new test files, 160+ new test cases:
| File | Coverage |
|---|---|
spinner_test.dart |
SpinnerModel state transitions |
select_list_test.dart |
SelectListModel navigation, wrap, view |
progress_test.dart |
ProgressModel rendering and clamping |
help_test.dart |
HelpModel expand/collapse, KeyMap |
paginator_test.dart |
PaginatorModel navigation, bounds, view |
cursor_model_test.dart |
CursorModel blink, focus, modes, view |
multi_select_test.dart |
MultiSelectModel toggle, select-all, wrap, view |
program_options_test.dart |
Integration: each ProgramOption emits correct ANSI sequences |
input_decoder_test.dart |
TerminalInputDecoder — 48 edge-case / fuzz-style tests |
style_properties_test.dart |
Property-based invariants for getWidth, getHeight, truncate, Style.render, joinH/V, stripAnsi |
Other #
- All 54 VHS GIFs regenerated from current kernel snapshots.
1.1.0 #
New features #
ListModel(bubbles/list.dart): full-featured scrollable list with fuzzy/subsequence filtering, keyboard navigation (↑↓ / jk), filter mode (/to enter,Esc/Backspaceto exit), viewport scrolling, optional descriptions, status bar (x/y items), andFullListStylesfor per-element theming.TabsModel(bubbles/tabs.dart): tabbed-interface component with(label, content)pairs,←/→ / h/l / Tab / Shift+Tabnavigation, andTabsStylesfor active/inactive/divider/content theming.- SGR attributes — three new text decorations on
Style:isReverse(SGR 7) — swap foreground and backgroundisBlink(SGR 5) — blinking textisOverline(SGR 53) — overline decoration
Style.inherit(parent)— fills everynullfield from a parentStyle, enabling clean style composition without property repetition. All boolean SGR fields (isBold,isDim,isItalic,isUnderline,isStrikethrough,isReverse,isBlink,isOverline) changed frombool→bool?to support three-valued inheritance semantics.underlineSpaces/strikethroughSpaces— control whether underline / strikethrough decorations extend over padding spaces (defaulttrue, matching Lipgloss).borderForeground/borderBackground— independentRgbColortinting for border characters, separate from text content color.borderTitle/borderTitleAlignment— embed a title string in the top border edge withAlign.left/.center/.rightpositioning.wordWrap—Style(wordWrap: true)wraps at word boundaries before padding/border/constraint are applied; respects thewidthconstraint.transform—Style(transform: fn)applies an arbitraryString Function(String)to the rendered content (useful for upper-casing, truncation, etc.).CompleteColor— per-profile color specification:CompleteColor(trueColor: …, ansi256: …, ansi: …); used viaforegroundComplete/backgroundCompleteonStylefor correct downgrade at each profile level.Border.hidden— a visible-but-blank border (spaces) that preserves box geometry without drawing any characters.EdgeInsets.symmetric({vertical, horizontal})andEdgeInsets.only({top, right, bottom, left})— additional named constructors matching Flutter conventions.- Layout helpers now accept enums instead of raw
doublefractions:joinHorizontal(AlignVertical, List<String>)— wasdoublejoinVertical(Align, List<String>)— wasdoubleplace(width, height, Align, AlignVertical, content)— was twodoubleargsplaceHorizontal(width, Align, content)— new helperplaceVertical(height, AlignVertical, content)— new helper
- Terminal control Cmd helpers (
cmd.dart):enterAltScreen()/exitAltScreen()— emitEnterAltScreenMsg/ExitAltScreenMsghideCursor()/showCursor()— emitHideCursorMsg/ShowCursorMsgsetWindowTitle(title)—Cmdthat emitsSetWindowTitleMsgclearScrollArea()— emitsClearScrollAreaMsgscrollUp([n = 1])/scrollDown([n = 1])—Cmdthat emitsScrollMsg
- Renderer interface extended:
setAltScreen(bool),setCursorVisibility(bool),scroll(int, {bool up})added toTeaRendererand implemented byAnsiRenderer,CellRenderer, andNilRenderer. stripAnsi(String)— public utility that strips all ANSI escape sequences from a string; previously internal-only.
New examples #
| Example | What it shows |
|---|---|
word_wrap.dart |
Style.wordWrap at multiple widths with box, rounded, and thick borders + border title |
border_style.dart |
All 6 Border variants, borderForeground / borderBackground, and all three borderTitle alignments |
sgr_attrs.dart |
All 8 SGR text attributes; Style.inherit() composition |
list_filter.dart |
ListModel with fuzzy filtering, descriptions, status bar, and selection |
tabs.dart updated to use the exported TabsModel bubble instead of an inline reimplementation.
Tests #
9 new test files, 90+ new test cases:
style_sgr_test.dart, style_border_test.dart, style_wordwrap_test.dart, canvas_test.dart, gradient_test.dart, adaptive_color_test.dart, cmd_terminal_test.dart, list_model_test.dart, tabs_model_test.dart.
Breaking changes #
joinHorizontal,joinVertical, andplacenow accept enum arguments (AlignVertical,Align) instead of rawdoublefractions. Migrate:0.0 → AlignVertical.top,0.5 → AlignVertical.middle,1.0 → AlignVertical.bottom;0.0 → Align.left,0.5 → Align.center,1.0 → Align.right.- All boolean SGR fields on
Style(isBold,isDim,isItalic,isUnderline,isStrikethrough) changed frombool(defaultfalse) tobool?(defaultnull). Existing code that reads these fields may need a null-aware comparison (style.isBold == trueorstyle.isBold ?? false).
1.0.0+1 #
Bug fixes #
- Terminal hang on exit: awaiting the stdin subscription cancel in the shutdown path so the Dart event loop is fully released before the process exits. Previously the
unawaitedcancel could leave stdin holding the event loop open, requiring a manual Ctrl-C to regain the shell prompt. - Terminal not restored on quit: flushing ANSI reset sequences (show cursor, exit alt-screen) before the process exits so the shell prompt appears on a clean line.
Other changes #
- Enter / LF key fix:
0x0a(LF /\n) now correctly maps toKeyCode.enter, fixing silent key drops on Linux/WSL terminals that send LF instead of CR for Enter. - Batch render loop: all pending messages are drained before each render; a single render fires per batch, eliminating up to 16 ms of FPS-throttle lag per key press.
- Deferred capability queries:
CSI ?2026$yandOSC 11are sent after the first rendered frame so startup is not delayed. - Makefile:
make format(dart format check),make test,make analyze,make run/run-fast/kernels/bench/gifs/new-example. - Analyzer clean: resolved all
strict_raw_type,unused_local_variable,prefer_const_constructors,library_private_types_in_public_api, andavoid_relative_lib_importswarnings.
1.0.0 #
New features #
- CellRenderer: cell-level diff renderer using grapheme clusters (
characterspackage) — only changed cells emit ANSI sequences, eliminating flicker. - Synchronized updates: CSI
?2026h/lwrapping for flicker-free frames on terminals that support it. - ExecMsg / execProcess(): run external processes (e.g.
$EDITOR) with full terminal hand-off and optional exit-code callback. - TextAreaModel: multi-line editor with
charLimit, ctrl+k/ctrl+u line-kill, cursor navigation. - ViewportModel: scrollable content pane with soft-wrap,
atBottom/scrollPercent, keyboard navigation. - TableModel + TableColumn: tabular data viewer with header, separator, scrolling cursor, and optional row styles.
- TimerModel: countdown timer,
start()/stop()/reset()builders,TickMsgrouting byid. - StopwatchModel: elapsed-time stopwatch with millisecond display.
- KeyMap + KeyBinding + HelpModel: declarative keybinding registry; help UI with compact/full toggle.
- FilePickerModel: async directory browser with extension filtering.
- Style system: Lipgloss-inspired
Stylewithwidth/heightconstraints,Align/AlignVertical,inlinemode,AdaptiveColor,Bordervariants;joinHorizontal(),joinVertical(),place()layout helpers. - EchoMode (normal / password / none) and
suggestions(tab-completion) onTextInputModel. - ValidationFailedMsg + validate callback on
TextInputModel. tickWithId(Duration, Object): tick Cmd with routing ID for composable timers.batch()/sequence(): concurrent vs. ordered command scheduling.- Mouse support:
MouseMode.cellMotion/allMotion;MouseClickMsg,MouseMotionMsg,MouseWheelMsg. - Cursor control:
View.cursorwithCursorShape(block / underline / bar) and blink flag. - Focus reporting:
View.reportFocus,FocusMsg,BlurMsg. - Window title:
View.windowTitleOSC sequence. - 41 examples: complete port of the Bubbletea example gallery.
Performance improvements #
- Enter / LF key fix:
0x0a(LF /\n) was silently decoded asctrl+jand dropped by all components. It is now correctly mapped toKeyCode.enter, matching Linux/WSL terminal behaviour. - Batch render loop: messages are now drained without re-rendering between each one; a single render fires per batch. Eliminates up to 16 ms of FPS-throttle lag per key press.
- Unawaited commands:
runCmdis now fire-and-forget so command results arrive as the next queued message without blocking key event processing. - Deferred capability queries:
CSI ?2026$y(synchronized-updates query) andOSC 11(background-color query) are sent after the first rendered frame, so the initial visible output is not delayed. - Kernel snapshot build:
tool/build.sh --kernelcompiles examples to.dillkernel snapshots (~550 ms startup vs ~1 050 ms JIT source on WSL2). - Makefile: added targets for
test,analyze,run,run-fast,kernels,bench,gifs,new-example, and more. - 48 GIFs re-recorded from kernel snapshots for faster, cleaner recordings.
Bug fixes #
- Terminal not restored on quit: added
_output.writeln()+await _output.flush()in thefinallyblock so ANSI reset sequences (show cursor, exit alt-screen) are flushed to the terminal before the shell regains control. - Terminal hang on exit: awaiting the stdin subscription cancel in the shutdown path so the Dart event loop is fully released before the process exits. Previously the
unawaitedcancel could leave stdin holding the event loop open, requiring a manual Ctrl-C to regain the shell prompt.
Breaking changes from 0.1.0 #
- Legacy Go-style uppercase API aliases removed (
Batch,Sequence,Quit,Println,WithInput, etc.) — use canonical lowercase equivalents. request*commands now emit real terminal protocol queries; decoder responses feed back as typedMsgvalues.ListModelrenamed toSelectListModel.
0.1.0 #
- Initial release:
Programruntime (Elm-styleModel,Msg,Cmd,Batch). - Terminal integration via
dart_console(raw keys, window size, resize). - Components:
ListModel,TextInputModel,SpinnerModel,ProgressModel. - Optional
promptshelpers:select,confirm,input. - Example: shopping list (Bubble Tea tutorial port).