Architecture
TermFlow is split into four published modules, layered so each one is publicly usable without forcing you to climb the whole stack:
| Module | What it gives you | Key types |
|---|---|---|
termflow-terminal | Raw access to the underlying TTY: backend, key decoding, capability detection. | TerminalBackend, KeyDecoder, Capabilities, Grapheme, WCWidth |
termflow-screen | A character grid you draw into, plus diff-rendering to ANSI. | RenderFrame, AnsiRenderer, Layout, HitTest |
termflow-app | The Elm-style runtime: TuiApp, Cmd, Sub, FocusManager, Dialogs. | TuiApp, TuiRuntime, Cmd, Sub, Tui |
termflow-widgets | Reusable components built on top of the app layer. | Button, TextField, ListView, Table, Tree, Tabs, Form, ... |
Plus an umbrella termflow artefact that depends on all four, and a
termflow-testkit artefact for testing apps without a real terminal.
The runtime loop
TuiRuntime.run(app) drives the loop:
┌─────────────────────┐
│ TuiApp[Model, Msg] │
│ init / update / │
│ view / toMsg │
└─────────────────────┘
▲ │
Msg │ │ Cmd / Tui
│ ▼
┌─────────────────────┐
Sub.* ────▶│ CmdBus │
(Sub.Every, │ (blocking queue) │
Sub.InputKey, └─────────────────────┘
Sub.Resize) │
▼
┌─────────────────────┐
│ AnsiRenderer │
│ (frame diffing) │
└─────────────────────┘
│
▼
ANSI
initproduces an initialTui[Model, Msg]— the starting model plus an optional startupCmd(typically registering subscriptions).- Subscriptions push
Cmds onto theCmdBus. Examples: every key press becomes aCmd.GCmd(msg), every timer tick the same. - The runtime drains the bus, dispatches each
Msgtoupdate, and gets back a newTui[Model, Msg]— the next model and an optional follow-up command. viewrenders the new model into aRootNodevirtual DOM.AnsiRendererdiffs the newRootNodeagainst the previous frame and emits the minimum ANSI escape sequence to bring the terminal up to date.
Loop, repeat.
What runs where
- Update is synchronous and pure. No I/O. If you need async work,
return a
Cmd.FCmd[A, Msg](or the friendlierCmd.asyncResult) and the runtime will await theFuture, then deliver the result back as aMsg. - View is pure. It returns a
RootNodefrom aModel. The renderer takes care of cursor placement, colour emission, and clipping. - Subscriptions live outside
update.Sub.InputKeyreads from JLine on a background thread;Sub.Everyschedules ticks. Both pushCmds onto the bus soupdateonly ever seesMsgs.
Why three layers?
The split lets you opt out of higher layers when they get in the way:
- Building a one-shot CLI utility that just needs raw key reads and
capability detection? Depend on
termflow-terminalonly. - Building a custom layout engine on top of a screen buffer? Depend on
termflow-screen. - Building a typical interactive TUI? Depend on the umbrella
termflowartefact, which pulls in all four.
The boundaries also mean each module can carry its own MiMa baseline once
1.0 ships, so a breaking change in widgets doesn't force a major bump
on the whole stack.
For deeper architectural rationale, see the contributor design doc and the render pipeline doc.
Next up: Install.