Commit Graph
352 Commits
Author SHA1 Message Date
Martino FerrariandClaude Opus 4.6 eb4372242c fix: time a FIRST_SAMPLE stream by its cadence, not by a second stamp
A UDPS_TIME_FIRST_SAMPLE time signal carries one meaningful value -- the
batch's first sample -- but a scalar signal in Accumulate mode still occupies
num_samples slots on the wire, which a producer fills by repeating that
anchor. Deriving the step from value_of(n - 1) therefore came out as zero, so
every sample of a frame landed on one x, the degeneracy guard tripped, and the
whole stream was timed by packet arrival instead. On a 1 MS/s source that
arrival clock is not monotonic within a frame: the export's time column held
90559 order breaks.

Space the batch by the cadence, exactly as the LAST_SAMPLE branch beside it
already dates its own batch backwards. The export's time column is now
strictly increasing (0 duplicates, 0 backwards steps) and reads the declared
1 MS/s.

mock_udpstreamer gains --time-mode {full,first}; it could only emit
FULL_ARRAY, so nothing exercised this branch. In 'first' it repeats the anchor
across the batch's wire slots -- the shape that produced the defect -- while
still generating the waveforms from the real per-sample stamps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-05 10:24:05 +02:00
Martino Ferrari 1fc2a3afe2 improving doc 2026-10-05 00:28:10 +02:00
Martino FerrariandClaude Sonnet 4.6 02c53bb7a0 docs: fix five accuracy findings in the libadwaita and icon documentation
- CLAUDE.md: the libadwaita dependency paragraph now names both AdwDialog
  widgets (AdwShortcutsDialog and AdwAboutDialog), explains the shared
  on_dialog_closed() handler keyed by pointer-to-pointer, and covers
  both show_about() and show_shortcuts() in the AdwDialog sentence.
- CLAUDE.md: the icon-authoring invariant's explanation of how Icon= in
  the desktop entry resolves now names the installed copies under
  $XDG_DATA_HOME/icons/hicolor/scalable/apps and .../symbolic/apps,
  not the GResource (a desktop shell cannot read a compiled-in resource).
- CLAUDE.md: [help] probe example byte count corrected from 1381 to 1379
  (measured: wc -c src/help/01-overview.txt = 1379).
- README.md + udpscope.gresource.xml: "file, line and column" corrected
  to "file and line number (plus a caret under the offending character)"
  to match xmllint's actual output; also removes -- from the XML comment
  where it was illegal.
- README.md: desktop-file-validate described as a hard failure "when the
  tool is on PATH", matching cmake/install_desktop_entry.cmake lines 44-54.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-04 21:13:22 +02:00
Martino FerrariandClaude Opus 4.6 e2be597698 docs: the app icon, AdwAboutDialog and the desktop entry
Records the app icons: assertion and why it stops short of claiming the icon
renders -- no runtime GTK call can tell a malformed SVG from a good one, which
is why the manifest validates through xmllint at build time instead. Scopes the
'filled paths only' authoring rule to symbolic icons, since the colour icon
strokes its sine and is never recoloured. Revises the windows invariant to one
GtkWindow and two AdwDialogs, and documents the desktop-entry target with the
build-path caveat it comes with.

Two findings from the regression run are recorded as well. The icons' generated
GResource crossed C99's 4095-byte string-literal limit when the app icons joined
the action ones, so the build's warning baseline is five, not four. And the
app icons: line's has_icon() half is satisfied by an installed theme copy, so on
a machine that has run --target desktop-entry it reads ok even with the resource
path broken; toolbar icons: is the sensitive line for that.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 21:05:35 +02:00
Martino FerrariandClaude Opus 4.6 f2978fe742 fix: guard unset HOME in desktop-entry cmake script and correct plan verification
Two code-review findings:

1. Add a guard in cmake/install_desktop_entry.cmake that fails visibly when both
   XDG_DATA_HOME and HOME are unset or empty, preventing silent installation to
   the filesystem root in minimal CI sandboxes.

2. Replace the plan's tail-based verification with grep, because cmake's
   FATAL_ERROR message and ninja's "build stopped" lines push the actual
   rejection message out of the last four lines. The grep command reliably finds
   the expected "desktop-file-validate rejected" string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 20:50:42 +02:00
Martino FerrariandClaude Opus 4.6 a055325831 feat: a desktop entry, installed by an opt-in CMake target
The entry's Exec is the built binary's absolute path, so it always launches what
was last built -- and moving the build directory breaks it, which is the price
of having no install step. An install() rule would make a second copy that can
diverge; a documented cp would need the binary on PATH, which it is not.

