termui 0.6.1
termui: ^0.6.1 copied to clipboard
A high-performance terminal UI and windowing system for Dart featuring overlapping windows, double buffering, layouts, and sub-pixel vector graphics.
termui #
A high-performance, double-buffered Terminal User Interface (TUI) and declarative layout engine for Dart.
termui enables you to build complex, rich, and highly interactive terminal applications with overlapping windows, layout grids, and a widget tree structure inspired by Flutter; without the performance pitfalls, low-level ANSI complexity, or terminal flickering of naive CLI output printing.
Interactive Demo #
See what is possible with termui in your browser! Check out our live interactive web demo hosting the entire Widget Book:
Looking for Flutter support? #
To host a termui TUI application in a graphical environment (e.g. within a Flutter mobile/desktop widget or a browser canvas), check out the companion package:
termui_flutter
Screenshots #
1. System Architecture #
The core rendering and windowing model of termui revolves around three central components: SceneManager, Compositor, and Renderer. Together, they coordinate hardware abstraction, overlapping UI layer flattening, and minimum-cost terminal updates.
+-----------------------------------------------------------------+
| SceneManager |
| - Listens to terminal events (Terminal.events) |
| - Routes input to focused SceneLayer / SceneRenderer |
| - Instigates render frames via Compositor & Renderer |
+-------------------------------+---------------------------------+
|
v
+-----------------------------------------------------------------+
| Compositor |
| - Flattens all active layers onto a single target screen Buffer|
| - Employs bit-packed Uint32List occlusion map for early-exit |
+-------------------------------+---------------------------------+
|
v
+-----------------------------------------------------------------+
| Renderer |
| - Diffs back-buffer against persistent _frontBuffer |
| - Outputs minimal ANSI escape codes to StringSink |
+-----------------------------------------------------------------+
SceneManager (lib/ui/widgets/core/scene_manager.dart) #
SceneManager is the orchestrator of the terminal windowing workspace. It manages the collection of renderable screen layers, coordinates user focus, captures terminal I/O events, and synchronizes the TTY hardware state.
- Key Roles:
- Tracks and manages the stack of
SceneLayerinstances. - Directs incoming keyboard
KeyEventand mouseMouseEventinstances from theTerminalevent stream to the active layer and focused elements. - Performs hit-testing to handle floating window drags and corner-based resizes.
- Synchronizes hardware properties, such as showing or hiding the cursor, and requesting native TTY mouse tracking.
- Tracks and manages the stack of
- Core Fields:
final Terminal terminal: The native/mock hardware terminal interface.final RenderingMode renderingMode: The target rendering mode (absolute alternate screen vs. cursor-relative inline).final List<SceneLayer> layers: Stored stack of layers active on the scene.SceneLayer? focusedLayer: The layer currently holding user keyboard focus.bool enableMouseTracking: Dictates whether mouse events are monitored.final Compositor _compositor: Internal instance of the layering compositor.Renderer? _renderer: internal diff-based terminal painter.Buffer? _targetBuffer: In-memory back-buffer coordinate grid for compositing.
- Component Interactions:
- It handles terminal resizing by adjusting fullscreen layers and invoking
render(). - It intercepts TTY events. If a mouse press hits a layer boundary, it updates
focusedLayer, relocates the layer to the top of the stack if needed, and sets up window dragging or resizing. - When
render()is called, it constructs a list ofLayeredBufferitems representing each layer's current state, instructs theCompositorto flatten them onto the_targetBuffer, and then prompts theRendererto diff and write the output.
- It handles terminal resizing by adjusting fullscreen layers and invoking
Compositor (lib/ui/buffer.dart) #
The Compositor flattens overlapping, out-of-order layers onto a single 2D target screen buffer.
- LayeredBuffer:
An auxiliary data class encapsulating the visual payload of a layer:
final Buffer buffer: The coordinate cell data of the layer.final int x,final int y: The coordinate offsets on the target screen.final int zIndex: Stacking priority (higher values are rendered on top).
- Composition Mechanism (
composite):- Accepts the
targetscreenBufferand a list oflayers. - Sorts the layers in descending order of
zIndex(topmost layers are processed first). If Z-indices are equal, it maintains original ordering. - Allocates a bit-packed
Uint32Listnamedwrittenof size(totalCells + 31) >> 5wheretotalCells = target.width * target.height. This array acts as a coordinate occlusion map. - For each layer, it computes target coordinates. If a coordinate is already marked in
written, that pixel is occluded by a higher layer, and writing is skipped. - Cell drawing is skipped if the cell is marked as transparent (
sourceCell.isTransparent). - If a layer's opaque cells cover the target screen completely,
remainingTargetCellsreaches 0, and the loop breaks early (early-exit optimization).
- Accepts the
Renderer (lib/ui/renderer.dart) #
The Renderer writes minimal ANSI escape sequences by comparing the new screen state with the previous state.
- Key Roles:
- Prevents screen flickering by avoiding full redraws.
- Restricts TTY throughput by identifying and repainting only changed rectangular runs of cells.
- Core Fields:
final Buffer _frontBuffer: Persistent buffer cache retaining the state of the terminal screen from the last write.final RenderingMode mode: Determines the coordinate baseline (absolute alternate screen vs. cursor-relative inline).int _lastHeight: Tracks the height of inline animations to reset the cursor offset upwards on subsequent frames.bool _firstFrame: Tracks whether the terminal must undergo a full initialization sweep.
- Diffing & Write Sequences (
render):- If in
inlinemode and it is not the first frame, it emits\x1b[<height>Fto move the cursor back to the top-left of the inline block. - If in
alternateScreenmode and screen size changes, it clears the screen (\x1b[2J\x1b[H) and resizes the front buffer. - Iterates row-by-row and cell-by-cell. It compares
backBuffercell properties against_frontBuffer. - If a mismatch in character cluster or style attributes is found, it scans forward to identify the end of the contiguous run of dirty cells.
- Positions the cursor at the run start (using relative offset jumps in inline mode or absolute coordinates
\x1b[y;xHin alternate screen mode). - Emits ANSI style transitions via
_writeStyleTransition(comparing active color/formatting states and outputting\x1b[38;2;r;g;bmfor foregrounds,\x1b[48;2;r;g;bmfor backgrounds, and corresponding numeric flags for bold/italic/underline/reverse/blink modifiers). - Writes the character run and copies the updated state into
_frontBuffer.
- If in
2. Deferred Microtask Rendering Pipeline #
The rendering system schedules builds using the Dart event and microtask loop to batch state changes and avoid redundant UI repaints.
State Mutation (e.g. setState, focus)
|
v
Element.markNeedsBuild()
|
v
BuildOwner.scheduleBuildFor(element)
|
v
PromptRunner._scheduleRender() (Deferred via microtask)
|
+---[ Guarded by _drawScheduled ]
|
v (Next microtask execution)
PromptRunner.render()
1. BuildOwner.buildScope() (Rebuild dirty nodes)
2. Root Element.layout(BoxConstraints) (Layout pass)
3. Root Element.paint(Buffer, Offset) (Paint pass)
4. Renderer.render(Buffer, StringSink) (Diff & write ANSI to TTY)
BuildOwner (lib/ui/widgets/core/build_owner.dart) #
The BuildOwner manages the lifecycle of the reactive element tree.
- Fields:
final Set<Element> isDirtyElements: Holds elements requiring a configuration rebuild.final void Function()? onNeedVisualUpdate: Callback fired when a visual update is requested.
- Methods:
scheduleBuildFor(Element element): Flags an element as dirty (element.isDirty = true), inserts it intoisDirtyElements, and firesonNeedVisualUpdate.buildScope(): Called at the start of a render pass. It filters and sortsisDirtyElementsbytreeDepthascending. This ensures ancestor elements rebuild before descendants, preventing redundant sub-tree builds. It iterates and triggerselement.performRebuild()on all elements that are still mounted and dirty.
PromptRunner (lib/ui/widgets/core/prompt_runner.dart) #
PromptRunner runs interactive inline prompts by managing the event loop and the element tree lifecycle.
- Render Scheduling (
_scheduleRender):- When an element calls
markNeedsBuild(), it registers with theBuildOwner, which triggers_scheduleRender. _scheduleRender()checks the_drawScheduledboolean guard. If it is false, it sets the flag to true and schedules a callback usingscheduleMicrotask().- When the microtask queue executes,
_drawScheduledis reset to false, and the synchronous visual update sequence is triggered.
- When an element calls
Visual Update Sequence #
Once render() is invoked, the framework executes these phases in order:
- Rebuild (
BuildOwner.buildScope): Dirty elements resolve their configuration updates. - Layout (
Element.layout): The layout pass traverses down the element tree. The parent callslayout(BoxConstraints constraints)on its children, which executesperformLayout()to compute sizes and assign offsets (relativeOffset). - Paint (
Element.paint): The paint pass traverses the tree. Elements write characters andStylestates onto the double-bufferedBufferusingperformPaint(). - Compositing & Terminal Write: The back-buffer is passed to the
Renderer. TheRendererdiffs the new frame against_frontBufferand writes the resulting ANSI sequences to the terminal's write channel.
3. Core Controllers API Blueprints #
Controllers in termui manage the mutable state of interactive widgets.
DiscreteScrollController (lib/ui/scroll_controller.dart) #
Manages vertical and horizontal offsets in discrete cell-based layouts.
- Fields:
int _scrollOffset: Current line/cell offset.int _totalExtent: Total content height/width.int _viewportExtent: Visible height/width.
- Public APIs:
int get scrollOffset: Returns the current scroll offset.set scrollOffset(int value): Updates the offset, clamping it between0andmaxScrollExtent. InvokesnotifyListeners()on change.int get totalExtent/set totalExtent(int value): The total size of scrollable content.int get viewportExtent/set viewportExtent(int value): The size of the visible window.int get maxScrollExtent: Returns the maximum valid scroll offset (max(0, totalExtent - viewportExtent)).void jumpTo(int offset): Sets the scroll offset directly.void addListener(void Function() listener): Registers a callback for scroll changes.void removeListener(void Function() listener): Removes a registered callback.void dispose(): Clears all listeners.
TextEditingController (lib/ui/widgets/interactive/text_field.dart) #
Manages text input state, cursor selection coordinates, and change history.
- Public APIs:
TextEditingValue get value/set value(TextEditingValue newValue): The current text and selection state.String get text/set text(String newText): Text helper that updates the cursor to the end of the text.void saveStateToHistory(): Pushes the current value to the undo history stack (caps at 100 entries).void undo(): Reverts to the last history state.void redo(): Reapplies the last undone change.void clear(): Resets the text and selection states to empty.
- Associated Structs:
TextEditingValue: Immutable snapshot containingfinal String textandfinal TextSelection selection.TextSelection: Represents selection boundaries. Fields includebaseOffset,extentOffset,cursorLine,cursorColumn, andbool get isCollapsed.
FocusManager (lib/ui/window.dart) #
A singleton registry that manages the application's focus tree.
- Public APIs:
static final FocusManager instance: The singleton instance.FocusNode? get primaryFocus: The currently focused leaf node.void setPrimaryFocus(FocusNode? node): Updates focus state. It traces the path from the old and new nodes to the root, calling_setFocused(false)on nodes exiting the path and_setFocused(true)on nodes entering the path.
FocusNode (lib/ui/window.dart) #
A node in the focus traversal tree.
- Properties:
final String id: Unique identifier.FocusNode? parent: Ancestor focus node.final List<FocusNode> children: List of children.bool get hasFocus: Returns true if this node is focused.bool get isFocused: Returns true if this node is focused.bool get hasPrimaryFocus: True if focused and no children are focused.void Function(bool hasFocus)? onFocusChange: Callback fired on focus state transitions.bool Function(KeyEvent event)? onKeyEvent: Callback to intercept key events.
- Traversals & Lifecycle:
void requestFocus(): Requests focus viaFocusManager.void unfocus(): Relinquishes focus and redirects it to the parent.void addChild(FocusNode child): Inserts a child node.void removeChild(FocusNode child): Removes a child node.bool bubbleKeyEvent(KeyEvent event): Bubbles key events up the parent chain until a node consumes it.FocusNode? findFocusedLeaf(): Resolves the deeply focused leaf node.void dispose(): Cleans up the node and removes it from the focus tree.
FocusScopeNode (lib/ui/window.dart) #
A specialized node that groups focusable siblings and manages focus cycles.
- Public APIs:
FocusNode? get focusedChild: The active child in this scope.void nextFocus(): Cycles focus forward to the next child in the list.void previousFocus(): Cycles focus backward to the previous child.
SelectionController #
Manages selection state for option-based widgets like lists, radio groups, or dropdowns.
- Public APIs:
final List<T> options: The list of selectable options.int get selectedIndex/set selectedIndex(int index): Index of the selected option.int get focusedIndex/set focusedIndex(int index): Index of the focused option.T get selected/set selected(T value): Getter and setter for the selected value.void addListener(void Function() listener)/void removeListener(void Function() listener): Listens for selection updates.
TabController (lib/ui/widgets/interactive/tab_bar.dart) #
Coordinates the active index across tab components.
- Public APIs:
final int length: Total number of tabs.int get index/set index(int value): The active tab index (clamped to bounds).void addListener(void Function() listener): Registers a callback for index changes.void removeListener(void Function() listener): Removes a registered callback.
4. Widget Taxonomy #
Widgets in termui are organized into four functional categories.
A. Layout Widgets #
These widgets configure constraints and position child widgets in 2D space:
Align: Aligns a child within itself.Column: Arranges children vertically.ConstrainedBox: Imposes explicit constraints on a child.Flexible: Controls child sizing relative to sibling flex ratios.LayoutBuilder: Builds a widget tree dynamically based on parent constraints.Padding: Adds margins around its child.Positioned: Positions children inside aStack.Row: Arranges children horizontally.SafeLayout: Guards children against terminal viewport boundaries.SingleChildScrollView: Enables single-axis scrolling for a child widget.SizedBox: Enforces a fixed size on a child.SplitPane: Divides space into resizable dual panes.Stack: Overlays multiple children on top of each other.
B. Interactive Widgets #
These widgets capture keyboard and mouse events:
AnimatedButton: A button that plays transition effects when selected.Button: Standard clickable button.Checkbox: A toggle widget for boolean values.Focus: Wraps a subtree and manages aFocusNode.Form: A container that groups input fields.HorizontalRadioGroup: Horizontal list of mutually exclusive options.InkwellButton: Button that displays selection indicators.NumberSelector: Incremental/decremental input selector.Radio: An individual option button inside a radio group.ScrollBar: Interactive scrollbar overlay.Slider: Numeric input slider.StatefulBuilder: Exposes a local builder callback to trigger widget rebuilds.Switch: Horizontal toggle switch.TabBar: Navigation tab selector.TextField: Multi-line or single-line text input field.
C. Display Widgets #
These widgets render static or dynamic visual elements:
Canvas: Vector drawing canvas utilizing sub-pixel Braille dot mapping.DecoratedBox: Draws borders and backgrounds around its child.Grid: A tile-aligned layout grid.Help: Renders a toolbar showing active hotkey references.LazyTable: High-performance table widget that renders only visible cells.LeftBorder: Draws a border on the left side of a widget.LinearProgressIndicator: Progress bar showing task completion status.ListView: Lists widgets or strings sequentially.Paginator: Pagination controls for structured lists.RichText: Displays text with mixed style configurations.SevenSegmentDisplay: Renders numerical digits in a seven-segment style.Spinner: Displays a rotating character to indicate loading status.Table: Renders grid-aligned tabular datasets.Text: Standard text wrapper.TimerWidget: A counting timer that updates periodically.TreeWidget: Collapsible tree node hierarchy.
D. Core/Scaffold Widgets #
These widgets handle screen overlays and modal dialog systems:
ModalOverlay: Displays an overlay box that intercepts background events.Overlay: Manages stacked independent overlay panels.Window: A floating, draggable panel with titles, borders, and controls.
5. Performance Tracing #
The performance tracing framework profiling tools are located in lib/perf and lib/trace.
Tracer (lib/perf/tracer.dart) #
A high-performance, low-overhead event tracer that writes to a binary log file.
- Recording Categories (
TraceCategory):build: Widget construction events.layout: Sizing and layout events.paint: Buffer drawing events.events: Mouse and keyboard events.compositor: Compositing events.
- Phases:
Phase.begin(1): The start of a profiled event block.Phase.end(2): The end of a profiled event block.Phase.instant(3): A point-in-time event.
- Ring Buffer Mechanism:
- Uses a circular buffer of size
262144(256k events) to record events with minimal allocation overhead. - Writes events using two 64-bit words:
- Word 0: Bit-packed metadata:
(stringId << 32) | (isolateId << 8) | phase. - Word 1: Monotonic timestamp in microseconds:
_stopwatch.elapsedMicroseconds.
- Word 0: Bit-packed metadata:
- Key-value metadata is mapped to the event index in
_activeMetadata. - When
_writeIndexwraps around, it swaps buffers and flushes the completed buffer to theTracerSink.
- Uses a circular buffer of size
Trace Viewer UI Components #
The profiling visualizer uses a split-screen dashboard:
-
TraceViewerApp (
lib/trace/widgets/trace_viewer_app.dart):- Provides the primary interface for loading and exploring binary trace outputs.
- Supports zoom and pan controls.
- Manages an undo/redo crop history stack (
_undoStack/_redoStack) to isolate and inspect specific time ranges.
-
SearchOverlay (
lib/trace/widgets/search_overlay.dart):- An interactive panel to search and filter trace spans.
- Tokenizes query strings using key-value rules:
dur:/duration:filters by duration using operators like<,>,<=, or>=(e.g.,dur:>=16ms).name:orcat:targets event names or categories.- Prefixing a token with
-excludes matching events (e.g.,-cat:events).
- Supports regular expression matching when query strings are enclosed in slashes (e.g.,
/layout_.*/). - Employs "Smart Case" matching; queries containing capital letters perform case-sensitive searches, otherwise searches are case-insensitive.
-
TimelineCanvas (
lib/trace/widgets/timeline_canvas.dart):- Renders a horizontal flame chart representing execution tracks.
- Culls off-screen spans using binary search (
_getCulledSpans) for smooth navigation. - Maps microsecond durations to columns using the current zoom level and offsets.
- Uses block character segments (
▏to█) to show fractional durations (sub-pixel rendering). - Includes a measurement caliper: press and drag to display the duration between two points (e.g.,
├─ 12.3ms ─┤).
-
buildInspectorPanel (
lib/trace/widgets/inspector_panel.dart):- Displays details for the hovered trace span.
- Displays the start time, end time, and duration in formatted notation (nanoseconds, microseconds, or milliseconds).
- Lists any arguments and metadata recorded with the event.
6. Imperative Breakout #
The framework bridges declarative widget state with imperative console drawing using the Element class.
Element Layout and Paint Lifecycle #
- Layout Phase (
layout):- Called by the parent element with
BoxConstraints. - Saves constraints in
_cachedConstraints. - Invokes
performLayout(constraints). - Caches the size returned by clamping the dimensions:
_cachedSize = constraints.constrain(resolvedSize).
- Called by the parent element with
- Paint Phase (
paint):- Called by the parent element with the current
Bufferand visualOffset. - Traces the paint phase and delegates to
performPaint(buffer, offset).
- Called by the parent element with the current
Custom Layout Implementations #
Custom elements override performLayout and performPaint to determine child placement and write pixels to the console.
Example: PaddingElement Walk-Through (lib/ui/widgets/layout/padding.dart)
PaddingElement manages the layout and paint operations for the Padding widget.
-
Child Constraints Construction (
performLayout):final padding = widget.padding; final doubleWidth = padding.left + padding.right; final doubleHeight = padding.top + padding.bottom; // Subtract padding from the current constraints to determine constraints for the child final childMinWidth = max(0, constraints.minWidth - doubleWidth); final childMaxWidth = constraints.maxWidth == BoxConstraints.infinity ? BoxConstraints.infinity : max(0, constraints.maxWidth - doubleWidth); final childMinHeight = max(0, constraints.minHeight - doubleHeight); final childMaxHeight = constraints.maxHeight == BoxConstraints.infinity ? BoxConstraints.infinity : max(0, constraints.maxHeight - doubleHeight); final childConstraints = BoxConstraints( minWidth: childMinWidth, maxWidth: childMaxWidth, minHeight: childMinHeight, maxHeight: childMaxHeight, ); -
Child Layout and Offset Placement: If
childElementis not null,PaddingElementcalls layout on the child with the computed constraints:final childSize = childElement!.layout(childConstraints); // Assign the child's relative offset relative to the parent top-left corner childElement!.relativeOffset = Offset(padding.left, padding.top); // Return the child size increased by the padding margins return Size( childSize.width + doubleWidth, childSize.height + doubleHeight, ); -
Imperative Painting (
performPaint): During the paint pass,PaddingElementdelegates painting to its child element by applying the relative offset:@override void performPaint(Buffer buffer, Offset offset) { if (size.width > doubleWidth && size.height > doubleHeight && childElement != null) { childElement!.paint(buffer, offset + childElement!.relativeOffset); } }