Commit Graph
116 Commits
Author SHA1 Message Date
Martino Ferrari 0a64e9e854 ui: the source chooser's window is a .ui file, and its pages an AdwViewStack
The last hand-built UI outside ui.cpp. The window, its header bar, its two pages and the
four buttons that are about the chooser rather than about a source now live in
src/ui/source-chooser.ui; the rows stay in code because a source row and a waveform row
are data, rebuilt whenever the known hosts or the recent files change.

The pages were a GtkNotebook and are an AdwViewStack under an AdwViewSwitcherBar, which
is what current libadwaita asks for and the same information without a tab strip. The
three actions moved from a button row at the foot into the header bar, the shape
source-form.ui already uses -- and deliberately without receives-default, since Enter
must keep activating the selected row.

Nothing carries a value: the tooltips and labels moved as they were, the handlers are
the same functions, and the switch is by page *name* rather than by index.

The probe reads the frame back -- `[sources] chooser ui: page='sources' title='Sources'
icon=yes page='waveforms' … switcher_wired=1 buttons=[prefs=1 new_host=1 open=1
cancel=1]` -- so a .ui that lost an id is named instead of opening a window with a hole
in it, and `check_ui` type-checks the file against the GIR (7 ok, 0 failed).

Two things the checks caught while doing it: the new frame dropped the call that fills
the waveforms page (the unused-function warning), and `AdwViewStackPage` is a GObject
that *wraps* a child rather than being one, so reading the pages needs
adw_view_stack_get_page().
2026-10-12 15:32:53 +02:00
Martino Ferrari f00d161900 fix: row titles mean their mnemonics instead of drawing the underscore
Fifteen display strings in the two preference forms were written as GTK mnemonics -- `_Directory`,
`_Server`, `Multicast _group`, `_Theme` and the rest -- but the property that makes GTK read them was
never set, so GTK drew the underscore literally: `_Directory` in the row, not a D with a line under it.

Two switches are involved, FALSE by default in each case. `GtkLabel`/`GtkButton` take `use-underline`;
the rows take `AdwPreferencesRow:use-underline`, "whether an embedded underline in the title indicates a
mnemonic". The two buttons in the source form already set it (so their mnemonics worked and showed no
underscore); the thirteen rows did not. All thirteen now do.

The row classes that carry the property are not all related -- AdwEntryRow has its own, while AdwSpinRow,
AdwComboRow and AdwSwitchRow get it through AdwActionRow -- so the probe reads it by name on a GObject
(g_object_class_find_property, then g_object_get) rather than casting to one row type; a cast to a
`GtkWidget*` where the type is only forward-declared does not compile at all.

`_Cancel` and `_Connect` both claimed C in the same form, so Alt+C could only reach one of them: that
marker was unreachable, which is worse than absent because it is in the source to be trusted. `_Connect`
becomes `Conn_ect`, freeing C for Cancel.

[prefs-probe] reads the count (4/4) on the real widgets; check_ui validates the property against the GIR
for every object in every .ui file, inherited properties included.
2026-10-12 15:10:22 +02:00
Martino Ferrari 2d9f327da7 fix: the sidebar button follows the panel, and the wish survives having no source
The toolbar button was the panel's only input: `update_side_visibility()` derived the
panel from the button and never the other way, so the button went on saying "shown" in
two cases where the panel was off -- while there were no signals to show, and after
anything else moved the split view's state.

Binding the two together is not the fix. AdwOverlaySplitView writes `show-sidebar`
itself, at startup and when it collapses, with no user involved; a binding pushes that
into the button, the button's handler reads it as the user's wish, and the sidebar then
never comes back when a source connects -- a worse bug than the one reported, and one
that a "was this our own write" flag does not catch either, because the toggle's signal
may fire after the write returns.

So the direction is stated instead of inferred: `show-sidebar` is an output, the button
is the only input, and `App::sidebar_wanted` holds the wish between them. The wish is
written by the toggle's handler alone (guarded while we set the button ourselves), the
no-signals case forces the panel off without touching the wish, and tick() re-asserts
both from the wish when the widget disagrees -- so a hide gesture does not stick either.

[sidebar-probe] now reads the pair (shown vs button) in both states and against a hide
that did not come from the button; shown=1 button=1 with a source, shown=0 button=0
without, and the source reconnecting brings both back is the reading this exists for.
2026-10-12 13:23:40 +02:00
Martino Ferrari bb01278d88 fix: the sidebar keeps its own width, so a small window can no longer hide it
The sidebar's width was a GtkPaned divider position: `window - 940` in pixels. On any window
narrower than 940 px the divider landed past the right edge and the sidebar came up 0 px wide,
reachable only by dragging the divider back.

