docs: design for the About/shortcuts/help windows and a two-tier status bar
Four features the user asked for together: an About dialog (version, build timestamp, settings path), a shortcut overview covering the keyboard and the mouse, a fourteen-page help window, and a status bar split into a short glance line plus a details popover so the plot's own row stops being crowded. Records one deviation from the approved design: keyboard zoom-x/zoom-y use ctrl/alt rather than shift, because `+` *is* shift+`=` on the common layouts and a bare zoom-in therefore already arrives with Shift set. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
53815ad8bc
commit
6b148f82a6
1 file changed
+436
@@ -0,0 +1,436 @@
|
|||||||
|
<!-- SPDX-License-Identifier: MIT -->
|
||||||
|
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||||
|
|
||||||
|
# About, shortcuts, help, and a two-tier status bar
|
||||||
|
|
||||||
|
udpscope has grown a lot of behaviour that is only written down in `CLAUDE.md` and `README.md`:
|
||||||
|
signals start disabled, a long window is decimated rather than refused, a measurement's rulers
|
||||||
|
are stepped because the pass is throttled, an export is the stored samples and not the LOD
|
||||||
|
buckets. None of it is reachable from the application. The plot's own keyboard and mouse
|
||||||
|
gestures are likewise undocumented in the app, and there is no way to tell which binary is
|
||||||
|
running. Meanwhile the status bar carries eleven fields on one line and is unreadable at a
|
||||||
|
glance.
|
||||||
|
|
||||||
|
This adds four things: an About dialog, a shortcut overview, a paged help window, and a
|
||||||
|
two-tier status bar. The first three are windows that explain the application and share a
|
||||||
|
content source; the fourth is independent, and is here because it is the same kind of work on
|
||||||
|
the same surface.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
In: the three windows, the accelerator table behind two of them, app-level keyboard shortcuts,
|
||||||
|
one addition to `deps/plotter`'s `input::Controller` (axis-restricted keyboard zoom), and the
|
||||||
|
status bar's split into a glance line and a details popover.
|
||||||
|
|
||||||
|
Out: a translated help (the app is English-only throughout), a search box over the help, a
|
||||||
|
preferences dialog, and any change to what the status line *measures* — the fields move, none
|
||||||
|
is added or deleted.
|
||||||
|
|
||||||
|
## The About dialog
|
||||||
|
|
||||||
|
Three facts, as asked: version, build timestamp, and the path of the persistent settings.
|
||||||
|
|
||||||
|
**Version** is hand-maintained, `project(udpscope VERSION 0.1.0 LANGUAGES C CXX)`. Not
|
||||||
|
`git describe`: the repository has no tags, so `describe` degrades to a bare hash and presents
|
||||||
|
it as a version number.
|
||||||
|
|
||||||
|
**Build timestamp and provenance** come from a generated translation unit. `src/build_info.hpp`
|
||||||
|
declares three constants and a CMake custom command writes `build_info.cpp` defining them on
|
||||||
|
**every** build:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace udpscope::build_info {
|
||||||
|
extern const char* const kVersion; ///< project(VERSION)
|
||||||
|
extern const char* const kCommit; ///< git describe --always --dirty, or "unknown"
|
||||||
|
extern const char* const kTimestamp; ///< "YYYY-MM-DD HH:MM UTC"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`__DATE__`/`__TIME__` are not used: they only move when that translation unit happens to
|
||||||
|
recompile, so a binary built today can honestly report last week. Regenerating unconditionally
|
||||||
|
costs one small compile and one relink per build, and it follows the pattern already in
|
||||||
|
`CMakeLists.txt:43-52`, where `udpscope_resources.c` is a generated source compiled into the
|
||||||
|
executable. The commit is included because it is the fact that actually answers "which binary
|
||||||
|
is this"; a timestamp alone does not. `kCommit` reads `unknown` when `git` is absent or the
|
||||||
|
tree is not a checkout — a build from a tarball must not fail for it.
|
||||||
|
|
||||||
|
**The settings path** is `layout_store::config_dir()`, the same function the session files use,
|
||||||
|
followed by `layout_path(host, port)` for the source this run is connected to — the file a user
|
||||||
|
actually wants to find. Both as selectable labels, so they can be copied.
|
||||||
|
|
||||||
|
The widget is a `GtkAboutDialog`. It is **not** deprecated in GTK 4.22 (it is
|
||||||
|
`GtkShortcutsWindow` that is), it gives the version field, a comments block and an MIT licence
|
||||||
|
section for free, and the two paths go in the comments block, which is the only part with room
|
||||||
|
for them.
|
||||||
|
|
||||||
|
## Keyboard shortcuts
|
||||||
|
|
||||||
|
### The plot's axis-restricted zoom, in `deps/plotter`
|
||||||
|
|
||||||
|
`input::Controller` has `+`/`-` zooming **both** ranges about the plot centre, and no way to
|
||||||
|
zoom one axis from the keyboard. It gains that, with the modifier convention the box-zoom drags
|
||||||
|
already use:
|
||||||
|
|
||||||
|
| keys | effect |
|
||||||
|
| --- | --- |
|
||||||
|
| `+` / `-` | zoom both ranges about the plot centre (unchanged) |
|
||||||
|
| `ctrl` + `+` / `-` | zoom the x axes only |
|
||||||
|
| `alt` + `+` / `-` | zoom the y axes only |
|
||||||
|
|
||||||
|
**Not `shift`, and this is the one deliberate deviation from the approved design.** On the
|
||||||
|
common layouts `+` *is* shift+`=`, and `key_from()` (`adapters/gtk4/gtk_input.hpp:258`) folds
|
||||||
|
both onto `kKeyZoomIn` precisely for that reason — so a bare zoom-in keystroke already arrives
|
||||||
|
with `Mod::Shift` set, and `shift + (+/-)` cannot be distinguished from `+`. Making Shift mean
|
||||||
|
x-only would turn every plain zoom into an x-only zoom. Ctrl and Alt are unambiguous for keys,
|
||||||
|
and they are already the controller's drag convention (`ctrl`+drag = x only, `alt`+drag = y
|
||||||
|
only), so the keyboard and the mouse-drag rows of the overview read the same. The **wheel**
|
||||||
|
keeps its own `shift`=x / `ctrl`=y: that is upstream behaviour with upstream tests, and the
|
||||||
|
overview shows both honestly rather than hiding one.
|
||||||
|
|
||||||
|
The change is smaller than it looks. `KeyEvent` already carries `mods`
|
||||||
|
(`include/plot/input/events.hpp:75`) and `PlotArea::on_key_pressed` already sets
|
||||||
|
`e.mods = mods_from(state)` (`adapters/gtk4/gtk_plot_area.cpp:862`), so no adapter change, no
|
||||||
|
new key constant, and no `key_from()` change: the whole change is the `if (zooms)` block of
|
||||||
|
`Controller::on_key`, which gains the same `has_mod(e.mods, Mod::Ctrl)` / `Mod::Alt` read the
|
||||||
|
drag branch makes at `src/input/controller.cpp:493-495` and skips the axis class the modifier
|
||||||
|
excluded. A comment on `kKeyZoomIn` records why Shift is not available here, so the next person
|
||||||
|
to reach for it finds the reason.
|
||||||
|
|
||||||
|
Two tests in the controller's existing suite — `ctrl_plus_zooms_x_only_and_leaves_y` and
|
||||||
|
`alt_plus_zooms_y_only_and_leaves_x` — then the submodule commit and the pin move.
|
||||||
|
|
||||||
|
### The application's keys
|
||||||
|
|
||||||
|
| keys | action |
|
||||||
|
| --- | --- |
|
||||||
|
| `Space` | pause / resume |
|
||||||
|
| `a` | arm / disarm the trigger |
|
||||||
|
| `t` | trigger panel |
|
||||||
|
| `c` | cursors pane |
|
||||||
|
| `m` | measurements pane |
|
||||||
|
| `g` | math pane |
|
||||||
|
| `l` | layout editor |
|
||||||
|
| `s` | side panel |
|
||||||
|
| `Ctrl+E` | export the visible range |
|
||||||
|
| `F1` | help |
|
||||||
|
| `Ctrl+slash` | shortcut overview |
|
||||||
|
| `Ctrl+Q` | quit |
|
||||||
|
|
||||||
|
Bare letters for the toggles, Ctrl for the things that touch files or windows. The plot's own
|
||||||
|
keys — `+`, `-`, `f`, the arrows, `Escape` — are not in this set and are not shadowed.
|
||||||
|
|
||||||
|
One `GtkEventControllerKey` on the toplevel, in the **bubble** phase. That is what makes a
|
||||||
|
focused widget win its own keys: the plot area returns `FALSE` for everything `key_from()` maps
|
||||||
|
to `0`, which is every letter in this table and `Space` too, so they bubble up to this
|
||||||
|
controller. A focused `GtkEntry` or `GtkSpinButton` consumes its printable keys first, so
|
||||||
|
typing into the window entry or a math field is unaffected. A focused `GtkButton` consumes
|
||||||
|
`Space` to activate itself, so `Space` pauses only when focus is not on a button — ordinary GTK
|
||||||
|
behaviour, and the right outcome. The handler additionally refuses every app key while the
|
||||||
|
focused widget is a `GtkEditable`: belt and braces, and the property a probe can assert, since
|
||||||
|
a probe calls the handler directly and so bypasses the bubbling the first guarantee rests on.
|
||||||
|
|
||||||
|
### The overview window
|
||||||
|
|
||||||
|
Hand-rolled, not `GtkShortcutsWindow`: that widget was deprecated in GTK 4.18, this project
|
||||||
|
builds `-Wall -Wextra -Wpedantic` clean, and 4.22 is what is installed.
|
||||||
|
|
||||||
|
A `GtkWindow` holding a `GtkScrolledWindow` over one two-column `GtkGrid` per section: the
|
||||||
|
accelerator on the left in a monospace face, the description on the right. Sections:
|
||||||
|
**Acquisition**, **Panes and editors**, **Files and windows**, **Plot keyboard**, **Plot
|
||||||
|
mouse**. The mouse section is the controller's gesture table in prose — `shift`/`ctrl`/`alt` +
|
||||||
|
drag for the three box zooms, drag and right-drag to pan, middle-drag on each label strip for
|
||||||
|
an axis zoom, the wheel with its two modifiers, horizontal wheel to pan x, double-click to
|
||||||
|
re-fit.
|
||||||
|
|
||||||
|
## The accelerator table
|
||||||
|
|
||||||
|
`src/shortcuts.hpp` holds **one** table, and both consumers read it:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
enum class AppAction { PauseToggle, TriggerArm, TriggerPanel, CursorsPane, MeasuresPane,
|
||||||
|
MathPane, LayoutEditor, SidePanel, Export, Help, Shortcuts, Quit };
|
||||||
|
|
||||||
|
struct Accel {
|
||||||
|
unsigned keyval; ///< GDK keyval
|
||||||
|
unsigned mods; ///< GdkModifierType mask, 0 for a bare key
|
||||||
|
AppAction action;
|
||||||
|
const char* accel_text; ///< what the overview prints
|
||||||
|
const char* description;
|
||||||
|
const char* section;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`ui.cpp`'s handler looks up `(keyval, mods)` in this table to get an `AppAction` and switches on
|
||||||
|
that; the overview renders `accel_text` and `description` from the same rows. A key that works
|
||||||
|
and a key that is documented therefore cannot drift apart — which is the failure this project
|
||||||
|
has hit before in another form, two comments in `build_toolbar()` disagreeing with each other
|
||||||
|
and one of them with reality.
|
||||||
|
|
||||||
|
The mouse gestures are a second, display-only table in the same file — a gesture text and a
|
||||||
|
description, with no keyval, since nothing dispatches them — because they are rendered by the
|
||||||
|
same window and belong beside the keys they are compared against.
|
||||||
|
|
||||||
|
## The help window
|
||||||
|
|
||||||
|
A `GtkWindow` split in two: a `GtkListBox` of page titles on the left, a `GtkScrolledWindow`
|
||||||
|
holding one wrapping `GtkLabel` on the right. Selecting a row swaps the label's markup. Opened
|
||||||
|
by `F1` and from the overflow menu; **non-modal**, so it can sit beside the scope while a user
|
||||||
|
follows it.
|
||||||
|
|
||||||
|
Each page is a file of **Pango markup** in a GResource (`src/help/*.txt`, bundled by
|
||||||
|
`src/help/udpscope-help.gresource.xml`, compiled by the same `glib-compile-resources` custom
|
||||||
|
command pattern the icons use). Pango markup rather than Markdown because `GtkLabel` renders it
|
||||||
|
natively — bold, italic and monospace spans with no parser to write — and a malformed span is
|
||||||
|
caught by `pango_parse_markup()` at load time rather than drawn wrong. A GResource rather than
|
||||||
|
files beside the binary for the reason already recorded for the icons: udpscope is run straight
|
||||||
|
out of the build tree and has no install step, so a search path would have to be located at
|
||||||
|
runtime and the failure mode of not locating it is silent.
|
||||||
|
|
||||||
|
A page's title, resource path and section header are one entry in a static table in
|
||||||
|
`help_window.cpp`, so the order on screen is explicit in code rather than implied by file names.
|
||||||
|
The section headers are non-selectable rows in the `GtkListBox`.
|
||||||
|
|
||||||
|
### Page shape
|
||||||
|
|
||||||
|
Every page has the same four parts:
|
||||||
|
|
||||||
|
1. what the feature does,
|
||||||
|
2. how to reach it,
|
||||||
|
3. **Worth knowing** — the surprising behaviour, distilled from the invariants in `CLAUDE.md`
|
||||||
|
and `README.md`,
|
||||||
|
4. **Example** — one worked case with real numbers taken from the probes, not round ones.
|
||||||
|
|
||||||
|
The approved template, which every other page follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
Time window
|
||||||
|
───────────
|
||||||
|
|
||||||
|
The window is how much time the plot shows. Set it from the header bar's
|
||||||
|
window button: type a number, pick a unit, or step a decade with the arrows.
|
||||||
|
|
||||||
|
Every row shares one window. The trigger's event span follows it, and the
|
||||||
|
pre-trigger share is a share of *this* window.
|
||||||
|
|
||||||
|
Worth knowing
|
||||||
|
A window too long to store at the wire rate is decimated, not refused. The
|
||||||
|
shared clock takes a power-of-two stride; each group of samples is
|
||||||
|
*averaged*, never strided, so a tone above the stored Nyquist is attenuated
|
||||||
|
instead of aliasing to a false low frequency.
|
||||||
|
|
||||||
|
Moving the window across a decimation boundary rebuilds the buffer and drops
|
||||||
|
the history.
|
||||||
|
|
||||||
|
Example
|
||||||
|
1 MS/s, 200 MB of buffer. A 2 s window stores every sample (time x1). Widen
|
||||||
|
to 600 s and the stride becomes x16: 625 S/s stored, anti-aliased at
|
||||||
|
312.5 Hz.
|
||||||
|
```
|
||||||
|
|
||||||
|
### The pages
|
||||||
|
|
||||||
|
**Getting started**
|
||||||
|
|
||||||
|
1. **Overview** — what each part of the window is: toolbar, side panel, plot rows, status bar.
|
||||||
|
The map the other pages refer back to.
|
||||||
|
2. **Choosing a source** — the startup chooser, why the list shows only sessions that name a
|
||||||
|
signal, what `×` deletes, the New host… form, and that a stored multicast session reconnects
|
||||||
|
to its group and interface.
|
||||||
|
3. **Signals and calibration** — signals start **disabled** and enabling is what starts
|
||||||
|
acquisition; a signal on no plot still gets a grey chip; calibration is applied at ingest, so
|
||||||
|
changing it rebuilds the buffer and drops history.
|
||||||
|
4. **Rows, layout and drag & drop** — drag a signal's chip from the side panel onto a row to
|
||||||
|
plot it; four slots per row; a signal's colour follows the *signal*, not the slot, so the same
|
||||||
|
signal on two rows is one colour; the layout editor's `+`/`×`, and that inserting a row
|
||||||
|
carries the existing rows' contents down with them rather than renumbering under the user.
|
||||||
|
|
||||||
|
**Viewing**
|
||||||
|
|
||||||
|
5. **Time window** — the template above.
|
||||||
|
6. **Navigating the plot** — the keyboard and mouse gestures in prose, plus what a *pinned* view
|
||||||
|
is: while an event is held, a capture is filling or the view is paused, a zoom in works but
|
||||||
|
the view cannot leave the data.
|
||||||
|
7. **Pause and play** — pausing freezes the *display*, not the producer: the ring keeps filling,
|
||||||
|
the view binds a snapshot, and the axis is pinned to the snapshot's own stamps, so a window
|
||||||
|
reaching back past the oldest retained sample shows a shorter span rather than an empty
|
||||||
|
margin.
|
||||||
|
8. **Trigger** — arm/disarm, edge and level, Normal vs Single, the pre-trigger share as a share
|
||||||
|
of *this* window, the four display states (live, capturing, held, waiting), that the trigger
|
||||||
|
sees the **stored** filtered signal, and that disarming mid-capture publishes what the capture
|
||||||
|
got to.
|
||||||
|
|
||||||
|
**Analysis**
|
||||||
|
|
||||||
|
9. **Cursors** — the shared vertical pair versus the per-row rulers, the four modes, what `mix`
|
||||||
|
reads, and that a marker past the newest sample reads `--`.
|
||||||
|
10. **Measurements** — the types, the threshold share, the on-plot marks, that a pass is
|
||||||
|
throttled to a cadence so a ruler is stepped, and that an invalid pass keeps its level marks
|
||||||
|
and drops its time marks.
|
||||||
|
11. **Math channels** — a graph evaluated as an ordered list, so a cycle is impossible by
|
||||||
|
construction; a derived channel *is* a channel; the filters run at the **stored** rate, so a
|
||||||
|
cutoff moves when the window changes the decimation; the output defaults to the last node.
|
||||||
|
|
||||||
|
**Output and state**
|
||||||
|
|
||||||
|
12. **Saving and export** — `.npy` and `.csv`, the visible range at the **stored** rate and never
|
||||||
|
the LOD buckets, the `time`/`stream_time` columns, and that the table is collected at click
|
||||||
|
time rather than when the file chooser returns.
|
||||||
|
13. **Sessions and settings** — where the files live, what a session persists (window, rows,
|
||||||
|
calibration, enabled signals, math graphs, measurements), when it is written, and why a
|
||||||
|
session whose streamer was down during a run is not overwritten.
|
||||||
|
14. **Status bar and diagnostics** — the glance line, what is behind ⓘ, and what `gaps`,
|
||||||
|
`reconnects` and the starvation notice actually mean.
|
||||||
|
|
||||||
|
## The status bar
|
||||||
|
|
||||||
|
### The glance line
|
||||||
|
|
||||||
|
One row on the bar, four fields and no more:
|
||||||
|
|
||||||
|
```
|
||||||
|
● live · 10.0 kS/s · 2 s window ⓘ
|
||||||
|
```
|
||||||
|
|
||||||
|
The badge is unchanged — one glyph in three colours. The state field becomes one short word:
|
||||||
|
`live`, `paused`, `armed`, `capturing`, `event held`, `disarmed`. The long prose it carries
|
||||||
|
today (`trigger: EVENT HELD (Arm to re-arm, or 'Go live' for streaming)`) moves into the
|
||||||
|
popover. Trigger mode adds exactly one field, `· event 4.2 s ago`.
|
||||||
|
|
||||||
|
Each field's width is bounded, for the reason `CLAUDE.md` already gives for `rate` collapsing
|
||||||
|
to one figure and for `format_elapsed()` stepping units: the status row sits directly above the
|
||||||
|
plot, so a field whose width tracks the stream relayouts the plot. That is why the state field
|
||||||
|
is a word from a fixed set rather than a sentence.
|
||||||
|
|
||||||
|
The transient notice (`App::notice`) stays on the bar, prefixed to the glance line as it is
|
||||||
|
today: it is the one piece of text that must be seen without a click.
|
||||||
|
|
||||||
|
### The details popover
|
||||||
|
|
||||||
|
Behind an ⓘ `GtkMenuButton` at the end of the row, a `GtkPopover` over a `GtkGrid` of
|
||||||
|
label/value pairs:
|
||||||
|
|
||||||
|
```
|
||||||
|
Stream connected, configured
|
||||||
|
packets 717 · frames 716 · gaps 0
|
||||||
|
3 signals · 3 channels
|
||||||
|
Buffer 200.0 MB · 1677 s span · time x1
|
||||||
|
stored 10.0 kS/s (wire 10.0 kS/s)
|
||||||
|
Plot 1 3 traces · 195000 samples
|
||||||
|
Reconnects 0
|
||||||
|
```
|
||||||
|
|
||||||
|
It is rewritten **only while it is visible**. A popover nobody opened costs nothing per frame,
|
||||||
|
and a visible one cannot relayout the plot because a popover is its own surface — which is the
|
||||||
|
property that makes the full-width detail safe to show at all.
|
||||||
|
|
||||||
|
### One composition, two consumers
|
||||||
|
|
||||||
|
`src/status_text.hpp` holds a `StatusSnapshot` of the fields the tick already computes, and two
|
||||||
|
formatters:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::string format_glance(const StatusSnapshot&); ///< the bar
|
||||||
|
std::string format_detail(const StatusSnapshot&); ///< the popover, and the exit dump
|
||||||
|
```
|
||||||
|
|
||||||
|
`CLAUDE.md` asserts that `rate` and `last event … ago` are checked in the exit dump's
|
||||||
|
`dbg_mode_live`/`dbg_mode_triggered`, which today are `gtk_label_get_text(app->status_label)`.
|
||||||
|
The tick therefore still assembles the complete detail text every frame, into
|
||||||
|
`App::status_detail`; the dump reads **that** instead of the label, and the popover formats
|
||||||
|
from the same snapshot. The existing dump assertions keep working unchanged, and the detail is
|
||||||
|
not a second expression of fields the glance line also computes.
|
||||||
|
|
||||||
|
## Files touched
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
| --- | --- |
|
||||||
|
| `src/build_info.hpp` | new: declares the three constants |
|
||||||
|
| *generated* `build_info.cpp` | new: defines them, rewritten every build |
|
||||||
|
| `src/shortcuts.hpp/.cpp` | new: the accelerator table and the mouse-gesture table |
|
||||||
|
| `src/help_window.hpp/.cpp` | new: `show_help_window()`, `show_shortcuts_window()`, `show_about_dialog()` |
|
||||||
|
| `src/help/*.txt`, `src/help/udpscope-help.gresource.xml` | new: the fourteen pages |
|
||||||
|
| `src/status_text.hpp/.cpp` | new: `StatusSnapshot`, `format_glance()`, `format_detail()` |
|
||||||
|
| `src/ui.cpp` | the key controller, three menu items, the ⓘ button and popover, the glance label, `App::status_detail`, the new probes |
|
||||||
|
| `CMakeLists.txt` | `VERSION`, the build-info command, the help resource, the new sources |
|
||||||
|
| `deps/plotter` | `Controller::on_key`'s zoom branch, two tests, the pin move |
|
||||||
|
| `CLAUDE.md`, `README.md` | the documentation this project requires for both |
|
||||||
|
|
||||||
|
The three windows share one translation unit because they share one purpose, the GResource
|
||||||
|
access and the accelerator table. `src/ui.cpp` is already ~7400 lines and the largest file in
|
||||||
|
the project, so none of them goes in it.
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
| Case | Behaviour |
|
||||||
|
| --- | --- |
|
||||||
|
| A help page's resource is missing | the page's label reads a one-line "this page is missing" and the probe's `pages`/`parsed` counts disagree; nothing else is affected |
|
||||||
|
| A help page's markup is malformed | `pango_parse_markup()` fails, the page falls back to the same line, and `parsed` < `pages` |
|
||||||
|
| `git` absent or not a checkout | `kCommit` is `unknown`; the build succeeds |
|
||||||
|
| An app key pressed while a `GtkEditable` has focus | refused, the widget keeps it |
|
||||||
|
| A window already open | presented, not rebuilt — each of the three keeps one static instance pointer, and closes with `gtk_window_close()`, never `gtk_window_destroy()`, so the handler that clears the pointer runs (the trap the math dialog hit) |
|
||||||
|
| The ⓘ popover opened with no CONFIG yet | the detail fields read what they read today in that state; no special case |
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Probes on stderr in the style of the existing ones, since this project's whole regression net is
|
||||||
|
those probes and `CLAUDE.md` requires extending them rather than adding a harness.
|
||||||
|
|
||||||
|
- **`UDPSCOPE_PROBE_HELP_MS`** — open all three windows, report, close them. Equal parts crash
|
||||||
|
test and content check, the way `UDPSCOPE_PROBE_MATH_MS` is:
|
||||||
|
|
||||||
|
```
|
||||||
|
[help] pages=14 parsed=14 shortest='Overview' 412 B; shortcuts: app=12 mouse=9;
|
||||||
|
about version=0.1.0 commit=53815ad built=2026-10-04 14:22 UTC
|
||||||
|
settings=/home/martino/.config/udpscope; closed ok
|
||||||
|
```
|
||||||
|
|
||||||
|
**`parsed` must equal `pages`**, which is what catches a malformed span as a number instead of
|
||||||
|
as mangled text on screen; **no page may be empty**, which catches a resource listed in the
|
||||||
|
`.xml` and never written; `settings=` must be a real path. `closed ok` with no critical is the
|
||||||
|
lifecycle assertion.
|
||||||
|
|
||||||
|
- **`UDPSCOPE_PROBE_KEYS_MS`** — fire every row of the accelerator table at the handler directly
|
||||||
|
(the way the other probes emit `::clicked`) and report the state each one moved:
|
||||||
|
|
||||||
|
```
|
||||||
|
[keys] a: armed 0->1 | space: paused 0->1 | c: cursors 0->1 | ... |
|
||||||
|
editable focused: 'm' refused=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Every row must move exactly its own state and nothing else, and the last field must read
|
||||||
|
`refused=1`.
|
||||||
|
|
||||||
|
- **`UDPSCOPE_PROBE_ZOOMKEYS_MS`** — the new plotter keys through the app, inside a **pinned**
|
||||||
|
view, so run it with the pause probe, which makes a stable pin
|
||||||
|
(`UDPSCOPE_PROBE_PAUSE_MS=1500 UDPSCOPE_PROBE_ZOOMKEYS_MS=2200`):
|
||||||
|
|
||||||
|
```
|
||||||
|
[zoomkeys] ctrl+plus x=[..]->[..] y_held=1 | alt+plus y=[..]->[..] x_held=1
|
||||||
|
```
|
||||||
|
|
||||||
|
Both `_held` must be 1 and both zooms must still happen: "a pinned view is navigable within
|
||||||
|
its data" is the invariant, and a modified zoom must not become its exception.
|
||||||
|
|
||||||
|
- **The status bar**, in the exit dump:
|
||||||
|
|
||||||
|
```
|
||||||
|
status bar: glance=[● live · 10.0 kS/s · 2 s window] max_glance_chars=38
|
||||||
|
detail_fields=11 popover_rebuilds=0
|
||||||
|
```
|
||||||
|
|
||||||
|
**`max_glance_chars` must stay under a small constant** over the whole run — that is "layout
|
||||||
|
must not depend on content" read from the one label still on the bar. **`popover_rebuilds`
|
||||||
|
must be 0** on a run that never opened it.
|
||||||
|
|
||||||
|
- **The plotter's own suite** — `xvfb-run -a deps/plotter/build/debug/tests/plot_tests`, `0
|
||||||
|
failed`, with the two new controller tests.
|
||||||
|
|
||||||
|
- **A full existing run must be unchanged.** The status refactor touches the line every other
|
||||||
|
probe's dump is read beside, so one `UDPSCOPE_EXIT_AFTER_MS` run with
|
||||||
|
`UDPSCOPE_ENABLE_ALL_MS` must still report `gaps=0`, `x-desync 0`, `0 collapsed, 0 staircase`
|
||||||
|
and every toolbar icon `(ok)`.
|
||||||
|
|
||||||
|
`CLAUDE.md` gains these probes, plus invariants for the shared accelerator table, the
|
||||||
|
Shift-is-unexpressible reason behind ctrl/alt, and the one-composition-two-consumers status
|
||||||
|
rule. `README.md` gains the four features and records the deviation from the approved
|
||||||
|
`shift + (+/-)`.
|
||||||
Reference in new issue
Block a user