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

+333
View File
@@ -0,0 +1,333 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The shortcut overview, as a UI file instead of a couple of hundred lines of widget construction, and
in gedit's arrangement: one GtkShortcutsWindow holding a section per page (the window draws the
sidebar and the search box over them), each section holding a group, each group holding rows.
The mouse rows are the reason this file exists. An accelerator cannot spell "Ctrl and drag", and
libadwaita's AdwShortcutsItem, the modern replacement for this widget, has no icon property at all,
so its cell reads "No Shortcut". GtkShortcutsShortcut has exactly one gesture type whose *icon* is
drawn (shortcut-type "gesture" plus the icon property, per its own documentation), and that is what
puts the mouse glyph where the keycaps would otherwise be.
Keep this file in step with the tables in shortcuts.cpp: those are what the application handles,
this is what it shows. The `[help]` probe's `ui=` against `tables=` is the guard against the two
drifting apart.
-->
<interface>
<object class="GtkShortcutsWindow" id="help_overlay">
<property name="modal">1</property>
<child>
<object class="GtkShortcutsSection">
<property name="visible">1</property>
<property name="section-name">Acquisition</property>
<property name="title" translatable="yes">Acquisition</property>
<property name="max-height">12</property>
<child>
<object class="GtkShortcutsGroup">
<property name="visible">1</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">space</property>
<property name="title" translatable="yes">Pause or resume the display</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">a</property>
<property name="title" translatable="yes">Arm or disarm the trigger</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">t</property>
<property name="title" translatable="yes">Show or hide the trigger panel</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsSection">
<property name="visible">1</property>
<property name="section-name">Panes and editors</property>
<property name="title" translatable="yes">Panes and editors</property>
<property name="max-height">12</property>
<child>
<object class="GtkShortcutsGroup">
<property name="visible">1</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">c</property>
<property name="title" translatable="yes">Show or hide the cursors pane</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">m</property>
<property name="title" translatable="yes">Show or hide the measurements pane</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">g</property>
<property name="title" translatable="yes">Show or hide the math pane</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">l</property>
<property name="title" translatable="yes">Show or hide the layout editor</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">s</property>
<property name="title" translatable="yes">Show or hide the side panel</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsSection">
<property name="visible">1</property>
<property name="section-name">Files and windows</property>
<property name="title" translatable="yes">Files and windows</property>
<property name="max-height">12</property>
<child>
<object class="GtkShortcutsGroup">
<property name="visible">1</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;Control&gt;e</property>
<property name="title" translatable="yes">Export the visible range</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">F1</property>
<property name="title" translatable="yes">Open the help window</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;Control&gt;slash</property>
<property name="title" translatable="yes">Open this shortcut overview</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;Control&gt;q</property>
<property name="title" translatable="yes">Quit udpscope</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsSection">
<property name="visible">1</property>
<property name="section-name">Plot keyboard</property>
<property name="title" translatable="yes">Plot keyboard</property>
<property name="max-height">12</property>
<child>
<object class="GtkShortcutsGroup">
<property name="visible">1</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">plus minus</property>
<property name="title" translatable="yes">Zoom in or out, both axes</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;Control&gt;plus &lt;Control&gt;minus</property>
<property name="title" translatable="yes">Zoom the time axis only</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">&lt;Alt&gt;plus &lt;Alt&gt;minus</property>
<property name="title" translatable="yes">Zoom the value axis only</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">Left Right</property>
<property name="title" translatable="yes">Pan along time</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">Up Down</property>
<property name="title" translatable="yes">Pan along values</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">f</property>
<property name="title" translatable="yes">Fit the view to the data</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="accelerator">Escape</property>
<property name="title" translatable="yes">Cancel the gesture in progress</property>
</object>
</child>
</object>
</child>
</object>
</child>
<child>
<object class="GtkShortcutsSection">
<property name="visible">1</property>
<property name="section-name">Plot mouse</property>
<property name="title" translatable="yes">Plot mouse</property>
<property name="max-height">12</property>
<child>
<object class="GtkShortcutsGroup">
<property name="visible">1</property>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="shortcut-type">gesture</property>
<!-- A GIcon property takes an object, not a name: GtkBuilder reads a bare string as a
reference to another object and fails the whole file with "Object with ID … not
found" (a Gtk-ERROR, which aborts the process by default). -->
<property name="icon">
<object class="GThemedIcon">
<property name="names">input-mouse-symbolic</property>
</object>
</property>
<property name="title" translatable="yes">Zoom about the pointer, both axes</property>
<property name="subtitle" translatable="yes">Wheel</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="shortcut-type">gesture</property>
<!-- A GIcon property takes an object, not a name: GtkBuilder reads a bare string as a
reference to another object and fails the whole file with "Object with ID … not
found" (a Gtk-ERROR, which aborts the process by default). -->
<property name="icon">
<object class="GThemedIcon">
<property name="names">input-mouse-symbolic</property>
</object>
</property>
<property name="title" translatable="yes">Zoom the time axis only</property>
<property name="subtitle" translatable="yes">Shift and wheel</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="shortcut-type">gesture</property>
<!-- A GIcon property takes an object, not a name: GtkBuilder reads a bare string as a
reference to another object and fails the whole file with "Object with ID … not
found" (a Gtk-ERROR, which aborts the process by default). -->
<property name="icon">
<object class="GThemedIcon">
<property name="names">input-mouse-symbolic</property>
</object>
</property>
<property name="title" translatable="yes">Zoom the value axis only</property>
<property name="subtitle" translatable="yes">Ctrl and wheel</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="shortcut-type">gesture</property>
<!-- A GIcon property takes an object, not a name: GtkBuilder reads a bare string as a
reference to another object and fails the whole file with "Object with ID … not
found" (a Gtk-ERROR, which aborts the process by default). -->
<property name="icon">
<object class="GThemedIcon">
<property name="names">input-mouse-symbolic</property>
</object>
</property>
<property name="title" translatable="yes">Pan</property>
<property name="subtitle" translatable="yes">Drag</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="shortcut-type">gesture</property>
<!-- A GIcon property takes an object, not a name: GtkBuilder reads a bare string as a
reference to another object and fails the whole file with "Object with ID … not
found" (a Gtk-ERROR, which aborts the process by default). -->
<property name="icon">
<object class="GThemedIcon">
<property name="names">input-mouse-symbolic</property>
</object>
</property>
<property name="title" translatable="yes">Box zoom on time</property>
<property name="subtitle" translatable="yes">Ctrl and drag</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="shortcut-type">gesture</property>
<!-- A GIcon property takes an object, not a name: GtkBuilder reads a bare string as a
reference to another object and fails the whole file with "Object with ID … not
found" (a Gtk-ERROR, which aborts the process by default). -->
<property name="icon">
<object class="GThemedIcon">
<property name="names">input-mouse-symbolic</property>
</object>
</property>
<property name="title" translatable="yes">Box zoom on values</property>
<property name="subtitle" translatable="yes">Alt and drag</property>
</object>
</child>
<child>
<object class="GtkShortcutsShortcut">
<property name="visible">1</property>
<property name="shortcut-type">gesture</property>
<!-- A GIcon property takes an object, not a name: GtkBuilder reads a bare string as a
reference to another object and fails the whole file with "Object with ID … not
found" (a Gtk-ERROR, which aborts the process by default). -->
<property name="icon">
<object class="GThemedIcon">
<property name="names">input-mouse-symbolic</property>
</object>
</property>
<property name="title" translatable="yes">Box zoom on time and values</property>
<property name="subtitle" translatable="yes">Shift and drag</property>
</object>
</child>
</object>
</child>
</object>
</child>
</object>
</interface>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- SPDX-License-Identifier: MIT -->
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
<!--
The application's UI files, compiled into the binary beside the icons and the help pages, so there
is still exactly one file to run. `preprocess="xml-stripblanks"` is what catches a malformed file:
gtk_builder_new_from_resource() on a broken .ui returns nothing usable and the window simply does
not open, which is a silent failure at runtime and a loud one here.
-->
<gresources>
<gresource prefix="/org/udpscope/ui">
<file preprocess="xml-stripblanks">shortcuts.ui</file>
</gresource>
</gresources>