Every attempt that kept the paned failed for that reason -- deriving the position from the paned's
own width, re-parenting the column, property-form children, code-set size requests. The mechanism was
the fault, so it is gone: the row is an AdwOverlaySplitView, which is what a utility sidebar is in
GNOME, with min-sidebar-width = max-sidebar-width = 320 in pixels. Its width is therefore its own at
any window size, and when there is no room beside the plot it *overlays* the plot (collapsed, set from
the window's width in tick()) instead of being squeezed to nothing.

Two things found on the way, both recorded in the invariants:

- sidebar-width-fraction is a 0..1 fraction of the window whatever sidebar-width-unit says. Setting a
  pixel count there is a GObject critical and it falls back to 0.25, which is why the sidebar first
  read 560. The width is pinned with min/max instead.
- the window opened at its content minimum (756 px, the plot column plus the sidebar) because the
  explicit gtk_window_set_default_size() was dropped when the window moved into the .ui. Restored.

[sidebar-probe] reads both states and is the guard: window=756 sidebar=320 under a 2x scale, which is
the case that used to read 0.
2026-10-12 12:17:56 +02:00
Martino Ferrari a8c25f0cb3 test: the layout probe reads the sidebar's width
`side_w` is the sidebar column's width, and it must be its own request (320) rather than 0. It turned
a report of "I cannot see the right side panel" into `side_w=0` in one run, which is what a reading is
for.

It also shows the shape of the fault: the width is 320 during the first frames and 0 once the window
settles, while the divider sits at 940 and the plot column ends around 752 -- so the paned is not
giving the sidebar any width, whatever the column asks for.

Four attempts to fix that here were each reverted, because each one left `side_w=0`: placing the
divider from the paned's own width instead of an absolute 940, the property form of the paned's
children, re-parenting the column in code, and a code-set size request on the scroller and the column.
The cause is not one of those, and the next step is to read the paned's own width and the column's
parent chain rather than to guess again.
2026-10-12 11:06:31 +02:00
Martino Ferrari 4c56172005 feat: the preferences are a UI file, applying as they change
src/ui/preferences.ui is an AdwPreferencesDialog whose rows are libadwaita's own -- AdwEntryRow,
AdwSpinRow, AdwSwitchRow, AdwComboRow, AdwButtonRow -- instead of a label and a bare GtkEntry in a
GtkBox. There is no Apply button: a row applies as it changes, which is what those widgets are
designed around, and [prefs-probe] reads the settings file back after every change.

The folder chooser is cancellable now, so a picker still open when the dialog closes cannot write
into a row that is gone; the cancellable is also how the probe takes the cancel path without a
mouse.

Two traps cost a run each and are recorded in docs/claude/invariants-gtk-and-chrome.md: an
AdwDialog is not a GtkWindow (it cannot parent a GtkFileDialog, and in floating mode it adds a
native surface to gtk_window_get_toplevels() that is neither a widget nor a window), and a .ui
that is not listed in the GResource is a Gtk-ERROR at runtime rather than a build failure -- which
is exactly what the probe opening every window is for.
2026-10-11 15:30:10 +02:00
Martino Ferrari 0e256764d5 udpscope: an oscilloscope view for a UDPStreamer source
GTK4 and libadwaita. It receives one UDP stream through the vendored
udpsclient, buffers it in a large ring, and draws it with the vendored
plotter: a trigger with a pre/post event window, a pause that freezes the
window, cursors, measurements, math channels in a node editor, an event cache,
named configurations, preferences, and an export of the visible window to
.npy/.csv. Waveform files can be opened and analysed in place.

The interface is two threads -- a producer that owns the socket and the GTK
main thread that owns every widget and the GL context -- sharing one model
under one mutex, with samples crossing through lock-free rings.

deps/plotter is a submodule pinned to the commit this tree builds against;
deps/udpsclient is vendored in place. docs/claude/ documents the rules that
cost a defect to learn, and SPECS.md is the requirement document.
2026-10-11 15:21:40 +02:00
Martino Ferrari 8f54841c2c working on todo.md features 2026-10-10 22:52:12 +02:00
Martino FerrariandClaude Opus 4.6 210d124814 docs: the five reseq probe cases as this build measures them, plus the overflow counter's reading
Re-measured all four recorded cases and added a fifth for the re-plan path,
which needs 100 kS/s: at 10 kS/s the ring already covers 838 s, so no window
the control can reach moves time_stride and the window probe re-plans nothing.
Also notes that `stale` is 0 by construction while resequencing is on, and that
the reservation cap is counted in packets, so its sample cost scales with the
batch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-09 06:52:31 +02:00
Martino FerrariandClaude Sonnet 4.6 eacb3f5efc docs: the reseq probe's readings, and the invariants resequencing changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-08 23:14:38 +02:00
Martino FerrariandClaude Opus 4.6 c8f10f59dc fix: a condition leg is reported once it is held, so a working leg cannot read nan
A finite trigger_time() only says the edge was accepted; the mode is Capturing
until the post window fills. Every field the report prints is read from
display_snapshot(), whose stamps span x = 0 only once the event is Held, so
reporting on the latch alone raced the capture: leg 2 printed
`measured=nan at0=nan ... mode=1` on one run and the documented
`at0=-0.500907 earlier=-0.498185 mode=2` on the next. The report now waits for
Held under the same poll budget, and says `latched but never held` when it runs
out -- a different defect from `no latch`, so the line names which.

The readings are corrected against three consecutive leg-2 runs and one leg-3
run: `t0` is a rebased stream stamp and varies every run, so it was never an
assertion; `hits=` likewise tracks the arm phase (12 at one arm, 11 at another).
The mode ordinals were wrong -- `mode=2` is `Held`, not `Capturing`. Shapes 2
and 3 are now measured rather than read from the arm code; shape 1 still is not.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 11:34:30 +02:00
Martino FerrariandClaude Opus 4.6 728e7fbda5 docs: help pages, probe reference and session docs updated for the three new features
- 08-trigger.txt: names all four condition shapes and the buffer search; fixes the first
  paragraph and 'How to reach it' to match what the panel actually contains (fix 3).
- 10-measurements.txt: adds a Statistics bullet — one observation per recompute, what
  resets the history, what does not (fix 3).
- probe-readings.md: re-quotes condition-probe lines with level2= and mode= fields;
  corrects leg-2 window geometry (0.2 s / 10 % pre, not 1.0 s / 50 %); moves the
  wide-window reason to legs 1 and 3; fixes the search-probe scheduling note
  (search_probe calls condition_probe directly; do not also set CONDITION_MS) (fix 6).
- verification.md: corrects shape-2 window geometry to match the code (fix 7).
- invariants-session-and-export.md, README.md: document the measure_stats session key —
  position, type, default false (fix 8).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-08 11:19:19 +02:00
Martino FerrariandClaude Sonnet 4.6 ffc3ec1b4d docs: the three new features are described where each of their readers looks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-08 10:43:39 +02:00
Martino FerrariandClaude Opus 4.6 f179fbbb44 fix: a clock glitch is bridged and a channel is known by name, so a capture keeps its t0
Two defects behind one report ("a trigger during a capture overrides the first,
and the hold-off does not apply"); the second is also what made a spike in one
channel look like the acquisition resetting.

The trigger engine keyed its invalidation on the trigger channel's *index*, which
every rebuild of the channel list renumbers -- enabling a signal that sorts before
it is enough. Any rebuild therefore abandoned an open capture and set no hold-off,
so the next edge inside the post window became t0. It keys on the channel's name
now, which changes only when the source genuinely does.

A packet whose stamps jump more than 256 packet spans ahead is now rejected as a
fault and *bridged* at the declared cadence, instead of poisoning the monotonic
mark (which pinned every later packet flat at the glitched value) and condemning
the next good packet. Only four consecutive failures abandon the declared time
signal, and that fallback no longer wipes the rings: the arrival clock is anchored
to continue from the monotonic mark, so the history is still on one timeline --
the wipe is what made one glitching packet look like the stream restarting from 0.
The declared unit is resolved before those checks, since they weigh the stamps
against a cadence in seconds.

Guards: UDPSCOPE_PROBE_TRIGRENUM_MS prints t0_held=1 through the real widgets,
and the mock's --time-glitch-at/--time-glitch-by injects the clock fault.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-07 10:29:05 +02:00
Martino FerrariandClaude Opus 4.6 4003084648 feat: a signal's chip is clickable, so a trace's colour is the user's to choose
Clicking the coloured chip in the signal list opens a GtkColorDialog and the
colour it returns overrides that signal's palette entry. Held by name, like
every other identity here, and persisted as a session `[[color]]` table.

RowSet::signal_color() becomes the one resolver: install() builds the trace
through it and all three chip sites read it, because a chip exists to say which
trace a row is about and an override is exactly the state in which two routes to
a colour would disagree. A grey chip names no trace and so is not clickable, and
the way back is `Default colour` in the signal's menu -- a setting with no undo
is a trap.

The gesture sits on the existing label rather than in a GtkButton: padding on
this row is what made the signal list twice as tall once. It claims its sequence
so a click on the chip cannot become the start of a row drag.

UDPSCOPE_PROBE_COLOR_MS reads the result out of plot::Plot::trace() and out of
the chip's own markup, with an override in no palette slot, so neither leg can
pass by asking the resolver what it already said.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-07 08:22:32 +02:00
Martino FerrariandClaude Opus 4.6 a7702acb63 docs: CLAUDE.md is a goal and a table of contents, so a context pays for what it reads
The file was 1967 lines and is auto-loaded into every context, so every
session paid for the whole of it to reach one invariant. It now carries the
project's goal, the three facts that are always relevant, and a table of
contents; the body is fourteen files under docs/claude/ reached by link
rather than by import, so nothing is loaded until it is asked for.

The content is moved verbatim -- the only edits are each file's own header
and the cross-references that became cross-file ("see the invariant below"
is now a link to the file that owns it).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-07 01:49:28 +02:00