npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@frixaco/letui

v0.4.0

Published

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

Downloads

115

Readme

LeTUI

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

Demos

Snake

https://github.com/user-attachments/assets/31f3efcc-0c2b-4b85-8fe1-99129e6b8394

Typing Speed

https://github.com/user-attachments/assets/7f4f53f5-deb6-4781-bdfa-bf769cabfbb1

AI agent

https://github.com/user-attachments/assets/1599de82-146d-4a0d-bc66-86a2b51a77c1

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.

Performance

The project target is <1ms average response time for each render. Use run(root, { debug: true }) while developing to inspect frame timings, and pass metricsPath when you want to persist the summary.

Docs

TODO

  • [x] All essential features except ones below
  • [x] Text styling with StyledText spans
  • [x] Text wrap, overflow, clipping, and explicit newline layout in the renderer
  • [x] Persistent Taffy tree
  • [x] Vertical scrolling with ScrollView
  • [x] 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
  • [ ] Better Input experience: multiline editing, shortcuts, cursor movement, scrolling, placeholder rendering, etc.
  • [ ] 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.