Skip to content

frixaco/letui

Repository files navigation

LeTUI

TUI library with a Rust rendering core and TypeScript wrapper API. Written from scratch.

Demos

Snake

letui-snake-demo.mov

Typing Speed

letui-typing-speed-demo.mov

AI agent

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-agent

Press 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.

Prerequisites

  • Runtime: Bun 1.3+ (Node.js not supported)
  • Prebuilt binaries: darwin-arm64, linux-x64, win32-x64
  • Rust toolchain if building locally

Quick start

git clone https://github.com/frixaco/letui.git
cd letui
bun install
bun run build-ffi

More 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:rust

Install as a library

bun add @frixaco/letui

On 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

How it works

  1. Signals-based TypeScript runtime drives updates
  2. Each reactive frame snapshots the current node tree into JS-side sent state
  3. If node shape stays compatible, JS sends only style deltas plus text ops; if shape changes, Rust tree state is rebuilt once
  4. Rust keeps persistent tree state, runs layout + paint, and owns the terminal buffers
  5. Frame data is synced back to JS nodes for frame / frameWidth() / frameHeight(), while Rust also exposes the final visible hitmap for interaction
  6. Terminal output is cell-based and incremental; flush only writes changed cells

Architecture

  • 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, and unicode-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.

Benchmarks

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.

Docs

TODO

  • All essential features except ones below
  • Text styling with StyledText spans
  • 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/left insets, 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 flush with BatchWriter pattern
  • Performance stats overlay

Releasing

See docs/releasing.md.

About

TUI library built with TypeScript (Bun FFI) and Rust from scratch

Topics

Resources

License

Stars

3 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors