demo.mp4
TUI library written using Rust and TypeScript
- Added socket-based test driver for automated checks (
ping,sleep,key,mouse,focused,snapshot,quit) - Stopped sending full text payload on every
paint()call - Added Rust-side text registry keyed by node id + per-frame text diff sync
- Batched text upsert/delete operations into single
sync_text_opsFFI call per frame - Improved terminal flush batching to track terminal cells (not UTF-8 byte length) for cleaner multibyte rendering
- Added stronger quit cleanup path (stop test driver, clear text registry, free buffers, deinit terminal)
Core dependencies:
TODO:
- Keep text on Rust side in
TEXT_REGISTRY(keyed by node id) - Detect text diffs in TS runtime (
upsert+delete) - Encode text diffs as compact ops and send once via
sync_text_ops - Apply all text ops in one Rust pass under one registry lock
- Result: text traffic scales with changed nodes, not total text nodes
- Add
overflow: "hidden"style prop → triggers clipping during paint - Add
scrollX/scrollYsignals per scrollable node - Pass scissor rect to Rust paint — skip cells outside bounds
- Horizontal scrolling first, then vertical
-
TextChunktype:{ text: string; fg?: number; bg?: number; bold?: boolean } - Update
Textcomponent to acceptTextChunk[]or plain string - Serialize chunks to Rust for rendering
- Single-line input improvements (cursor position, selection)
- Multi-line text editor (builds on scrollable + input)
- Tree-sitter integration (Rust bindings → FFI)
- TextMate-compatible theme loading (like OpenTUI's
SyntaxStyle)
-
Move paint to Rust (currently 81% of frame time @ 1.7ms avg)
- Why: remove JS per-cell paint work from hot path
- Current flow:
- JS serializes node tree into
nodeData - Rust does layout + paint into
CURRENT_BUFFER - JS only syncs frames/event hit areas, then calls
flush() - Text sync now happens separately via
sync_text_ops
- JS serializes node tree into
-
Add scrollable containers
-
How to handle serialization of walls of text: https://ampcode.com/threads/T-019bdac3-ba03-745f-a3d3-c9d53bfa0648
-
Add render caching - skip serialize/layout if signals unchanged (pi-mono pattern)
-
Incremental tree updates - don't rebuild entire Taffy tree each frame, cache structure and update only changed nodes
-
Visibility culling - skip
paint()for off-screen nodes (OpenTUI's_getVisibleChildrenpattern) -
Neovim as text input (use Bun PTY support)
-
Will SIMD work if I wanna implement caching for serialization. For example, when comparing trees I used SIMD (idk what i'm talking about)
-
Refactor flush function with BatchWriter pattern to reduce nesting
- BatchWriter struct holds stdout ref, char_seq, batch_start_x/y, prev_fg/bg
new()initializes with sentinel colors (u64::MAX) to force first color emitpush(x, y, ch, fg, bg)handles gap detection, color changes, and accumulates charsflush_pending()emits MoveTo + Print for accumulated batch- Encapsulates all batching logic, main loop just calls push() for changed cells
-
Add performance stats overlay that update independently from rest of the app (can i use a separate thread?)
-
Add
flexGrowsupport for dynamic width components (e.g., progress bars)- TypeScript side:
- Add
flexGrow?: numbertoStylePropsinsrc/types.ts - Update
createStyleSignals()insrc/components.tsto includeflexGrow: $(input.flexGrow) - Update serialization in
src/runtime.tsto passflexGrowvalue to Rust (add toFIELDS_PER_NODE)
- Add
- Rust side:
- Increment
FIELDS_PER_NODEfrom 12 to 13 inlib.rs - Add
flex_grow: f32field toNodestruct - Parse
flex_growinparse_node()function - Apply
style.flex_grow = node.flex_growinget_styles()for all node types
- Increment
- Progress bar update:
- Remove fixed
widthprop fromProgressBar - Instead of
" ".repeat(n), use a single space" "for text - Set
flexGrow: progress / 100on filled node,flexGrow: (100 - progress) / 100on unfilled node - Layout engine distributes space proportionally - bar auto-sizes to container
- Remove fixed
- Benefits: No fixed width needed, bar fills available space, cleaner API
- TypeScript side:
- push your changes
- update versions in
Cargo.tomlandpackage.json git tag v0.0.1- tag a commitgit push origin v0.0.1- push the tag- release action will build and deploy it as package
Latest run: 2026-02-20 (terminal-rerender, full profile, PTY mode)
Run config:
warmup=100iterations=1000replicates=7(first replicate discarded)- command:
bun run bench/run.ts --profile full --scenario terminal-rerender
| Metric | letui | Rezi | Delta |
|---|---|---|---|
| Mean latency | 20 us | 259 us | letui 12.69x faster |
| p95 latency | 21 us | 260 us | letui lower |
| Throughput | 48.6K ops/s | 3.9K ops/s | letui 12.46x higher |
| Peak RSS | 60.4 MB | 128.2 MB | letui 2.12x lower |
| PTY bytes | 43.2 KB | 30.1 KB | letui 1.43x higher |