Full-Screen TUI Design Specification¶
Document Control¶
| Field | Value |
|---|---|
| Status | Implemented |
| Command surface | canarchy tui |
| Related architecture | docs/architecture.md |
| Related test spec | docs/tests/tui-shell.md |
Goal¶
Provide a full-screen Textual dashboard that reuses the canonical command engine while safely presenting live CAN traffic. Live capture must remain responsive during idle buses, preserve every buffered event until it is consumed or explicitly reported as dropped, and expose enough telemetry for operators to assess capture health.
Scope¶
This specification covers TUI launch, shared command execution, live capture lifecycle, pause behavior, queue telemetry, and error presentation. Protocol enrichment and pane-specific decoding are separate follow-up work.
Requirements¶
| ID | Type | Requirement |
|---|---|---|
| REQ-TUI-01 | State-driven | While standard input or output is not attached to a TTY, the system shall reject canarchy tui with the structured TUI_REQUIRES_TTY error. |
| REQ-TUI-02 | Ubiquitous | The system shall present status, traffic, decoded, J1939, UDS, alerts, and command-input surfaces in the full-screen TUI. |
| REQ-TUI-03 | Event-driven | When an operator submits a command, the system shall execute it through the same command engine used by the CLI. |
| REQ-TUI-04 | Unwanted behaviour | If an operator submits a nested shell or tui command, the system shall reject it without starting another interactive front end. |
| REQ-TUI-05 | Event-driven | When an operator starts live capture, the system shall run transport ingestion outside the Textual event loop and continue accepting UI input. |
| REQ-TUI-06 | Event-driven | When a capture stop is requested, the system shall signal the transport receive loop, poll an idle python-can bus at intervals no greater than 100 ms, close the bus, and report whether the worker stopped within the 250 ms join budget. |
| REQ-TUI-07 | Unwanted behaviour | If a capture worker does not stop within the join budget, the system shall retain the session, emit CAPTURE_STOP_TIMEOUT, and prevent a replacement worker from starting. |
| REQ-TUI-08 | Event-driven | When a capture source ends naturally or after a requested stop, the system shall retain the session until all buffered events and worker errors have been consumed. |
| REQ-TUI-09 | State-driven | While presentation is paused, the system shall continue capture ingestion and retain buffered events after producer completion until presentation resumes. |
| REQ-TUI-10 | Ubiquitous | The system shall expose immutable capture snapshots containing received, drained, dropped, current queue-depth, and high-water-mark counts. |
| REQ-TUI-11 | Event-driven | When the bounded capture queue overflows, the system shall evict the oldest buffered event, increment the dropped count, and surface the loss in both alerts and status telemetry. |
| REQ-TUI-12 | Event-driven | When buffered events are drained, the system shall update shared TUI state and render the applicable traffic and protocol panes without bypassing the command or protocol layers. |
| REQ-TUI-13 | Ubiquitous | The system shall discard retained pane rows only in response to an explicit clearing action. |
| REQ-TUI-14 | Event-driven | When an operator requests the in-TUI help, the system shall render the hotkey table into the alerts log and leave retained rows, pane row stores, filters, sort state, counters, and the capture lifecycle unchanged. |
| REQ-TUI-15 | Unwanted behaviour | If a slash command that tokenises its arguments (/capture, /stop) cannot tokenise them, the system shall report the parse failure in the alerts log, leave capture and pane state unchanged, and continue accepting commands. |
| REQ-TUI-16 | Unwanted behaviour | If /capture receives no interface, an empty interface token, or more than one argument, the system shall reject the command with an alerts diagnostic and shall neither stop nor replace a running capture. |
| REQ-TUI-17 | Unwanted behaviour | If /stop receives any argument, the system shall reject the command with an alerts diagnostic and shall not stop a running capture. |
| REQ-TUI-18 | Ubiquitous | The system shall treat /filter and /sort arguments as raw text rather than shell tokens, so that a filter needle may contain quote characters. |
| REQ-TUI-19 | Event-driven | When a submitted command returns non-event data, an error, help, or version text, the system shall display its output in a scrollable, selectable result view without discarding retained pane state. |
| REQ-TUI-20 | Event-driven | When the operator switches between results and panes, the system shall preserve the latest result and all folded event rows while keeping the command entry accessible at small terminal sizes. |
| REQ-TUI-21 | Unwanted behaviour | If a submitted command resolves to an active-transmit operation, the system shall refuse it before invoking the shared executor, even when a help- or version-looking token is a positional argument after --. |
| REQ-TUI-22 | Event-driven | When decoded-message and signal events share a source, frame index, message, and signal name, the system shall render one signal observation using the child event's units and the available source timestamp, while preserving distinct frame indexes and retaining uncorrelated parent-only or child-only observations. |
| REQ-TUI-23 | Ubiquitous | The system shall present one dominant workspace at a time for Traffic, Signals, J1939, UDS, or Findings, without dividing terminal height equally among empty panes. |
| REQ-TUI-24 | State-driven | While the terminal is narrower than 110 columns, the system shall give the active table the full body width and make the complete selected row available through a drill-down view; at wider widths it shall show a contextual inspector beside the table. |
| REQ-TUI-25 | Event-driven | When the operator switches workspaces, opens results or activity, or resizes the terminal, the system shall retain folded rows, per-table selection, and capture state; closing drill-down shall restore table focus, and widening the terminal shall close drill-down. |
| REQ-TUI-26 | State-driven | While no traffic has been observed, the system shall show first-run capture and offline-demo guidance; after traffic arrives, the guidance shall yield space to the table. |
Command Surface¶
canarchy tui
Inside the TUI, /capture <interface>, /stop, /clear, /filter, and /help control presentation and capture; the spacebar pauses or resumes presentation. Other commands are delegated to the canonical command executor.
The body is a single active workspace rather than a fixed four-pane grid. Alt+1 through Alt+5 select Traffic, Signals, J1939, UDS, and Findings. The current workspace owns the available height; the J1939 ribbon appears only in its workspace. At 110 columns or wider, a side inspector shows the selected row's complete field values. Narrower terminals show a full-width table; Enter drills into the same inspector and Esc returns. The activity log rests as a one-line band and F3 expands or collapses it. The source/capture status band and command entry remain visible. An empty Traffic workspace offers the live-capture and offline dataset starting paths. These are presentation controls only; commands and event folding remain shared with CLI/REPL.
Traffic now opens in a per-identifier summary and provides a separate event-log view with v; its bounded analysis, freeze behavior, and shared filter contract are specified in docs/design/tui-identifier-explorer.md.
The latest command result is shown in a read-only TextArea using the CLI's text formatter (or JSON/JSONL when explicitly requested). Its border identifies the submitted command and completion/error status. Non-event answers and errors open the result view automatically; event-bearing answers retain the live panes in front. F2 switches between the result and panes, and Esc returns to the panes. The result can be scrolled, selected, and copied with the TextArea controls. CLI help and version output are captured into this view rather than printed behind the full-screen interface. Live capture drains continue while results are open.
The active-transmit precheck parses the command with the shared argument parser and suppresses only help/version text from that precheck. It never infers safety from raw token membership: --help and --version can be positional values after --, in which case an active command remains refused.
The decoded-signal fold pairs parent and child events by (source, frame_index, message_name, signal_name), never by value. It retains the child event's units and whichever event supplies a timestamp (including zero). When an event lacks frame_index, it remains a separate observation rather than risking a false merge. The current four-column Signals pane does not present time; the folded observation carries the numeric timestamp for future trend work, while standalone DBC signal events also preserve source-frame timestamps and #551 governs time-basis display.
/capture takes exactly one interface. It is a hotkey for the app-native live stream and carries no options, so an empty token, a whitespace-only token, or extra arguments are rejected with an alerts diagnostic rather than silently ignored; run the full capture command for anything that needs flags. Rejection happens before the capture session is touched, so malformed input never stops or replaces a running capture.
/stop takes no arguments. It previously discarded whatever followed it, so /stop " ended a running capture instead of reporting the unmatched quote; arguments are now validated before the capture is touched (issue #542).
Argument handling differs by command on purpose. /capture and /stop tokenise with shlex and report a parse failure as a diagnostic. /filter and /sort take raw text — a filter needle may legitimately contain a quote character — so they validate their pane argument instead of tokenising, and leave state unchanged when it is not recognised. REQ-TUI-15 is scoped to the tokenising commands for that reason.
/clear (and the c key) is the only slash command that discards pane data. The shared hotkey layer signals this to front ends with a dedicated disposition (_HotkeyResult.CLEARED) that is distinct from "handled locally without producing a command" (_HotkeyResult.LOCAL); a read-only handler such as /help returns LOCAL and front ends leave their view state alone.
Data Model¶
CaptureSession owns one daemon worker, a bounded event queue, an error queue, and stop/finished events. Its CaptureStats snapshot contains:
received: int
drained: int
dropped: int
queue_depth: int
high_water_mark: int
The producer never blocks on a full queue. It discards the oldest event and records that loss before adding the newest event. The Textual timer drains bounded batches but does not release a completed session until the queue is empty.
Output¶
Capture health is rendered in the status pane using stable operator-facing labels:
capture: received=1000 drained=1000 queued=0 dropped=0 high-water=1000
Dropped-event alerts include the newly observed loss and cumulative count. TUI output remains presentation-only and does not alter CLI JSON schemas.
Errors¶
| Code | Condition | Operator action |
|---|---|---|
TUI_REQUIRES_TTY |
TUI launched without an interactive terminal | Run from an interactive terminal. |
CAPTURE_STOP_TIMEOUT |
Capture worker remains alive after the stop join budget | Wait for backend shutdown and inspect the configured adapter before retrying. |
CAPTURE_FAILED |
Unexpected capture worker failure | Check transport configuration and retry. |
| Transport error code | Backend raises a structured transport error | Follow the backend-provided hint. |
Slash-command validation failures are presentation-level diagnostics, not structured command errors: they are written to the alerts log, carry no exit code, and leave the session running.
| Alerts diagnostic | Condition |
|---|---|
error: could not parse /<name> arguments: <reason> |
Unmatched quote or dangling escape in a slash command's arguments |
/capture requires an interface; e.g. /capture vcan0 |
/capture submitted with no argument |
/stop takes no arguments; got N. … |
/stop submitted with any argument |
/capture interface must not be empty; e.g. /capture vcan0 |
/capture "" or a whitespace-only interface token |
/capture takes a single interface; got N arguments. … |
/capture submitted with more than one token |
Deferred Work¶
- Live DBC signal decoding and explicit decode-database selection.
- Protocol-aware J1939 and UDS transaction routing from live frames.
- Capture-rate visualizations beyond bounded queue counters.