Flutter's model, in the terminal
A retained widget / element / render tree with a real layout engine, hot reload, and a broad widget library. Write it like Flutter; run it in a terminal.
Fleury brings a real UI framework to the terminal — the retained widget model that powers Flutter, rebuilt for a grid of cells — and combines it with two less-common capabilities: a UI that agents and tests can read and drive, and one app that runs in a terminal or a browser. The terminal already has excellent frameworks; this is the particular combination Fleury is built for.
Flutter's model, in the terminal
A retained widget / element / render tree with a real layout engine, hot reload, and a broad widget library. Write it like Flutter; run it in a terminal.
A UI that agents can read
Fleury builds a semantic tree of roles, state, and typed actions for tests, agents, and the browser, so tests assert on meaning and AI agents drive the UI instead of scraping it.
One app, terminal or browser
The same tree runs native, compiles into a page with dart2js, or streams over a socket, with regression tests checking each target’s output against the shared cell buffer.
Fast, and measured
Output diffed down to the cell, data widgets virtualized past 100k rows, and measured with repeatable local, real-terminal, and browser-pipeline scenarios.
Fleury is a retained-mode framework: a widget tree, an element tree that
holds your state, a render tree that lays out and paints, and a semantics
tree beside them — the same four-tree architecture that powers Flutter,
specialized for a grid of cells. You get a real layout engine (constraints down,
sizes up; rows, columns, flex), composition, StatefulWidget / setState, and
hot reload in a running terminal. If you’ve written Flutter it transfers directly;
if you haven’t, it’s a small model to pick up.
That foundation is why the widget set runs deep — charts (Gauge, Sparkline,
LineChart, BarChart, Histogram, Heatmap), virtualized data (DataTable,
TreeTable), document viewers (CodeView, MarkdownView, JsonView,
DiffView), and surfaces shaped for agent and dev tooling (MessageList,
PatchReview, CommandPalette, TraceTimeline). Eighty-plus widgets in the box,
each composable like any other. Tab through a few — real widgets, running live;
click into one and use it (type, arrow, select):
Browse the catalog in the widget reference, or see them composed into real apps in the showcases.
When the app runs in a browser or an agent is connected, Fleury keeps a
semantic tree alongside the visual tree and updates it as the UI changes.
Tests build a snapshot when tester.semantics() asks, and a plain terminal run
builds one only when a debugging tool asks for it. Meaningful content
and interactive nodes carry a role plus the labels, values, state, and typed
actions relevant to them, while layout-only structure may be folded away. The
vocabulary is broad and domain-specific: not just button and slider but
conversation, messageList, patchReview, commandPalette, and traceTimeline,
with concrete actions like activate, submit, select, increment, and copy.
Tests assert on meaning — “row 3 is selected”, “the CPU gauge reads 0.62” — and
survive a re-theme, a layout change, or a port to the browser. An AI agent reads
the tree and drives the UI through those actions instead of scraping ANSI and
guessing keystrokes — run the app under fleury_mcp and point any
Model Context Protocol host at it, with no
agent code in the app. The tree is produced by the core, runs alongside the visual frame
without adding terminal-paint work, and crosses the fleury serve
wire too — so a served preview is as inspectable as a terminal session. See
Built for agents.
The same widget tree runs three ways: natively in a real terminal, compiled into a
web page client-side with dart2js (no backend — every demo on this page is exactly
that), or previewed from a native process with fleury serve.
A dart:io-free core lets one widget definition span all three. Regression
tests check each target’s output, the browser DOM and the terminal’s ANSI
stream, against the core cell buffer for covered fixtures;
platform input, focus, accessibility, and terminal capabilities still need
testing where your app runs.
A setState queues dirty elements, while clean builds and unchanged layout can
be reused. Each visual frame paints a back buffer from the root, using culling
and repaint caches where available; comparing buffers keeps output tied to the
cells that changed. An idle app can skip the visual pipeline entirely.
Data widgets virtualize visible rows so navigation need not build the entire dataset. Filtering and indexing still depend on how the app supplies its data. The benchmark suite measures these workloads, local pipeline work, real-terminal bytes and process cost, and the browser frame path. Compare results with the fixture, machine, and run conditions attached. The chart below streams a new point several times a second and presents only changed cells:
The honest first reaction to “a TUI framework in Dart” is why Dart? — most terminal tooling is Rust, Go, or Python. The answer is that Dart is what makes Fleury’s core promise possible, not just convenient.
It compiles to both a native binary and the browser. Dart AOT-compiles to a
fast, self-contained terminal executable — ship it like a Go binary, no runtime
on the box — and compiles to JavaScript with dart2js. That dual target is the
whole reason one widget tree can run in a terminal and a browser with no second
implementation. In most languages, “the same app, native and on the web” means
two codebases; in Dart it’s one source and two dart compile invocations.
The retained UI model is Dart’s home turf. The widget / element / render /
setState architecture that makes Fleury a real framework — incremental rebuilds,
a layout engine, composition — was proven at scale in Dart by Flutter, and the
language’s ergonomics (const constructors, named parameters, mixins, sound null
safety) co-evolved with exactly this kind of declarative tree. Fleury isn’t
bending Dart to fit; it’s using it for what it was shaped to do.
Hot reload, in a terminal. Save a file and your running TUI updates in
place — widget state, focus, and scroll intact — under fleury run, in any
editor. This is the Dart VM’s stateful code-swapping, the loop Flutter
developers lean on, built into runApp. nocterm,
also written in Dart, reloads the same way. Outside Dart, the stacks here
mostly iterate by restarting: Textual live-applies CSS edits but restarts for
code changes, and the Go, Rust, and JavaScript stacks rerun the app when a file
changes — either way, your app’s state and your place in it are lost on every
edit. The exception is experimental: Dioxus’s Subsecond tool can hot-patch a
running Ratatui app.
None of this makes Dart the leanest option at the floor — Rust boots quicker and idles smaller, and the benchmarks say so plainly — or the most common in the space. It makes it the right fit for this job: a real, retained UI framework that spans the terminal and the browser from one codebase.
A factual snapshot of where Fleury sits among today’s TUI frameworks — what you can build with each, how each is put together, and where the others lead. Every peer cell is checked against that project’s own docs and releases (September 2026); corrections welcome.
Capabilities — what you can build with it. Hover the marked cells for detail.
| Capability | Fleury | nocterm | OpenTUI | Textual | Bubble Tea | Ratatui | Ink |
|---|---|---|---|---|---|---|---|
| AI-agent drivable (MCP) | ✅ | — | — | ◐ | — | — | — |
| Screen-reader accessible | ◐ | — | — | — | — | — | ◐ |
| Stateful hot reload | ✅ | ✅ | — | ◐ | — | ◐ | — |
| Runs in the browser | ✅ | ◐ | ◐ | ✅ | — | ◐ | ◐ |
| Terminal images (Kitty·iTerm2·Sixel) | ✅ | ✅ | ✅ | ◐ | — | ◐ | ◐ |
| Component library | ✅ | ✅ | ✅ | ✅ | ◐ | ◐ | ◐ |
| Single-binary distribution | ✅ | ✅ | ◐ | ◐ | ✅ | ✅ | ◐ |
| Advanced graphics / 3D | — | — | ✅ | — | — | — | — |
Under the hood — how each framework is built.
| Fleury | nocterm | OpenTUI | Textual | Bubble Tea | Ratatui | Ink | |
|---|---|---|---|---|---|---|---|
| Language | Dart | Dart | TS / Zig | Python | Go | Rust | JS |
| Model | Retained framework | Retained framework | Retained (React/Solid) | Retained (CSS DOM) | Elm (MVU) | Immediate-mode (library) | Retained (React) |
| Familiar if you know… | Flutter | Flutter | React / Solid | the web (CSS) | Elm | immediate-mode GUIs | React |
| State / reactivity | setState + Scope + Notifier | setState + Riverpod | hooks / signals | reactive attributes | immutable Model | you own it | React hooks |
| Styling | props + theme | props | props | CSS (TCSS) | Lip Gloss | Style structs | props |
| Rendering | retained tree → cell diff | cell diff | Zig core + shadow-buffer diff | compositor + spatial map | Cursed Renderer (cell diff) | double-buffer diff | reconciler diff |
Two capabilities carry Fleury’s edge — agents and accessibility both fall out of a single semantic tree; no other framework here has first-party support for both. Hot reload, browser, and images it leads or ties; components put it in the batteries-included camp, with 80+ built-in widgets. Where it doesn’t win: OpenTUI’s terminal 3D, Rust and Go idle leaner, screen readers reach it only in the browser, and — most of all — maturity.
Fleury is one good option among several. Reach for a peer when its strengths line up with yours:
The flip side of that bet: Dart’s terminal ecosystem is young — fewer libraries and fewer examples to crib from than Go, Rust, or Python have. Fleury is also pre-1.0: the API is still settling, and it’s distributed as a Git dependency today. If you need a long-stable, widely-deployed framework right now, one of the above may fit better. If the retained model, the batteries-included widgets, the agent- and accessibility-facing semantics, and the web target are what you’re after, that’s exactly the gap Fleury fills.