Skip to content

Architecture

This document is the implementation-aware architecture map for tui-lipan. It is intended for contributors and maintainers who need to understand how the runtime is assembled across modules.

It deliberately avoids duplicating API reference material. For user-facing APIs, use the focused docs:

Design Goals

tui-lipan is a component-based TUI framework with a React/Elm-like runtime model:

  • User code defines Components with typed Message, Properties, and State.
  • view() returns a declarative Element tree.
  • The runtime expands nested components, reconciles to a realized node tree, routes input, and renders.
  • Builder APIs are primary; ui! and rsx! are syntax sugar over the same model.

Core architectural constraints:

  • No ratatui types in the public API.
  • UI tree and component state are single-threaded.
  • Background work returns through Command / CommandLink messages.
  • Mouse, focus, hover, overlays, and scrollbars are routed through precise runtime hit-testing instead of ad-hoc widget-local coordinates.

Runtime Flow

text
terminal event
  -> AppRunner input dispatch
  -> widget callback or scoped key bubbling
  -> scoped message queue
  -> Component::update / Component::on_key
  -> optional Command scheduling
  -> CommandLink::send(...) back to queue
  -> view expansion if dirty
  -> reconcile + layout
  -> render

The important boundary is that component state only mutates on the UI/runtime thread. Commands may perform background work, but they communicate back by sending typed messages into the same scoped queue.

Module Map

Primary runtime modules:

AreaModules
App orchestrationsrc/app/, src/app/runner/
Runtime core for app/testssrc/runtime.rs
Component and element modelsrc/core/
Nested component registrysrc/core/nested/
Realized node treesrc/core/node/
Layout and reconciliationsrc/layout/
Input dispatchsrc/app/input/, src/app/runner/
Overlay state and routingsrc/overlay.rs, app runner overlay handlers
Internal terminal backendsrc/backend/ratatui_backend/
Headless test runtimesrc/test_backend.rs
Widgetssrc/widgets/
Styling and themessrc/style/

Keep backend-specific types inside src/backend/ and app/runner internals. Public types should remain crate-owned.

Trees

tui-lipan uses two main tree representations.

Element is the declarative tree returned by view():

  • keyable for stable identity
  • cloneable
  • stores layout constraints
  • stores widgets, group wrappers, and nested component placeholders

NodeTree is the realized runtime tree after expansion and reconciliation:

  • stable NodeId values from an arena plus generation
  • computed rects
  • parent/child links
  • widget-specific runtime payloads

The NodeTree is used by rendering, focus traversal, hit-testing, hover testing, scrollbar routing, overlay routing, and pointer capture.

Reconciliation

Reconciliation lives under src/layout/reconcile/ and is epoch-based:

  • Existing nodes are reused when shape, type, and key permit.
  • Keyed children preserve identity across reorder.
  • Unkeyed children fall back to order-based matching.
  • Removed nodes and nested component instances are swept after the epoch.
  • Capability flags such as hoverability, mouse-move handlers, and animated nodes are tracked incrementally to avoid extra full-tree scans.

Nested components are expanded before layout. Each expanded nested subtree is wrapped in a layout-transparent group that carries scope metadata for message and key routing. See Components for author-facing usage.

Layout

The layout engine lives in src/layout/.

Current sizing model:

  • Containers default to flex-like fill.
  • Leaf widgets generally measure to content.
  • Sizing is expressed with crate-owned layout types such as Length, Size, Rect, Padding, Align, and Justify.

Key implementation areas:

  • measurement in src/layout/measure.rs
  • stack algorithms for VStack and HStack
  • min/max/focus/collapse constraints via LayoutConstraints
  • overlay roots laid out relative to viewport bounds

For app-author layout usage, see Styling and Layout widgets. For contributor guidance, see Widget authoring.

Input Routing

Input dispatch is intentionally thin. It should resolve the target and delegate behavior to widgets, component scopes, or overlay policy rather than embedding large widget-specific behavior in the runner.

Keyboard flow:

  • Crossterm events are converted to crate-owned KeyEvent values.
  • Focused widgets get first chance.
  • Unhandled keys bubble by component scope from child to parent to root.
  • Focus traversal is handled centrally, including overlay-local traversal.

Mouse flow:

  • Deep hit-testing resolves the target node, including overlay z-order rules.
  • Click, drag, scroll, move, and scrollbar thumb drags use explicit routed state.
  • Motion and scroll streams may be coalesced for render efficiency.

For behavior-level documentation, see Focus, Keybindings, and Events.

Commands

Commands preserve a single-threaded UI tree while allowing concurrent I/O or compute work:

  • Command::new(...) runs a UI-thread action.
  • Command::spawn(...) runs background work.
  • Command::spawn_keyed(...) coalesces keyed background work according to TaskPolicy.

External full-screen subprocesses such as $EDITOR must use UI-thread terminal handoff rather than a background worker. See External programs.

Rendering

The terminal backend is internal and lives under src/backend/ratatui_backend/. It maps realized NodeKind values to ratatui rendering primitives and owns terminal guard behavior such as raw mode, alternate screen, mouse capture, keyboard enhancement negotiation, and panic-safe restoration.

Render invalidation has three broad levels:

  • full: rebuild view, expand, reconcile, layout, and draw
  • layout-only: reuse expanded view where possible, then reconcile/layout/draw
  • paint-only: redraw the existing realized tree

Renderer responsibilities include clipping, overlay clears, chrome frames, scrollbars, and widget-specific painting.

Overlays

Overlay primitives are implemented as portal-style roots plus centralized overlay manager state. The app runner owns routing policy for focus capture, pointer capture, outside/escape dismissal, toast ticking, and overlay-local tab traversal.

In inline viewport mode, root overlays are intentionally suppressed. See Inline mode and Overlay widgets.

Testing

TestBackend provides a headless runtime that exercises the same message queue, scoped routing, nested expansion, reconciliation, and command-drain behavior used by real apps. Prefer it for deterministic component tests that should not enter terminal raw mode.

Architectural Invariants

These invariants should remain true unless the architecture is intentionally redesigned:

  • Public API uses crate-owned types, not backend types.
  • Component and UI tree state mutate on the runtime thread only.
  • Async/background work returns through typed messages.
  • Keyed nodes and nested components preserve identity across reorder.
  • Focus, hover, mouse, scrollbar, and overlay routing are centralized over the realized node tree.
  • Overlay focus/pointer capture and dismiss behavior are explicit runtime policy.

This is a fast-moving prototype. Prefer improving the architecture over preserving obsolete internal shapes.

MIT OR Apache-2.0