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.
1302 lines
61 KiB
Markdown
1302 lines
61 KiB
Markdown
# App icon, AdwAboutDialog and desktop entry — Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Give udpscope a HIG-conformant application icon in colour and symbolic form, show it in an `AdwAboutDialog` that replaces the stock `GtkAboutDialog`, and provide a `.desktop` entry an opt-in CMake target installs.
|
||
|
||
**Architecture:** Two new SVGs join the existing icon GResource under a `scalable/apps/` tree, so there is still one file to run and no install step. The About dialog moves from `GtkAboutDialog` (a `GtkWindow`) to `AdwAboutDialog` (an `AdwDialog`), which closes differently — the same trap the shortcut overview already documents. The desktop entry is generated into the build tree with the built binary's absolute path and copied into `$XDG_DATA_HOME` only when a user names the `desktop-entry` target.
|
||
|
||
**Tech Stack:** C++20, GTK4, libadwaita ≥ 1.8 (1.9.4 installed), CMake 3.24 + Ninja, `glib-compile-resources`, `xmllint` (new build dependency, ships with libxml2).
|
||
|
||
## Global Constraints
|
||
|
||
- The design spec is `docs/superpowers/specs/2026-10-04-app-icon-and-desktop-entry-design.md`. Read it before Task 1.
|
||
- **The application ID stays `org.udpscope.app`.** The icon name, both SVG basenames and the desktop file's basename are that same string. Do not touch `gtk_application_new()`.
|
||
- Every source file carries an SPDX MIT header. New `.svg` and `.xml` files carry it as an XML comment; new `.cmake` files as `# SPDX-License-Identifier: MIT`.
|
||
- **`--` is illegal inside an XML comment** and `xmllint` (which Task 1 wires into the build) will fail the build for it. Never write `--` in an SVG or GResource comment.
|
||
- Commit to `main` directly. **Do not push.** Do not create branches.
|
||
- **Four build warnings are pre-existing and expected**: two `-Wmissing-field-initializers` on `MathNode::source`, one `unused variable 'output_built'`, one `-Woverlength-strings` on the generated help resources. Introduce no fifth.
|
||
- **Two `Gtk-CRITICAL` messages are baselined** in an exit-dump run (`gtk_widget_get_height`, `gtk_widget_get_width`). A third is a regression.
|
||
- Headless probes run **one at a time** under `xvfb-run -a`, always pass `--server`, and every run is followed by `pkill -f 'mock_udpstream[e]r'`. Two concurrent `xvfb-run -a` instances share a display and the second gets no GL context.
|
||
- **Layout must not depend on content.** Nothing added here may make a widget's minimum size track a string.
|
||
- GNOME palette values, exact: Dark 2 `#5e5c64`, Dark 3 `#3d3846`, Dark 4 `#241f31`, Green 1 `#8ff0a4`.
|
||
- Website URL for the About dialog: `https://git.ferrarinas.it/martino/udpscope`. Developer name: `Martino Ferrari`.
|
||
|
||
## File Structure
|
||
|
||
| file | status | responsibility |
|
||
|---|---|---|
|
||
| `src/icons/scalable/apps/org.udpscope.app.svg` | create | the 128×128 colour icon |
|
||
| `src/icons/scalable/apps/org.udpscope.app-symbolic.svg` | create | the 16×16 symbolic icon |
|
||
| `src/icons/udpscope.gresource.xml` | modify | bundle the two new icons; `xml-stripblanks` on every file |
|
||
| `CMakeLists.txt` | modify | glob the new icon dir; generate and install the desktop entry |
|
||
| `cmake/install_desktop_entry.cmake` | create | the `desktop-entry` target's script: copy, validate, refresh, print |
|
||
| `data/org.udpscope.app.desktop.in` | create | the desktop entry, with `@UDPSCOPE_EXEC@` to substitute |
|
||
| `src/help_window.hpp` | modify | `about_icon_name()`; revise the two stale doc comments |
|
||
| `src/help_window.cpp` | modify | `show_about()` on `AdwAboutDialog`; one shared `::closed` handler |
|
||
| `src/ui.cpp` | modify | register the resource path in `run_ui()`; window icon; `app icons:` line; probe field |
|
||
| `CLAUDE.md` | modify | the new assertions and the revised invariants |
|
||
| `README.md` | modify | the feature narrative |
|
||
|
||
---
|
||
|
||
### Task 1: The artwork, bundled and validated at build time
|
||
|
||
**Files:**
|
||
- Create: `src/icons/scalable/apps/org.udpscope.app.svg`
|
||
- Create: `src/icons/scalable/apps/org.udpscope.app-symbolic.svg`
|
||
- Modify: `src/icons/udpscope.gresource.xml`
|
||
- Modify: `CMakeLists.txt:50-51` (the `file(GLOB UDPSCOPE_ICON_FILES ...)` call)
|
||
|
||
**Interfaces:**
|
||
- Consumes: nothing.
|
||
- Produces: two GResource paths later tasks look icons up by —
|
||
`/org/udpscope/icons/scalable/apps/org.udpscope.app.svg` and
|
||
`/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg` — resolving to the icon
|
||
**names** `org.udpscope.app` and `org.udpscope.app-symbolic`.
|
||
|
||
**Background you need.** `gtk_icon_theme_add_resource_path()` expects the directory tree an icon
|
||
theme has, so a bundled icon's path is `<prefix>/<size>/<context>/<name>.svg`. The four existing
|
||
icons are `scalable/actions/`; these two are `scalable/apps/`, which is the context an application
|
||
icon belongs to. `scalable` means "an SVG, use it at any size" — it is correct for the 16px
|
||
symbolic file too, and the *name's* `-symbolic` suffix is what makes GTK recolour it.
|
||
|
||
**The one authoring asymmetry.** CLAUDE.md's existing rule says every icon is "a filled `path`",
|
||
because every icon in the tree so far has been symbolic and GTK's symbolic recolour only reaches
|
||
`fill` on `path`, `rect` and `circle` — a `stroke` keeps the colour written in the file and stays
|
||
dark on a dark theme. **A 128px colour icon is never recoloured, so it may stroke freely**, and
|
||
stroking is the sane way to draw a smooth sine. The symbolic file below is fills only; the colour
|
||
file below strokes its trace. Do not "fix" either one to match the other.
|
||
|
||
- [ ] **Step 1: Write the colour icon**
|
||
|
||
Create `src/icons/scalable/apps/org.udpscope.app.svg`:
|
||
|
||
```svg
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!-- SPDX-License-Identifier: MIT -->
|
||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||
<!--
|
||
The application icon: an oscilloscope's face, which is the clearest metaphor available and
|
||
the kind of physical object the HIG's top-and-front depth language was written for.
|
||
|
||
Drawn to <https://developer.gnome.org/hig/guidelines/app-icons.html>. The artwork spans
|
||
x 12..116 and y 22..106, so it does not fill the 128px canvas, and every feature sits on
|
||
the 2px grid. Depth is the 4px front profile along the bottom and nothing else: that is
|
||
the HIG's maximum of 2 detail units, and since every surface here is straight, the rest
|
||
is flat colour with no gradients and no shadows.
|
||
|
||
The profile is the bottom 4px of the chassis silhouette, clipped to it. A plain rect from
|
||
(12, 102) to (116, 106) would protrude past the radius-8 corners, which reads as a
|
||
misdrawn bump at 128px and as a dirty edge at 32px; the clip follows the corners exactly
|
||
and keeps every coordinate even, which hand-computing the arc would not.
|
||
|
||
Two deliberate omissions. There is no graticule: the grid lines a real scope face carries
|
||
cannot survive 32px, and the trace carries the metaphor without them. And the trace is
|
||
one cycle, a single crest and a single trough, because at 32px the screen is about
|
||
22x16 px and anything busier reads as a smudge.
|
||
|
||
Colours are the GNOME palette: Dark 3 chassis, Dark 2 profile, Dark 4 screen, Green 1 trace.
|
||
-->
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||
<defs>
|
||
<clipPath id="chassis-silhouette">
|
||
<rect x="12" y="22" width="104" height="84" rx="8"/>
|
||
</clipPath>
|
||
</defs>
|
||
<rect x="12" y="22" width="104" height="84" rx="8" fill="#3d3846"/>
|
||
<g clip-path="url(#chassis-silhouette)">
|
||
<rect x="12" y="102" width="104" height="4" fill="#5e5c64"/>
|
||
</g>
|
||
<rect x="20" y="30" width="88" height="64" rx="4" fill="#241f31"/>
|
||
<path d="M 24 62 C 37 38 51 38 64 62 C 77 86 91 86 104 62"
|
||
fill="none" stroke="#8ff0a4" stroke-width="6" stroke-linecap="round"/>
|
||
</svg>
|
||
```
|
||
|
||
The trace's control points are at thirds of each half cycle with a y offset of `4A/3`, which
|
||
puts a cubic's midpoint at exactly the amplitude: the crest peaks at y = 44 and the trough at
|
||
y = 80, both 18 from the centreline at y = 62. Those extrema, the endpoints and the thickness are
|
||
the even coordinates the grid rule is about; a Bézier control point is not a visible feature.
|
||
|
||
- [ ] **Step 2: Write the symbolic icon**
|
||
|
||
Create `src/icons/scalable/apps/org.udpscope.app-symbolic.svg`:
|
||
|
||
```svg
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!-- SPDX-License-Identifier: MIT -->
|
||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||
<!--
|
||
The symbolic form of the app icon: the same metaphor reduced to a screen frame with one
|
||
wave inside it, following the UI-icon rules rather than the app-icon ones.
|
||
|
||
Filled paths only, with no stroke anywhere. GTK's symbolic recolour wraps the file in a
|
||
stylesheet that reaches `fill` on `path`, `rect` and `circle` only, so a stroked line keeps
|
||
the colour written here and stays dark on a dark theme. The wave is therefore a closed
|
||
2px band: a sine along its top edge and the same sine 2px lower along its bottom, which is
|
||
how a stroke's look is had from a fill.
|
||
|
||
`fill-rule="evenodd"` over three subpaths. The frame is the outer rect minus the inner one,
|
||
so the screen's interior is two crossings deep and empty; the wave inside it is three, which
|
||
is odd, so it fills. Whole-pixel coordinates throughout, because this is drawn at 16px far
|
||
more often than not and a half-pixel feature is a grey smear at 1x.
|
||
-->
|
||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||
<path fill="#2e3436" fill-rule="evenodd"
|
||
d="M 1 2 H 15 V 14 H 1 Z
|
||
M 2 3 H 14 V 13 H 2 Z
|
||
M 3 7 C 5 3 6 3 8 7 C 10 11 11 11 13 7 L 13 9 C 11 13 10 13 8 9 C 6 5 5 5 3 9 Z"/>
|
||
</svg>
|
||
```
|
||
|
||
- [ ] **Step 3: Bundle both, and make `glib-compile-resources` validate every icon**
|
||
|
||
Replace the whole of `src/icons/udpscope.gresource.xml` with:
|
||
|
||
```xml
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!-- SPDX-License-Identifier: MIT -->
|
||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||
<!--
|
||
The icons this app had to draw for itself, compiled into the binary so there is still
|
||
exactly one file to run. `gtk_icon_theme_add_resource_path()` wants the tree an icon theme
|
||
has, so the prefix is followed by `<size>/<context>`; `scalable` is the one that means
|
||
"an SVG, use it at any size", and `apps` is where an application icon belongs.
|
||
|
||
Every file carries `preprocess="xml-stripblanks"`, which makes glib-compile-resources pipe
|
||
it through xmllint. That is the only thing that catches a malformed SVG: librsvg refuses
|
||
such a file outright, but the icon *name* still resolves, so `gtk_icon_theme_has_icon()`
|
||
returns true and the icon simply draws nothing. No runtime GTK call can tell the two apart
|
||
(`lookup_icon()` never returns NULL and always reports the size it was asked for), so the
|
||
check belongs here, where a double hyphen inside a comment fails the build with a file,
|
||
line and column instead.
|
||
-->
|
||
<gresources>
|
||
<gresource prefix="/org/udpscope/icons">
|
||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-trigger-symbolic.svg</file>
|
||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-cursors-symbolic.svg</file>
|
||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-measure-symbolic.svg</file>
|
||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-math-symbolic.svg</file>
|
||
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app.svg</file>
|
||
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app-symbolic.svg</file>
|
||
</gresource>
|
||
</gresources>
|
||
```
|
||
|
||
- [ ] **Step 4: Make the build track the new directory**
|
||
|
||
In `CMakeLists.txt`, replace lines 50-51:
|
||
|
||
```cmake
|
||
file(GLOB UDPSCOPE_ICON_FILES CONFIGURE_DEPENDS
|
||
${UDPSCOPE_ICON_DIR}/scalable/actions/*.svg)
|
||
```
|
||
|
||
with:
|
||
|
||
```cmake
|
||
file(GLOB UDPSCOPE_ICON_FILES CONFIGURE_DEPENDS
|
||
${UDPSCOPE_ICON_DIR}/scalable/actions/*.svg
|
||
${UDPSCOPE_ICON_DIR}/scalable/apps/*.svg)
|
||
```
|
||
|
||
A glob that misses the new directory does not fail — it just stops rebuilding the resource when
|
||
an icon is edited, which is a silently stale binary. (`${UDPSCOPE_ICON_FILES}` is already in the
|
||
`add_custom_command`'s `DEPENDS`, so widening the glob is the whole change; Step 6's negative test
|
||
depends on this working.)
|
||
|
||
Change that section's heading comment too, four lines above, from
|
||
|
||
```cmake
|
||
# --- icons: four symbolic SVGs compiled into the binary -----------------------
|
||
```
|
||
|
||
to
|
||
|
||
```cmake
|
||
# --- icons: six SVGs compiled into the binary --------------------------------
|
||
```
|
||
|
||
- [ ] **Step 5: Reconfigure and build**
|
||
|
||
```bash
|
||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo && cmake --build build 2>&1 | tail -20
|
||
```
|
||
|
||
Expected: the configure step finds `xmllint` implicitly through `glib-compile-resources` (nothing
|
||
new is printed for it), `Compiling udpscope icon resources` runs, and the build succeeds with
|
||
**exactly the four pre-existing warnings** listed in Global Constraints and no XML parser errors.
|
||
|
||
- [ ] **Step 6: Prove the build-time validation actually fires**
|
||
|
||
This is the negative test, and it is the whole point of Step 3. Break one comment, confirm the
|
||
build fails, then restore it:
|
||
|
||
```bash
|
||
cp src/icons/scalable/apps/org.udpscope.app.svg /tmp/icon-backup.svg
|
||
sed -i 's/one cycle, a single crest/one cycle -- a single crest/' src/icons/scalable/apps/org.udpscope.app.svg
|
||
cmake --build build 2>&1 | grep -c "Double hyphen within comment"
|
||
cp /tmp/icon-backup.svg src/icons/scalable/apps/org.udpscope.app.svg
|
||
cmake --build build 2>&1 | tail -3
|
||
```
|
||
|
||
Expected: the `grep -c` prints `1` or more, then the restored build succeeds. If the grep prints
|
||
`0` the `preprocess` attribute is not taking effect and Step 3 is wrong — fix it before moving on.
|
||
|
||
The icon rebuilds because `add_custom_command`'s `DEPENDS` already lists `${UDPSCOPE_ICON_FILES}`,
|
||
which Step 4 widened to include this file. If editing it rebuilds nothing, Step 4 did not take.
|
||
|
||
- [ ] **Step 7: Confirm the restore was exact**
|
||
|
||
The file is still untracked at this point, so `git diff` would print nothing whatever the file
|
||
holds — compare against the backup instead, then discard it:
|
||
|
||
```bash
|
||
cmp /tmp/icon-backup.svg src/icons/scalable/apps/org.udpscope.app.svg && echo "restore exact"
|
||
rm /tmp/icon-backup.svg
|
||
```
|
||
|
||
Expected: `restore exact`. `cmp` naming a differing byte means the restore failed — copy the backup
|
||
over again before committing, or the broken comment goes into the repository.
|
||
|
||
- [ ] **Step 8: Commit**
|
||
|
||
```bash
|
||
git add src/icons/scalable/apps/org.udpscope.app.svg \
|
||
src/icons/scalable/apps/org.udpscope.app-symbolic.svg \
|
||
src/icons/udpscope.gresource.xml CMakeLists.txt
|
||
git commit -m "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."
|
||
```
|
||
|
||
---
|
||
|
||
### Task 2: The window icon, the registration ordering, and the `app icons:` assertion
|
||
|
||
**Files:**
|
||
- Modify: `src/ui.cpp:5585-5589` (remove the registration from `build_toolbar()`)
|
||
- Modify: `src/ui.cpp:7527` (add the registration after `adw_init()` in `run_ui()`)
|
||
- Modify: `src/ui.cpp:5766-5768` (set the window's icon name in `on_activate()`)
|
||
- Modify: `src/ui.cpp:150` (the `App` struct, beside `dbg_toolbar_icons`)
|
||
- Modify: `src/ui.cpp:4440-4457` (`note_panel_controls()`, where `dbg_toolbar_icons` is built)
|
||
- Modify: `src/ui.cpp:7801` (the dump, beside the `toolbar icons:` line)
|
||
|
||
**Interfaces:**
|
||
- Consumes: the two GResource paths and icon names Task 1 produced.
|
||
- Produces: `App::dbg_app_icons`, a `std::string` computed on the first tick, printed by the exit
|
||
dump as `[udpscope] app icons: <report> <report>`. Nothing later depends on it.
|
||
|
||
**Why the ordering matters.** `gtk_icon_theme_add_resource_path()` currently runs at the top of
|
||
`build_toolbar()`, which was fine while the toolbar buttons were the only consumers. The main
|
||
window's icon is now set from a name in the same resource, so the registration has to run before
|
||
**both** `gtk_window_set_icon_name()` and the `build_toolbar()` call — otherwise whichever of the
|
||
two runs first resolves its name to nothing, silently. It moves to `run_ui()` immediately after
|
||
`adw_init()`: `adw_init()` calls `gtk_init()`, which opens the default display, so
|
||
`gdk_display_get_default()` is valid from that line onwards and there is exactly one place the
|
||
registration can be read.
|
||
|
||
- [ ] **Step 1: Write the failing assertion first — the dump line**
|
||
|
||
Add the field to the `App` struct. In `src/ui.cpp`, after line 150 (`std::string dbg_toolbar_icons;`):
|
||
|
||
```cpp
|
||
std::string dbg_app_icons;
|
||
```
|
||
|
||
Add the helpers immediately above `note_panel_controls()` (so above line 4393's
|
||
`void note_panel_controls(App& app) {`):
|
||
|
||
```cpp
|
||
/// The width and height the root `<svg>` element of `data` declares, or 0x0 when it has none.
|
||
/// GMarkup rather than a string scan because the attributes may be in any order and on any
|
||
/// line; it is *not* a validity check -- GMarkup is a deliberately lenient subset parser and
|
||
/// accepts a double hyphen inside a comment, which is exactly the file librsvg refuses. That
|
||
/// check is `xml-stripblanks` in the GResource manifest, at build time.
|
||
void svg_declared_size(const char* data, gsize size, int* w, int* h) {
|
||
struct Found { int w = 0; int h = 0; };
|
||
Found found;
|
||
GMarkupParser parser{};
|
||
parser.start_element = [](GMarkupParseContext*, const char* element, const char** names,
|
||
const char** values, gpointer user, GError**) {
|
||
if (std::strcmp(element, "svg") != 0) return;
|
||
Found* f = static_cast<Found*>(user);
|
||
for (int i = 0; names[i] != nullptr; ++i) {
|
||
if (std::strcmp(names[i], "width") == 0) f->w = std::atoi(values[i]);
|
||
if (std::strcmp(names[i], "height") == 0) f->h = std::atoi(values[i]);
|
||
}
|
||
};
|
||
GMarkupParseContext* ctx = g_markup_parse_context_new(&parser, GMarkupParseFlags(0), &found,
|
||
nullptr);
|
||
if (g_markup_parse_context_parse(ctx, data, static_cast<gssize>(size), nullptr)) {
|
||
g_markup_parse_context_end_parse(ctx, nullptr);
|
||
}
|
||
g_markup_parse_context_free(ctx);
|
||
*w = found.w;
|
||
*h = found.h;
|
||
}
|
||
|
||
/// One entry of the `app icons:` line: the icon is *named* (the theme resolved it, so the
|
||
/// resource path was registered) and *present* (the bytes the name resolved to are really in
|
||
/// the binary), at the size its own root element declares.
|
||
///
|
||
/// It deliberately does not claim the file renders. `gtk_icon_theme_lookup_icon()` never
|
||
/// returns NULL, its paintable's intrinsic size is the size that was asked for, and a valid
|
||
/// icon, a malformed one and a name that does not exist at all all snapshot to the same
|
||
/// texture node -- GTK substitutes `image-missing` and there is nothing left to compare. The
|
||
/// malformed case is a build error instead; see the GResource manifest.
|
||
std::string icon_report(GtkIconTheme* theme, const char* name, const char* resource) {
|
||
const bool named = gtk_icon_theme_has_icon(theme, name) != FALSE;
|
||
GBytes* bytes = g_resources_lookup_data(resource, G_RESOURCE_LOOKUP_FLAGS_NONE, nullptr);
|
||
const bool present = bytes != nullptr;
|
||
int w = 0;
|
||
int h = 0;
|
||
if (present) {
|
||
gsize size = 0;
|
||
const char* data = static_cast<const char*>(g_bytes_get_data(bytes, &size));
|
||
svg_declared_size(data, size, &w, &h);
|
||
g_bytes_unref(bytes);
|
||
}
|
||
std::string out = std::string(name) + "(";
|
||
out += (named && present) ? "ok " : "MISSING ";
|
||
out += std::to_string(w) + "x" + std::to_string(h) + ")";
|
||
return out;
|
||
}
|
||
```
|
||
|
||
Now build the line. At the end of `note_panel_controls()`, after line 4457's closing `}` of the
|
||
`for (const auto& [widget, name] : named)` loop and before the function's own `}`:
|
||
|
||
```cpp
|
||
// The application icon, the same two ways and for the same reason: a resource path that was
|
||
// never registered is not an error, it is a missing-image placeholder. This one is read on
|
||
// the first tick like the line above, because the dump runs after the window is destroyed.
|
||
app.dbg_app_icons =
|
||
icon_report(theme, "org.udpscope.app",
|
||
"/org/udpscope/icons/scalable/apps/org.udpscope.app.svg") +
|
||
" " +
|
||
icon_report(theme, "org.udpscope.app-symbolic",
|
||
"/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg");
|
||
```
|
||
|
||
And print it. After line 7801:
|
||
|
||
```cpp
|
||
std::printf("[udpscope] app icons: %s\n", app.dbg_app_icons.c_str());
|
||
```
|
||
|
||
No new includes are needed: `<cstring>` (for `std::strcmp`) and `<cstdlib>` (for `std::atoi`) are
|
||
both already at the top of `src/ui.cpp`.
|
||
|
||
- [ ] **Step 2: Build and run it to watch it fail**
|
||
|
||
```bash
|
||
cmake --build build 2>&1 | tail -5
|
||
python3 tools/mock_udpstreamer.py --rate 10000 --duration 20 &
|
||
sleep 1
|
||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=6000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||
./build/udpscope --server 127.0.0.1 --port 8000 2>/dev/null | grep "app icons:"
|
||
pkill -f 'mock_udpstream[e]r'
|
||
```
|
||
|
||
Expected: `app icons: org.udpscope.app(MISSING 128x128) org.udpscope.app-symbolic(MISSING 16x16)`
|
||
— the sizes are read straight from the resource so they are already right, but the *names* do not
|
||
resolve, because `build_toolbar()` registers the path and `note_panel_controls()` has nothing to do
|
||
with that ordering yet. If it already reads `ok`, the registration happened to run first; the
|
||
remaining steps are still required, because the window icon is set before `build_toolbar()`.
|
||
|
||
- [ ] **Step 3: Move the registration and set the window's icon**
|
||
|
||
In `src/ui.cpp`, delete these five lines from `build_toolbar()` (lines 5585-5589):
|
||
|
||
```cpp
|
||
// The app's own icons, before the first button asks for one. They are a GResource
|
||
// compiled into the binary (see CMakeLists), and this is what makes the theme look
|
||
// there: an unregistered path is not an error, it is a missing-image placeholder.
|
||
gtk_icon_theme_add_resource_path(gtk_icon_theme_get_for_display(gdk_display_get_default()),
|
||
"/org/udpscope/icons");
|
||
```
|
||
|
||
In `run_ui()`, immediately after line 7527's `adw_init();`:
|
||
|
||
```cpp
|
||
// The app's own icons, before anything asks for one. They are a GResource compiled into the
|
||
// binary (see CMakeLists), and this is what makes the theme look there: an unregistered path
|
||
// is not an error, it is a missing-image placeholder.
|
||
//
|
||
// It is here rather than in `build_toolbar()` because there are now two consumers -- the
|
||
// window's own icon and the toolbar's buttons -- and this is the one place that is ahead of
|
||
// both. `adw_init()` calls `gtk_init()`, which opens the default display, so the line above
|
||
// is what makes `gdk_display_get_default()` valid here.
|
||
gtk_icon_theme_add_resource_path(gtk_icon_theme_get_for_display(gdk_display_get_default()),
|
||
"/org/udpscope/icons");
|
||
```
|
||
|
||
And revise the comment the move falsifies, in `note_panel_controls()` at lines 4440-4445. It
|
||
currently reads:
|
||
|
||
```cpp
|
||
// ... and the icon each of them carries, asked of the theme rather than of our own
|
||
// string. Four of these are drawn for this app and live in a GResource inside the
|
||
// binary, which the theme only looks in because `build_toolbar()` adds the resource
|
||
// path -- and a path that was never added fails *silently*: the button keeps working,
|
||
// the layout is unchanged, and GTK draws a missing-image placeholder no probe can see.
|
||
```
|
||
|
||
Replace it with:
|
||
|
||
```cpp
|
||
// ... and the icon each of them carries, asked of the theme rather than of our own
|
||
// string. Four of these are drawn for this app and live in a GResource inside the
|
||
// binary, which the theme only looks in because `run_ui()` adds the resource path
|
||
// before building anything -- and a path that was never added fails *silently*: the
|
||
// button keeps working, the layout is unchanged, and GTK draws a missing-image
|
||
// placeholder no probe can see. The two app icons below come out of the same
|
||
// registration, which is why a MISSING on either line is one defect.
|
||
```
|
||
|
||
In `on_activate()`, after line 5766's `gtk_window_set_title(GTK_WINDOW(app->window), "udpscope");`:
|
||
|
||
```cpp
|
||
// The shell takes a window's icon from this name, and GTK takes the WM class from the
|
||
// application ID -- which is the same string, which is what associates this window with the
|
||
// desktop entry.
|
||
gtk_window_set_icon_name(GTK_WINDOW(app->window), "org.udpscope.app");
|
||
```
|
||
|
||
- [ ] **Step 4: Run it to verify it passes**
|
||
|
||
```bash
|
||
cmake --build build 2>&1 | tail -5
|
||
python3 tools/mock_udpstreamer.py --rate 10000 --duration 20 &
|
||
sleep 1
|
||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=6000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||
./build/udpscope --server 127.0.0.1 --port 8000 2>/dev/null \
|
||
| grep -E "app icons:|toolbar icons:"
|
||
pkill -f 'mock_udpstream[e]r'
|
||
```
|
||
|
||
Expected, both lines:
|
||
|
||
```
|
||
[udpscope] app icons: org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16)
|
||
[udpscope] toolbar icons: trigger=udpscope-trigger-symbolic(ok) pause=...(ok) ... menu=...(ok)
|
||
```
|
||
|
||
**Both `app icons:` entries must read `ok` at those exact sizes**, and **every `toolbar icons:`
|
||
entry must still read `(ok)`** — that second line is the regression check on having moved the
|
||
registration. A `MISSING` on the toolbar means the move went to the wrong place.
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add src/ui.cpp
|
||
git commit -m "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."
|
||
```
|
||
|
||
---
|
||
|
||
### Task 3: The About dialog on `AdwAboutDialog`
|
||
|
||
**Files:**
|
||
- Modify: `src/help_window.cpp:18-35` (the file-scope pointers and their close handlers)
|
||
- Modify: `src/help_window.cpp:99-126` (`show_about()`)
|
||
- Modify: `src/help_window.cpp:255-290` (`show_shortcuts()`'s signal connection, and
|
||
`close_help_windows()` at 285)
|
||
- Modify: `src/help_window.hpp:11-13` (`show_about()`'s doc comment) and `:36-40`
|
||
(`close_help_windows()`'s)
|
||
- Modify: `src/ui.cpp:7497-7520` (`help_probe()`)
|
||
|
||
**Interfaces:**
|
||
- Consumes: the icon name `org.udpscope.app` from Task 1, registered by Task 2's `run_ui()` line.
|
||
- Produces: `const char* udpscope::about_icon_name()` — declared in `src/help_window.hpp`, returns
|
||
the icon name the open About dialog carries, or `""` when it is not open. Valid only between a
|
||
`show_about()` and the matching close. The `[help]` probe is its only caller.
|
||
|
||
**The trap, which this project has already documented once.** `adw_about_dialog_new()` returns an
|
||
`AdwDialog*`, and an `AdwDialog` is **not** a `GtkWindow`: there is no `::close-request` to clear
|
||
the pointer from and no `gtk_window_close()` to call. It is `::closed` and `adw_dialog_close()`,
|
||
exactly as `show_shortcuts()` already does. It is also modal on the widget it is presented on, so
|
||
the `gtk_window_set_transient_for()` / `gtk_window_set_modal()` pair goes away.
|
||
|
||
**Two gains to take while mapping the fields.** `debug_info` renders as a Troubleshooting page with
|
||
a copy button where `system_information` was inert text; and the string behind it becomes a
|
||
`std::string`, removing the `char details[1024]` ceiling a previous review flagged for silent
|
||
truncation on a long settings path. Removing the ceiling, not widening it, in code already being
|
||
rewritten.
|
||
|
||
Credits, release notes and an issue URL are deliberately **not** added. Nothing asked for them.
|
||
|
||
- [ ] **Step 1: Write the failing assertion first — the probe field**
|
||
|
||
In `src/help_window.hpp`, after the `show_about()` declaration (line 13):
|
||
|
||
```cpp
|
||
/// The icon name the open About dialog carries, or "" when it is not open. The probe reads it
|
||
/// so the dialog is asserted to *show* the app icon rather than merely to open.
|
||
const char* about_icon_name();
|
||
```
|
||
|
||
In `src/ui.cpp`'s `help_probe()`, read it **before** `close_help_windows()` — the dialog is gone
|
||
afterwards. Replace lines 7503-7506:
|
||
|
||
```cpp
|
||
show_help(GTK_WINDOW(a->window));
|
||
show_about(GTK_WINDOW(a->window), session);
|
||
show_shortcuts(GTK_WINDOW(a->window));
|
||
close_help_windows();
|
||
```
|
||
|
||
with:
|
||
|
||
```cpp
|
||
show_help(GTK_WINDOW(a->window));
|
||
show_about(GTK_WINDOW(a->window), session);
|
||
show_shortcuts(GTK_WINDOW(a->window));
|
||
// Read while it is open: the icon is the point of the dialog carrying one, and
|
||
// `close_help_windows()` is right below.
|
||
const std::string about_icon = about_icon_name();
|
||
close_help_windows();
|
||
```
|
||
|
||
and extend the report (lines 7512-7517) to:
|
||
|
||
```cpp
|
||
std::fprintf(stderr,
|
||
"[help] pages=%d parsed=%d shortest='%s' %zu B; shortcuts: app=%d plot=%d; "
|
||
"about version=%s commit=%s built=%s settings=%s icon=%s; closed ok\n",
|
||
report.pages, report.parsed, report.shortest_title.c_str(),
|
||
report.shortest_bytes, accel_count, gesture_count, build_info::kVersion,
|
||
build_info::kCommit, build_info::kTimestamp, config_dir().c_str(),
|
||
about_icon.c_str());
|
||
```
|
||
|
||
- [ ] **Step 2: Add the stub and watch the assertion fail**
|
||
|
||
In `src/help_window.cpp`, after `show_about()`'s closing brace (line 126):
|
||
|
||
```cpp
|
||
const char* about_icon_name() { return ""; }
|
||
```
|
||
|
||
That position matters: the file's anonymous namespace closes at line 97, so everything from
|
||
`show_about()` onwards is in `namespace udpscope` proper, which is where the header declares this.
|
||
Putting it in the anonymous namespace instead gives an internal-linkage definition and the link
|
||
fails on the declared one.
|
||
|
||
Build and run:
|
||
|
||
```bash
|
||
cmake --build build 2>&1 | tail -5
|
||
python3 tools/mock_udpstreamer.py --rate 10000 --duration 20 &
|
||
sleep 1
|
||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||
UDPSCOPE_PROBE_HELP_MS=3000 ./build/udpscope --server 127.0.0.1 --port 8000 \
|
||
2>&1 >/dev/null | grep -E "^\[help\]|Gtk-CRITICAL"
|
||
pkill -f 'mock_udpstream[e]r'
|
||
```
|
||
|
||
Expected: the `[help]` line ends `settings=/home/martino/.config/udpscope icon=; closed ok` —
|
||
`icon=` is empty, which is the failing state. Note the two baselined `Gtk-CRITICAL`s do not appear
|
||
here; this run's criticals, if any, are new.
|
||
|
||
- [ ] **Step 3: Migrate the dialog**
|
||
|
||
In `src/help_window.cpp`, replace lines 16-35:
|
||
|
||
```cpp
|
||
/// Nulled by the `close-request` handler so a second open cannot present a dead widget.
|
||
/// One pointer per window, file-scope because there is one application.
|
||
GtkWidget* g_about = nullptr;
|
||
GtkWidget* g_help = nullptr;
|
||
|
||
/// The shortcut overview is an `AdwDialog`, not a window, so it has its own pointer and
|
||
/// its own clearing signal (`::closed` rather than `::close-request`).
|
||
AdwDialog* g_shortcuts = nullptr;
|
||
|
||
gboolean on_window_close(GtkWindow* window, gpointer slot) {
|
||
(void)window;
|
||
*static_cast<GtkWidget**>(slot) = nullptr;
|
||
return FALSE; // let the default handler destroy it
|
||
}
|
||
|
||
void on_shortcuts_closed(AdwDialog* dialog, gpointer user) {
|
||
(void)dialog;
|
||
(void)user;
|
||
g_shortcuts = nullptr;
|
||
}
|
||
```
|
||
|
||
with:
|
||
|
||
```cpp
|
||
/// Nulled by each window's own close handler, so a second open cannot present a dead widget.
|
||
/// One pointer per window, file-scope because there is one application.
|
||
///
|
||
/// The help window is a `GtkWindow`. The other two are `AdwDialog`s, which are **not**
|
||
/// `GtkWindow`s: they have no `::close-request`, so they clear on `::closed` and close with
|
||
/// `adw_dialog_close()`.
|
||
GtkWidget* g_help = nullptr;
|
||
AdwDialog* g_about = nullptr;
|
||
AdwDialog* g_shortcuts = nullptr;
|
||
|
||
gboolean on_window_close(GtkWindow* window, gpointer slot) {
|
||
(void)window;
|
||
*static_cast<GtkWidget**>(slot) = nullptr;
|
||
return FALSE; // let the default handler destroy it
|
||
}
|
||
|
||
/// Shared by both `AdwDialog`s: the pointer to clear is the `user_data`, so there is one
|
||
/// handler rather than one per dialog.
|
||
void on_dialog_closed(AdwDialog* dialog, gpointer slot) {
|
||
(void)dialog;
|
||
*static_cast<AdwDialog**>(slot) = nullptr;
|
||
}
|
||
```
|
||
|
||
Replace the whole of `show_about()` (lines 99-126) with:
|
||
|
||
```cpp
|
||
void show_about(GtkWindow* parent, const std::string& session_path) {
|
||
if (g_about != nullptr) {
|
||
adw_dialog_present(g_about, GTK_WIDGET(parent));
|
||
return;
|
||
}
|
||
|
||
// `AdwAboutDialog` rather than `GtkAboutDialog`: it shows the application icon as the page's
|
||
// own header, which is the whole reason the app has one, and `adw_about_window_new()` is
|
||
// deprecated in favour of it. It is an `AdwDialog`, so it is modal on what it is presented
|
||
// on and needs no transient-for or modal call of its own.
|
||
AdwDialog* dialog = adw_about_dialog_new();
|
||
g_about = dialog;
|
||
AdwAboutDialog* about = ADW_ABOUT_DIALOG(dialog);
|
||
adw_about_dialog_set_application_name(about, "udpscope");
|
||
adw_about_dialog_set_application_icon(about, "org.udpscope.app");
|
||
adw_about_dialog_set_developer_name(about, "Martino Ferrari");
|
||
adw_about_dialog_set_version(about, build_info::kVersion);
|
||
adw_about_dialog_set_comments(about, "An oscilloscope for UDPStreamer sources.");
|
||
adw_about_dialog_set_license_type(about, GTK_LICENSE_MIT_X11);
|
||
adw_about_dialog_set_website(about, "https://git.ferrarinas.it/martino/udpscope");
|
||
|
||
// `debug_info` rather than `system_information`: it renders as a Troubleshooting page with a
|
||
// copy button, where the old field was inert text a user had to retype. A `std::string`
|
||
// rather than a fixed buffer, because a long settings path used to truncate it silently.
|
||
const std::string debug = "Commit: " + std::string(build_info::kCommit) +
|
||
"\nBuilt: " + build_info::kTimestamp +
|
||
"\nSettings: " + config_dir() +
|
||
"\nSession file: " + session_path;
|
||
adw_about_dialog_set_debug_info(about, debug.c_str());
|
||
|
||
g_signal_connect(dialog, "closed", G_CALLBACK(on_dialog_closed), &g_about);
|
||
adw_dialog_present(dialog, GTK_WIDGET(parent));
|
||
}
|
||
|
||
const char* about_icon_name() {
|
||
if (g_about == nullptr) return "";
|
||
return adw_about_dialog_get_application_icon(ADW_ABOUT_DIALOG(g_about));
|
||
}
|
||
```
|
||
|
||
In `show_shortcuts()`, replace its signal connection:
|
||
|
||
```cpp
|
||
g_signal_connect(dialog, "closed", G_CALLBACK(on_shortcuts_closed), nullptr);
|
||
```
|
||
|
||
with:
|
||
|
||
```cpp
|
||
g_signal_connect(dialog, "closed", G_CALLBACK(on_dialog_closed), &g_shortcuts);
|
||
```
|
||
|
||
And replace `close_help_windows()`:
|
||
|
||
```cpp
|
||
void close_help_windows() {
|
||
if (g_help != nullptr) gtk_window_close(GTK_WINDOW(g_help));
|
||
if (g_about != nullptr) adw_dialog_close(g_about);
|
||
if (g_shortcuts != nullptr) adw_dialog_close(g_shortcuts);
|
||
}
|
||
```
|
||
|
||
`<cstdio>` may now be unused in this file — check whether anything else still calls `std::snprintf`
|
||
(the help window's section headings do, at `char markup[128]`), and leave the include alone if so.
|
||
|
||
- [ ] **Step 4: Revise the two doc comments the migration falsifies**
|
||
|
||
In `src/help_window.hpp`, replace lines 11-13:
|
||
|
||
```cpp
|
||
/// The About dialog: version, commit, build timestamp and where the session files live.
|
||
/// Modal on `parent`. `session_path` is this run's own session file.
|
||
void show_about(GtkWindow* parent, const std::string& session_path);
|
||
```
|
||
|
||
with:
|
||
|
||
```cpp
|
||
/// The About dialog: the app icon, version, commit, build timestamp and where the session
|
||
/// files live. An `AdwAboutDialog`, so it is modal on `parent` by being presented on it, and
|
||
/// it is an `AdwDialog` rather than a `GtkWindow`. `session_path` is this run's own session
|
||
/// file; it goes on the Troubleshooting page, which has a copy button.
|
||
void show_about(GtkWindow* parent, const std::string& session_path);
|
||
```
|
||
|
||
and replace lines 36-40:
|
||
|
||
```cpp
|
||
/// Closes whichever of the three windows are open. The About dialog and the help window are
|
||
/// GtkWindows and close with gtk_window_close(), so their close-request handlers run; the
|
||
/// shortcut overview is an AdwDialog and closes with adw_dialog_close(), which emits ::closed.
|
||
/// Either way the file-scope pointer is nulled by the handler. Used by the probe.
|
||
void close_help_windows();
|
||
```
|
||
|
||
with:
|
||
|
||
```cpp
|
||
/// Closes whichever of the three windows are open. One of them is a GtkWindow -- the help
|
||
/// window, closed with gtk_window_close(), which emits close-request so its handler runs. The
|
||
/// other two are AdwDialogs, which are not GtkWindows: they close with adw_dialog_close(),
|
||
/// which emits ::closed. Either way the file-scope pointer is nulled by the handler, which is
|
||
/// what prevents a second open presenting a dead widget. Used by the probe.
|
||
void close_help_windows();
|
||
```
|
||
|
||
- [ ] **Step 5: Run the probe to verify it passes**
|
||
|
||
```bash
|
||
cmake --build build 2>&1 | tail -5
|
||
python3 tools/mock_udpstreamer.py --rate 10000 --duration 20 &
|
||
sleep 1
|
||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||
UDPSCOPE_PROBE_HELP_MS=3000 ./build/udpscope --server 127.0.0.1 --port 8000 \
|
||
2>&1 >/dev/null | grep -E "^\[help\]|Gtk-CRITICAL|Gtk-WARNING"
|
||
pkill -f 'mock_udpstream[e]r'
|
||
```
|
||
|
||
Expected: one `[help]` line reading `pages=14 parsed=14`, ending
|
||
`icon=org.udpscope.app; closed ok`, and **no `Gtk-CRITICAL` and no `Gtk-WARNING`**. The three
|
||
windows are now built, presented and closed in one run through two different close calls, so this
|
||
is a crash test as much as a state check — `parsed` must still equal `pages`, and `closed ok`
|
||
appearing at all means nothing aborted on the way.
|
||
|
||
- [ ] **Step 6: Confirm the full exit dump still has only its two baselined criticals**
|
||
|
||
The About dialog is reached by the overflow menu in a normal run, so check the ordinary path too:
|
||
|
||
```bash
|
||
python3 tools/mock_udpstreamer.py --rate 10000 --duration 20 &
|
||
sleep 1
|
||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||
./build/udpscope --server 127.0.0.1 --port 8000 2>&1 >/dev/null \
|
||
| grep -c "Gtk-CRITICAL"
|
||
pkill -f 'mock_udpstream[e]r'
|
||
```
|
||
|
||
Expected: `2`. A third is a regression introduced here.
|
||
|
||
- [ ] **Step 7: Commit**
|
||
|
||
```bash
|
||
git add src/help_window.cpp src/help_window.hpp src/ui.cpp
|
||
git commit -m "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."
|
||
```
|
||
|
||
---
|
||
|
||
### Task 4: The desktop entry and its opt-in target
|
||
|
||
**Files:**
|
||
- Create: `data/org.udpscope.app.desktop.in`
|
||
- Create: `cmake/install_desktop_entry.cmake`
|
||
- Modify: `CMakeLists.txt` (append a section after line 127)
|
||
|
||
**Interfaces:**
|
||
- Consumes: the `udpscope` executable target, and the two SVG source paths from Task 1.
|
||
- Produces: a `desktop-entry` CMake target. Nothing in the C++ depends on it.
|
||
|
||
**The consequence to accept knowingly.** `@UDPSCOPE_EXEC@` is the built binary's absolute path in
|
||
the build tree, so **moving or deleting the build directory breaks the entry.** That is the price
|
||
of having no install step, and it is the right price: the entry then always launches exactly the
|
||
binary that was last built. `install()` rules were rejected because they create a second copy that
|
||
can diverge; a documented `cp` was rejected because `Exec` would need the binary on `PATH`, which
|
||
it is not.
|
||
|
||
No `StartupWMClass` is needed. GTK takes the WM class from the application ID, and the desktop
|
||
file's basename already matches it, which is what associates a running window with this entry.
|
||
|
||
- [ ] **Step 1: Write the desktop entry template**
|
||
|
||
Create `data/org.udpscope.app.desktop.in` — note there is no `data/` directory yet:
|
||
|
||
```
|
||
[Desktop Entry]
|
||
Type=Application
|
||
Name=udpscope
|
||
GenericName=Oscilloscope
|
||
Comment=An oscilloscope for UDPStreamer sources
|
||
Exec=@UDPSCOPE_EXEC@
|
||
Icon=org.udpscope.app
|
||
Terminal=false
|
||
Categories=Development;Science;Electronics;
|
||
Keywords=oscilloscope;UDP;signal;waveform;trigger;
|
||
StartupNotify=true
|
||
```
|
||
|
||
No SPDX header: a `.desktop` file's only comment syntax is `#`, and a comment before
|
||
`[Desktop Entry]` is accepted but `desktop-file-validate` prefers the group first. Leave it out.
|
||
|
||
- [ ] **Step 2: Write the installer script**
|
||
|
||
Create `cmake/install_desktop_entry.cmake`:
|
||
|
||
```cmake
|
||
# SPDX-License-Identifier: MIT
|
||
#
|
||
# The `desktop-entry` target's body. Copies the generated entry and the two app icons into
|
||
# $XDG_DATA_HOME, validates the entry, refreshes the caches that make the shell notice, and
|
||
# prints every path it wrote.
|
||
#
|
||
# This runs only when the target is named. The project deliberately has no install step -- that
|
||
# is the stated reason its icons are a GResource -- and a target that does nothing until asked
|
||
# preserves that while still producing a working entry.
|
||
#
|
||
# Expects -DENTRY=, -DICON= and -DSYMBOLIC= on the command line.
|
||
|
||
if(DEFINED ENV{XDG_DATA_HOME} AND NOT "$ENV{XDG_DATA_HOME}" STREQUAL "")
|
||
set(data_home "$ENV{XDG_DATA_HOME}")
|
||
else()
|
||
set(data_home "$ENV{HOME}/.local/share")
|
||
endif()
|
||
|
||
set(apps_dir "${data_home}/applications")
|
||
set(icon_dir "${data_home}/icons/hicolor/scalable/apps")
|
||
set(sym_dir "${data_home}/icons/hicolor/symbolic/apps")
|
||
|
||
# The icons are copied from the source tree rather than extracted from the GResource: the same
|
||
# two files serve both the in-binary resource and the installed theme, so there is one copy to
|
||
# edit. `file(COPY)` creates the directories.
|
||
file(COPY "${ENTRY}" DESTINATION "${apps_dir}")
|
||
file(COPY "${ICON}" DESTINATION "${icon_dir}")
|
||
file(COPY "${SYMBOLIC}" DESTINATION "${sym_dir}")
|
||
|
||
get_filename_component(entry_name "${ENTRY}" NAME)
|
||
get_filename_component(icon_name "${ICON}" NAME)
|
||
get_filename_component(sym_name "${SYMBOLIC}" NAME)
|
||
|
||
# Validated here rather than left to the shell, which ignores a malformed entry in silence: the
|
||
# failure belongs at the moment the entry is asked for.
|
||
find_program(DESKTOP_FILE_VALIDATE desktop-file-validate)
|
||
if(DESKTOP_FILE_VALIDATE)
|
||
execute_process(COMMAND "${DESKTOP_FILE_VALIDATE}" "${apps_dir}/${entry_name}"
|
||
RESULT_VARIABLE validate_result)
|
||
if(NOT validate_result EQUAL 0)
|
||
message(FATAL_ERROR "desktop-file-validate rejected ${apps_dir}/${entry_name}")
|
||
endif()
|
||
message(STATUS "desktop-file-validate: ok")
|
||
else()
|
||
message(STATUS "desktop-file-validate: not on PATH, entry not validated")
|
||
endif()
|
||
|
||
find_program(UPDATE_DESKTOP_DATABASE update-desktop-database)
|
||
if(UPDATE_DESKTOP_DATABASE)
|
||
execute_process(COMMAND "${UPDATE_DESKTOP_DATABASE}" "${apps_dir}")
|
||
endif()
|
||
|
||
find_program(GTK4_UPDATE_ICON_CACHE gtk4-update-icon-cache)
|
||
if(GTK4_UPDATE_ICON_CACHE)
|
||
execute_process(COMMAND "${GTK4_UPDATE_ICON_CACHE}" -f -t "${data_home}/icons/hicolor")
|
||
endif()
|
||
|
||
# Printed because there is no uninstall target: undoing this is these three paths.
|
||
message(STATUS "wrote ${apps_dir}/${entry_name}")
|
||
message(STATUS "wrote ${icon_dir}/${icon_name}")
|
||
message(STATUS "wrote ${sym_dir}/${sym_name}")
|
||
```
|
||
|
||
- [ ] **Step 3: Wire the target**
|
||
|
||
Append to `CMakeLists.txt`, after line 127's closing `endif()`:
|
||
|
||
```cmake
|
||
# --- the desktop entry: opt-in, never part of `cmake --build` ------------------
|
||
# `Exec` is the built binary's absolute path in the build tree, so the entry always launches
|
||
# exactly the binary that was last built -- and moving or deleting the build directory breaks
|
||
# it. That is the price of having no install step, and it is the right one here: an install()
|
||
# rule would create a second copy of the app that can diverge from the build tree, and a
|
||
# documented `cp` would need the binary on PATH, which it is not.
|
||
#
|
||
# configure_file() cannot evaluate a generator expression and file(GENERATE) cannot substitute
|
||
# an @VAR@, so the two run in sequence: the first writes the genex out literally, the second
|
||
# evaluates it. This is the documented idiom.
|
||
set(UDPSCOPE_EXEC "$<TARGET_FILE:udpscope>")
|
||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/data/org.udpscope.app.desktop.in
|
||
${CMAKE_CURRENT_BINARY_DIR}/desktop-entry.in @ONLY)
|
||
file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/org.udpscope.app.desktop
|
||
INPUT ${CMAKE_CURRENT_BINARY_DIR}/desktop-entry.in)
|
||
|
||
# No ALL: this runs only when it is named, so `cmake --build build` is unaffected.
|
||
add_custom_target(desktop-entry
|
||
COMMAND ${CMAKE_COMMAND}
|
||
-DENTRY=${CMAKE_CURRENT_BINARY_DIR}/org.udpscope.app.desktop
|
||
-DICON=${UDPSCOPE_ICON_DIR}/scalable/apps/org.udpscope.app.svg
|
||
-DSYMBOLIC=${UDPSCOPE_ICON_DIR}/scalable/apps/org.udpscope.app-symbolic.svg
|
||
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/install_desktop_entry.cmake
|
||
COMMENT "Installing the udpscope desktop entry into XDG_DATA_HOME"
|
||
VERBATIM)
|
||
add_dependencies(desktop-entry udpscope)
|
||
```
|
||
|
||
- [ ] **Step 4: Verify an ordinary build does not install anything**
|
||
|
||
```bash
|
||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo >/dev/null
|
||
rm -f ~/.local/share/applications/org.udpscope.app.desktop
|
||
cmake --build build 2>&1 | tail -5
|
||
ls ~/.local/share/applications/org.udpscope.app.desktop 2>&1
|
||
```
|
||
|
||
Expected: the build succeeds with the four known warnings, and the `ls` reports
|
||
**`No such file or directory`**. An ordinary build that writes into `$XDG_DATA_HOME` is the
|
||
failure this step exists to catch — check for a stray `ALL` on the target.
|
||
|
||
- [ ] **Step 5: Verify the generated entry has a real absolute path**
|
||
|
||
```bash
|
||
cat build/org.udpscope.app.desktop
|
||
```
|
||
|
||
Expected: the ten lines of the template with
|
||
`Exec=/home/martino/Projects/tools/udpscope/build/udpscope`. A literal
|
||
`Exec=$<TARGET_FILE:udpscope>` means the `file(GENERATE)` step did not run; an empty `Exec=`
|
||
means `configure_file` did not substitute.
|
||
|
||
- [ ] **Step 6: Run the target and verify all three files land and validate**
|
||
|
||
```bash
|
||
cmake --build build --target desktop-entry
|
||
echo "--- exit=$?"
|
||
ls -l ~/.local/share/applications/org.udpscope.app.desktop \
|
||
~/.local/share/icons/hicolor/scalable/apps/org.udpscope.app.svg \
|
||
~/.local/share/icons/hicolor/symbolic/apps/org.udpscope.app-symbolic.svg
|
||
desktop-file-validate ~/.local/share/applications/org.udpscope.app.desktop && echo "validate ok"
|
||
```
|
||
|
||
Expected: `desktop-file-validate: ok` among the `-- ` status lines, three `wrote ...` lines naming
|
||
the three paths, `exit=0`, all three files listed by `ls`, and `validate ok`. A validation failure
|
||
is a `FATAL_ERROR` from the script, which is the intended behaviour — fix the template.
|
||
|
||
- [ ] **Step 7: Verify the failure path, so the FATAL_ERROR is not decorative**
|
||
|
||
Note the exit status is captured **before** anything is piped: `cmd | tail` reports `tail`'s status,
|
||
not `cmake`'s, which would make this check always pass.
|
||
|
||
```bash
|
||
cp data/org.udpscope.app.desktop.in /tmp/entry-backup.in
|
||
sed -i 's/^Type=Application$/Type=Nonsense/' data/org.udpscope.app.desktop.in
|
||
cmake -S . -B build -G Ninja >/dev/null
|
||
cmake --build build --target desktop-entry >/tmp/de.log 2>&1; echo "--- exit=$?"
|
||
tail -4 /tmp/de.log
|
||
cp /tmp/entry-backup.in data/org.udpscope.app.desktop.in
|
||
cmp /tmp/entry-backup.in data/org.udpscope.app.desktop.in && echo "restore exact"
|
||
rm /tmp/entry-backup.in /tmp/de.log
|
||
```
|
||
|
||
Expected: a **non-zero** `exit=`, `desktop-file-validate rejected ...` in the tail, and
|
||
`restore exact`. If `exit=0` the `RESULT_VARIABLE` check is wrong. The file is untracked at this
|
||
point, so `git diff` cannot verify the restore — `cmp` against the backup is what can.
|
||
|
||
- [ ] **Step 8: Restore a good entry and commit**
|
||
|
||
```bash
|
||
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo >/dev/null
|
||
cmake --build build --target desktop-entry 2>&1 | tail -4
|
||
git add data/org.udpscope.app.desktop.in cmake/install_desktop_entry.cmake CMakeLists.txt
|
||
git commit -m "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."
|
||
```
|
||
|
||
---
|
||
|
||
### Task 5: Documentation, and the full regression run
|
||
|
||
**Files:**
|
||
- Modify: `CLAUDE.md` — the Verification list, the icon-authoring rules, the windows invariant
|
||
- Modify: `README.md` — the feature narrative
|
||
|
||
**Interfaces:**
|
||
- Consumes: every assertion and behaviour Tasks 1-4 produced.
|
||
- Produces: nothing code depends on.
|
||
|
||
- [ ] **Step 1: Add the `app icons:` assertion to CLAUDE.md's Verification list**
|
||
|
||
Insert immediately after the `toolbar icons:` bullet (search for `` `toolbar icons: trigger= ``):
|
||
|
||
```markdown
|
||
- `app icons: org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16)` (in the exit dump,
|
||
beside `toolbar icons:`) — the application icon, named and present at the size its own root
|
||
`<svg>` element declares. **Both entries must read `ok` at those sizes.** `ok` is
|
||
`gtk_icon_theme_has_icon()` *and* the resource bytes being found under the registered path, so a
|
||
`MISSING` is a resource path that was never registered or a file that was renamed, and a size
|
||
other than 128 or 16 is a canvas that drifted off the HIG's. Read it together with
|
||
`toolbar icons:`: the resource-path registration is one call in `run_ui()` serving both the
|
||
window's icon and the toolbar's buttons, so a `MISSING` on either line is the same defect.
|
||
**It deliberately does not claim the icon renders**, because no runtime GTK call can:
|
||
`gtk_icon_theme_lookup_icon()` never returns `NULL`, its paintable's intrinsic size is the size
|
||
it was *asked* for, and a valid icon, a malformed one and a name that does not exist at all all
|
||
snapshot to the same `GSK_TEXTURE_NODE` of the requested bounds — GTK substitutes
|
||
`image-missing`. A malformed SVG is a *build* error instead; see the icon-authoring rules.
|
||
```
|
||
|
||
- [ ] **Step 2: Amend the icon-authoring rules in CLAUDE.md**
|
||
|
||
Find the invariant beginning **`Four of the toolbar icons are this app's own`**. Two edits.
|
||
|
||
First, scope the existing filled-path rule. Replace:
|
||
|
||
```markdown
|
||
* **GTK's symbolic recolour only reaches `fill` on `path`, `rect` and `circle`.** It wraps the
|
||
SVG in a stylesheet for those selectors, so a `stroke`, a `line` or a `polyline` keeps the
|
||
colour written in the file and stays dark on a dark theme. Everything is a filled `path`.
|
||
```
|
||
|
||
with:
|
||
|
||
```markdown
|
||
* **GTK's symbolic recolour only reaches `fill` on `path`, `rect` and `circle`.** It wraps the
|
||
SVG in a stylesheet for those selectors, so a `stroke`, a `line` or a `polyline` keeps the
|
||
colour written in the file and stays dark on a dark theme. **In a symbolic icon everything is
|
||
therefore a filled `path`** — including anything that wants to look stroked, which is drawn as
|
||
a closed band (two offset curves joined) instead. The rule is scoped to symbolic icons on
|
||
purpose: `org.udpscope.app.svg` is 128px colour artwork that is never recoloured, so it strokes
|
||
its sine freely, which is the sane way to draw a smooth one. Do not "fix" one to match the
|
||
other.
|
||
```
|
||
|
||
Second, replace the rule about `--`:
|
||
|
||
```markdown
|
||
* `--` is illegal inside an XML comment and librsvg refuses the whole file for it.
|
||
```
|
||
|
||
with:
|
||
|
||
```markdown
|
||
* **A malformed SVG is a build error, and that is the only place it can be caught.** Every
|
||
`<file>` in `src/icons/udpscope.gresource.xml` carries `preprocess="xml-stripblanks"`, which
|
||
makes `glib-compile-resources` pipe it through `xmllint` — so `--` inside an XML comment, which
|
||
is illegal and which librsvg refuses the whole file for, fails the build with a file, line and
|
||
column. It has to be here because **no runtime check can see it**: librsvg refuses the file but
|
||
the icon *name* still resolves, so `gtk_icon_theme_has_icon()` returns true and the icon draws
|
||
nothing. `gtk_icon_theme_lookup_icon()` cannot tell either (see the `app icons:` line under
|
||
Verification), `GMarkup` accepts the double hyphen because it is a lenient subset parser, and
|
||
`gdk_pixbuf` goes through glycin here, which sandboxes a subprocess per load and refuses this
|
||
project's own icons outright. The cost is that the `xmllint` binary is a build dependency; it
|
||
ships with libxml2, which GTK already requires.
|
||
```
|
||
|
||
Also change that invariant's opening sentence from `Four of the toolbar icons are this app's own`
|
||
to `Six of this app's icons are its own` and mention the two app icons beside the four action ones,
|
||
including their `scalable/apps/` context and the fact that the registration is now one call in
|
||
`run_ui()` rather than at the top of `build_toolbar()`.
|
||
|
||
- [ ] **Step 3: Revise the windows invariant in CLAUDE.md**
|
||
|
||
Find the invariant beginning **`The About dialog, the help window and the shortcut overview are
|
||
opened by`**. It currently says the About dialog and the help window are both `GtkWindow`s; that is
|
||
now false. Revise it to: **one** `GtkWindow` (the help window, closed with `gtk_window_close()`,
|
||
which emits `close-request` so the handler that nulls the pointer runs — and
|
||
`gtk_window_destroy()` does not emit it and must not be used, the trap the math dialog fell into)
|
||
and **two** `AdwDialog`s (About and the shortcut overview, which are *not* `GtkWindow`s, close with
|
||
`adw_dialog_close()` and clear on `::closed`, there being no `::close-request`). Note that both
|
||
dialogs share one `::closed` handler keyed by a pointer-to-pointer, and that one pointer per
|
||
window nulled in its own close handler is the shape that prevents the double-open defect across
|
||
all three.
|
||
|
||
- [ ] **Step 4: Add the `icon=` field to CLAUDE.md's `[help]` probe bullet**
|
||
|
||
Find the bullet beginning `` `[help] pages=14 parsed=14 ``. Update the quoted example line to end
|
||
`settings=… icon=org.udpscope.app; closed ok`, add that **`icon=` must read `org.udpscope.app`** —
|
||
read back through `adw_about_dialog_get_application_icon()` while the dialog is open, so the dialog
|
||
is asserted to carry the icon rather than merely to open — and correct the sentence "Two of the
|
||
three are `GtkWindow`s" to "One of the three is a `GtkWindow`", noting that `closed ok` with no
|
||
`Gtk-CRITICAL` is now a stronger crash test because the three windows are closed by two different
|
||
calls.
|
||
|
||
- [ ] **Step 5: Document the `desktop-entry` target in CLAUDE.md**
|
||
|
||
Add to the **Build and run** section, after the `cmake --build build` block:
|
||
|
||
```markdown
|
||
The app can be launched from the shell rather than only from a terminal, but only on request:
|
||
|
||
```sh
|
||
cmake --build build --target desktop-entry
|
||
```
|
||
|
||
That writes `org.udpscope.app.desktop` plus both app icons under `$XDG_DATA_HOME` (default
|
||
`~/.local/share`), runs `desktop-file-validate` as a **hard failure**, refreshes
|
||
`update-desktop-database` and `gtk4-update-icon-cache` when they are on `PATH`, and prints the
|
||
three paths it wrote — which is the uninstall instructions, since there is no uninstall target.
|
||
`cmake --build build` alone never touches `$XDG_DATA_HOME`.
|
||
|
||
The entry's `Exec` is the built binary's **absolute path in the build tree**, so **moving or
|
||
deleting the build directory breaks it.** That is the price of having no install step and it is
|
||
deliberate: the entry then always launches exactly the binary that was last built. No
|
||
`StartupWMClass` is needed — GTK takes the WM class from the application ID, and the desktop
|
||
file's basename is that same string, which is what associates a running window with the entry.
|
||
```
|
||
|
||
- [ ] **Step 6: Write the README narrative**
|
||
|
||
Add a section to `README.md` alongside the other feature narratives, covering, in prose:
|
||
|
||
- **The icon.** It is the instrument face — a dark panel carrying one bold trace — because that is
|
||
the clearest available metaphor, it is the kind of physical object the HIG's top-and-front depth
|
||
language was written for, and its silhouette survives scaling down better than a bare waveform or
|
||
a colour version of the trigger glyph. The HIG numbers it is built to: a 128px canvas the artwork
|
||
does not fill, a 2px grid, a front profile no taller than 4px as the only depth treatment, flat
|
||
colour because every surface is straight, no shadows, the GNOME palette. **The two omissions and
|
||
why:** no graticule, because the grid lines a real scope face carries cannot survive 32px and the
|
||
trace carries the metaphor without them; and one cycle rather than several, because at 32px the
|
||
screen is about 22×16 px and anything busier reads as a smudge. And the authoring asymmetry — the
|
||
colour icon strokes its sine, the symbolic one cannot, because symbolic recolour reaches `fill`
|
||
only.
|
||
- **What the move to `AdwAboutDialog` gained.** The application icon in the dialog's own header,
|
||
which is what the app now has one for; a Troubleshooting page with a copy button where
|
||
`system_information` was inert text a user had to retype; and the removal of a `char[1024]` that
|
||
truncated a long settings path in silence. What it cost: `AdwAboutDialog` is an `AdwDialog` and
|
||
not a `GtkWindow`, so the close path differs from the help window beside it — the same trap the
|
||
shortcut overview already had. What was deliberately left out: credits, release notes, an issue
|
||
URL.
|
||
- **How to get the desktop entry**, the `Exec` caveat, and that there is no uninstall target
|
||
because the three paths are printed.
|
||
|
||
- [ ] **Step 7: Run the full documented regression set, one at a time**
|
||
|
||
Each of these is a separate serial run; `pkill` between them.
|
||
|
||
```bash
|
||
# 1. the exit dump, which carries `app icons:` and `toolbar icons:`
|
||
python3 tools/mock_udpstreamer.py --rate 10000 --duration 20 &
|
||
sleep 1
|
||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||
./build/udpscope --server 127.0.0.1 --port 8000 > /tmp/dump.txt 2>/tmp/dump.err
|
||
pkill -f 'mock_udpstream[e]r'
|
||
grep -E "app icons:|toolbar icons:|layout over|axes.w changes|x-desync|status bar:" /tmp/dump.txt
|
||
grep -c "Gtk-CRITICAL" /tmp/dump.err
|
||
```
|
||
|
||
Expected: both icon lines all `ok`; `x-desync 0`; `axes.w changes` 0 or a handful near the start;
|
||
`popover_rebuilds=0` in the `status bar:` line; and the critical count exactly **2**.
|
||
|
||
```bash
|
||
# 2. the help probe
|
||
python3 tools/mock_udpstreamer.py --rate 10000 --duration 20 &
|
||
sleep 1
|
||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||
UDPSCOPE_PROBE_HELP_MS=3000 ./build/udpscope --server 127.0.0.1 --port 8000 \
|
||
2>&1 >/dev/null | grep -E "^\[help\]|Failed to parse|Gtk-CRITICAL"
|
||
pkill -f 'mock_udpstream[e]r'
|
||
```
|
||
|
||
Expected: `pages=14 parsed=14`, `icon=org.udpscope.app`, `closed ok`, no `Failed to parse`, and
|
||
only the baselined criticals.
|
||
|
||
```bash
|
||
# 3. the keyboard accelerators, which reach show_about through the menu
|
||
python3 tools/mock_udpstreamer.py --rate 10000 --duration 20 &
|
||
sleep 1
|
||
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||
UDPSCOPE_PROBE_KEYS_MS=3000 ./build/udpscope --server 127.0.0.1 --port 8000 \
|
||
2>&1 >/dev/null | grep -E "^\[keys\]"
|
||
pkill -f 'mock_udpstream[e]r'
|
||
```
|
||
|
||
Expected: every `X->Y` pair differs and `refused=1`.
|
||
|
||
- [ ] **Step 8: Confirm the working tree holds only intended changes**
|
||
|
||
```bash
|
||
git status --short
|
||
```
|
||
|
||
Expected: only `CLAUDE.md` and `README.md` modified. A stray `data/*.in` or `src/icons/*` change
|
||
means one of Task 1's or Task 4's negative tests was not fully restored.
|
||
|
||
- [ ] **Step 9: Commit**
|
||
|
||
```bash
|
||
git add CLAUDE.md README.md
|
||
git commit -m "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."
|
||
```
|
||
|
||
---
|
||
|
||
## Verification summary
|
||
|
||
| assertion | where | must read |
|
||
|---|---|---|
|
||
| the colour and symbolic icons are bundled and resolve | exit dump, `app icons:` | `org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16)` |
|
||
| moving the registration broke nothing | exit dump, `toolbar icons:` | every entry `(ok)` |
|
||
| a malformed SVG | `cmake --build build` | build fails, naming file, line, column |
|
||
| the About dialog carries the icon | `[help]` probe | `icon=org.udpscope.app` |
|
||
| three windows, two close calls, no crash | `[help]` probe | `closed ok`, no `Gtk-CRITICAL` |
|
||
| the exit-dump critical count | exit dump stderr | exactly `2` |
|
||
| an ordinary build installs nothing | `ls ~/.local/share/applications` | no such file |
|
||
| the entry's `Exec` is absolute and real | `build/org.udpscope.app.desktop` | the build-tree binary path |
|
||
| a malformed entry | `--target desktop-entry` | `FATAL_ERROR`, non-zero exit |
|
||
| the layout still settles under the new window icon | exit dump | `x-desync 0`, `axes.w changes` small |
|