SuperLightTUI

April 29, 2026 · View on GitHub

SuperLightTUI

Superfast to write. Superlight to run.

Crate Badge Docs Badge CI Badge MSRV Badge Downloads Badge License Badge

Docs Index · Quick Start · Widget Guide · Patterns Guide · Examples Guide · Backends Guide · Architecture Guide

English · 中文 · Español · 日本語 · 한국어

SuperLightTUI is an immediate-mode TUI library for Rust with a deliberately small public grammar. You write one closure, SLT calls it every frame, and the library handles layout, focus, diffing, and rendering.

It is designed for fast product iteration, approachable Rust syntax, and serious backend discipline. That makes it work equally well for humans prototyping a tool and for coding agents generating UI from docs.

Showcase

Widget Demo
Widget Demo
cargo run --example demo
Dashboard
Dashboard
cargo run --example demo_dashboard
Website
Website Layout
cargo run --example demo_website
Spreadsheet
Spreadsheet
cargo run --example demo_spreadsheet
Games
Games
cargo run --example demo_game
DOOM Fire
DOOM Fire Effect
cargo run --release --example demo_fire
Pretext Reflow
Pretext Reflow — text reflows around the mouse cursor in real time
cargo run --example demo_pretext

Quick Start

cargo add superlighttui
fn main() -> std::io::Result<()> {
    slt::run(|ui: &mut slt::Context| {
        ui.text("hello, world");
    })
}

5 lines. No App trait. No Model/Update/View. No manual event loop. Ctrl+C just works.

MSRV: Rust 1.81. Default features enable the crossterm backend.

60-Second Grammar

There are four ideas most apps start with:

  1. State lives in normal Rust variables or structs.
  2. Layout is mostly row(), col(), and container().
  3. Styling is method chaining.
  4. Interactive widgets usually return Response.
ui.bordered(Border::Rounded).title("Status").p(1).gap(1).col(|ui| {
    ui.text("SLT").bold().fg(Color::Cyan);
    ui.row(|ui| {
        ui.text("mode:");
        ui.text("ready").fg(Color::Green);
        ui.spacer();
        if ui.button("Quit").clicked {
            ui.quit();
        }
    });
});

That is the core mental model. Everything else is depth, not a second framework.

A Real App

use slt::{Border, Color, Context, KeyCode};

fn main() -> std::io::Result<()> {
    let mut count: i32 = 0;

    slt::run(|ui: &mut Context| {
        if ui.key('q') {
            ui.quit();
        }
        if ui.key('k') || ui.key_code(KeyCode::Up) {
            count += 1;
        }
        if ui.key('j') || ui.key_code(KeyCode::Down) {
            count -= 1;
        }

        ui.bordered(Border::Rounded).title("Counter").p(1).gap(1).col(|ui| {
            ui.text("Counter").bold().fg(Color::Cyan);
            ui.row(|ui| {
                ui.text("Count:");
                let color = if count >= 0 { Color::Green } else { Color::Red };
                ui.text(format!("{count}")).bold().fg(color);
            });
            ui.text("k +1 / j -1 / q quit").dim();
        });
    })
}

Runtime Modes

The same closure runs across several entry points. Pick one based on UI shape, not size.

ModeAPIWhen to use
Full-screenslt::run / slt::run_withStandard TUI app — alternate screen, mouse, theme.
Inlineslt::run_inline / slt::run_inline_withFixed-height widget below the prompt — no alternate screen.
Static + inlineslt::run_staticLog lines stream into scrollback while an inline UI stays live below.
Async messagesslt::run_async (feature: async)Background tasks push messages into the closure via tokio::mpsc.
Custom backendslt::frame + Backend + AppStateDrive rendering yourself — tests, GUI embeds, WASM, snapshot harnesses.

RunConfig tunes mouse, kitty keyboard, color depth, max FPS, scroll speed, theme, and title across every mode.

Feature Flags

[dependencies]
superlighttui = { version = "0.20", features = ["async", "image"] }
FeatureWhat it adds
asynctokio + run_async for background message loops
serdeSerialize/Deserialize on selected state types
imagePNG/JPEG decoding for ui.image
qrcodeQR code rendering
kitty-compresszlib compression for the Kitty image protocol
syntaxTree-sitter highlighting for all bundled languages
syntax-<lang>Highlighting for a single language (rust, python, typescript, ...)
fullasync + serde + image + qrcode + kitty-compressdoes not include syntax (grammars are heavy; opt in explicitly)

Why SLT

  • Small public grammar. Most screens start with normal Rust state, row() / col() / container(), method chaining, and Response.
  • Less framework ceremony. Many apps do not need an app trait, retained tree, or message enum just to get moving.
  • Batteries included, backend still serious. Common widgets auto-wire focus, hover, click, and scroll behavior, while the runtime keeps a conservative low-level path through Backend, AppState, and frame().
  • Conservative internals. SLT keeps the public surface small, but the internals stay deliberately boring: shared frame kernel, explicit backend contract coverage, zero unsafe, feature-gated runtime paths, and validation across all-features, no-default-features, WASM, clippy, examples, cargo-hack, semver, and deny checks.

For Rust users, that usually means less setup than retained-mode TUI frameworks. For AI-assisted workflows, it means the public grammar is easy to infer from docs and examples.

SLT fits best when you want to build terminal apps quickly without giving up Rust type safety or backend escape hatches. If you want a retained component tree or a GUI-first toolkit, another library may be a better fit.

How It Renders

SLT's rendering pipeline is why the grammar stays small. Your code only touches the first stage — the engine handles the rest.

