Ansiq is a runtime-first TUI framework for long-running terminal applications.
It is designed around one core idea:
Streams, tasks, and input should be coordinated by a single terminal runtime.
Ansiq is not a widget-heavy component kit. It is a Rust-only runtime for streaming-first terminal UX, aimed at AI agents, developer tools, dashboards, and similar interactive systems.
Ansiq focuses on:
- async-native terminal applications
- retained UI tree rendering
- incremental framebuffer diffing
- focus-aware input routing
- background task integration through message passing
- streaming output as a first-class UI behavior
- a signal-first Rust API with function components and a
view!macro
The render path is:
retained tree -> layout -> framebuffer -> diff -> terminal
The runtime path is:
input / async message -> update -> rebuild tree -> layout -> diff render
Key runtime rules:
- UI changes are coordinated by one runtime loop
- async tasks never mutate the UI directly
- background work sends messages back to the runtime through channels
- the renderer flushes only changed cells instead of clearing and repainting the whole screen
- terminal session policy lives in
ansiq-surface, so the runtime does not invent its own terminal occupancy rules - reactive handles are thread-affine and intentionally non-
Send - terminal input intake uses async event streams instead of blocking a Tokio worker with synchronous polling
Ansiq runtime is responsible for:
- app lifecycle and the single UI loop
- reactive flush scheduling
- dirty scope collection
- subtree rerender and replacement
- focus management and input routing
- partial relayout
- invalidated region tracking
- terminal session and viewport management
- framebuffer diff and terminal patch emission
Ansiq runtime is not responsible for:
- business domain modeling
- network protocol implementations
- higher-level AI agent orchestration
- complex rich-text parsing or markdown semantics
- persistence and external storage policy
The detailed version of this boundary is documented in
docs/runtime-boundary.md.
The release process and publish order are documented in
docs/release-checklist.md.
This repository is a Cargo workspace monorepo. Framework crates live under packages/, and runnable demos live under examples/.
packages/ansiq-coreCore element model, geometry, style types, signal-first reactivity primitives, and function-component helpers.packages/ansiq-macrosview!proc macro for JSX-like declarative trees that compile to the same retainedElementmodel.packages/ansiq-runtimeMain UI loop, focus handling, input routing, async message integration, and tree rendering orchestration.packages/ansiq-surfaceTerminal session lifecycle, viewport policy, raw mode, cursor handling, and event intake viacrossterm.packages/ansiq-renderCell buffer, diff generation, and ANSI patch emission.packages/ansiq-layoutMinimal row/column layout with fixed/fill sizing and pane inset handling.packages/ansiq-widgetsBuilder-style primitives:Box,Block,Text,Paragraph,List,Table,Tabs,Gauge,LineGauge,Sparkline,Scrollbar,Clear,Pane,ScrollView,StreamingText,Input,StatusBar.examplesRunnable scenario demos and example applications.
Each package keeps its tests in a dedicated tests/ directory instead of inline #[cfg(test)] modules.
The current runtime is no longer just an idea sketch. It already has working implementations for the main kernel responsibilities, but the maturity level is not uniform across every subsystem.
- terminal session setup and teardown without taking over the whole terminal by default
- surface-side viewport planning for initial reserve, resize, reanchor, and fit operations
- history reanchor based on counted rendered rows instead of terminal cursor-position queries
ReservePreferred(n)now returns tonafter history commit reanchor instead of treating temporary growth as the new steady-state shell heightHistoryEntry::Textnow normalizes through the same commit-timeHistoryBlockwrapping path as structured history blocks- terminal session teardown clamps the final exit row to the visible terminal height before restoring the cursor
- framebuffer-based diff rendering
- retained
Elementtree - signal-first component API via
Cx:cx.signal,cx.computed, andcx.effect - standalone signal-first reactivity core in
ansiq-core(signal,computed,effect, explicitflush)
effect is not React's useEffect: there is no dependency array. Dependencies are tracked automatically from reactive reads performed while the effect runs.
Signal::set_if_changed(...)for non-breaking same-value propagation suppression whenT: PartialEq- focusable widgets and tab traversal
- optional focus trapping to a continuity-keyed subtree
- focused input routing for the
Inputwidget - app-level
on_unhandled_key(...)for keys not consumed by widgets or runtime focus traversal - async app messages with
tokio - dirty component scope collection
- subtree rerender and replacement
- explicit continuity keys for preserving focus and local widget state across subtree and root rerenders
- component subtree replacement refreshing wrapper-node measured height before ancestor relayout
- partial relayout along dirty ancestor chains
- invalidated region tracking
- overlapping dirty paths normalized before relayout and damage collection
- layout-only containers avoiding full-rect self-invalidation when only descendants shift
- separation between rerendering the tree and redrawing in-place interactive widget state
- core-owned layout contracts consumed by
ansiq-layout - debug-time hardening of strict layout primitives such as three-slot
Shell - core-owned widget key routing contracts consumed by
ansiq-runtime - core-owned render math consumed by
ansiq-runtime::draw - core-owned text shaping helpers consumed by
ansiq-runtime::draw - runtime-side drawing and cursor lookup split into focused internal modules instead of one monolithic
draw.rs - function component API via
Cx view!macro for declarative UI trees
- partial redraw planning and damage tracking for complex shells
- viewport growth, scrollback flush, and inline session behavior across varied terminal environments
- more advanced viewport pinning and detached scrollback semantics for long-running shells
- committed scrollback history currently wraps at commit-time width and does not reflow after later terminal resizes
- widget parity with
ratatui - Unicode/grapheme correctness beyond common terminal cases
- higher-level session/shell composition patterns
- keyed reconciliation
- mouse support
- a fully mature shell/layout model for complex workspaces
- virtualization for very large scroll regions
- complete
ratatuiwidget parity
Ansiq started from an MVP direction, but ongoing implementation work should not aim for “good enough for a demo”.
The current expectation is:
- prefer stable, reusable behavior over scenario-specific patches
- prefer library-level abstractions over example-local workarounds
- prefer explicit runtime boundaries over convenient coupling
- treat partially correct behavior as unfinished, not “close enough”
The following areas are still intentionally minimal or incomplete:
- no keyed reconciliation
- no mouse support
- no rich text or multiline editing
- no advanced layout engine or full flexbox behavior
- no virtualization for very large scroll regions
- Unicode handling is conservative and optimized for common terminal text, not every grapheme edge case
ScrollViewis optimized for text-like children in this MVP- the
view!macro is intentionally smaller than JSX and does not yet support inline control flow
The examples crate now provides multiple runnable demos:
activity_monitor: macOS-style process monitor with live CPU, memory, energy, disk, and network tabslist_navigation: interactive list selectionscroll_sync: shared scroll state betweenScrollViewandScrollbartable_interaction: keyboard-driven table selection
Run:
cargo run -p ansiq-examples --example activity_monitor
cargo run -p ansiq-examples --example list_navigation
cargo run -p ansiq-examples --example scroll_sync
cargo run -p ansiq-examples --example table_interactionControls:
Tab/Shift-Tab: move focusLeft/Right: switch tabs whenTabsis focusedUp/Down: move selection in tables and listsj/k: move focus when the focused widget does not consume the keyCtrl+C: exit
The example entrypoints opt into scenario-appropriate viewport policies.
The framework default remains a conservative inline mode that preserves visible terminal content above the app.
Logical next steps after the MVP:
- keyed tree reconciliation
- richer layout and sizing policies
- better Unicode and grapheme support
- mouse input
- additional widgets
- higher-level reactive helpers built on top of the signal core
- smarter damage tracking for large scrolling regions
- richer
view!syntax, including controlled support for conditional and repeated children