--- layout: page title: TUI description: Eleven-panel full-screen operator console over the daemon's REST + SSE API nav_group: Operate --- # GopherTrunk TUI A full-screen operator console over the daemon's REST + SSE API. The TUI ships in the same binary as the daemon — no separate install — and points at a running daemon over HTTP. Eleven panels cover every read surface; the Scanner cockpit, talkgroup table, and a curated set of mutations are interactive when the daemon is started with `api.allow_mutations: true` and the TUI with `--write`. ## Quick start ```bash # In one terminal: run the daemon as usual. gophertrunk run -config config.yaml # In another: open the TUI. gophertrunk tui ``` By default the TUI connects to `http://127.0.0.1:8080`. Override with `-server`: ```bash gophertrunk tui -server http://10.0.0.5:8080 gophertrunk tui -server https://radio.example.com -insecure ``` ## Flags | Flag | Default | Purpose | | --- | --- | --- | | `-server URL` | `http://127.0.0.1:8080` | daemon base URL | | `-insecure` | `false` | skip TLS certificate verification (for self-signed test daemons) | | `-timeout DURATION` | `5s` | per-request timeout; SSE streams are unaffected | | `-no-color` | `false` | strip ANSI colour, useful when piping or on a monochrome terminal | | `-write` | `false` | surface mutation keybindings; daemon must also have `api.allow_mutations: true` | ## Panels | # | Panel | What it shows | | --- | --- | --- | | 1 | Dashboard | At-a-glance health, active call count, recent events, recent tone alerts. Default landing panel. | | 2 | Systems | Configured trunking systems: name, protocol, control channels, IDs (WACN / SystemID / RFSS-Site). | | 3 | Talkgroups | Full talkgroup table from the daemon. Substring filter (`/`) and a sort cycle (`s`: ID → Alpha → Priority). | | 4 | Active calls | Calls currently being followed. Driven by a 1 s poll plus live `call.start` / `call.end` events for instant updates. The Flags column shows encryption (`E`), emergency (`!`), and — for DMR — the TDMA timeslot (`TS1` / `TS2`), so a carrier's two concurrent calls are distinguishable. | | 5 | Call history | Ended calls from `/api/v1/calls/history`. On-demand reload (`r`); the panel does not poll continuously. Row formatting runs off the Update goroutine so the reducer never blocks on history rebuilds. | | 6 | Events | The full SSE feed in chronological order. Substring filter (`/`), pause auto-scroll (`p`), clear filter (`c`). 500-entry ring buffer. | | 7 | Tone alerts | Just `tone.alert` events with profile / device / matched frequencies. 100-entry ring buffer. | | 8 | Metrics | Curated subset of `/metrics`: calls active / total, grant total, CC locked, SSE clients, devices attached, tone alerts. | | 9 | Devices | The SDR pool snapshot: serial, driver, tuner, role, configured gain / PPM / bias-tee, attach state. Refreshed on `sdr.attached` / `sdr.detached` events for instant updates. | | 0 | Scanner | Police-scanner cockpit: per-trunked-system CC hunter state, conventional FM scan list (current dwell highlighted), talkgroup scan-list summary, audio cockpit. | | — | Settings | Tabbed read-only inspector of the live daemon configuration (Daemon · Storage · Audio · Recording · Tones · API · Vocoders · SDR · FEC). Reach via `Tab` / `Shift+Tab` or the command palette — there's no number shortcut. | ## Keybindings ### Global | Key | Action | | --- | --- | | `Tab` | next panel | | `Shift+Tab` | previous panel | | `1`–`9`, `0` | jump directly to a panel (0 = Scanner; Settings has no number) | | `Ctrl+P` | open the fuzzy command palette (see below) | | `Ctrl+T` | toggle theme (dark ↔ monochrome) | | `?` | toggle help overlay | | `q` / `Ctrl+C` | quit | ### Inside tables | Key | Action | | --- | --- | | `j` / `↓` | next row | | `k` / `↑` | previous row | | `g` | top | | `G` | bottom | | `Page Down` / `Page Up` | scroll a page | ### Panel-local | Panel | Keys | | --- | --- | | Systems | `Enter` open detail card | | Talkgroups | `/` filter, `s` cycle sort, `l` toggle lockout, `S` toggle scan flag, `+` / `-` priority up/down, `Enter` open detail card, `Esc` exit filter input | | Active calls | `e` end highlighted call (write) | | Call history | `r` reload | | Events | `/` filter, `p` pause auto-scroll, `c` clear filter | | Tone alerts | `R` reset detector for highlighted device (write) | | Metrics | `S` run retention sweep now (write) | | Devices | (table navigation only) | | Scanner | `j` / `k` move row, `h` hold/resume highlighted row, `r` force re-hunt (Systems section, confirms), `Enter` dwell on highlighted conv channel, `L` lockout / unlockout highlighted conv channel (skipped from scan rotation, runtime-only), `m` cycle scan_mode, `+` / `-` volume up/down (5% step), `M` mute toggle, `R` recording toggle, `f` manual tune (type frequency in MHz, Enter to listen, Esc to cancel) | | Settings | `[` / `]` / `h` / `l` / `←` / `→` cycle through inspector tabs | ## Mouse The tab strip is clickable, and every table-backed panel (Systems · Talkgroups · Active · History · Devices · Tones · Metrics) reacts to body clicks and scroll-wheel ticks: - **Left-click on a tab** switches to that panel. - **Left-click on a data row** moves the cursor onto that row. Follow up with `Enter` to open the detail card, or with a mutation key (`e` to end the highlighted call, `R` to reset the highlighted tone detector, `L` to toggle conventional channel lockout, etc.). - **Scroll-wheel up/down** advances the cursor one row at a time in the same panels. Bubbles v1.0.0's `table.Update` is `KeyMsg`-only, so wheel forwarding is plumbed by the TUI itself via a shared `MouseAware` interface. Chrome clicks (border / title / column header) are ignored; out-of-range clicks clamp to the last row. ## Command palette `Ctrl+P` opens a centered fuzzy-matched action list. Type to filter; `↑` / `↓` or `Ctrl+J` / `Ctrl+K` to move; `Enter` to run; `Esc` or another `Ctrl+P` to dismiss. The list is rebuilt every time you open the palette so newly-arrived systems / talkgroups / devices are reachable without a restart. Discovered actions: - **Panel jumps** for every panel (including Settings, which has no number shortcut). - **System / talkgroup / device drill-ins** — pre-positions the destination panel's cursor on the matching row before opening the detail modal, so keyboard and mouse paths converge on the same selection. Talkgroup actions cap at the first 200 to keep the list legible on large rosters. - **Audio mutations** — volume ±5%, mute toggle, recording toggle. - **Retention sweep** (confirms). - **Scanner** — toggle scan_mode list ↔ all, per-system hold/resume. - **Theme toggle**, **help overlay**, **quit**. ## Theme `Ctrl+T` cycles between the dark and monochrome palettes at runtime. `-no-color` starts the TUI on monochrome and pins the lipgloss renderer to a nil writer so ANSI is stripped even when piping output — `Ctrl+T` flips the palette in that mode but the renderer won't re-emit colour. Panels that own a cached `bubbles/table` style bundle handle a `ThemeChangedMsg` broadcast by re-applying `tableStyles()` so the swap takes effect on the next render without a restart. ## Settings panel Read-only inspector of the live daemon configuration, fetched once at startup and refreshed every 30 s from `GET /api/v1/runtime`. Cycle through tabs with `[` / `]` (or `h` / `l` / `←` / `→`): | Tab | Coverage | | --- | --- | | Daemon | version, log level / format, metrics enabled | | Storage | call-log DB path, CC cache file, retention windows (days) + sweeper interval | | Audio | enabled, device, sample rate, buffer ms, available backends, auto-fallback disable flag (Linux) | | Recording | output dir, sample rate, raw vocoder frames, CMA equalizer (taps + step size) | | Tones | per-profile name, cooldown, frequencies | | API | HTTP / gRPC / WebSocket / metrics listener addresses, mutations allowed | | Vocoders | per-protocol vocoder mapping | | SDR | registered backend names, per-device serial / role / gain | | FEC | per-system FEC state — every protocol's chain is on by default, this view surfaces explicit `: off` opt-outs and the current channel-coding / colour-code parameters | Every config knob the daemon reads has a touch-point here. Mutating the settings still requires editing `config.yaml` and restarting the daemon. ## Polling cadences The TUI keeps SharedState fresh with a fan of polling Cmds: | Endpoint | Period | | --- | --- | | `/api/v1/calls/active` | 1 s | | `/api/v1/health` | 2 s | | `/api/v1/scanner` | 2 s | | `/api/v1/audio` | 3 s | | `/metrics` | 5 s | | `/api/v1/systems` | 10 s | | `/api/v1/devices` | 10 s (also refreshed on `sdr.attached` / `sdr.detached`) | | `/api/v1/talkgroups` | 30 s | | `/api/v1/runtime` | 30 s | | `/api/v1/calls/history` | on-demand (`r` in History panel) | | `/api/v1/systems/{name}` | on-demand (Systems panel `Enter`) | | `/api/v1/talkgroups/{id}` | on-demand (Talkgroups panel `Enter`) | A long-lived `/api/v1/events` SSE stream complements the polls, so `call.start` / `call.end` / `cc.locked` / `tone.alert` / `audio.state` arrive instantly. If the SSE stream drops, the TUI shows a transient toast and reconnects with exponential backoff (1 s → 30 s cap). ## Mutations The TUI drives a curated set of operator actions when both sides opt in: ```bash # Daemon side: enable in config.yaml. api: http_addr: 127.0.0.1:8080 # bind to loopback if you can allow_mutations: true # TUI side: pass --write to surface the keybindings. gophertrunk tui --write ``` Both ends are gated separately on purpose. The daemon's HTTP API has no authentication today, so any host that can reach the listener can call mutations once the gate is open — bind to `127.0.0.1` and trust the operator before flipping it on. | Panel | Key | Action | Confirm? | | --- | --- | --- | --- | | Active calls | `e` | End the highlighted call (`POST /api/v1/calls/{serial}/end`, reason=manual) | yes | | Talkgroups | `l` | Toggle lockout on the highlighted talkgroup (`PATCH /api/v1/talkgroups/{id}`) | no — reversible | | Talkgroups | `S` | Toggle scan flag (relevant when `scan_mode: list`) | no | | Talkgroups | `+` / `-` | Bump priority up / down (clamped 0–99) | no | | Tone alerts | `R` | Reset tone-out match progress on the highlighted device (`POST /api/v1/devices/{serial}/tone-reset`) | yes | | Metrics | `S` | Run a retention sweep now (`POST /api/v1/retention/sweep`) | yes | | Scanner | `h` | Hold/resume highlighted system or conv channel (`POST /api/v1/scanner/hunt/{system}/{hold,resume}` or `/conventional/{hold,resume}`) | no | | Scanner | `r` | Force re-hunt the highlighted system (`POST /api/v1/scanner/hunt/{system}/retune`) | yes | | Scanner | `Enter` | Dwell on the highlighted conv channel (`POST /api/v1/scanner/conventional/{index}/dwell`) | no | | Scanner | `L` | Lockout / unlockout the highlighted conv channel (`POST /api/v1/scanner/conventional/{index}/{lockout,unlockout}`) | no — reversible | | Scanner | `m` | Cycle global scan_mode list ↔ all (`PATCH /api/v1/scanner`) | no | | Scanner | `f` | Manual tune: type a frequency in MHz and Enter to listen (`POST /api/v1/scanner/manual_tune`) | no | | Scanner | `+` / `-` / `M` / `R` | Volume ± 5%, mute toggle, recording toggle (`PATCH /api/v1/audio`) | no | When a key requires confirmation, a centered modal opens. The modal captures keyboard focus until you press: - `y` or `Enter` — fire the action and close the modal - `n` or `Esc` — cancel and close the modal On success the status bar shows `