
LeTUI
TUI library built from scratch with TypeScript and a Rust core, featuring a component-based API, signal-based state management, CSS-like styling API, a Rust-powered layout and rendering engine, and cell-based diffing.


TUI library built from scratch with TypeScript and a Rust core, featuring a component-based API, signal-based state management, CSS-like styling API, a Rust-powered layout and rendering engine, and cell-based diffing.
original
anime4kIn-browser MKV video streamer and player built from scratch, featuring real-time 4K anime upscaling powered by WGSL (WebGPU) shaders ported from Anime4K.

Web app and CLI for exporting X threads, articles, posts, and replies as Markdown or JSON, built around resumable jobs.
Paid music collaborations and song-cover campaigns for artists worldwide. Led frontend and backend development across both products, built the Next.js frontend from scratch, and owned payments, payouts, and mobile audio features. Cut data processing for 1M+ artists from nine days to seven hours and roughly halved Soundcharts costs.
AI mapping platform for satellite and aerial imagery. Built high-performance React interfaces for large imagery datasets, simplified component state and data flow, and helped move users to centralized sign-in with zero downtime.
Social content and analytics platform with shoppable feeds for brands. Led React adoption within an Angular application and improved embedded app performance for e-commerce sites across Europe. Built a custom state manager that cut the JavaScript bundle by 20% and automated package publishing.
Online clothing brand. Fully automated stock updates with a tested pipeline that transformed Airtable inventory data and stored it in the website's database.
Online sports insurance company for individuals, tournaments, etc. Built payment and subscription workflows and internal support tools, redesigned the PostgreSQL schema, and consolidated two backends into one codebase.
Initial implementation with classes, then rewriting with signals-based reactivity - Bun, Rust and functional components
I've been using Neovim for years and love living in my terminal. CLIs and TUIs always interested me, and that interest grew when I started hearing more about Claude Code, Codex, Aider and similar terminal-based AI agents. I did some research and found react-ink, then opencode. Soon after I saw this post:
yeah unfortunately the best tooling for TUIs is in go
we'll build OpenTUI one day for js but till then this hack works
— dax (@thdxr), May 31, 2025
and right then I decided to build my own TUI library someday.
When I started building LeTUI around September, I decided not to follow any tutorial, guide, or existing repo. I wanted to build from scratch, on my own. I used AI for a general roadmap and learning, but wrote every line of code myself.
I reached for the most "simple to reason about" pattern — classes — and just went with the flow. I'd never written a library or worked with terminals before, so instead of making a detailed plan, I just started playing around with code.
The initial implementation had View, Row, Column, Text, Button, and Input classes. Complete mess, but I learned a lot and it gave me enough of a base to build on.
Here's an example snippet. Enjoy!
class Button {
id: number;
px = 4;
py = 1;
text: string;
fg: number;
active_fg: number;
bg: number;
active_bg: number;
border: Border;
prebuilt: BigUint64Array = new BigUint64Array();
width: number;
height: number;
onClick: (() => Promise<void>) | (() => void) | null = null;
// ... constructor and other methods ...
render(xo: number, yo: number, { w, h }: { w: number; h: number }) {
this.xo = xo;
this.yo = yo;
this.prerender(); // layout? painting? who knows!
// top part - manual pixel pushing
for (let cy = yo; cy < this.py + yo; cy++) {
for (let cx = xo; cx < xo + this.size().w; cx++) {
buffer.set(
new BigUint64Array([
BigInt(" ".codePointAt(0)!),
BigInt(this.fg),
BigInt(this.bg),
]),
(terminalWidth * cy + cx) * 3
);
}
}
// bottom part - more manual pixel pushing
for (let cy = yo + this.size().h - this.py; cy < yo + this.size().h; cy++) {
for (let cx = xo; cx < xo + this.size().w; cx++) {
buffer.set(
new BigUint64Array([
BigInt(" ".codePointAt(0)!),
BigInt(this.fg),
BigInt(this.bg),
]),
(terminalWidth * cy + cx) * 3
);
}
}
// middle part - even more manual pixel pushing
for (
let cy = yo + this.size().h - 2 * this.py;
cy < yo + this.size().h - 2 * this.py + this.height;
cy++
) {
for (let cx = xo; cx < xo + this.size().w; cx++) {
if (cx < xo + this.px || cx > xo + this.px + this.width - 1) {
buffer.set(
new BigUint64Array([
BigInt(" ".codePointAt(0)!),
BigInt(this.active_fg),
BigInt(this.active_bg),
]),
(terminalWidth * cy + cx) * 3
);
}
}
}
// actual text
buffer.set(
this.prebuilt.subarray(0),
(terminalWidth * (yo + this.py) + xo + this.px) * 3
);
this.updateHitMap(xo, yo); // hit-testing mixed in here too!
}
updateHitMap(xo: number, yo: number) {
for (let cy = yo; cy < yo + this.size().h; cy++) {
for (let cx = xo; cx < xo + this.size().w; cx++) {
hitMap.set(cy * terminalWidth + cx, this.id);
}
}
}
// ...
}Layout and painting were hopelessly tangled — render() calculates positions and writes to the buffer and updates the hit-map for click detection. Every component duplicates border-drawing logic. Want to change how buttons look when pressed? Good luck finding the right place. Want to add a new component type? Copy-paste 100 lines and pray you got the coordinate math right.
The real problem hit when I needed state management and dynamic updates. User types in an Input, clicks a Button, and the UI needs to update. My class-based approach required manual setText() calls that triggered prerender() and render(). Every component held mutable state, and coordinating updates became a mess of method calls.
This first attempt kinda worked. I could render containers and primitives, nest them, show some colors. More importantly, I started understanding the problem space — what a render loop actually needs, which parts should be separate, how hit-testing works, why layout and painting need to be separate, and how I want to handle dynamic updates.
Time to stop playing around and start from scratch again, but now with a better understanding.
As a small fan of SolidJS, I decided to try out a signals approach and implement the base primitives from scratch. A signal is deceptively simple - a function that tracks its subscribers and schedules updates when values change:
export function $<T>(defaultValue: T): Signal<T> {
let v: T = defaultValue;
let subs = new Set<Sub>();
function $$(next?: T): T | void {
if (arguments.length === 0) {
if (caller) subs.add(caller); // auto-subscribe on read
return v;
}
if (Object.is(v, next)) return;
v = next as T;
for (let s of subs) schedule(s); // notify all subscribers
}
return $$;
}With signals in place, components became pure functions returning Node objects. No classes, no internal state — just data structures describing what to render. The run() function handles the entire lifecycle: layout (offloaded to Rust via taffy), painting, and event handling. When any signal changes, the effect re-runs and the UI updates automatically. Boom!
export function Button(props: ButtonProps & { id: string }): Node {
return {
id: props.id,
type: "button",
props,
frame: getInitialFrame(),
children: [],
};
}
// Usage: reactive text that updates automatically
let buttonText = $("Click me");
Button({ id: "btn", text: buttonText, onClick: () => buttonText("Clicked!") });Designing an actual architecture paid off. Layout and painting are separate concerns — Rust calculates coordinates, TypeScript paints to the buffer. Mouse and keyboard events flow through a spatial lookup table. Derived state with dd() and async effects with af() compose naturally. What started as 700+ lines of class-based spaghetti became ~60 lines of signal primitives driving a declarative component tree.
I started adding layout calculations for Column and Row — nothing fancy, just positioning children correctly. Then flex and fixed components. Then coordinate conversions. Gap calculations. Nested containers...
I stepped back and realized: I was writing a layout engine from scratch without knowing it. Every fix introduced two new edge cases. After a few late nights debugging row alignment, I had a moment of clarity: this is completely out of scope.
So I reached for taffy — a Rust layout library that implements flexbox. One integration later and all my layout headaches disappeared. The lesson? Know when something deserves its own library. I'm building a TUI framework, not a layout engine.
When you stop fighting the data flow and let reactivity handle updates, everything becomes easier. Instead of manually calling setText() and prerender() and render(), you just update a signal. The system figures out what needs to change. The library became usable enough that I re-wrote my Anitrack TUI project using LeTUI.
Time for optimizations and more features.
Relatively simple but interesting: when a container has fixed dimensions, child content shouldn't overflow into surrounding components. Text that's too long needs to be cut off at the boundary.
During painting I had to track each container's bounds and skip pixels that fall outside. For text, I added wrapping support — if a line is too long, break it and continue on the next line (within the container's height). Simple in concept, but the edge cases took some debugging.