packages/tui is the repo's flagship PTY primitive: spawn and control multiple
interactive terminal programs (gdb, vim, a shell, a REPL) from one process as if
a human were typing, and read back a VT-rendered screen instead of a raw byte
stream. It is a Rust library crate (name = "tui",
packages/tui/tui/Cargo.toml:2), a workspace member with no standalone flake output;
it is consumed directly by tui-node and
tui-py, and indirectly anywhere those bindings are
used.
The crate-level mechanism (the actor mailbox, the dedicated VT engine thread, the cursor-key rewrite, the initial-paint wait) is in internals. This page is the public surface and how it builds.
manager(mod.rs,spawn.rs,reader.rs):TuiManagerandTuiInstance, the public entry points.actor(mod.rs,engine.rs): the per-child PTY actor task and the VT engine thread. Private; see internals.types: the plain value types (SpawnConfig,StyledCell,Color,CursorPos,CursorShape,ExitState,FullOutput).slice:slice_2dwithRowRange/ColRangefor sub-rectangle extraction.error: thesnafu-derivedErrorenum andResultalias.frame+publish+dashboard: gated on thepublish/dashboardfeatures (below); the bridge from a live manager to dashboard panes.
TuiManager (src/manager/mod.rs:249):
new()/default(): create a manager owning a fresh multi-threaded tokio runtime (mod.rs:266). Panics only if the runtime cannot be created.spawn(command, args, SpawnConfig) -> Result<TuiInstance>(mod.rs:277): open a PTY, launch the child, register the instance, and wait briefly for the first paint before returning.list() -> Vec<TuiInstance>(mod.rs:290),get(&Uuid) -> Result<TuiInstance>(mod.rs:306),remove(&Uuid) -> Option<TuiInstance>(mod.rs:301). Removal stops tracking; the actor keeps running until every handle drops, so kill first if you want the process gone.
TuiInstance (src/manager/mod.rs:26) is a cheap Clone handle; every clone
addresses the same process and carries its own clone of the runtime, so it keeps
working as long as held. Public fields: id: Uuid, command, args,
spawned_at, scrollback_limit. Every blocking method below has an _async
twin returning a future for callers already inside tokio.
- Shape:
rows(),cols()(mod.rs:51,57),cursor_shape() -> CursorShape(mod.rs:64),read_cursor() -> Result<CursorPos>(mod.rs:69). - Input:
write(&str)(mod.rs:106). Applies the DECCKM cursor-key rewrite (see internals); all other bytes pass through. - Reads:
read_viewport() -> Vec<String>(mod.rs:115),read_scrollback()(mod.rs:121),read_full() -> FullOutput(mod.rs:127),read_blocking(timeout)(polls the viewport until non-empty or timeout, thenNoOutputAvailable,mod.rs:133),read_chars() -> Vec<Vec<char>>(mod.rs:139),read_styled_cells() -> ndarray::Array2<StyledCell>(mod.rs:145). - Lifecycle:
exit_state() -> ExitState(mod.rs:154),is_alive()(mod.rs:160),wait(Option<Duration>) -> Option<ExitState>(mod.rs:169,Noneon timeout),kill()(SIGKILL,mod.rs:190),resize(rows, cols)(resizes the kernel PTY window, which deliversSIGWINCH, and the emulator together; visible from every clone,mod.rs:84).
Value types (src/types.rs):
SpawnConfig { rows, cols, scrollback_lines }, default 24x80 / 10,000 lines (types.rs:122).StyledCell { character, fg, bg, bold, italic, underline, inverse }(types.rs:35); an unwritten cell is a space withColor::Default.Color::{Default, Indexed(u8), Rgb(u8,u8,u8)}(types.rs:10), converted fromix_vt::StyleColor.CursorShape::{Block, Underline, Bar}(types.rs:66), from the engine's DECSCUSR state; blink and ghostty's hollow block collapse toBlock.ExitState::{Running, Exited(Option<i32>)}(types.rs:100);Exited(None)means a signal killed it.CursorPos { row, col, visible }(viewport coordinates, 0-based, cursor scrolled off the viewport reports(0,0)).FullOutput { scrollback, viewport }.
slice_2d(&[String], RowRange, ColRange) -> Result<Vec<String>>
(src/slice/core.rs:53): extract a rectangular sub-region, 1-indexed inclusive,
None endpoints filled from the available extent. An empty input is Ok(empty);
bad bounds are InvalidRowRange/InvalidColRange/RowIndexOutOfBounds/
ColIndexOutOfBounds.
Error is a snafu enum: ProcessSpawn, TuiNotFound (the actor has exited /
the channel is closed), WriteToTui, ReadFromTui, SignalTui, ResizeTui,
NoOutputAvailable, VtEngine, the four slice-bounds variants, ArrayConversion,
and the feature-gated Dashboard / Publish. Under the pyo3 feature the enum
also implements From<Error> for pyo3::PyErr, mapping each variant to a Python
exception class (error.rs:78); tui-py turns that on.
Three optional features (packages/tui/tui/Cargo.toml), all off by default so the
core PTY library stays free of the HTTP/CRDT closure:
pyo3: adds thePyErrconversion only. Used bytui-py.publish: producer side.tui::publish(&manager, path, poll) -> Publisher(src/publish/mod.rs:32) binds a Unix socket and streams the manager's terminals as NDJSON pane snapshots on a poll loop.socket_path()anddiscovery_dir()(re-exported fromdashboard-core) give the per-process path.dashboard: in-process viewer.tui::serve(&manager, addr, poll) -> Dashboard(src/dashboard/mod.rs:52) binds thedashboard-coreserver and drives it from a poll loop over one manager, filing every terminal as a pane under the single"local"scope.
Both features re-export dashboard-core names (Dashboard, Hub, serve_hub,
Pane, ProducerSnapshot, TerminalView, View, discovery_dir,
socket_path, src/lib.rs:50-55). The only engine-bound code here is
frame::collect_panes (src/frame/mod.rs:23), which samples each terminal's
styled cells, encodes the screen as minimal ANSI SGR (frame/sgr.rs), and wraps
it as a TerminalView. Everything downstream (the Loro document, the SSE stream,
the browser page, the multi-process aggregator) lives in the
dashboard domain; see
dashboard-core. The poll and accept
loops run on the manager's own runtime (runtime_handle,
src/manager/mod.rs:321), so a dashboard started from a temporary runtime keeps
running after that runtime drops.
packages/tui/tui/package.nix is just { id = "tui"; inRustWorkspace = true; }: no
flake output, built only as a workspace library. It depends on
ix-vt (Cargo.toml:21), so a build links the libghostty-vt
dylib that the workspace injects via IX_VT_GHOSTTY_LIB_DIR
(lib/rust/workspace.nix:215). Other deps: pty-process (PTY creation), tokio
(runtime), ndarray (the cell grid), parking_lot (registry lock), uuid,
snafu.
Note: the README still says "no runtime resize today"
(packages/tui/tui/README.md:143), but resize/resize_async exist
(src/manager/mod.rs:84,92) and the bindings expose them; trust the source.