The target writes the entry and both icons under XDG_DATA_HOME, refreshes the
desktop and icon caches when the tools are there, and runs
desktop-file-validate as a hard failure -- the shell ignores a malformed entry
in silence, so the failure belongs at the moment the entry is asked for. It
prints the three paths it wrote, which is the uninstall instructions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 20:46:28 +02:00
Martino Ferrari 33eee0c8d0 feat: the About dialog on AdwAboutDialog, showing the app icon
AdwAboutDialog puts the application icon in its own header, which is what the
app now has one for, and adw_about_window_new() is deprecated in favour of it.
It is an AdwDialog and not a GtkWindow, so it clears on ::closed and closes with
adw_dialog_close() -- the same trap the shortcut overview documents, and both
dialogs now share one handler keyed by the pointer to clear.

Two gains while mapping the fields: system_information becomes debug_info, which
renders as a Troubleshooting page with a copy button, and the string behind it
becomes a std::string rather than a char[1024] that truncated a long settings
path in silence.

The probe now reads the icon name back out of the open dialog, so it is asserted
to carry the icon rather than merely to open.
2026-10-04 20:40:05 +02:00
Martino Ferrari 6deb53c458 feat: show the app icon on the window, and assert it is really there
The resource-path registration moves from build_toolbar() up into run_ui(),
because the window's icon is now a second consumer of it and this is the one
place ahead of both. Whichever consumer ran first would otherwise resolve its
name to nothing, with no error -- GTK draws a missing-image placeholder.

The new 'app icons:' dump line is the guard, computed on the first tick beside
'toolbar icons:'. It reports named-and-present at the size the file declares,
and deliberately does not claim the file renders: no runtime GTK call can see a
malformed SVG, which is why the manifest validates at build time instead.
2026-10-04 20:32:52 +02:00
Martino Ferrari f3d68cfdd4 feat: an app icon in colour and symbolic form, validated at build time
Both are drawn to the GNOME HIG: a 128px instrument face that leaves its
margins, carries one 4px front profile for depth and no gradients or shadows,
and a 16px symbolic reduction of the same metaphor in fills only. The colour
icon strokes its sine because it is never recoloured; the symbolic one cannot.

Every file in the manifest now preprocesses through xmllint, because a
malformed SVG is the one icon failure no runtime check can see: librsvg refuses
the file while the name still resolves, so the icon draws nothing and
has_icon() says it is fine.
2026-10-04 20:27:07 +02:00
Martino Ferrari 69eab02729 docs: the implementation plan for the app icon, AdwAboutDialog and desktop entry
Five tasks, each ending in a commit: the two SVGs with build-time xmllint
validation, the window icon plus the registration move and the app icons:
assertion, the AdwAboutDialog migration, the opt-in desktop-entry target, and
the documentation with the full regression run.

Written assertion-first, since the project has no unit suite: each task's first
step adds the probe or dump line and watches it read MISSING. Both negative
tests are real -- one breaks an SVG comment and requires the build to fail, the
other breaks Type= and requires the target to FATAL_ERROR.
2026-10-04 20:24:10 +02:00
Martino FerrariandClaude Opus 4.6 e17c650ea7 docs: correct the spec's icon verification to a build-time check
The spec asserted that a paintable's intrinsic size reads 0x0 for an SVG
librsvg refuses. It does not: gtk_icon_theme_lookup_icon() never returns NULL
and always reports the requested size, and a valid icon, a malformed one and an
absent name all snapshot to the same texture node. GMarkup accepts the double
hyphen, and gdk-pixbuf goes through glycin here and refuses our own icons.

So the malformed file is caught by preprocess="xml-stripblanks", which runs
xmllint during glib-compile-resources and fails the build with file, line and
column. The runtime line keeps its format but asserts only what it can: the
name resolves, the resource is present, and the root element declares the size.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 20:11:42 +02:00
Martino FerrariandClaude Opus 4.6 7816f11c81 docs: spec for the app icon, AdwAboutDialog and desktop entry
Records the decisions taken before design — the application ID stays
org.udpscope.app, the icon is the instrument face, and the desktop entry is an
opt-in CMake target rather than an install() rule — along with the HIG
constraints the artwork is built to and the two silent failures the new
verification line has to tell apart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 20:07:04 +02:00
Martino FerrariandClaude Opus 4.6 8a37e9d412 docs: correct the shortcut and help entry points, and the rate example
The whole-branch review found README describing a toolbar "?" button that
does not exist, Ctrl+? for an overview bound to Ctrl+/, bare `e` for an
export bound to Ctrl+E, and a help window that opens "on the page nearest
to what the user was doing" when it unconditionally selects Overview. All
four would send a reader looking for something that is not there. The key
table also claimed to be full while omitting F1, Ctrl+/ and Ctrl+Q, and
did not say that `f` is the plot's own key rather than an app accelerator.

