Architecture¶
Data flow¶
flowchart LR
subgraph TUI["virtufin-tui (your laptop)"]
DA["Dashboard: 3 top tabs, config-driven sub-tabs"]
subgraph ST["Shared DataPump"]
W["WorkersStore"]
C["ConnsStore"]
TR["TriggersStore"]
E["EventsStore"]
end
DA --> ST
subgraph PAGES["Per-pane widgets (mounted one at a time)"]
HP["HomePage"]
IP["InstrumentsPage / OrderBookPage / PortfolioPage / PnLPage / RiskPage"]
WP["WorkersPage / ConnectionsPage / ScriptsPage / TriggersPage / EventsPage"]
CP["ConfigPage"]
XP["Custom panes (~/.config/virtufin/tui/panes/)"]
end
WP --> W
WP --> C
WP --> TR
WP --> E
end
subgraph BG["Background tasks (DataPump)"]
P["Poller (2s interval): ListWorkers, ListConnections, ListTriggers"]
S["Subscriber: Pubsub.Subscribe"]
end
P --> W
P --> C
P --> TR
S --> E
API["virtufin-api (cluster)<br/>Gateway.* / State.* / Pubsub.* RPCs<br/>(talks to Dapr pubsub + state)"]
TUI -- "gRPC" --> BG
BG -- "gRPC" --> API
Components¶
1. App (src/virtufin_tui/app.py)¶
The root App subclass. Holds a single TuiConfig (from contexts.toml),
connects to the active context, and pushes the Dashboard screen. Also
configures file-based logging (~/.local/state/virtufin/tui/tui.log) before
anything else runs, since Textual owns the terminal in alt-screen mode --
anything written to stderr is invisible while the TUI is running.
2. Dashboard (src/virtufin_tui/screens/dashboard.py)¶
A Screen that owns the DataPump and renders a two-level tab bar:
3 top-level tabs (Home, Trading, Infra by default) and, for any tab
with more than one pane, a sub-tab bar underneath. Only one pane widget is
mounted at a time -- switching tabs unmounts the current one and mounts
the next.
The tab/sub-tab structure is not hardcoded: it's read from
pane_order in config.toml at runtime (see
Custom panes). right/left cycle top-level tabs;
1-9 pick a sub-tab by position within the active tab.
Dashboard also routes a small set of generic keys (r, a, d, enter,
space, /, c, s, n, w, t) to whichever action_* method the
currently-mounted pane happens to implement (see
Keybindings for the full per-pane mapping) --
check_action hides a key from the footer entirely if the current pane
doesn't implement the corresponding method.
3. Data store (src/virtufin_tui/store.py)¶
DataPump runs two background tasks against the active TuiClient and
populates four reactive stores:
| Store | Source | Refresh |
|---|---|---|
WorkersStore |
client.list_workers() |
2s polling |
ConnsStore |
client.list_connections() |
2s polling |
TriggersStore |
client.list_triggers() |
2s polling |
EventsStore |
Pubsub.Subscribe server-streaming |
Push, bounded deque (max 500) |
The subscriber retries with exponential backoff (1s up to a 30s cap) on
disconnect; events are not replayed on reconnect. The DataPump
survives pane switches within a context; switching context (via the
Config page) stops the old pump, opens a fresh gRPC channel, and starts a
new one.
InstrumentsPage, OrderBookPage, PortfolioPage, PnLPage, and
RiskPage (the Trading tab's sub-tabs) are, as of this writing, mostly
placeholders not yet wired to a dedicated store or RPC -- they exist as
the scaffolding for future trading-specific views.
4. Config (src/virtufin_tui/config.py)¶
Two independent files, both XDG-aware ($XDG_CONFIG_HOME, default
~/.config):
virtufin/api/contexts.toml-- the list ofvirtufin-apiendpoints ("contexts": host/port/TLS/API key/etc.) and which one is active. See Contexts.virtufin/tui/config.toml-- everything else: theme, event-topic presets, the tab/pane layout (pane_order), and display toggles. Loaded into aTuiUserConfigdataclass and round-tripped throughtomlkiton save, so comments and formatting you haven't touched survive.
5. Client wrapper (src/virtufin_tui/client.py)¶
A thin async wrapper (TuiClient) around virtufin.api.client.ApiClient
exposing the operations the TUI needs as plain async methods (no Textual
dependency) -- workers, connections, triggers, pubsub subscribe, gRPC
reflection browsing (the service browser).
6. Per-pane widgets (src/virtufin_tui/screens/*.py)¶
Every pane is a plain textual.widget.Widget (not a Screen) following
the same duck-typed convention: __init__(self, stores, client, **kwargs),
compose(), optionally on_mount() and whichever action_* methods it
wants Dashboard's generic keys routed to.
| Tab | Sub-tab (pane id) | File |
|---|---|---|
| Home | (single pane, no sub-tabs) | home_page.py |
| Trading | instruments / orderbook / portfolio / pnl / risk / config | instruments_page.py, orderbook_page.py, portfolio_page.py, pnl_page.py, risk_page.py, config_page.py |
| Infra | workers / connections / scripts / triggers / events / config | workers_page.py, connections_page.py, scripts_page.py, triggers_page.py, events_page.py, config_page.py |
config is the same ConfigPage class mounted under both Trading and
Infra by default. Detail popovers (WorkerDetail, ConnectionDetail),
the action modal, and the service browser are pushed as modals/screens on
top of the active pane.
Beyond the built-in pages, users can add their own panes and even new top-level tabs without touching the source tree -- see Custom panes.
Lifecycle¶
virtufin-tuiis invoked (fromuvx,pip install, or local).app.main()configures file-based logging, then parses--configand--contextflags.config.load_config()readscontexts.tomland applies env-var overrides (VIRTUFIN_TUI_API_HOST/_PORT/VIRTUFIN_TUI_CONTEXT).- If no contexts are configured, the TUI prints an error and exits --
create
contexts.tomlby hand (see Contexts). - Otherwise,
VirtufinTuiApp.on_mount()builds aTuiClient, connects, and pushes theDashboardscreen, which starts theDataPumpand mounts the first tab's first pane after a short delay. - On
q(quit), the TUI cancels background tasks, closes the gRPC channel, and exits 0.