Architecture overview
Fleury runs the same widget tree in a real terminal and in a browser. This section is how that works under the hood — start here for the shape of the whole system, then follow the links into each piece.
If you’ve used Flutter, a lot of this will feel familiar: Fleury borrows its retained, reactive pipeline and rebuilds it for a grid of character cells instead of pixels.
One tree, four jobs
Section titled “One tree, four jobs”Fleury is a retained-mode framework. Its pipeline has four related views (the fourth, semantics, is retained only where a surface consumes it):
| Tree | Job |
|---|---|
| widget | Immutable, throwaway configuration. Widgets describe what the UI should be; rebuilding one is cheap. |
| element | The durable spine. Elements hold identity and state across rebuilds — your setState lives here, and it’s what lets Fleury rebuild only the subtree that changed. |
| render | Layout and paint over a grid of cells: constraints flow down, sizes come back up, and each render object paints graphemes-plus-style into a shared buffer. |
| semantics | A machine-readable projection of the UI — roles, state, geometry, and supported actions. Browser and agent surfaces retain and update it; tests and plain-terminal debug collect it on demand. |
A state change runs one incremental pipeline:
setState → rebuild dirty elements → update layout → paint the nextCellBuffer → compare buffers → present the changed cellsElements retain state and skip clean builds; render objects reuse layout when their constraints and layout state are unchanged. A visual frame still paints from the root into a cleared back buffer, using culling and explicit repaint caches where available. Comparing the previous and next buffers identifies the cells the target needs to update. An idle app can skip the visual pipeline entirely. (The retained model is influenced by Flutter — see Influences.)
A platform-neutral core, plus targets
Section titled “A platform-neutral core, plus targets”Everything above is platform-neutral in the way that matters: free of
dart:io (it compiles to JavaScript, guarded by a transitive-import test) and
presenter-agnostic — nothing in it writes bytes to a terminal, a socket, or
the DOM. Terminal vocabulary (capability enums, the ANSI renderer as a pure
CellBuffer → bytes function) does live in the core; actually writing bytes
to a device is the targets’ job. The core’s primary visual output is a CellBuffer — an abstract
grid where each cell is a grapheme plus a style (fg/bg, bold, dim, inverse…).
The same mounted tree also produces semantics for hosts that need accessibility,
agent control, or structured browser sessions. A target takes the visual
buffer and semantic model it needs and presents them somewhere real.
your app (widgets) │ ┌───────────▼────────────┐ │ Fleury core │ platform-neutral, dart:io-free │ widget · element · │ → compiles to JavaScript │ render · semantics │ │ ↓ │ │ CellBuffer │ an abstract grid of styled cells └───────────┬────────────┘ │ host SPI (the seam) ┌───────────┼───────────────────────────┐ ▼ ▼ ▼ Terminal Browser, embedded Browser, served (ANSI) (mountApp) (fleury serve) dart:io the app compiles to JS the app runs on a server; and runs in the page the browser paints streamed framesThree terms recur across these pages, all visible in the diagram: the host
SPI is the seam itself, named for the fleury_host.dart library that defines
it; a target is a pluggable implementation behind that seam (terminal,
embedded browser, served browser); and a presenter is the piece of a target
that turns a finished frame into output — the ANSI writer, the DOM grid.
The two browser targets are not the same thing. Embedded compiles your whole
app to JavaScript with dart2js and runs it client-side — no backend, every live
demo on this site works this way. Served keeps the app running natively on a
server (so it can use the filesystem, processes, anything dart:io) and streams
the changed cells to a thin browser client. Same widget tree, different place the
code lives.
The targets share the framework above the seam; each host supplies its input, clipboard, and presentation services. Parity tests check the browser DOM against the core cell buffer through scroll and overlay sequences, and an equivalence test checks that the terminal’s ANSI output reproduces the buffer. They catch regressions in those paths; terminal capabilities, browser focus, and accessibility still need testing in the environments an app supports.
How this section is organized
Section titled “How this section is organized”This page is the map. The rest of the architecture section splits the system by the question you are trying to answer:
- Architecture deep dive — the retained trees in detail: widget, element, render, cell buffer, semantics, frame damage, presentation planning, tradeoffs, and pressure points.
- Core and targets — the package/import split: the
dart:io-free core, the host SPI, and exactly which code compiles where. - Serving and embedding — the two browser paths:
embedded
mountAppversus servedfleury serve, and when to reach for each. - Built for agents — the semantic app graph, and how tests, accessibility, and agents drive the UI by meaning instead of scraping ANSI.
- Performance — the performance contract and benchmark surface that keep the incremental pipeline honest.
Influences
Section titled “Influences”Fleury is its own framework, but it stands on prior art. The retained four-tree
pipeline (widget → element → render → semantics) is most directly influenced by
Flutter. The cell-grid model and the terminal focus follow the long line of
TUI toolkits — from ncurses to modern ones like Ratatui, Textual, and
Bubble Tea. And the semantic app graph echoes the accessibility trees of the
web (ARIA) and native UI platforms. What’s new is the combination: that
pipeline, on a cell grid, with one app definition spanning a terminal and the
browser.