format_rate() is %.3g, so it prints "10 kS/s"; the two doc comments in
status_text.hpp and the example in help page 01 still said "10.0 kS/s" —
the same error already corrected in 14-status.txt and CLAUDE.md, left in
the header that declares the function and in a second user-facing page.

close_help_windows()'s comment described only gtk_window_close(), and
show_shortcuts()'s called the overview "hand-rolled"; the Adwaita rework
made one half-true and the other false.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 19:50:40 +02:00
Martino FerrariandClaude Opus 4.6 ccb4964182 docs: help, shortcuts, About and the two-tier status bar
Adds the four new features to README.md (About dialog, shortcut overview
with Ctrl/Alt key table and deviation note, 14-page help window, two-tier
status bar), adds the four new probes and six new invariants to CLAUDE.md,
and fixes the `10.0 kS/s` rate-format error in src/help/14-status.txt
(format_rate() uses %.3g, which prints `10 kS/s`).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 16:04:41 +02:00
Martino FerrariandClaude Opus 4.6 8242812338 feat: two-tier status bar, one glance line with the detail behind an info popover
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 15:51:42 +02:00
Martino FerrariandClaude Sonnet 4.6 f9490061a1 feat: keyboard accelerators for panes, trigger, export and the help windows
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-04 15:39:46 +02:00
Martino FerrariandClaude Opus 4.6 d65a121a9a refactor: build the shortcut overview from AdwShortcutsDialog
The hand-rolled grid existed to avoid GTK's deprecated GtkShortcutsWindow;
libadwaita 1.8's AdwShortcutsDialog is the supported replacement, so use the
platform widget and get its search, sectioning and shortcut labels for free.

The table strings become GTK accelerator syntax, since the shortcut label parses
them. The six mouse rows cannot: no accelerator syntax names a wheel or a drag,
so they carry an empty accelerator and prose in the new subtitle field.

adw_init() is called in run_ui() and in choose_source() (replacing its
gtk_init()), which means the whole app is drawn with libadwaita's stylesheet.
Verified settled: viewport.w 870..870, axes.w changes 2, x-desync 0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 15:34:03 +02:00
Martino FerrariandClaude Sonnet 4.6 018b98eddf feat: keyboard and mouse shortcut overview window
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-04 15:15:47 +02:00
Martino Ferrari 3e0d0de325 docs: help pages for export, sessions and the status bar 2026-10-04 15:09:05 +02:00
Martino Ferrari ba4de92f7a docs: help pages for cursors, measurements and math channels 2026-10-04 15:05:00 +02:00
Martino Ferrari e5efd54752 docs: help pages for the time window, navigation, pause and trigger 2026-10-04 15:01:01 +02:00
Martino Ferrari 04b6181926 feat: paged help window with the four Getting started pages
The pages are Pango markup in a GResource, bundled for the same reason the icons
are: udpscope runs straight out of the build tree with no install step, so a
runtime search path's failure mode would be a silently empty help window.
--c-name=udpscope_help keeps the two generated bundles from colliding.

Every page is validated with pango_parse_markup() at load and falls back to a
one-line stand-in rather than rendering raw markup or nothing. show_help() reuses
help_window.cpp's existing close-request handler, and closes via
gtk_window_close() so that handler runs and the pointer is nulled.

UDPSCOPE_PROBE_HELP_MS loads and validates every page, opens the help and About
windows, closes both and reports; parsed must equal pages.
2026-10-04 14:53:20 +02:00
Martino Ferrari 856f538681 test: explain why the zoomkeys probe reads its axes synchronously
The probe's doc-comment advised running it inside a pinned state "so the _held
readings mean the other axis did not move" — but the reads happen in the same
callback as the key events, so no frame can intervene and nothing can re-frame.
Worse, the advice hid the real reason the read is synchronous, which is the
opposite of what the pan and zoom probes do and so invites a future reader to
"fix" it: hold_pinned_window() clamps the x span into the pinned window, so a
reading taken a frame later would report that clamp's answer rather than the
restriction's for the ctrl case. Name the actual bug class a deferred read would
confuse it with, and point at the probe that does assert figure adoption.

