diff --git a/README.md b/README.md index 50cb4d2..4c52a0a 100644 --- a/README.md +++ b/README.md @@ -730,7 +730,11 @@ same session twice). Connector settings are stored even for a configuration with which is why those rows exist at all. The app's menu has **Configurations…**: the same list for the running source, with *Save this session* — a name and a note — and a delete button per row. Opening one saves the current session first, so -switching is never the gesture that loses what it switches from. +switching is never the gesture that loses what it switches from. Beside **Preferences…** the chooser +also carries a **help menu** with the window's three help entries — the manual, the contextual-help +toggle and the guided tour — because the chooser is shown to exactly the user who has not started yet +and does not know how to; the tour is started by the app once its window is up (see +[the help section](#the-help-submenu-the-contextual-help-and-the-guided-tour)). A session written before configurations existed is imported on first use as the configuration named `default`: copied into the directory, remembered as active, and the old file renamed to @@ -901,14 +905,32 @@ button is a sibling of the heading's title, not an overlay on its corner, for th toolbar's icons are not overlaid either: an overlay child sits *on* whatever is under it, and the one surface whose corner is free (the plot) is the one that uses one. -**Off means gone, not greyed.** The toggle removes the buttons rather than disabling them, and -which buttons exist is decided by a data key rather than a list of pointers, so the math editor -- -whose heading is created afresh every time it opens -- cannot leave a stale pointer behind. The -setting is `contextual_help` in `settings.toml`, written by +**Off means gone, not greyed, and off is the default.** The toggle removes the buttons rather than +disabling them, and which buttons exist is decided by a data key rather than a list of pointers, so +the math editor -- whose heading is created afresh every time it opens -- cannot leave a stale +pointer behind. The setting is `contextual_help` in `settings.toml`, written by `Settings`/`load_settings()`/`save_settings()`: the same file, the same three functions and the same -`~/.config/udpscope/settings.toml` the Preferences dialog writes. The dialog's own rows start from -`load_settings()`, so a save from there carries the two help keys through untouched; the Help menu's -row is the only place that states `contextual_help`, which is why the row's check *is* the setting. +`~/.config/udpscope/settings.toml` the Preferences dialog writes. **A fresh install has no such key +and the default is `false`**, so a first run shows the window as anyone would expect it and the layer +is asked for -- from the Help menu, from the source chooser's own help menu, or with `Ctrl+H`. The +dialog's own rows start from `load_settings()`, so a save from there carries the two help keys +through untouched; the Help menu's row is the only place in the *window* that states `contextual_help` +(which is why the row's check *is* the setting), and the chooser's row states it too, before any +window exists. + +**The source chooser carries the same three affordances, because it is shown to exactly the user who +has not started yet.** Its header bar has a help `GtkMenuButton` beside *Preferences*, whose rows are +the window's own `menu_button()`/`menu_toggle()` -- the same shape, and the same `.udpscope-menu` +stylesheet, whose rules `choose_source()` installs before building them since the app installs them +later. *Documentation* opens the manual directly (it is another process and needs nothing of the app), +*Enable contextual help* writes the settings file the app reads a moment later, and *Guided tour* +cannot start anything -- the tour is drawn on the app's window, which does not exist yet -- so it sets +**`Config::tour_pending`**, the value `main()` owns and hands to both the chooser and the front end +(`open_waveform` crosses the same way). Half a second and a half after the window is up, the same +delay the first-run auto-start uses, the pending request starts the tour; it sets `tour_requested` +rather than `tour_auto_started`, so the readings say which of the two it was. The chooser's rows show +**no accelerators** -- the window prints `F1`/`Ctrl+T` because the window binds them, and this dialog +binds none. **The guided tour is five steps on a `GtkOverlay`** inserted above the window's content: the plot, the side panel, the trigger, the signals and the status line. There is no popover anywhere in it and @@ -2439,7 +2461,14 @@ identifier `org.udpscope.app` is the desktop file's own basename, so it is the ` GApplication id and the install directory at once. The set is installed by an `install()` rule to `share/help/C/org.udpscope.app/` — the path the freedesktop Help System Specification's `help:` URI resolves — and `show_help()` hands `help:org.udpscope.app` to whatever the desktop registered for the -scheme (Yelp on GNOME), reporting a launch failure with an alert rather than failing silently. +scheme (Yelp on GNOME), reporting a launch failure with an alert rather than failing silently. That +alert says **which** thing is missing, because the two have different fixes: `help_installed_path()` +looks for the document under `$XDG_DATA_HOME/help//org.udpscope.app/` then +`$XDG_DATA_DIRS/help//org.udpscope.app/`, so with the set installed the alert names the missing +*viewer* (`yelp`), and without it the alert names the identifier, the expected installed path and the +install command — `cmake --install build --prefix ~/.local`, which writes +`~/.local/share/help/C/org.udpscope.app/`. A build tree has neither the installed copy nor a +`help:` handler, and **either is enough to make `F1` do nothing**; that is the failure this names. `cmake --build build --target test-help` checks the set the way `test-ui` checks the `.ui` files: `yelp-check`'s offline subcommands (links, media, orphans, ids) and a `yelp-build html` render; `yelp-check validate` is left out because it fetches the Mallard RelaxNG schema over the network. @@ -2447,7 +2476,11 @@ scheme (Yelp on GNOME), reporting a launch failure with an alert rather than fai (GTK4 has to be forced to the Cairo renderer or X11 reads the surface back blank). `UDPSCOPE_PROBE_HELP_MS` asserts the set is *present in the tree* — its `[help] topics=… index=… figures=…` reading — rather than parsing pages, since they are files now -and not resources. Mallard is translatable with `itstool`, but only the `C` set is authored here. +and not resources, and it **launches the manual too**: `[help] launch attempted=1 done=1 ok=1 +error='' installed=/home/…/.local/share/help/C/org.udpscope.app` is the reading, `ok=1` meaning the +`help:` URI reached a registered handler and not that a window appeared (Yelp is another process and +cannot map one headlessly). Mallard is translatable with `itstool`, but only the `C` set is authored +here. **The status bar is two tiers.** The bar itself shows one glance line: the activity badge, the display state (one word from a fixed set — `connecting`, `live`, `paused`, `armed`, `capturing`, diff --git a/docs/claude/invariants-gtk-and-chrome.md b/docs/claude/invariants-gtk-and-chrome.md index 2bb3d36..b4c7ce3 100644 --- a/docs/claude/invariants-gtk-and-chrome.md +++ b/docs/claude/invariants-gtk-and-chrome.md @@ -127,7 +127,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear honest without knowing which dialog it is. One `GtkWidget*` (or `AdwDialog*`) per window, nulled in its own close handler, is the shape that prevents the double-open defect. `close_help_windows()` (misnamed now) closes just the two dialogs; a failed *launch* is reported by the launcher's own - callback with a `GtkAlertDialog`, so a click that cannot open the manual says so. + callback with a `GtkAlertDialog`, so a click that cannot open the manual says so. That callback is + also `show_help()`'s **optional reporter** (`std::function`) + — the one hook that runs on the success path, where no window of ours is left to observe; the probe + uses it and nothing else does. - **`build_info.cpp` is generated, never committed.** `cmake/write_build_info.cmake` runs at configure time (so `add_executable` can name the file) and again on every build through `udpscope_build_info` (so the commit and the timestamp belong to the binary that is running). @@ -149,7 +152,21 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear → `org.udpscope.app`), so it is the `.desktop` name, the GApplication id and the install directory at once; `UDPSCOPE_HELP_NAME` and `UDPSCOPE_HELP_DIR` are compiled in from that one place. `show_help()` opens `help:` with `GtkUriLauncher` (Yelp runs in its own process, so - nothing here holds a pointer to it), and its failure callback shows a `GtkAlertDialog`. + nothing here holds a pointer to it), and its failure callback shows a `GtkAlertDialog`. That + failure is **careful about which thing is missing**: `gtk_uri_launcher_launch_finish()` reports a + launch that could not be handed over — the *handler's* absence — and cannot tell that from a + document that was never installed, so `help_installed_path()` walks the specification's own + `$XDG_DATA_HOME/help///` then `$XDG_DATA_DIRS/help///` for an `index.page` and + the dialog says which one it is: with the manual there, install a viewer (for example `yelp`); with + it absent, the identifier, the expected installed path and the install command + (`cmake --install build --prefix ~/.local`, which writes `~/.local/share/help/C//`) — and a + viewer as well, the second possibility the error alone cannot rule out. Reporting a missing + *document* as a missing *handler* is the defect that shape avoids. + + `UDPSCOPE_PROBE_HELP_MS` launches it too, and its `[help] launch attempted= done= ok= error='' + installed=` line is the reading — `ok=1` is the URI handed to a viewer and not a window on screen, + since Yelp is another process and cannot map one headlessly; `installed=` sits beside it because a + failing `ok=0` has to be read against whether the document was there at all. `UDPSCOPE_PROBE_HELP_MS` no longer parses pages — they are files, not markup in a resource — so its `[help]` line asserts the set is *there*: `topics` (the `.page` files other than @@ -163,6 +180,25 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear (GTK4 must be forced to the Cairo renderer, `GSK_RENDERER=cairo`, or the X11 surface reads back blank). Mallard is translatable with `itstool`, but there is no i18n set-up here: only the `C` set is authored. +- **The chooser carries the same help affordances as the window, and the tour request crosses as a + value — not a global.** The source chooser runs *before* `App` exists, so a user who has not started + yet is the one least able to find the help; its header bar therefore has a help `GtkMenuButton` + (the `.ui` holds the button, code holds the rows) whose three entries are the window's Help view's + the same way — `ui_impl::menu_button()`/`menu_toggle()`, the app's own row shape — with the + `.udpscope-menu` class on the popover. That class's rules only exist once `install_css()` has run, so + `choose_source()` calls it before building the rows (`install_css()` is idempotent precisely because + it now has two callers; the app's `on_activate()` still calls it and is a no-op by then). The rows + show **no accelerators**: the window's Help view prints `F1`/`Ctrl+T` because the window binds them, + and this dialog binds none, so a key printed here would be documentation that does nothing. + *Documentation* calls `show_help()` straight from the chooser (the manual is another process and + needs nothing of the app); *Enable contextual help* writes the settings file, which is what the app + reads when it starts a moment later; *Guided tour* cannot start anything — the tour is drawn on the + app's own window — so it sets **`Config::tour_pending`**, the one value `main()` owns and hands to + both `choose_source()` and `run_ui()` (`open_waveform` and the preferences' relative time cross the + same way). `on_activate()` schedules `tour_start_requested()` 1.5 s later, the same delay the + first-run auto-start uses, and **that callback is separate from `tour_autostart()` on purpose**: it + sets `tour_requested`, not `tour_auto_started`, so `help layer:` says which of the two started the + tour instead of conflating a user's request with a first run. - **A `GtkPaned` hands its start child at least its minimum unless the position says otherwise.** Putting the events pane in a paned is what makes its width the user's to drag -- but with the position left alone, `shrink-start-child` or not, the pane's minimum is reserved as an empty column diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 4efce08..7ad77dc 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -395,17 +395,21 @@ Two lines of the self-check output are assertions in disguise: on the first tick for the same reason the toolbar's are. **Both must read `(ok)`**: the page titles are empty on purpose, so an icon that does not resolve leaves a *blank* pill, and a blank pill looks exactly like a page with nothing in it. -- `help layer: contextual=on hints=8 ids={events plot status cursors measures trigger signals window} icon=dialog-question-symbolic(ok); tour=idle steps=5 step=0 auto=1` (in the exit +- `help layer: contextual=on hints=8 ids={events plot status cursors measures trigger signals window} icon=dialog-question-symbolic(ok); tour=idle steps=5 step=0 auto=1 requested=0` (in the exit dump, beside `switcher:`) — the help affordance layer as the last tick found it. **`hints=` must equal the number of ids**, and the ids are the surfaces the contextual help is on: the eight of the window itself, and `math` only while the math editor is open (its heading lives in a builder that is rebuilt - on every opening, so it is not one of the window's own). `contextual=off` beside `hints=0` is the - toggle's other state — **off is *gone*, not greyed**, so a non-zero count with `off` is a row that hid - its buttons instead of removing them. **`icon=` must read `(ok)`**: a `?"` whose name Adwaita lacks is a + on every opening, so it is not one of the window's own). The layer is **off by default** + (`contextual_help` in `settings.toml` is false for a missing file), so a fresh-config run reads + `contextual=off hints=0` and that line above is the state *after* the Help row was switched on; + `contextual=off` beside a non-zero count is the toggle not having removed its buttons — **off is + *gone*, not greyed**. **`icon=` must read `(ok)`**: a `?"` whose name Adwaita lacks is a *blank* control that still works, the failure the pane [edit] buttons already paid for once. `tour=` - is `running`/`idle` with the plan's `steps=` and the current `step=`, and `auto=1` is the first-run + is `running`/`idle` with the plan's `steps=` and the current `step=`; `auto=1` is the first-run auto-start having fired *in this process* — the flag is `tour_seen` in `settings.toml`, so a second run - against the same config directory reads `auto=0`. + against the same config directory reads `auto=0` — and `requested=1` is the **chooser's** "Guided tour" + (`Config::tour_pending`) having been carried into this run instead, which is `auto=0` because it is not + the auto-start. - `help figures: trigger_head=X,Y,W,H trigger_summary=… trigger_panel=… signal_list=… side_switcher=… side_stack=… window_button=… status_label=… header_bar=… plot=…` (in the exit dump, beside `help layer:`) — the **window-coordinate** rectangle of each surface the help's annotated figures label, `x,y,width,height`, or `-1,-1,0,0` for a surface that is **not on screen**. It is @@ -634,7 +638,7 @@ Two lines of the self-check output are assertions in disguise: so the panes the tick rewrites are measured directly. The `[width-probe]` lines are the per-change detail behind the `axes.w changes` count, and a step over 100 px dumps the widget tree's wide subtrees — that is what the documented, intermittent ~320 px step wants read. -- `[help] topics=14 index=1 figures=3 dir=/…/help/C/org.udpscope.app; shortcuts: app=14 plot=14 tables=28 ui=28 ui_accel=21 ui_icon=7 ui_bare=0; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — the manual is a Mallard set on disk now, not a GResource in the binary, so this is a **file-system reading**: `topics` is the `.page` files other than `index.page`, `index` must be 1, and `figures` counts the screenshots under `figures/`. **`topics=0 index=0` means the manual is missing** from `dir` (the source directory the build was configured from, `UDPSCOPE_HELP_DIR`) — the finding the old `parsed != pages` used to name. The manual itself opens in Yelp, another process, which this probe does not and cannot exercise. The `shortcuts:` numbers are the shortcut overview four ways. +- `[help] topics=14 index=1 figures=3 dir=/…/help/C/org.udpscope.app; shortcuts: app=14 plot=14 tables=28 ui=28 ui_accel=21 ui_icon=7 ui_bare=0; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — the manual is a Mallard set on disk now, not a GResource in the binary, so this is a **file-system reading**: `topics` is the `.page` files other than `index.page`, `index` must be 1, and `figures` counts the screenshots under `figures/`. **`topics=0 index=0` means the manual is missing** from `dir` (the source directory the build was configured from, `UDPSCOPE_HELP_DIR`) — the finding the old `parsed != pages` used to name. The manual itself opens in Yelp, another process, which the probe now launches and reports (the `[help] launch` line below). The `shortcuts:` numbers are the shortcut overview four ways. `app` and `plot` are the rows in the C tables -- what the application actually *handles* -- and `tables` is their sum. `ui`, `ui_accel`, `ui_icon` and `ui_bare` are what `src/ui/shortcuts.ui` *declares*, read out of the resource with a markup parse rather than off the built widgets, because a tree walk counts @@ -642,7 +646,8 @@ every row twice: `GtkShortcutsWindow` reaches each one from more than one place **`ui` must equal `tables`** -- two lists of one thing, and a row in one but not the other is a gesture that works and is not documented, or is documented and does nothing -- and **`ui_bare` must be zero**: a row with no accelerator and no icon is a cell reading "No Shortcut". `ui_accel + ui_icon == ui`, so those -two also catch a row whose accelerator went missing. `settings=` must be a real path (the same one the About dialog shows). **`icon=` must read `org.udpscope.app`** — it is read back through `adw_about_dialog_get_application_icon()` while the dialog is open, so the dialog is asserted to *carry* the icon rather than merely to open (a `set_application_icon()` lost in an edit leaves a dialog that opens, looks almost right, and shows a generic placeholder). `closed ok` with **no `Gtk-CRITICAL`** is as much a crash test of the two dialogs built, presented and closed in one run as it is a state check: they are closed by **two different calls**, so a mismatched pair crashes or leaks here. One is a `GtkWindow` (the shortcut overview, `gtk_window_close()`/`::close-request`); the About dialog is an `AdwDialog` and closes with `adw_dialog_close()`, cleared on `::closed`. The manual is *not* one of them — it opens in Yelp, a separate process, so there is nothing here to open, read back or close. +two also catch a row whose accelerator went missing. `settings=` must be a real path (the same one the About dialog shows). **`icon=` must read `org.udpscope.app`** — it is read back through `adw_about_dialog_get_application_icon()` while the dialog is open, so the dialog is asserted to *carry* the icon rather than merely to open (a `set_application_icon()` lost in an edit leaves a dialog that opens, looks almost right, and shows a generic placeholder). `closed ok` with **no `Gtk-CRITICAL`** is as much a crash test of the two dialogs built, presented and closed in one run as it is a state check: they are closed by **two different calls**, so a mismatched pair crashes or leaks here. One is a `GtkWindow` (the shortcut overview, `gtk_window_close()`/`::close-request`); the About dialog is an `AdwDialog` and closes with `adw_dialog_close()`, cleared on `::closed`. The manual is *not* one of them — it opens in Yelp, a separate process, so there is no window of ours to read back or close; the line below is the reading of that path instead. +- `[help] launch attempted=1 done=1 ok=1 error='' installed=/home/…/.local/share/help/C/org.udpscope.app` (stderr, `UDPSCOPE_PROBE_HELP_MS`, a moment after the line above) — the **launch of the manual**, the one part of the help path no other reading reaches: a successful launch leaves no window of ours, so nothing else can say whether `show_help()` did anything at all. `attempted=` is set at the call (`0` would be a probe that never got as far as calling it); `done=` says the asynchronous callback ran, and **`done=0` is the reading for a launch that never called back**; `ok=1` is the `help:` URI handed to a viewer, `ok=0` with the launcher's `error=` text is a launch that failed. **`ok` is not "a window appeared"**: Yelp is another process, and under a headless X server it cannot map one — the launch is *this* process's half (the URI reached a registered handler), which is exactly what the desktop does when a user presses F1, and what an absent handler or an uninstalled viewer breaks. `installed=` is `help_installed_path()`'s answer — the `$XDG_DATA_HOME`/`$XDG_DATA_DIRS` directory the document was found in, or `(not found)` — and it is what a failing `ok=0` should be read beside, because the launch error alone cannot tell a missing document from a missing handler. On a machine with the set installed by `cmake --install build --prefix ~/.local` and `yelp` present, the line reads `ok=1 … installed=~/.local/share/help/C/org.udpscope.app`. A **failure** leaves the launcher's `GtkAlertDialog` up on purpose — that is what a user gets, and the probe must not report a failed path as a silence. - `[menu] N 'label' accel='key' h=P label_x=X accel_right=R/W order=D ok=O`, one line per row, then `[menu] rows=R expected=E order=D rows_bad=B popover_visible=V mapped=M button_mapped=B` and `[menu] status pos=top bubble=WxH button_top=T button_right=R fits=F` (stderr, @@ -671,7 +676,10 @@ two also catch a row whose accelerator went missing. `settings=` must be a real run against the same config directory, which is the assertion that the flag persisted. Then: - `[helpui] contextual on: hints=8 ids={events plot status cursors measures trigger signals window}` — the eight "?" affordances, by id. The layer's ninth surface, `math`, is not here: its host exists - only while the math editor window is open. + only while the math editor window is open. **On a fresh config this reads `hints=0 ids={}`**, because + the layer is off by default; the example is the state after the row below was switched on, and a + fresh run's `hints=0` beside a `settings.toml` with no `contextual_help` key is the default being + read rather than assumed. - `[helpui] main view: rows=7 help_row=1` and one `[helpui] entry N '