TUI library with a Rust rendering core and TypeScript wrapper API. Written from scratch.
letui-snake-demo.mov
letui-typing-speed-demo.mov
letui-ai-agent-demo.mov
The AI agent demo is a functional Amp-inspired coding agent built entirely with LeTUI. It uses the Codex app-server for ChatGPT subscription authentication and agent execution, and keeps its own thread index, transcript, archive state, and cross-thread references in local SQLite.
# Requires the Codex CLI on PATH. Sign-in can be completed from the command palette.
bun run ai-agentPress Ctrl+O for commands, Ctrl+S to cycle reasoning effort, Alt+T to expand tool details,
@ to mention files, or @@ to reference a previous local thread. The footer reports the Codex
five-hour subscription window instead of API cost or credit data.
- Runtime: Bun 1.3+ (Node.js not supported)
- Prebuilt binaries:
darwin-arm64,linux-x64,win32-x64 - Rust toolchain if building locally
git clone https://github.com/frixaco/letui.git
cd letui
bun install
bun run build-ffiMore examples:
bun run examples/typing-speed.ts(bun run anitrack is for personal testing and requires mpv player configured with Anime4K shaders)
Checks:
bun run typecheck
bun run check:rustbun add @frixaco/letuiOn supported targets, install pulls the matching native binary automatically.
Minimal reactive app:
import { $, Column, Text, appearance, ff, onKey, run } from "@frixaco/letui";
const THEME = {
fg: 0xf5f7fa,
muted: 0x94a0b2,
surface: 0x16181a,
border: 0x3c4048,
} as const;
const count = $(0);
const counterText = Text({
text: "count: 0",
foreground: THEME.fg,
});
ff(() => {
counterText.setText(`count: ${count()}`);
});
const root = Column(
{
flexGrow: 1,
gap: 1,
padding: "1 1",
background: THEME.surface,
border: { color: THEME.border, style: "rounded" },
},
[
Text({ text: "hello from letui", foreground: THEME.fg }),
counterText,
Text({
text: "+ / - update, q quit, Ctrl+Q default quit",
foreground: THEME.muted,
}),
],
);
ff(() => {
const mode = appearance();
root.setStyle({
background: mode === "light" ? 0xffffff : THEME.surface,
});
});
const app = run(root);
onKey("+", () => count(count() + 1));
onKey("-", () => count(count() - 1));
onKey("q", () => app.quit());bun run app.ts- Signals-based TypeScript runtime drives updates
- Each reactive frame snapshots the current node tree into JS-side sent state
- If node shape stays compatible, JS sends only style deltas plus text ops; if shape changes, Rust tree state is rebuilt once
- Rust keeps persistent tree state, runs layout + paint, and owns the terminal buffers
- Frame data is synced back to JS nodes for
frame/frameWidth()/frameHeight(), while Rust also exposes the final visible hitmap for interaction - Terminal output is cell-based and incremental; flush only writes changed cells
- TypeScript — component API, signals, input routing, sent-tree diffing, op encoding
- Rust — persistent tree state, style/text op application, layout, paint, incremental flush
- Bun FFI — bridge for op buffers, frame buffers, and lifecycle hooks
- Packaged native binaries for
darwin-arm64,linux-x64,win32-x64 - TypeScript runtime deps: none. Rust deps:
crossterm,taffy,unicode-width, andunicode-segmentation.
Text wrapping, clipping, and overflow are resolved in the Rust renderer. Explicit newlines are treated as hard row boundaries after text normalization.
Vertical scrolling is available on ScrollView:
const viewport = ScrollView(
{
flexGrow: 1,
minHeight: 0,
scrollY: 12,
},
[content],
);ScrollView always scrolls vertically. scrollY is a row offset; Rust clamps oversized values, floors fractional values to whole rows, and owns the final hit-testing for the visible scrolled region.
Debug metrics split the frame into js, render, sync, and flush, plus a worst-frame breakdown. Enable with run(root, { debug: true }) to print the summary on quit. If you also want a file, pass run(root, { debug: true, metricsPath: "dump/metrics.txt" }).
Appearance detection requests the current terminal color scheme at startup and listens for live theme updates on terminals that support DEC 2031. Startup detection asks for DEC color-scheme status first and also sends an OSC 11 background-color query as a fallback. appearance() returns "light", "dark", or "unknown".
import { Column, Text, appearance, ff, onKey, run } from "@frixaco/letui";
const label = Text({ text: "theme: unknown" });
const root = Column({ flexGrow: 1, padding: "1 1" }, [label]);
ff(() => {
const mode = appearance();
label.setText(`theme: ${mode}`);
root.setStyle({
background: mode === "light" ? 0xf6f6f6 : 0x101215,
});
label.setStyle({
foreground: mode === "light" ? 0x111111 : 0xf5f7fa,
});
});
const app = run(root, { appearance: "auto" });
onKey("q", () => app.quit());Pass appearance: "light", "dark", or "unknown" to run() to override detection.
Generally speaking it's on par if not faster than OpenTUI, Ink and probably any other popular TUI library at its current state. It loses to Ratatui though, as it is pure Rust.
- All essential features except ones below
- Text styling with
StyledTextspans - Text wrap, overflow, clipping, and explicit newline layout in the renderer
- Persistent Taffy tree
- Vertical scrolling with
ScrollView - Minimal theming support with startup detection and DEC 2031 live updates
- Full grapheme rendering support: store/render whole grapheme strings per lead cell instead of a single codepoint
- Full cursor-based Input editor: mid-buffer insertion/deletion, selection, multiline navigation, caret-following scroll, shortcuts, and placeholder rendering
- Add configurable multiline submission: submit on Return while preserving Ctrl+J or Shift+Enter for newline
- Normalize keyboard and paste events: structured keys/modifiers, bracketed paste, and terminal-independent escape-sequence handling
- Add ScrollView conveniences: optional scrollbar, follow-end mode, scroll anchoring, and post-layout scrolling
- Expose renderer-owned text measurement: visual row count, wrap boundaries, source ranges, and styled-text width
- Complete overlay support:
top/leftinsets, explicit layer ordering, overlay hosts, and modal focus containment - Expand border styling: custom glyph sets, heavy/double styles, embedded labels, styled segments, and wrapped-text gutters
- Safer quit/cleanup when used as a library
- Experiment: Neovim as text input via a Bun-compatible PTY workflow
- Refactor
flushwithBatchWriterpattern - Performance stats overlay
See docs/releasing.md.