graph LR
    subgraph your_code ["Your Code"]
        A["Closure"]
    end
    subgraph engine ["SLT Engine"]
        B[Commands] --> C[Build Tree] --> D[Flexbox] --> E[Collect] --> F[Render] --> G["Diff + Flush"]
    end
    A -->|"records intent"| B
    G -.->|"prev-frame feedback"| A

Every ui.*() call records a command to a flat list — no tree construction, no layout math. The engine replays those commands through a four-stage DFS pipeline — each stage specializes: build the layout tree, compute flexbox, collect interaction and feedback data, render cells to a back buffer — then diffs against the previous frame and flushes only what changed.

This architecture is what makes the simple grammar possible:

  • No ceremony. Immediate-mode means no App trait, no Model/Message/Update/View. Your closure is the entire UI. State is normal Rust variables. Control flow is if/for.
  • Invisible layout. ui.col(|ui| { ... }) records an "open column" command. The engine builds the tree and runs flexbox — you never see LayoutNode.
  • Automatic performance. The double-buffer diffs cells between frames and only emits changed ANSI attributes. You redraw everything; the engine makes it fast. No manual dirty tracking.
  • Auto-wired interaction. ui.button("Save") gives you hover, click, and focus for free. The collect stage fused seven independent sub-walks (hit areas, focus rects, scroll regions, group rects, content rects, focus groups, raw-draw rects) into one DFS — so the top-level pipeline is four passes, not ten.
  • Synchronous feedback. Interaction uses the previous frame's layout positions (imperceptible at 60 FPS). No callbacks, no async layout queries — your code stays linear.

For the full eight-stage lifecycle, see Architecture Guide.

Common API Surface

Stateless calls take values; stateful widgets take an explicit state struct so your data keeps living in normal Rust variables across frames.

// Text and layout
ui.text("Hello").bold().fg(Color::Cyan);
ui.row(|ui| {
    ui.text("left");
    ui.spacer();
    ui.text("right");
});

// Inputs and actions
ui.text_input(&mut input);          // input: TextInputState
if ui.button("Save").clicked {}
ui.checkbox("Dark mode", &mut dark);

// Data and navigation
ui.tabs(&mut tabs);                 // tabs: TabsState
ui.list(&mut list);                 // list: ListState
ui.table(&mut table);               // table: TableState
ui.command_palette(&mut palette);   // palette: CommandPaletteState

// Overlays and rich output
ui.toast(&mut toasts);              // toasts: ToastState
ui.modal(|ui| {
    ui.text("Confirm?").bold();
});
ui.markdown("# Hello **world**");

// Visualization
ui.chart(|c| {
    c.line(&data);
    c.grid(true);
}, 50, 16);
ui.sparkline(&values, 16);
ui.canvas(40, 10, |cv| {
    cv.circle(20, 20, 15);
});

State structs (ListState, TableState, TextInputState, TabsState, CommandPaletteState, ToastState, FilePickerState, RichLogState, ...) are public — keep them in your app struct or local variables and pass them in each frame.

For the categorized widget list, see Widget Guide. For composition advice, see Patterns Guide.

Learn The Library

DocumentWhat it covers
Quick StartInstall, first app, closure mental model, layout, widget state
Widget GuideComplete API catalog of widgets, runtime methods, and state types
Patterns GuideState placement, screen composition, helper extraction, large-app structure
Examples GuideRunnable examples grouped by product shape and feature area
Backends GuideBackend, AppState, frame(), inline mode, static output
Testing GuideTestBackend, EventBuilder, multi-frame tests, backend contract tests
Debugging GuideF12 overlay, clipping, focus surprises, previous-frame behavior
AI GuideFastest path for AI-assisted builders and coding agents
Architecture GuideModule map, frame lifecycle, layout/render pipeline
Features GuideFeature flags, optional dependencies, recommended combos
Animation GuideTween, spring, keyframes, sequence, stagger
Theming GuideTheme struct, presets, ThemeBuilder, custom themes
Design PrinciplesAPI constraints and design philosophy
API DesignFive consistency rules for new widgets and PR review checklist

Representative Examples

ExampleCommandFocus
hellocargo run --example helloSmallest possible app
countercargo run --example counterState + keyboard input
democargo run --example demoBroad widget tour
demo_dashboardcargo run --example demo_dashboardDashboard layout
demo_clicargo run --example demo_cliCLI tool layout
demo_infovizcargo run --example demo_infovizCharts and data viz
demo_gamecargo run --example demo_gameImmediate-mode interaction
demo_design_systemcargo run --example demo_design_systemDesign tokens, theming, style inheritance
inlinecargo run --example inlineInline rendering below a normal prompt
async_democargo run --example async_demo --features asyncBackground messages

The full categorized index — including per-release feature tours and showcase demos — lives in Examples Guide.

Demo Launcher

scripts/ghostty_demos.sh opens demos in fresh Ghostty windows, which is useful for skimming the full set side by side. See Examples Guide for the list of demos at each release.

./scripts/ghostty_demos.sh             # interactive picker
./scripts/ghostty_demos.sh --features  # full feature-tour spread
./scripts/ghostty_demos.sh --showcase  # integration showcases only

Custom Widgets And Backends

  • Implement Widget when you want reusable high-level building blocks.
  • Implement Backend and drive frame() when you want a non-terminal target, external event loop, or embedded runtime.
  • Use TestBackend for headless rendering checks and stable interaction tests.

The public grammar stays small even when you need the escape hatches.

Contributing

Read Contributing, then Design Principles and Architecture Guide. The release process expects format, check, clippy, tests, examples, and backend gates to stay green.

License

MIT