std::fabs for the two span comparisons, matching the file's existing idiom —
<cstdlib> is also in scope here.
2026-10-04 12:14:00 +02:00
Martino Ferrari dc7821a504 test: probe the ctrl/alt keyboard zoom restriction through the app 2026-10-04 12:09:27 +02:00
Martino Ferrari 0112999e6a chore: bump deps/plotter for ctrl/alt keyboard zoom 2026-10-04 12:03:03 +02:00
Martino FerrariandClaude Sonnet 4.6 a0fa845b58 feat: About dialog with version, commit, build time and settings path
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-04 11:52:43 +02:00
Martino FerrariandClaude Opus 4.6 82f91d00c0 docs: implementation plan for the help/shortcuts/status work
Eleven tasks over the approved spec: build info and About, the plotter's
ctrl/alt axis-restricted zoom, 14 help pages, the accelerator table and
overview window, the app key controller, and the two-tier status bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 11:48:06 +02:00
Martino Ferrari f7e5d99ae8 max holdoff 1h 2026-10-04 11:43:26 +02:00
Martino FerrariandClaude Opus 4.6 6b148f82a6 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>
2026-10-04 11:08:23 +02:00
Martino FerrariandClaude Opus 4.6 53815ad8bc fix a GLib critical from cancelling the pick probe's timeout
The lifetime fix in ffec797 removes the UDPSCOPE_PROBE_PICK_MS timeout after
the nested loop returns, so a row picked before the delay elapsed cannot leave
an armed source pointing at a gone stack frame. But the probe returns
G_SOURCE_REMOVE on every path, so on the ordinary run -- the probe fires, picks
row 0 and the loop quits -- the id was already stale and the removal printed
"Source ID N was not found when attempting to remove it" on every probe run.

The id moves onto the Chooser and pick_probe clears it on entry, so the
cancellation applies only to a probe that never fired.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-04 10:11:02 +02:00
Martino FerrariandClaude Sonnet 4.6 ffec797462 fix code-review findings in the source-chooser feature
Seven correctness and documentation fixes found in the final review:

- `known_sources()`: check the strtol end pointer and add a round-trip
  guard so a filename that layout_path() could not have produced is
  rejected; without it `foo_8000x.toml` listed as `foo:8000` whose `x`
  would unlink `foo_8000.toml`, a different configured session.
- `chooser_finish()`: destroy the form window before the chooser window;
  a row click or Escape while the form was up left a window whose handlers
  pointed at a dead stack frame (latent use-after-free).
- `choose_source()`: keep the pick_probe timeout id and g_source_remove
  it after the loop; a row picked before the delay elapsed left an armed
  source on the main context pointing at the gone frame.
- `chooser_populate()`: replace a false comment ("not a GtkListBoxRow")
  with the truth — GTK4 auto-wraps, but the wrapped row carries no
  source-index data so the bounds check refuses it.
- `pick_probe()`: collapse the duplicate `known.empty()` / `row==nullptr`
  guards; the second was unreachable and two identical lines for one cause
  makes probe output ambiguous.
- `save_layout()`: state the TOML rule directly in the multicast/interface
  comment rather than cross-referencing the adjacent enabled comment.
- `save_layout_now()`: refuse to write when signals() is empty; a run
  against a dead port receives no CONFIG, so every slot name is empty and
  the save would overwrite a configured session with signals=["",...],
  which the chooser filter refuses, silently removing that source from
  the only list it can be picked from.
- Fix include order in layout_store.cpp (<cstdio> before <cstdlib>).
- Update CLAUDE.md and README.md to document the empty-signals guard and
  fix the probe-output quote to match the emitted text exactly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-04 10:03:48 +02:00
