# App icon, Adwaita About dialog, and desktop entry Date: 2026-10-04 ## Goal Three related pieces of desktop integration udpscope currently lacks: 1. An application icon drawn to the GNOME HIG, in both full colour and symbolic form. 2. An About dialog that shows it, built on `AdwAboutDialog` rather than `GtkAboutDialog`. 3. A `.desktop` entry, so the app can be launched from the shell rather than only from a terminal. ## Decisions taken before design **The application ID stays `org.udpscope.app`.** GNOME convention is that the application ID, the icon name and the desktop file's basename are one string, so that string is `org.udpscope.app` throughout: the icon is named `org.udpscope.app`, the desktop file is `org.udpscope.app.desktop`, and `gtk_application_new()` is left exactly as it is. The alternative (moving to `it.ferrarinas.Udpscope`, the reverse-DNS of the project's actual forge domain) was offered and declined. Nothing else in the tree depends on the ID — the config directory is `~/.config/udpscope` and the app is `G_APPLICATION_NON_UNIQUE` — so this choice costs nothing beyond the naming itself. **The icon is the instrument face**, not a bare waveform and not a colour version of the existing trigger icon. A dark panel carrying one bold trace 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 being scaled down better than either alternative. **The desktop entry is installed by an opt-in CMake target**, not by `install()` rules and not by documented `cp` commands. 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. `install()` rules were rejected because they would create a second copy of the app that can diverge from the build tree; manual `cp` was rejected because `Exec` would need the binary on `PATH`, which it is not. ## The HIG constraints this is built to Taken from : - Nominal canvas **128×128**, which the artwork should *not* fill. - A **2px grid** for all detail. - Depth comes from combining the top and front of an object: a front profile along the bottom, **no taller than 2 detail units (4 nominal px)**. - Flat colour on straight surfaces; **gradients reserved for curved surfaces**. - Shadows **avoided** where possible. - The standard GNOME palette. - A **symbolic version is required**, reusing the same metaphor, following the UI-icon (single colour) rules. - The icon must still read at **64px** and **32px**. ## Component 1 — the artwork Two new source files, both added to the existing `src/icons/udpscope.gresource.xml` under the `scalable/apps/` tree (the icons already there are `scalable/actions/`). ### `src/icons/scalable/apps/org.udpscope.app.svg` Full colour, 128×128, every coordinate on the 2px grid: | element | geometry | palette colour | |---|---|---| | chassis | rounded rect (12, 22)–(116, 106), radius 8 | Dark 3 `#3d3846` | | front profile | the bottom band of that same silhouette, y 102–106 | Dark 2 `#5e5c64` | | screen | rounded rect (20, 30)–(108, 94), radius 4 | Dark 4 `#241f31` | | trace | one sine cycle, centred y = 62, amplitude 18, over x 24–104, 6 px thick | Green 1 `#8ff0a4` | The chassis is **one** rounded rect and the front profile is the bottom 4px *of that silhouette*, drawn as a path that follows its bottom corners — not a separate full-width rectangle underneath it. A plain rect from (12, 102) to (116, 106) would protrude past the radius-8 corners, which at 128px reads as a misdrawn bump and at 32px as a dirty edge. That band is exactly the HIG's 4px maximum and is the only depth treatment: no gradients (every surface here is straight, so flat colour is what the HIG asks for) and no shadows. The artwork occupies x 12–116 and y 22–106, leaving 12px side margins and 22px top and bottom, so it does not fill the canvas. Every coordinate above is even. Two deliberate omissions. There is **no graticule** — the grid lines a real scope face carries cannot survive 32px, and the trace already carries the metaphor without them. And the trace is **one** cycle, a single trough and a single crest, rather than several: at 32px the screen is about 22×16 px, and anything busier reads as a smudge. ### `src/icons/scalable/apps/org.udpscope.app-symbolic.svg` 16×16, the same metaphor reduced to a screen frame with a wave inside it. Unlike the colour icon this is **filled paths only, with an `evenodd` hole for the screen's interior**, because 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. Whole-pixel coordinates throughout. **This asymmetry is the trap worth documenting:** CLAUDE.md's existing icon-authoring rules say "everything is a filled `path`" without qualifying it, because every icon in the tree so far has been symbolic. A 128px colour icon is never recoloured, so it may stroke freely — and stroking is the sane way to draw a smooth sine. The rule becomes "fills only in a symbolic icon". ## Component 2 — the About dialog `adw_about_dialog_new()` returns an `AdwDialog*`. An `AdwDialog` is **not** a `GtkWindow`, which is the trap CLAUDE.md already documents for the shortcuts overview, so in `src/help_window.cpp`: - `g_about` changes from `GtkWidget*` to `AdwDialog*`. - It is cleared on `::closed`, not `::close-request` (`AdwDialog` has no such signal). - It closes with `adw_dialog_close()`, not `gtk_window_close()`. - It is presented with `adw_dialog_present()`. - Both `AdwDialog`s can share one `::closed` handler keyed by a pointer-to-pointer, leaving `on_window_close` to the help window alone. `close_help_windows()` therefore becomes one `GtkWindow` (help) plus two `AdwDialog`s (About, shortcuts), and both its doc comment and the CLAUDE.md invariant that says "two of the three are `GtkWindow`s" have to be revised to "one of the three". Field mapping: | `GtkAboutDialog` | `AdwAboutDialog` | |---|---| | `program_name` | `application_name` | | `version` | `version` | | `comments` | `comments` | | `license_type` | `license_type` | | `system_information` | `debug_info` | | — | `application_icon` = `"org.udpscope.app"` | | — | `developer_name`, `website` | Two gains are taken while mapping. `debug_info` renders as a Troubleshooting page with a copy button, where `system_information` was inert text. And the string behind it is built as a `std::string` rather than the `char details[1024]` a previous review flagged for silent truncation on a long settings path — removing the ceiling rather than widening it, in code already being rewritten. Credits, release notes and an issue URL are deliberately **not** added; nothing asked for them. One ordering detail: `gtk_icon_theme_add_resource_path()` currently runs at the top of `build_toolbar()`. The main window's own icon is now set from a name in the same resource, so the registration moves up into `run_ui()`, before both `gtk_window_set_icon_name()` and the `build_toolbar()` call — otherwise whichever of the two runs first resolves its name to nothing. ## Component 3 — the desktop entry ### `data/org.udpscope.app.desktop.in` ``` [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 ``` `@UDPSCOPE_EXEC@` is configured to the built binary's absolute path in the build tree. The consequence to accept knowingly: **moving or deleting the build directory breaks the entry.** That is the price of having no install step, and it is the right price here because 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 already matches it, which is what associates a running window with this entry. ### The `desktop-entry` target A custom target driving `cmake/install_desktop_entry.cmake`, which writes three files under `$XDG_DATA_HOME` (defaulting to `~/.local/share`): - `applications/org.udpscope.app.desktop` - `icons/hicolor/scalable/apps/org.udpscope.app.svg` - `icons/hicolor/symbolic/apps/org.udpscope.app-symbolic.svg` The two SVGs are copied from `src/icons/scalable/apps/` in the source tree, not extracted from the GResource: the same two files serve both the in-binary resource and the installed icon theme, so there is one copy to edit. It then refreshes `update-desktop-database` and `gtk4-update-icon-cache` if either is on `PATH`, runs `desktop-file-validate` if it is available, and prints every path it wrote. `cmake --build build` is unaffected: the target runs only when named. No uninstall target is provided — the three paths are printed, so undoing it is three `rm`s. ## Verification Two silent failures are possible, and they fail differently. A GResource path that was never registered draws `image-missing` while every other line of the dump passes; the existing `toolbar icons:` line already guards that class with `gtk_icon_theme_has_icon()`. But a **malformed SVG is worse**: librsvg refuses the whole file, `has_icon()` still returns true because the *name* resolves, and the icon draws nothing. `has_icon` cannot see it — and these SVGs have already been bitten once by librsvg, which rejects a file for a `--` inside an XML comment. The two are therefore caught in two different places, because **no runtime GTK call can see the malformed one.** This was measured rather than assumed, and three candidate checks were tried and rejected: - `gtk_icon_theme_lookup_icon()` never returns `NULL` and its paintable's intrinsic size is the size that was *asked for*, so it reads `128x128` for a valid icon, a malformed one and a name that does not exist at all. The same three cases snapshot to an identical `GSK_TEXTURE_NODE` of the requested bounds, since GTK substitutes `image-missing`. There is nothing to compare. - `GMarkup` accepts `` without complaint. It is a deliberately lenient subset parser, so it cannot stand in for the strict one that refuses the file. - `gdk_pixbuf_new_from_file()` on an SVG goes through glycin on this system, which sandboxes a subprocess per load, emits fontconfig errors, and refuses this project's existing icons outright for reasons of its own. Not a usable oracle. So the malformed file is caught at **build time**, which is better than a probe anyway: every `` line in `src/icons/udpscope.gresource.xml` gains `preprocess="xml-stripblanks"`, which makes `glib-compile-resources` pipe each SVG through `xmllint`. A double hyphen inside a comment then **fails the build** with the file, line and column, instead of drawing nothing at runtime. The four existing icons already pass it. The cost is that the `xmllint` binary becomes a build dependency; it ships with libxml2, which GTK already requires. The runtime line then asserts what a runtime check genuinely can: ``` app icons: org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16) ``` `ok` is `gtk_icon_theme_has_icon()` **and** the resource bytes being present under the registered path, and the size is the one the file's root `` element declares, read out of those bytes. **Both entries must read `ok` at those sizes.** That covers the unregistered-path case, a renamed or mis-pathed file, and a canvas size that drifted off the HIG's 128 — and it is computed on the first tick beside `toolbar icons:`, not in the dump, which runs after the window is destroyed. Alongside it: - The `[help]` probe gains `about icon=org.udpscope.app`, read back through `adw_about_dialog_get_application_icon()`, so the dialog is asserted to carry the icon rather than merely to open. - That probe's existing `closed ok` becomes a stronger crash test, because About is now an `AdwDialog` closed by a different call than the help window beside it. It must still produce no `Gtk-CRITICAL` beyond the two already baselined. - `desktop-file-validate` runs inside the `desktop-entry` target, so a malformed entry fails at the moment it is asked for rather than being silently ignored by the shell later. ## Documentation - **CLAUDE.md**: the `app icons:` dump line as a documented assertion, and the `xml-stripblanks` build-time check beside the five existing icon-authoring rules — the sixth rule is that the malformed-SVG failure is a *build* error now and no runtime check can replace it; the windows invariant revised to one `GtkWindow` and two `AdwDialog`s; the `desktop-entry` target and the build-path caveat; and the icon-authoring rules amended so "filled paths only" is scoped to symbolic icons. - **README.md**: the feature narrative — the icon's HIG constraints and the two omissions (graticule, multiple cycles) with the 32px reasoning, the move to `AdwAboutDialog` and what it gained, and how to get the desktop entry. ## Out of scope - An AppStream `metainfo.xml`. Nothing asked for it, and it is only required for store submission. - Redrawing the four existing action icons. - A nightly/devel icon variant. - An uninstall target for the desktop entry.