# 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