Martino Ferrari b8d0ce8625 document the source chooser
Both files, per this project's rule: the probe entries and the two invariants
in CLAUDE.md, the feature and why the list is filtered in README.
2026-10-04 00:35:58 +02:00
Martino Ferrari 54e840e590 the new-host connection form
Server, port, multicast group and interface, and nothing else: --buffer-mb and
friends are tuning rather than which source, and a form that asks for them is
one a user has to read first. The form's widgets live on the chooser, which
outlives them, so no close path frees a live context.
2026-10-04 00:32:32 +02:00
Martino Ferrari b18b8a5b04 the source chooser's list
One row per stored session, titled by its server -- the host the file is named
after and the one CONFIG comes from -- with the group as subtitle. A forget
repopulates rather than removing one row, because the rows carry their index
into the list and dropping one renumbers the rest.
2026-10-04 00:26:20 +02:00
Martino Ferrari 0be3feecc5 ask which source to open when --server is absent
The chooser runs before ScopeModel exists, so declining costs no buffer and no
socket. UDPSCOPE_PROBE_SOURCES=1 prints what the discovery found and exits,
before gtk_init(), so the list is checkable with no display.
2026-10-04 00:17:42 +02:00
Martino Ferrari 9ae059db1b save the connection with the session
collect_layout() now reports the multicast group and interface the app was
launched with, so a stored session says how to reach its source and not just
which host and port it was.
2026-10-04 00:12:03 +02:00
Martino Ferrari eabdde34f9 list the sources with a stored session
known_sources() reads the session files the layout store already owns, keeping
only those that name a signal: a development machine accumulates one file per
probe run on a throwaway port, and all 140 of them here hold empty slots.
2026-10-04 00:08:15 +02:00
Martino Ferrari c604d62917 session file records how the source was reached
A session is named <host>_<port>.toml, which cannot say whether that source
was multicast or which interface it joined on, so a chooser row could not
reconnect to one. Both go in the top block: a bare key after a [[table]]
header belongs to that table, which is how the enabled key was lost once.
2026-10-04 00:04:52 +02:00
Martino Ferrari a066d48a3a plan: source chooser at startup
Seven tasks, each ending in a probe assertion rather than a unit test -- this
project's regression net is the headless probes, not a suite. The spec's
multicast round trip is split across three tasks so nothing in the plan needs a
group this machine can join.
2026-10-04 00:00:42 +02:00
Martino FerrariandClaude Opus 4.6 78065715b6 design: source chooser at startup
Launched with no --server, udpscope silently connects to 127.0.0.1:8000 and
offers no way to pick a source or see which ones it already knows. The design
is a pre-flight dialog: it runs before ScopeModel exists, so a cancel costs no
buffer and no socket and the existing pipeline is untouched.

The list is built from the stored session files, filtered to those naming at
least one signal -- a development machine accumulates one file per probe run on
a throwaway port, 140 of them here -- and the session format gains multicast and
interface keys so a row can reconnect exactly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-03 23:47:53 +02:00
Martino FerrariandClaude Opus 4.6 a92a6b5bf0 fixed plot flicker, measurement cost and toolbar icons
The flicker ("no data" at no constant frequency, worse with the measurements
and cursors panes up) was a torn read of the window's first stamp: acquire()
pins the window to the ring's oldest slot, and reading it at paint time races
the producer lapping it, returning a stamp one ring span too new. x0() is the
origin every trace point is reconstructed from, so one torn read produced a
refused neighbours(), a filter that dropped three quarters of the points, and
a polyline shifted off its axis. It is cached by acquire() under the lock now.

EventSeries::neighbours() answered from the (x - x0)/dx grid, whose drift from
the real arrival stamps arrives as a constant offset in seconds -- a doubling
share of the screen per zoom step, which is the "zooming into a held event
clips it more from the left" report. Both series binary-search the stamps now.

The measurements pane went through the LOD pyramid rather than reading its
window raw (19.7 ms per recompute to 1.2 ms, 0.1% of a 2 M-sample window) and
recomputes on a cadence; the math engine lost a per-slot heap allocation, a
per-sample source lookup by name and a fetch_add that cost more than the
arithmetic it counted.

The cursors pane's mix mode drew a line per *named* slot, so a slot holding a
signal that can never produce a channel -- the stream's timestamp -- got a row
that could only read "--", and such names were re-saved every session. It
counts bound traces now and apply_layout() refuses them.

Four toolbar icons are drawn for this app and compiled in as a GResource: the
stock ones were a record dot for the trigger and three magnifiers for cursors,
measurements and math.

New probes and dump lines for all of it (trace ink, measure cost, math cost,
event zoom, toolbar icons); README and CLAUDE.md record what they cure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-10-03 23:36:07 +02:00
Martino Ferrari 714d042809 fixed toolbar order and included math/measuraments files 2026-10-03 17:39:29 +02:00
Martino Ferrari aa2508e431 added math signals 2026-10-03 17:28:17 +02:00
Martino Ferrari 02620ca946 improved measuraments ,cursors, zooming and user interactions 2026-10-03 16:05:45 +02:00
Martino Ferrari 0eceaf537b added auto measuraments 2026-10-03 10:34:15 +02:00
Martino Ferrari 44fc4cdf36 improved timing acq 2026-10-02 14:03:57 +02:00
Martino Ferrari 2218aed839 Updated local storage 2026-10-02 11:30:54 +02:00
Martino Ferrari 30db44eaa0 Correct downsampling and better layout 2026-10-02 08:19:10 +02:00
Martino Ferrari 0d741c401b Fixed triggering mode change when acquiring 2026-10-01 17:54:17 +02:00
Martino Ferrari 3d4085898e added status line info: 2026-10-01 16:26:54 +02:00