Records the decisions taken before design — the application ID stays org.udpscope.app, the icon is the instrument face, and the desktop entry is an opt-in CMake target rather than an install() rule — along with the HIG constraints the artwork is built to and the two silent failures the new verification line has to tell apart. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
11 KiB
App icon, Adwaita About dialog, and desktop entry
Date: 2026-10-04
Goal
Three related pieces of desktop integration udpscope currently lacks:
- An application icon drawn to the GNOME HIG, in both full colour and symbolic form.
- An About dialog that shows it, built on
AdwAboutDialograther thanGtkAboutDialog. - A
.desktopentry, 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 https://developer.gnome.org/hig/guidelines/app-icons.html:
- 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_aboutchanges fromGtkWidget*toAdwDialog*.- It is cleared on
::closed, not::close-request(AdwDialoghas no such signal). - It closes with
adw_dialog_close(), notgtk_window_close(). - It is presented with
adw_dialog_present(). - Both
AdwDialogs can share one::closedhandler keyed by a pointer-to-pointer, leavingon_window_closeto the help window alone.
close_help_windows() therefore becomes one GtkWindow (help) plus two AdwDialogs (About,
shortcuts), and both its doc comment and the CLAUDE.md invariant that says "two of the three are
GtkWindows" 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.desktopicons/hicolor/scalable/apps/org.udpscope.app.svgicons/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 rms.
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.
So the new assertion distinguishes named from renderable:
app icons: org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16)
It looks each icon up and queries the resulting paintable's intrinsic size. Both entries must read
ok with a non-zero size; a parse failure reports 0x0 with the name still resolving, which is
precisely the case the existing check misses.
Alongside it:
- The
[help]probe gainsabout icon=org.udpscope.app, read back throughadw_about_dialog_get_application_icon(), so the dialog is asserted to carry the icon rather than merely to open. - That probe's existing
closed okbecomes a stronger crash test, because About is now anAdwDialogclosed by a different call than the help window beside it. It must still produce noGtk-CRITICALbeyond the two already baselined. desktop-file-validateruns inside thedesktop-entrytarget, 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; the windows invariant revised to oneGtkWindowand twoAdwDialogs; thedesktop-entrytarget 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
AdwAboutDialogand 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.