This repository is a read-only mirror. The canonical source for
eddacraft-tuilives inside the Anvil project repository. Direct commits and source PRs againstmainhere are not accepted —mainis force-pushed by the mirror automation whenever the canonical tree changes, and any local commits will be overwritten on the next sync.
Depend on the crates.io release, not on git main:
[dependencies]
eddacraft-tui = "0.2"main is mirror-managed and may be force-pushed at any time. Release tags
(eddacraft-tui-v*) are append-only and never rewritten by the mirror job — pin
to a tag if you need a frozen reference.
Issues are open on this mirror — file them here and a maintainer will triage and
forward into the canonical project as needed. Source PRs opened against this
mirror are auto-closed by the
pr-redirect.yml workflow with a link to
the contribution guide; auto-close protects your work, because main here is
force-pushed by automation on every canonical change and any local commits would
be silently overwritten on the next sync. If a maintainer accepts a change you
proposed, they will port it into the canonical tree and the next mirror sync
will carry it out — credit goes via a Co-Authored-By: trailer on the ported
commit.
Anvil is the load-bearing consumer of eddacraft-tui. Hosting the canonical
source in the Anvil monorepo lets widget and consumer changes ship atomically
without a publish-then-bump round trip; mirroring out and publishing to
crates.io preserves the public trust surface for external users. See
CONTRIBUTING.md "External contributors" section for the
contribution path that applies from this mirror.
A curated, themed, animation-ready Ratatui component library. Drop-in compatible with
ratatui0.30. It gives you a polished widget set, thepretextreflow-free streaming text engine, a JSON-driven UI renderer, smooth animations, composable overlays, and responsive layout primitives — with theming and shell branding fully optional. The default theme follows the eddacraft Terminal Standard, but nothing forces you to use it.
ratatui |
eddacraft-tui |
|
|---|---|---|
| Core widget set | ✓ | ✓ |
| Curated themed widgets (status, data, …) | — | ✓ |
pretext two-phase text layout |
— | ✓ |
json-render JSON → terminal renderer |
— | ✓ (json-render) |
| Themed spinners, progress & status | — | ✓ |
| Image pane (Kitty / Sixel / iTerm2) | — | ✓ (image) |
| Big-text branded splashes | — | ✓ (big-text) |
Theming and shell branding are optional. ShellBranding::Plain (the default)
gives you a clean header/footer with no logo mark. Implement the Theme trait
for a completely custom palette and semantic roles. The brand-specific spinner
presets (.anvil(), .eddacraft()) are just ergonomic helpers.
eddacraft-tui provides a generic engine (developed by the eddacraft team
as part of the Anvil/TUIDASH work) for the @json-render/core flat element
spec format. The spec format and overall approach were inspired by Vercel’s
json-render product. A spec describes a tree of components (Stack, Grid,
Card, Heading, Text, Table, Alert, charts, etc.) using a flat
id-referenced structure. The crate parses it, validates structural integrity
against a Catalog (unknown types, dangling/cyclic children, missing root),
and renders it via a pluggable TuiRegistry.
serde/serde_json are behind the json-render feature so the core
library stays free of them. A separate producer (build step, server,
LLM-emitted artefact, …) can ship one UI description that the TUI renders
with no additional glue code.
While the initial consumer and example specs are dashboards and monitoring
surfaces, the engine is not dashboard-specific. The base components are
general layout and content primitives, the catalogue is extensible, and
features like data binding ($data references), sanitisation for untrusted
specs, and responsive breakpoints (Narrow/Medium/Wide) make it
suitable for many declarative UI use cases (detail views, status pages,
settings surfaces, LLM-generated interfaces, etc.).
[dependencies]
eddacraft-tui = { version = "0.2", features = ["json-render"] }use eddacraft_tui::json_render::{
base_registry, parse, render_spec, validate, Catalog, RenderSpec, TuiRegistry,
};
let spec_json = r#"{"root":"page","elements":[...]}"#;
let spec: RenderSpec = parse(spec_json).expect("valid JSON");
validate(&spec, &Catalog::base()).expect("spec is valid");
// In your render loop, draw the spec to a ratatui Frame:
let registry: TuiRegistry = base_registry();
render_spec(&spec, ®istry, frame, area);Components consult shared width breakpoints (Narrow / Medium / Wide)
so a single spec degrades gracefully: grids collapse to one column, tables
progressively drop columns, etc. Thresholds are tuned for real terminal sizes
(80×24, 120×40, 200×60, …).
See the json_render API docs for the base element set and the
TuiComponent / TuiRegistry extension points.
Two-phase text layout inspired by Cheng Lou's Pretext for the browser.
pretext measures word widths once with unicode-width, caches the
layout per container width, and re-runs only on resize. Streaming
tokens land in a frame with no reflow stutter; container resizes
invalidate the cache and re-measure in a single pass. Exclusion zones
let text flow around moving shapes — a sidebar that animates, a live
chart. The widget itself is zero-sized: all caching lives on
PretextState, so caching works correctly across moves and
reparenting. See Pretext layout below for a worked
example.
The widgets/ module ships a curated set of production-ready components
with a default theme that follows the eddacraft Terminal Standard (easily
replaced by implementing Theme). Highlights: TextInput, Editor, Select,
Confirm (inputs); Spinner, ProgressBar, ParallelProgress,
StatusBadge, StatusBar (status); Container, Divider, Header
(layout); DataTable (sortable, themed ▲/▼ indicators), Tree
(expand/collapse via TreeState), LogPanel (data); OverlayStack +
Layer + Placement (overlays for modals, toasts, command palettes);
HelpBar (auto-generated key hints that stay in sync with your KeyHandler
bindings); PretextWidget (text reflow); and Hideable, Disableable,
Padded wrappers. See Widgets below for the full reference.
Other standouts include smooth animated progress (call animate_tick each
frame; the library handles easing), ParallelProgress with per-task status,
ETA calculation, and live spinners, plus careful engineering details like
iterative tree teardown to avoid stack overflows on deep hierarchies.
EddaCraftTheme provides the default 8-colour palette and semantic Role
tokens. ProgressBar and ParallelProgress animate toward their targets
(250 ms quad-out by default). Your event loop just calls animate_tick(delta_ms)
each frame (and can check is_animating() to decide whether to short-poll
input for smooth playback). The underlying animation engine is
vyfor/animate. See
docs/animations.md for the wiring pattern.
Brand marks are provided via ShellBranding (Plain, Edda, Anvil, Custom, …)
and the convenience spinner presets, but are never required.
[dependencies]
eddacraft-tui = "0.2"use eddacraft_tui::prelude::*;
let theme = EddaCraftTheme;
// Plain (unbranded) shell chrome is the default and recommended for most apps.
let inner = render_shell(
frame, area,
ShellBranding::Plain, // or Custom("[*]"), Edda, Anvil, ...
"myapp", "Main", "q quit j/k move",
&theme, env!("CARGO_PKG_VERSION"),
);
// Most widgets only need a &impl Theme. Brand presets are optional helpers:
let spinner = Spinner::new(&theme).label("Loading...");
// ... or Spinner::new(&theme).anvil().label("Forging...");ParallelProgress can show a live animated spinner for the "Running" state
(brand mark is configurable). render_shell supports a few reusable marks
plus ShellBranding::Plain and ShellBranding::Custom("…").
theme/—Themetrait +Roletokens for semantic styling, plus the built-inEddaCraftTheme(the default palette)keyboard/— key binding definitions, action mapping, and introspectableBindingtablewidgets/— reusable TUI widgets (see Widgets below)pretext/— two-phase prepare/layout text engine for streaming AI output and dynamic reflowsurface.rs— baseSurfacetrait for TUI screensshell.rs— shared shell chrome renderer
The built-in EddaCraftTheme follows the eddacraft Terminal Standard
palette. All colours are exposed through the Theme trait, so you can
implement your own without touching widget code.
| Token | Colour |
|---|---|
| Void | rgb(13, 13, 15) |
| Structure | rgb(42, 42, 46) |
| Off-White | rgb(235, 235, 235) |
| Ghost Grey | rgb(133, 133, 138) |
| anvil Ember | rgb(204, 85, 0) |
| edda Growth | rgb(46, 139, 87) |
| Brick Red | rgb(201, 74, 74) |
| Dull Amber | rgb(208, 140, 56) |
The Role enum (Primary, Accent, Success, BorderEmphasis, …) gives
widgets a semantic handle; custom themes can override role_style centrally.
The widgets/ module ships a curated component set. Highlights:
- Inputs —
TextInput,Editor,Select,Confirm - Status —
Spinner,ProgressBar,ParallelProgress,StatusBadge,StatusBar - Layout —
Container,Divider,Header - Data —
DataTable(sortable, themed▲/▼indicators),Tree(expand/collapse viaTreeState),LogPanel - Overlays —
OverlayStack+Layer+Placementfor layered modals;ModalandToast/ToastStackas ready-made consumers - Chrome —
HelpBarauto-renders key hints fromKeyHandler::default_bindings(), so help text stays in sync with the keymap - Text reflow —
PretextWidget+PretextStatefor cached two-phase layout (see Pretext layout below) - Wrappers —
Hideable,Disableable,Paddeddecorate anyWidget/StatefulWidgetwithout bloating each widget's API
| Feature | Adds |
|---|---|
image |
ImagePane — themed wrapper around ratatui-image (Kitty / Sixel / iTerm2 / halfblocks) |
big-text |
BigBanner — themed wrapper around tui-big-text for branded splashes |
lifecycle |
TerminalGuard raw-mode / alternate-screen RAII guard + panic restore |
runner |
Small fallback CLI shell — global flags + first-level commands (enables lifecycle) |
json-render |
Parser + registry for the @json-render/core declarative spec format |
test-utils |
Snapshot testing helpers re-exported for downstream crates |
[dependencies]
eddacraft-tui = { version = "0.2", features = ["image", "big-text"] }Building a terminal tool that doesn't have a CLI yet? The runner feature ships
a small fallback shell so you don't hand-roll the same envelope every time. It
parses shared global flags (--help, --version, --theme, --no-tui,
--config) and selects a first-level command, then hands the rest to you — in
~3 lines, with no clap-weight dependency (it uses zero-dep lexopt
internally).
[dependencies]
eddacraft-tui = { version = "0.3", features = ["runner"] }fn main() -> std::process::ExitCode {
eddacraft_tui::runner::launch_cli(MyTool::new())
}It is a fallback, not a CLI framework. It intentionally does not do nested
command trees, argument validation, generated per-command --help,
environment-variable binding, or shell completions. A serious CLI wants those —
and that is the expected outcome, not a missing feature.
When you reach that point, bring your own parser and hand the runner
pre-parsed options via launch_with. You keep full control of your CLI surface
and still get the runner's command dispatch, config handoff, and lifecycle/theme
integration. The runner never owns or re-exports your parser, so its API stays
independent of your clap major version.
[dependencies]
clap = { version = "4", features = ["derive"] }
eddacraft-tui = { version = "0.3", features = ["runner"] }use clap::Parser;
use eddacraft_tui::runner::{launch_with, RunnerMode, RunnerOptions};
#[derive(Parser)]
#[command(name = "mytool", version)]
struct Cli {
command: String,
#[arg(long)]
theme: Option<String>,
#[arg(long = "no-tui")]
no_tui: bool,
#[arg(long)]
config: Option<std::path::PathBuf>,
}
fn main() -> std::process::ExitCode {
let cli = Cli::parse(); // clap owns help, validation, completions, …
let options = RunnerOptions {
command: Some(cli.command),
config_path: cli.config,
theme: cli.theme,
mode: if cli.no_tui { RunnerMode::Plain } else { RunnerMode::Tui },
..RunnerOptions::default()
};
launch_with(MyApp::new(), options)
}See the runner module docs
for the full contract.
pretext is a two-phase text layout engine inspired by Cheng Lou's
Pretext library for the browser. Measure
word widths once with unicode-width, cache the layout per container width, and
re-run only on resize — eliminating reflow stutter for streaming AI output and
animated layouts.
use eddacraft_tui::prelude::*;
use ratatui::widgets::StatefulWidget;
let theme = EddaCraftTheme;
let mut state = PretextState::new("streaming tokens flow here…");
let widget = PretextWidget::themed(&theme);
// Each frame:
// widget.render(area, frame.buffer_mut(), &mut state);
// On new tokens:
state.append(" more text from the model");
// Flow text around moving shapes:
state.set_exclusions(vec![ExclusionZone::circle(40, 8, 5)]);The widget itself is zero-sized — all caching lives on PretextState. At
unchanged container width subsequent renders skip layout entirely; the cache is
invalidated by a width change, by any text mutation (set_text,
set_styled_text, append, append_styled), by set_exclusions, or by an
explicit invalidate_layout() call.
Extended guides live in docs/. Contributor docs remain at the repo
root (CONTRIBUTING.md, RELEASE.md,
SECURITY.md).
- Public crate: https://crates.io/crates/eddacraft-tui
- Canonical source (Anvil monorepo): https://github.com/eddacraft/anvil
- pretext-tui origin: https://github.com/joshuaboys/pretext-tui
Smooth progress/spinner animations are powered by
vyfor/animate.
The pretext two-phase layout engine and widget were originally prototyped in
joshuaboys/pretext-tui, a
terminal port of ideas from Cheng Lou's browser Pretext library.
The layered overlay model (OverlayStack / Layer / Placement) draws
inspiration from Cursive's StackView.
The json-render engine and supporting types were developed by the eddacraft
team (TUIDASH work in the Anvil monorepo) for portable declarative UI specs.
Apache-2.0