feat: the manual is a Mallard set opened in Yelp, not a GResource in the binary

The help was 14 Pango-markup `.txt` bodies in a second GResource, drawn into a paged `GtkWindow`
built in `help_window.cpp`. That made F1 a viewer of ours with no search index, no cross-references
and no translation, and it kept the manual in the *binary* when every other desktop application's is
a document on disk. It is now a real Mallard set:

  help/C/org.udpscope.app/  index.page + 14 topic pages + figures/*.png

and `show_help()` hands `help:org.udpscope.app` to whatever the desktop registered for the `help:`
scheme -- Yelp on GNOME -- through a `GtkUriLauncher`, reporting a launch failure with a
`GtkAlertDialog` rather than a click that silently does nothing. The paged window's builder, the
page table and `help_self_check()`'s markup validation are gone with the resource.

The document identifier is the desktop file's own basename (its desktop-file id), derived in
CMakeLists from `data/org.udpscope.app.desktop.in`, so the same string is the `.desktop` name, the
GApplication id and the install directory at once. The set is installed by a new `install()` rule to
`<datadir>/help/C/org.udpscope.app/` -- where the freedesktop Help System Specification's `help:`
URI resolves `help:<id>` -- so the manual is read from a datadir by another process even though the
binary has no install step. `UDPSCOPE_HELP_NAME` and `UDPSCOPE_HELP_DIR` are compiled in from that
one place.

The 14 pages were migrated fact for fact, but restructured the Mallard way: a `<desc>` per page
(what a search result shows), `<steps>` for procedures, `<terms>` for option lists,
`<gui>`/`<key>`/`<keyseq>` for interface words and keys, `<note>` for the asides, and a topic graph
via `<link type="guide" xref="index#main"/>`. The figures are annotated screenshots produced by
`tools/make-help-shots.sh`, which drives the app headlessly against `tools/mock_udpstreamer.py` and
draws the boxes with ImageMagick; the side panel's box is measured from the app's own `side=NNNx`
dump reading, the header and status bands come from a documented table, and a crop whose standard
deviation is 0 fails the script.

`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 deliberately absent -- it fetches the Mallard RelaxNG schema over the
network and fails offline for that reason, not for a fault in the pages.

The `[help]` probe reading changes with the resource it was written for. `parsed == pages` over
in-binary markup cannot exist now, so `help_self_check()` scans `UDPSCOPE_HELP_DIR` and the line
reads `topics=14 index=1 figures=3`; `topics=0 index=0` is the manual missing from the tree, which
is the finding the old count named.

Verified: clean build with the documented three warnings (the help bundle's `-Woverlength-strings`
is gone, so the baseline moves from four to three); test-ui 14/14; test-help clean (the four
subcommands and 15 pages rendered); a temp-prefix install lands 15 `.page` files and 3 figures at
`share/help/C/org.udpscope.app/`; the headless run reads `[help] topics=14 index=1 figures=3 dir=...`
with 0 Gtk-CRITICAL. Yelp's own window could not be exercised on this box -- it starts under Xvfb,
activates its portals and never maps, the same WebKit/portal limitation that stops a grab -- so its
rendering and its search index are unverified here; the content pipeline was (`yelp-build` renders
the whole set).

Docs: the manual's build/install/open story in `build-and-run.md` and README; the warning baseline
in `verification.md`/CLAUDE.md; the help bullets in `invariants-gtk-and-chrome.md`; the `[help]`
line in `probe-readings.md`.
This commit is contained in:
Martino Ferrari committed 2026-10-14 16:49:30 +02:00
1 parent 620473a5be
commit c1460a6977
47 files changed
+1910 -1056

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
<page xmlns="http://projectmallard.org/1.0/"
type="topic" style="task"
id="export">
<info>
<link type="guide" xref="index#output"/>
<link type="seealso" xref="sessions"/>
<desc>Writing the visible range of every plotted trace to NumPy, CSV or MDF 3.30, and what
each format carries.</desc>
<credit type="author copyright">
<name>Martino Ferrari</name>
</credit>
</info>
<title>Saving and export</title>
<p>Export writes the <em>visible range</em> of every plotted trace to a file: NumPy
<file>.npy</file>, <file>.csv</file>, or <gui>MDF 3.30</gui> (<file>.mf3</file> or
<file>.mdf</file>), chosen by the extension you type.</p>
<section id="reach">
<title>How to reach it</title>
<steps>
<item>
<p>Pick <gui>Export visible range…</gui> in the overflow menu, or press
<keyseq><key>Ctrl</key><key>E</key></keyseq>.</p>
</item>
<item>
<p>A file chooser asks where; the format follows the path's extension.</p>
</item>
</steps>
</section>
<section id="knowing">
<title>Worth knowing</title>
<list>
<item><p>The table is collected at the moment you click, not when the chooser returns. A
live view keeps scrolling while a dialog is up, so a late collection would write a window
you never saw.</p></item>
<item><p>The rows are the <gui>stored samples</gui>, never the LOD buckets the plot draws
from. A row count far below the span times the stored rate means something cut the table
from the pyramid.</p></item>
<item><p>The stored rate is not always the wire rate. A window long enough to need
decimation is stored averaged at a lower rate, and the export carries <em>that</em> —
read the status bar's ⓘ popover for the figure before trusting a sample count.</p></item>
<item><p>Two time columns. <code>time</code> is the display timeline — negative ages
ending at 0 while streaming, offsets from the trigger edge while an event is held — so it
is the x you read a value at on screen. <code>abs_seconds</code> is the source's own
clock for the same sample, which is what lets two exports taken in different modes line
up. Both are strictly increasing and evenly spaced; duplicate or backwards stamps would
mean the file held two clocks.</p></item>
<item><p>An export taken <em>mid-capture</em> writes the partly filled window that is on
screen, which is shorter than the event span. Export outside a capture if you want the
whole event.</p></item>
<item><p><gui>MDF</gui> exists for handing a window to another tool, and its shape follows
the contract that reader uses: one channel group, every channel record-interleaved, the
master channel as the x axis and the unit in the channel metadata. So there the
<em>master is <code>abs_seconds</code></em>, named <code>time</code> — the clock that is
the same in every display mode — and the display axis goes out beside it as an ordinary
channel, <code>time_relative</code>. Both are seconds; each signal carries the source's
own unit. That is also what <gui>ScryLab</gui> reads: channel name → signal name, master
→ x axis, unit → the signal's unit.</p></item>
</list>
</section>
<section id="example">
<title>Example</title>
<p>A 2 s window at 10 kS/s over 3 traces writes 20001 rows and 5 columns
(<code>time</code>, <code>abs_seconds</code>, three signals). The <file>.npy</file> is
<code>64·⌈(10+len(header)+1)/64⌉ + rows·(16 + 4·cols)</code> bytes. Read it back with
<code>pd.DataFrame(np.load(path))</code>. The <file>.mf3</file> is bigger — a block per
channel plus a comment — <code>272 + comment + rows·record + 28 + channels·274 + 30</code>
bytes, with <code>record = 16 + 4·cols</code> and <code>channels = cols + 2</code> for the
master and the relative axis. Read that one back with <code>asammdf.MDF(path)</code> or in
ScryLab.</p>
</section>
</page>