# TUI as an App Platform
The Deft TUI is not just a terminal UI library — it's a small
**application platform** in the Emacs/Blender sense. A single binary
(`retro` for OpenGL, `retra` for pure ANSI over SSH) hosts a tree of
independent apps, each running in its own runtime on its own OS thread,
cooperating through well-defined channels. You can build a single
focused tool, a multi-panel IDE, or a company-wide operations dashboard
on the same primitives.
This page covers the platform model end-to-end: panels, workspace
configs, cross-app communication, deployment patterns, and reference
architectures.
## The Platform Model
```
┌─────────────────────────────────────────────────────────────┐
│ TUI host process (retro / retra) │
│ │
│ Workspace │
│ ├── Panel A ─ app ─ runtime ─ OS thread │
│ ├── Panel B ─ app ─ runtime ─ OS thread │
│ └── Panel C ─ app ─ runtime ─ OS thread │
│ │
│ Shared platform layer │
│ ├── pub/* broker (events, broadcast) │
│ ├── config/* registry (persistent preferences) │
│ └── rpc registry (typed direct calls) │
└─────────────────────────────────────────────────────────────┘
```
The host owns one **workspace** — a panel tree. Each leaf panel is a
rectangular screen region running one Deft app. Apps are isolated by
value (separate VM + heap + scheduler) but share the platform layer
for coordination.
This is documented piece-by-piece across the platform and TUI pages:
- [TUI Overview](tui-overview) — panels, focus, AppThread lifecycle
- [Widgets & Protocols](widgets-and-protocols) — what each app renders
- [Runtimes](../02-Platform/runtimes) — isolation semantics, child
runtimes, memory
- [Pubsub](../02-Platform/pubsub) / [RPC](../02-Platform/rpc) /
[Config](../02-Platform/config) — the three coordination channels
This page ties them together for *deploying multi-panel applications*.
## Workspace Configs
A workspace is defined declaratively as a nested map in
`.deft/workspaces/.dft`. On startup the host loads one of these
files and builds the panel tree from it.
```bash
retro workspace # uses prelude :workspace :default, or START
retro workspace ide # loads .deft/workspaces/ide.dft
retro ws monitoring # `ws` is a shorter alias
```
The file is plain Deft data — a map describing the panel tree.
### Panel tree structure
%{
:theme "Nord"
:vars %{ :editor:filepath "" :editor:cursor-row "0" }
:group :horizontal
:group-title "Main"
:ratios @{ 0.6 0.4 }
:children @{
# Leaf panel — runs one app
%{
:id "editor"
:app "editor"
:source "packages/editor/src/editor.dft"
:focus true
:maximise true
}
# Nested group — split further
%{
:group :vertical
:group-title "Side"
:toggle-visibility "F2" # F2 collapses/expands this group
:children @{
%{ :id "files" :app "files" :source "packages/files/src/files.dft" }
%{ :id "shell" :app "shell" :source "packages/shell/src/shell.dft" }
}
}
}
}
### Group keys
| Key | Purpose |
|---|---|
| `:group` | `:vertical` / `:horizontal` (or `:v` / `:h`) split direction |
| `:group-title` | Optional title bar shown when `:explicit true` |
| `:explicit` | Show title bar; enable "minimise others on maximise" semantics |
| `:ratios` | Vector of fractional widths/heights (0.0–1.0); defaults to even split |
| `:collapsed` | Initial collapsed state |
| `:toggle-visibility` | Key binding (e.g. `"F2"`) that toggles this group's visibility |
| `:children` | Vector of nested groups or leaf panels |
### Leaf keys
| Key | Purpose |
|---|---|
| `:app` | Short app name (used for `tui/find-panel`, RPC addressing) |
| `:source` | Path to the `.dft` entry file (resolved through prelude `:mounts` mounts) |
| `:id` | Panel-specific tag, addressable via `tui/focus-id` |
| `:focus` | `true` to focus this panel on startup |
| `:maximise` | `true` to start maximised (collapses siblings) |
| `:terminal` | `true` to launch a system terminal instead of a Deft app |
### Top-level keys
| Key | Purpose |
|---|---|
| `:theme` | Workspace-level theme name (overrides prelude `:themes :default`) |
| `:vars` | Workspace-state key/value pairs (string values), saved/restored with the workspace |
Workspace configs are diff-friendly plain data — check them into
version control alongside your app code so teammates and ops get an
identical environment.
## Building a Multi-Panel App
A "multi-panel app" is just N regular Deft apps coordinated through
the platform layer. Each panel implements its own `AppLifecycle`,
draws its own widgets, and talks to the others via the channels below.
### Pattern 1 — Workspace-as-app (most common)
Ship a workspace config that lays out your app's panels, plus the
panel apps themselves. Users launch the whole thing with one command.
```bash
retro ws my-dashboard
```
This is how the bundled IDE works: `ide.dft` lays out
editor + files + git + tests + chat + sysmon + log + help + find +
shell + terminal + diff + config + symbols; each panel runs its own
app from `packages//src/.dft`.
### Pattern 2 — Launcher-driven (interactive)
Drop users on a custom launcher (set `:launcher` in the prelude) that
lists the apps/workspaces your project ships. The launcher calls
`tui/launch` to spawn apps into panels on demand:
def open-monitor {|panel-id| @doc "Open the monitoring dashboard in the given panel." tui/split :horizontal $panel-id tui/launch [tui/self] "sysmon" "apps/sysmon/src/sysmon.dft" }
### Pattern 3 — Single-app kiosk
One panel, one app, no launcher. Set the prelude `:workspace :default`
to your app's workspace, or just point `:launcher` at your app file.
The TUI host becomes a single-purpose terminal application.
# prelude.dft %Prelude{ # ... :workspace %{ :default "kiosk" } }
# .deft/workspaces/kiosk.dft %{ :group :vertical :children @{ %{ :app "pos" :source "apps/pos/src/pos.dft" :focus true } } }
## Cross-Panel Communication
Three independent channels. Pick by coupling model:
| Channel | Coupling | Direction | Use |
|---|---|---|---|
| `pub/*` | Loose (string topics) | One-to-many | Events, broadcasts, status updates |
| `@rpc` + `rpc` | Tight (typed fn) | One-to-one | Direct request/response between apps |
| `config/*` | Indirect (shared state) | Any-to-any | Persistent preferences, theme, shared flags |
### `pub/*` — broadcast events
Best when the sender doesn't care who's listening. The classic fan-out
pattern: a metrics app publishes a tick; dashboards, loggers, and
alerts each subscribe independently.
# In the metrics app def emit-tick {|metrics| pub/publish "metrics/tick" $metrics } # In a chart panel def on-tick {|ev| @doc "Update the chart when a new metric arrives." append-point $ev~data } pub/subscribe "metrics/tick" "on-tick"
See [Pubsub](../02-Platform/pubsub) for the full event-map shape and
subscriber semantics.
### `@rpc` — direct typed calls
When one app needs a specific answer from another, expose a function
with `@rpc` and call it via `rpc`. Returns a Future; `await` it inside
an `@async` function.
# In the editor app def cmd-jump-to {|file line col| @rpc true @doc "Open a file at a location. Called by symbols/find apps." open-file $file move-cursor $line $col } # In the symbols app def jump {|sym| @async set loc [symbol-location $sym] [await [rpc "editor" "cmd-jump-to" @{ $loc~file $loc~line $loc~col }]] }
The `"editor"` string is the target app's name (its `:app` field in
the workspace config). See [RPC](../02-Platform/rpc).
### `config/*` — shared persistent state
When multiple panels should react to a preference change (theme,
active project, filter text), use the config registry. Setting a key
publishes a change event on the `"config"` topic.
# Anywhere [config/set "ui/theme" "Dracula"] # Any panel that cares def on-config {|ev| if [starts-with? $ev~data~key "ui/"] { reapply-theme $ev~data~value } } [pub/subscribe "config" "on-config"]
See [Config](../02-Platform/config) for load/save semantics.
## Reference Architectures
### IDE
The bundled `ide.dft` workspace is the canonical example. It composes
14 panels into a full development environment, grouped for ergonomic
collapse via F1 (info row) and F2 (git column):
```
┌──────────────┬───────────────────────────────────┐
│ git column │ chat │ editor │ diff │
│ (F2 toggle) ├──────┴────────┴────────┬──────────┤
│ tests │ find │ shell │ terminal │ files │
│ config │ │ │ │ │
│ symbols ├──────┴───────┴──────────┴──────────┤
│ git │ help │ log │ sysmon │
│ │ (F1 toggle info row) │
└──────────────┴─────────────────────────────────────┘
```
The interesting property: every panel is independent. The editor
exposes `@rpc` commands (`cmd-jump-to`, `cmd-open-file`); find and
symbols call them. Tests publish `"test/result"` events; the editor
subscribes to gutter-mark failures. Git emits `"git/refresh"` after
mutations; everything that cares re-reads state.
This is what makes the platform work: **loose coupling via pubsub,
tight integration via RPC, no shared mutable state**.
### Sysadmin dashboard
A workspace that lays out monitoring panels — process table, log
tail, network stats, service health. The same primitives, different
composition:
# .deft/workspaces/sysadmin.dft %{ :theme "Operator" :group :horizontal :ratios @{ 0.25 0.5 0.25 } :children @{ %{ :group :vertical :group-title "Hosts" :children @{ %{ :id "hosts" :app "hosts" :source "ops/hosts.dft" } %{ :id "alerts" :app "alerts" :source "ops/alerts.dft" :maximise true } } } %{ :id "log" :app "logtail" :source "ops/logtail.dft" } %{ :group :vertical :group-title "Resources" :children @{ %{ :id "cpu" :app "sysmon" :source "ops/sysmon.dft" } %{ :id "net" :app "netstat" :source "ops/netstat.dft" } } } } }
A central data collector app (or external agent) publishes to topics
like `"host/metrics"`, `"alert/raised"`, `"log/line"`. Each panel
subscribes to what it renders. The `hosts` panel double-clicks issue
`@rpc` calls into the `logtail` panel to refocus on the selected
host — no shared mutable state required.
### Financial dashboard
Real-time trading/positions view:
```
┌──────────────────────────────────────────────┐
│ blotters (time & sales, fills) │
├──────────────────────┬───────────────────────┤
│ positions table │ P&L chart │
├──────────────────────┴───────────────────────┤
│ market data (quotes, depth) │
└──────────────────────────────────────────────┘
```
The market-data app subscribes to an external feed (websocket, FIX)
and republishes normalised ticks on `"market/tick"`. Blotters,
positions, and chart panels subscribe and render their slice. Order
entry panels expose `@rpc cmd-place-order` so any panel (or a voice
macro app, or a hotkey handler) can route orders through the same
chokepoint.
`config/*` holds the active account, scheme, and risk limits — every
panel reacts when the operator switches account via the status bar.
### Domain-specific launchers
Replace the default launcher with a project-specific picker that
lists the apps/workspaces your users care about. Useful for embedded
deployments (kiosks, operator consoles, dedicated trader workstations)
where the full launcher is noise:
# prelude.dft %Prelude{ # ... # Repoint the launcher mount at your app directory # (launches myapp/launcher.dft): :mounts %{ "launcher" "myapp" } # ...or set an explicit source path: # :launcher "myapp/picker.dft" }
The launcher file is itself just a Deft app — typically a menu of
`[tui/launch ...]` calls plus a few status indicators. Build it the
same way you'd build any panel app. See
[Prelude: `:launcher`](../02-Platform/05-prelude#launcher) for the
full resolution order.
## Deployment Patterns
### Workspace-as-preset
Treat workspace configs as saved presets. An ops team can ship a
`night-shift.dft`, a `trading-hours.dft`, and a `post-mortem.dft`,
each laying out the panels that role needs. Users flip between them
with `retro ws `.
### Version-controlled environments
Check `.deft/prelude.dft`, `.deft/workspaces/*.dft`, and your app
sources into git. A new clone gets the same environment by running
`deft init` (which scaffolds `.deft/`) followed by `git checkout`.
The prelude's `:mounts` mounts let you pin a specific stdext version
so the binary and project agree.
### SSH-only deployments
For ops / server-side deployments where OpenGL isn't available, run
`retra` — the pure-ANSI host. Same apps, same workspace configs, same
stdlib functions; renders over a bare SSH session. Useful for:
- Jump-box operator consoles
- Embedded devices without a display server
- CI / incident response from a phone
### Per-user overrides
Workspace and prelude configs are project-level. Per-user preferences
(theme override, key bindings, status-bar layout) live in the
`config/*` registry, persisted via `[config/save-overrides]`. Each
user gets the same project layout but their own runtime preferences.
## When NOT to Use the TUI Platform
The TUI platform is right when:
- You want multiple coordinated apps in one terminal window.
- Operators work over SSH or in a terminal multiplexer.
- You're already in the Deft ecosystem.
Reach for a different shape when:
- **Single script.** A one-shot `.dft` script run by the `deft`
binary is simpler than launching a workspace. See
[Shell Scripting](../01-Language/shell-scripting).
- **HTTP service.** Long-running services that other systems call
over HTTP fit `[http/serve]` (single process) or
`[http/serve-cluster]` (multi-worker) — see
[HTTP Server](../04-App-Server/http-server).
- **Pixel-perfect GUI.** The TUI has graphics primitives (see
[Graphics](graphics)), but a native GUI toolchain will win for
rich visualisation.
## See Also
- [TUI Overview](tui-overview) — panels, focus, lifecycle
- [Prelude](../02-Platform/prelude) — project-level config that feeds
the workspace builder
- [Runtimes](../02-Platform/runtimes) — what's actually isolated per
panel
- [Pubsub](../02-Platform/pubsub) / [RPC](../02-Platform/rpc) /
[Config](../02-Platform/config) — coordination channels