Conversation
Add the plan for a Bubble Tea terminal UI mode of satpulsewb: a third shell over gps/app/session alongside the web workbench and the Wails desktop app, usable over ssh with no browser. The plan's prerequisite payload-aliasing check is done and its outcome recorded: every session event payload is safe to hold by reference after Sink.Emit returns, so the TUI sink does not need to copy on receipt.
satpulsewb -t runs a Bubble Tea (v2) terminal UI over gps/app/session instead of starting the HTTP server and opening a browser, so the workbench's ground is covered over ssh on a headless box with a receiver attached. The session is used in process: a coalescing Sink feeds session events into the Bubble Tea message loop (keep-latest per event name for the high-rate snapshot events, a bounded queue for the packet stream), slog is routed through session.NewLogHandler with a discard base handler so nothing writes to the terminal behind Bubble Tea's back, and blocking session methods run as commands. The views mirror the workbench tabs: Monitor (summary, clock, PVT message tables, satellite signals as a text bar chart, survey, and the position statistics the scatter panel computes, without the plot), Packets (scrolling list with pause and a decode detail view, with gps:packet gated on visibility through Wants), Corrections (source form, status, and the per-message summary), Configuration (read and apply, controls gated on the probed ConfigSupport flags as in the workbench), and Messages (library catalog, tag list, send progress, and receiver responses). A connection form overlay replaces the workbench's fixed connection bar. The -t shorthand moves from --token to the new --terminal; --token keeps only its long form. See plan/tui.md.
Driving the TUI against the FIFO replay, the ubxsim simulator with the fake NTRIP caster, and a live ZED-F9P turned up a batch of usability defects: - The corrections and connection text inputs had no width set, so they rendered a single column wide. - The corrections NTRIP port default 2101 was only a placeholder, so Start stayed refused until the user typed the port the workbench fills in for them; it is now a real initial value, kept per mode. - The correction message descriptions pushed the Count/Splits/Age columns off a normal-width terminal; they are capped at 44 characters. - The Messages send status and the decode of the selected response rendered after the form and were clipped off the bottom of the window; the status now follows the Send button and the decode sits inline under the selected response. The configuration status line moved above the form for the same reason. - Scrolling a form down pinned the focused row to the bottom edge, hiding anything attached below it; the window now keeps a third of its height below the focus. - The configuration form starts with the first focusable row focused instead of an invisible focus on the heading.
Cover the pieces of the TUI whose behaviour is not obvious from a glance: the coalescing sink (keep-latest per event name and per gps:msg kind, ordered delivery of stream events, the bounded packet queue with its drop counter, and the Wants gating), the position statistics accumulator (mean, CEP, RMS, and ARP distances on known geometry, plus result caching and reset), the workbench's per-epoch PVT row eviction, the correction message counting rules (fragments count once per epoch with extras as splits), and the root model's header line, Messages tab appearance, and a render smoke test across all tabs. The sink's batch handoff is extracted as takeBatch so the tests exercise exactly what the drain goroutine delivers.
The TUI shipped as satpulsewb -t, taking the -t shorthand from --token, but -t as --token is established satpulsewb usage that must not change (the e2e harness and existing habits rely on it). With the mnemonic shorthand unavailable, a mode flag on satpulsewb loses its appeal, so the terminal UI becomes a separate satpulsetui command for now. satpulsewb is restored exactly to its pre-TUI state. The tui package moves to cmd/satpulsetui as package main, with the former tui.Run becoming the command's run alongside satpulsewb-style flag parsing (-d, -s, --vendor, --packet-log, -v). Not added to install or deb/rpm packaging yet. The plan's Decided section records the reversal.
Being functionally the same tool as the web workbench, the terminal UI belongs in satpulsewb rather than a separate binary: satpulsewb --tui runs the terminal UI instead of starting the HTTP server and opening a browser, reversing the satpulsetui split. The long flag name follows the gdb --tui precedent; the -t shorthand stays with --token on this branch, to be freed on master (where --token, being scripted-use only, does not need a shorthand) so --tui can take it when that lands. The tui package moves back to cmd/satpulsewb/tui with its former Options/Run entry restored; the standalone flag parsing is deleted. --tui cannot be combined with --listen or --token. The man page documents the mode, and the NEWS, packages.md, and plan entries follow the flag.
Match the branch, now named wb-tui, and the feature's final shape as a satpulsewb mode rather than a standalone TUI.
The terminal UI now has issue #395; put its number in the plan heading per the plan/issue conventions.
Brings in the removal of the satpulsewb -t token shorthand, freeing -t to become the shorthand for --tui. # Conflicts: # cmd/satpulsewb/satpulsewb.go # docs/man/satpulsewb.1.md
With master's removal of the --token shorthand merged in, -t is free to take its natural meaning, following the gdb --tui precedent. The man page, usage summary, NEWS entry, and plan record the shorthand. For #395.
The first pass implemented the Packets view as a flat scrolling packet stream with pause, following a summary sentence in the plan rather than the workbench packet panel that the plan's own rule names as the reference. Replace it with the workbench design: one aggregated row per (protocol, message, direction) with a running count, last timestamp and data, the current ~900 ms burst retained per row and expandable, stale rows dimmed after 1.5 s, freeze/unfreeze of a display copy while accumulation continues, a snapshot listing of the currently active packets, clear, and a decode pane for the selected entry below the table. The ring buffer, pause semantics, and the dropped-packet marker go away with the stream; aggregation makes a stalled UI cost at most an undercount. The plan's Packets bullet now describes the panel it was summarizing.
Keep-latest coalescing was applied to gps:msg (keyed by kind) and gps:basearp, which are keyed streams, not snapshots: two messages of the same kind but different native message IDs routinely land in one batch, so the earlier one was silently replaced and the Monitor's per-native-message rows intermittently lost entries; likewise base ARPs with multiple station IDs. Coalescing is now limited to the true singleton snapshots (state, receiver, speed, corrections, time, epochPVT, nmeaPosition, initialPos), and everything else is delivered in order through a FIFO bounded like the packet queue, dropping the oldest non-singleton entries on overflow. The bound only matters while the render loop is stalled (a blocked terminal: Ctrl-S, dead ssh, suspension) -- Bubble Tea's event loop wedges on the renderer mutex when the tty write blocks -- and what gets dropped then either repopulates within an epoch or merely undercounts, mirroring how the workbench SSE hub treats a stalled client. The unused packet drop counter goes away. A superseded singleton now also moves to its latest emission position instead of keeping its first-emission slot, so a state change can no longer be delivered ahead of stream events that were emitted before it (a disconnect followed by messages from the dead connection would have repopulated a just-cleared view).
Brings in the typed session event payloads (#396-adjacent; see plan/wb-typed-event.md): session.Event is now a sealed union, and the TUI is adapted to it in this merge's follow-up commit.
With session.Event now the sealed union from plan/wb-typed-event.md (merged from master), every view's handleEvent becomes a plain type switch on the payload value, dropping the guarded name-plus-assertion pairs; the sink keys singletons and packets off EventName(); and the test fixtures become typed values, so an invalid name/payload pairing is no longer representable.
The session clears its cached probe on Disconnect but emits only StateDisconnected, so the header kept showing the previous receiver identification and baud rate beside "disconnected" until another connection started. Drop the mirrored values when the state arrives; corrections state stays, carrying its own lifecycle.
StartCorrections requires exactly StateConnected, but the form only locked on disconnected: during connecting, reconnecting, configuring, or sending it appeared editable and Enter attempted a start that must fail. Gate editing and start on StateConnected.
Connecting through the overlay with the Configuration tab already selected never triggered the tab's automatic readback: only a tab switch sent viewShownMsg, and the connected transition alone updates state. Closing the overlay now notifies the view it reveals, so the readback (and the Messages catalog refresh) runs as it does when the tab becomes visible.
After readback selects Mobile, Survey-in, or Fixed, cycling could select the blank "--" state and mark the time mode touched, but buildTarget has no case for it: a mode-only apply failed as a no-op, while an apply bundled with other changes cleared the pending mode without changing the receiver. The blank state now only reports that nothing is selected; it cannot be selected as a request.
The tab was gated on the message-file catalog being non-empty, a compromise reading of a plan sentence that misdescribed the workbench. The built-in library is compiled in, so the catalog is never empty: the gate's only observable effect was the tab popping in a beat after startup once the asynchronous catalog listing arrived, shifting the tab digits. Show it unconditionally, as the workbench does for satpulsewb.
The workbench disables the Configuration tab until the probe identifies a vendor; the TUI allowed entering it, enabled its controls on StateConnected alone, and even fired the automatic readback against an unidentified receiver. The tab now renders dimmed and cannot be activated until the receiver is identified (tab cycling skips it), and the view's master gate requires identification as well as StateConnected, which also covers the readback trigger and the case of sitting on the tab across a reconnect to an unidentified receiver.
Stale-row dimming in the Packets table and the Age column of the corrections table recomputed only when an event happened to trigger a render, so a stopped stream quietly stopped aging. A 1 Hz tea.Tick now runs while a view showing ages is active, matching the workbench's per-second refresh of those tables.
The header showed raw state strings ("connected") and a bare
firmware field. Use the workbench labels ("Connected",
"Connecting..."), its receiver identity form "Vendor HW (FW fw)",
"Unknown (formats)" for an unidentified receiver, and
"Identifying..." while the probe runs.
The workbench has Clear buttons on the scatter panel and the correction message table; the TUI only cleared on disconnect. Add x as the clear key: position statistics in Monitor, the correction message table (ignored while a text field has focus), and the Packets table, whose advertised clear key c was dead -- the root model intercepts c globally for the connection overlay, so the binding never fired.
The row interval was tracked but unused; apply the workbench's adaptive staleness threshold, max(10 s, 2.5 x interval), dimming rows whose message has stopped arriving. A row with only one message seen is never stale.
The workbench Time table shows the local time of day next to UTC; the TUI omitted it. Local is derived from the row's UTC (or the TAI-derived UTC) and never bold, per the bold-means-native convention.
The summary's GS/C accuracy fields are shown only while moving; the workbench requires ground speed and course both present before treating the receiver as moving, the TUI only speed. A receiver reporting speed without course showed accuracies the workbench hides.
Two workbench apply behaviours were missing. A fixed-position ECEF triple is now validated as on Earth (a direct geopos CheckOnEarth call, where the workbench uses its geo endpoint) and refused with the workbench's wording. And the survey fields (time, accuracy, do a new survey, report progress) are cleared when an apply is issued, as the workbench does, so a later apply does not silently re-request a survey.
The workbench swaps the survey-accuracy and position-accuracy placeholders to 'not supported' when the probed ConfigSupport lacks surveyAcc/fixedPosAcc; the TUI only disabled the fields. Set the placeholders when the receiver event arrives.
Default the speed field to 9600 and prefill a sole enumerated port into an empty device field, as the workbench connection bar does. While a connection exists the device and speed fields lock and the two buttons collapse into the workbench's single toggle: enter connects when disconnected and disconnects otherwise (previously enter re-ran connect unless the Disconnect button was focused).
A changed vendor preselect on catalog refresh means a new receiver was identified; it now overrides the current selection as in the workbench, instead of only filling in when the selection is gone. And binary responses with a label carry the workbench's truncated hex preview beside it.
The form validated only inside buildTarget when Apply was pressed; the workbench validates continuously, marks invalid fields, and disables Apply while any error exists. The rules now live in one place: each numeric field is a cfgField carrying its relevance (mode-scoped, like its control's gating), required-ness, and range rule, with the cross-field rules (pulse width shorter than a non-zero period, fixed ECEF on Earth) beside them. fieldErrors runs per render: invalid fields render their label and message in red, the status line shows the first error, and Apply is disabled while any exist. buildTarget starts from the same fieldErrors, so its value extraction cannot fail and its inline parse-and-validate closure (including the separately added ECEF check) is gone.
The workbench signals panel has per-constellation toggle chips and a Used-only checkbox; the TUI showed the full table unfiltered. Add keyboard toggles (the SVID prefix letters, except b for BeiDou since c belongs to the connection overlay) and u for used-only, gated on the receiver reporting used flags as the workbench disables the checkbox. A filter bar above the table shows the constellations present, dimming excluded ones. The filters are view preferences and survive disconnects, as the workbench panel's state does.
Editing and starting are different things: preparing the source details makes sense in any receiver state, including disconnected, while starting requires exactly StateConnected. The fields now lock only while a correction session runs or a start/stop is pending; the start gate keeps its StateConnected requirement. Neither prior rule had this split: the workbench locks editing when disconnected, and the TUI locked it in every non-connected state.
The decode pane re-decoded the selected row's newest entry every render, tracking the stream; the workbench decodes the clicked entry once and the pane stays put. The selection now captures the packet it denotes (parent row: its newest entry at that moment), and the pane shows that until the selection changes or is cleared.
The Corrections view and the connection overlay used hand-rolled focus indexes, field rendering, and key dispatch written before the shared cfgItem machinery existed, giving two implementations of traversal, styling, and input capture. Both now build cfgItem rows: same up/down traversal skipping disabled rows, same dimming, same capture semantics, less code. Two behaviour changes ride along. The overlay's speed becomes a select over the standard speed list (with an off-list -s value offered as an extra option), matching the workbench control, and its enumerated ports are focusable rows that fill the device field on enter, replacing the up/down cycling. The corrections start/stop moves onto its button rather than enter-anywhere, consistent with the other forms.
Two impurities bent Bubble Tea's update/render separation. Text inputs called Focus()/Blur() during rendering; the focus sync moves into moveItemFocus via a setFocus hook on the row, which also stops discarding the cursor command Focus() returns. And the Messages view ran the response decode on every render; it now decodes once when a response is selected (or auto-selected), like the packet decode pane. Derived display data (the config validation cache, the Messages item list) stays computed at render deliberately: side-effect-free memoization, one View call per Update.
The status line showed only the latest log message, so history was lost and a debug line could overwrite a warning. It is now backed by a 200-entry ring (the workbench's depth) with a minimum-severity filter, cycled with v: the line shows the newest message at or above the filter, and a right-aligned indicator cluster shows one letter per severity present, the current filter bracketed, and a star on any severity whose newest message is newer than the one displayed -- so filtered-out activity is visible without the line churning. l opens a transient pane over the lower third of the view for browsing the filtered history (arrows/page keys scroll, v refilters live, esc closes), so screen space is spent only while reading and there is no separate tab to forget. No severity colouring: the level word carries it, avoiding light/dark background readability problems.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
satpulsewb --tui(shorthand-t): a Bubble Tea (v2) terminal UI over the samegps/app/sessioninterface as the web workbench, run in process instead of the web server. The views follow the web workbench's panels (monitor, packets, corrections, configuration, message files), dropping the inherently graphical parts; the scatter panel's statistics are kept as a text readout.The
-tshorthand comes from master's removal of the--tokenshorthand, merged into this branch, and the TUI is adapted to master's typed session event payloads.Later commits fold in the results of review passes: workbench-parity fixes across the views (gating, labels, filters, validation, clear controls, a 1 Hz age tick), and structural cleanups (one shared form-row machinery for all forms, side-effect-free rendering, bounded event queues in the sink).
For #395. See
plan/wb-tui.md.