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.
This commit is contained in:
Martino Ferrari committed 2026-10-11 15:21:40 +02:00
1 parent 8f54841c2c
commit 0e256764d5
41 files changed
+2081 -154

No files matched your search

+32
View File
@@ -210,3 +210,35 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
- **`find_accel()` masks the modifiers before comparing.** Lock, Num-Lock and the pointer buttons
all arrive in `GdkModifierType`, so an unmasked compare matches no table row at all once
Caps Lock is on.
- **A UI file is data, and a mistake in it is a runtime abort, not a build failure.** `.ui` files live in
`src/ui/` and are compiled into the binary by the third GResource (`udpscope-ui.gresource.xml`,
`prefix=/org/udpscope/ui`), loaded with `gtk_builder_new_from_resource()`. `preprocess="xml-stripblanks"`
catches a *malformed* file at build time -- and nothing else does, which is why it is on every file in
every bundle. What it cannot catch is a file that is valid XML but wrong for GTK: `GtkBuilder` reports
that with a **`Gtk-ERROR`, which aborts the process** by default, so a window built by hand from bad
markup takes the application down rather than failing to appear. The `[help]` probe opening every
window is what catches that. Two specifics already paid for: a `GIcon` property takes an **object**, not
a name (`<property name="icon">foo</property>` is read as a reference to another object and fails with
"Object with ID foo not found" -- the icon is a nested `GThemedIcon`), and a `.ui` big enough to exceed
4095 characters needs `-Wno-overlength-strings` on its generated source, because glib-compile-resources
writes each file as one C string literal and the warning fires on generated *data* (the icon and help
bundles are small enough per file never to need it).
- **Deprecated widgets may be the right widget, as long as no deprecated *function* is called.** The
shortcut overview is a `GtkShortcutsWindow` (`gtk/deprecated/`, deprecated in 4.20) because libadwaita's
successor, `AdwShortcutsDialog`, cannot draw a mouse gesture: `AdwShortcutsItem` has no icon property at
all, so its shortcut cell can only be an accelerator, and its cell reads "No Shortcut" when there is
none. `GtkShortcutsShortcut` has one generic gesture type whose `icon` *is* drawn. Loading the window
from a `.ui` and presenting it as a `GtkWindow` names no deprecated function, so the build stays at its
four warnings. When such a window closes, its builder is **replaced on the next opening**, not kept: the
window belongs to the builder's object list and a destroyed `GtkWindow` cannot be presented again.
- **A dialog reachable from two windows cannot own its state in either.** The preferences open from
the main window's menu *and* from the source chooser, which exists before `App` does (`main()`
chooses a source, then builds the model and the UI). The widgets therefore live in a `PrefsForm` the
caller owns — `App::prefs`, `Chooser::prefs` — and what a change *means* is a callback, because it
means different things on the two sides. Both are plain members of their owners, on the same stack
frame, so nothing can outlive the frame that built it.
- **A transient window is destroyed before its parent.** `chooser_finish()` destroys the preferences
window before the chooser, and nulls the pointer *before* the destroy so `close-request` cannot
re-enter a dead window. GTK does not promise to take a transient down with its parent, and a window
whose handlers point into a returned stack frame is how this project already earned one
use-after-free.