Skip to content

Tabs & custom panes

The dashboard's tab layout — which top-level tabs exist, and which panes sit under each as sub-tabs — is driven by pane_order in ~/.config/virtufin/tui/config.toml, not hardcoded. You can reorder built-in panes, add your own, and add whole new top-level tabs.

pane_order schema

[[pane_order]]
id = "home"
label = "Home"
panes = ["home"]

[[pane_order]]
id = "trading"
label = "Trading"
panes = ["instruments", "orderbook", "portfolio", "pnl", "risk", "config"]

[[pane_order]]
id = "infra"
label = "Infra"
panes = ["workers", "connections", "scripts", "triggers", "events", "config"]

Each [[pane_order]] entry is one top-level tab: id (used internally), label (shown in the tab bar), and panes (ordered pane ids shown as sub-tabs, selectable with keys 1-9). A tab with a single pane — like home above — renders with no sub-tab bar; that one pane is shown directly.

Pane ids must match either a built-in page (home, workers, connections, scripts, triggers, events, config, instruments, orderbook, portfolio, pnl, risk) or the filename stem of a custom pane under ~/.config/virtufin/tui/panes/ (see below).

Important: keep the [[pane_order]] block last in config.toml. In TOML, any bare key = value line after an array-of-tables header belongs to that table, not the document root — anything you add below it would silently attach to the last tab entry instead of being a real top-level setting.

Editing pane_order from the TUI

The Config page (Infra → Config, or any tab's Config sub-tab) has a "Tabs & Panes" section below the contexts table:

Config page's Tabs & Panes section

Key Action
ctrl+up / ctrl+down Move the highlighted pane up/down within its tab
delete Remove the highlighted pane from its tab
i Insert the pane highlighted in "Available custom panes" after the highlighted row in "Tabs & Panes"
x Add a brand-new top-level tab (prompts for id + label)

Reordering, removing, and inserting take effect immediately — the next time you switch into that tab, its sub-tab bar is rebuilt from the current config. Adding a new top-level tab needs a restart to appear in the tab bar (the top-level tab bar itself is only built once, at startup).

Custom panes

Drop files into ~/.config/virtufin/tui/panes/ (created automatically). Two kinds, distinguished by what's alongside the .py file:

Script + form pane

A <name>.py with a sibling <name>.form.json — the same convention already used by the Scripts page's built-in examples (see create_worker.py / create_worker.form.json there for a full reference). The form's fields are collected and passed to your script as JSON via the VIRTUFIN_TUI_FORM_DATA env var when submitted; the script also receives the same VIRTUFIN_TUI_API_{HOST,PORT,TLS,KEY} / VIRTUFIN_TUI_WORKER_API_{HOST,PORT} env vars every script gets — see Scripts → The env-var contract for the full table and who sets these. Output streams to a log below the form. This is a one-shot action pane, not a continuously updating one. The form schema itself is defined by textual_jsonforms -- see docs/textual-jsonforms-spec.md in the repo root for the full field-type reference. (The same library also powers the Workers page's n/New Worker form, built dynamically from a worker's JSON-Schema config_schema rather than a hand-written .form.json.)

Live widget pane

A <name>.py with no sibling .form.json, defining a module-level class named exactly Pane:

from textual.widget import Widget

class Pane(Widget):
    def __init__(self, stores, client, **kwargs):
        super().__init__(**kwargs)
        self._stores = stores
        self._client = client

    def compose(self):
        ...

    def on_mount(self):
        self.set_interval(1.0, self.refresh)

Pane must subclass textual.widget.Widget and follow the same __init__(self, stores, client, **kwargs) convention as built-in pages (stores is the dashboard's DataPump, client is the active TuiClient). This gives you a genuine persistent page — live polling, subscriptions, whatever you need — not just a form submission.

A file matching neither pattern (no form, and no Pane class) is skipped and logged as a warning; it won't crash the TUI.