Conversation
The mark existed only as a 256x256 PNG, committed three times byte-identical, with no vector source. Nothing wired it to the Windows executable, the macOS bundle or the Linux window, so all three showed a generic icon. packaging/icons/sendspin.svg is now the only hand-authored artwork, and scripts/generate-icons.sh renders the committed set from it: the .ico, the .icns, the hicolor theme and the menu bar silhouette. The set is committed rather than built because a plain `dotnet build` needs the .ico, and a clean machine has no reason to own a rasterizer. scripts/pack-icons.py writes the .ico and .icns containers directly, so the generator does not need iconutil and runs on Linux as well as macOS. The CI guard hashes the masters and the generator rather than regenerating and diffing the outputs. A byte diff would be asserting that two builds of librsvg agree on edge antialiasing, which they need not; it would eventually fail whenever the runner image moved, for a reason no contributor could act on. One <ApplicationIcon> fixes two Windows symptoms at once. The compiler emits RT_GROUP_ICON 32512, which is both what Explorer, the taskbar and alt-tab read and what ShellBalloonNotificationService's ExtractIconEx call was falling through on, so the notification balloons pick up the mark with no change to that file. On Linux the window now carries WM_CLASS=io.sendspin.client, and every desktop file names the same string in StartupWMClass — the pair a desktop matches a running window against. Every packaging path installs the whole hicolor size range instead of only 256x256, and the AppImage, deb and Flatpak paths now install the checked-in desktop entry rather than each writing its own, which is what let the icon basename drift. Wayland is not addressed here: Avalonia.Wayland 12.1.1 never calls xdg_toplevel.set_app_id, so no compositor can match the window to the desktop file. The X11 half and the packaging half are correct and land regardless.
Avalonia cannot set it. The Wayland backend never sends xdg_toplevel.set_app_id, so the compositor sees an empty app id and cannot match the window to io.sendspin.client.desktop; StartupWMClass does not cover it, because that key is X11-only. Verified against the shipped assemblies: Avalonia.Wayland 12.1.2, the newest release, contains no app_id reference at all, while NWayland does expose XdgToplevel.SetAppId. The upstream API is WaylandPlatformOptions.AppId — Avalonia#21783 asks for it, Avalonia#21982 implements it, and Avalonia's API review accepted it on 2026-08-28. It has not shipped. When it does, the Wayland arm of ConfigureWindowing takes the same one-line .With(...) the X11 arm already has, which is what the comment there records. No workaround is attempted. Reaching the toplevel means walking a proxy to a worker-thread object and marshalling a libwayland call onto that thread, timed against a surface commit the app cannot observe — untestable here, and a soft-failing version would be indistinguishable from the current behaviour.
Walking the working tree also found the icons a local build.sh run leaves under artifacts/, and reported them as duplicates of the committed ones. The rule is about what is committed, so ask git. Comparing content hashes rather than guessing from size also drops the docs/ exclusion the heuristic needed.
macOS has no sha256sum and Linux CI should not depend on shasum being present from perl. The generator picks whichever exists; CI checks with sha256sum. The two write the same format, so a .source-hash written on either verifies on the other.
The drift manifest recorded only the inputs, so it caught a master edited without regenerating and nothing else — a hand-edited .ico, or a regeneration only half committed, both left CI green while the committed set was no longer what the generator produces. It now records the outputs too. That does not reintroduce the reproducibility problem the input-only form avoided: the hashes are written at generation time and committed beside the files they describe, so the check asserts "this set is what the generator last wrote", never "two builds of librsvg agree". The no-duplicate test asked git, which meant spawning a process that is not always there and asserting a repository-wide rule from a test named for the icon set. It now walks packaging/ and src/, where all three copies lived and which stays clear of the artifacts/ tree a local build.sh run fills. packaging/flatpak/io.sendspin.client.desktop was byte-identical to packaging/io.sendspin.client.desktop, which is the condition that justified deleting the AppImage copy; flatpak-builder resolves a relative source against the manifest's own directory, so it reads the shared one now. Also: pin the menu bar template's two sizes, since AppKit scales a mismatched template image rather than refusing it; drop the dead local in the status item fallback chain; drop the Assets placeholder, whose directory now holds only a linked item; and say plainly in build.sh that its inline Flatpak manifest is a known duplicate of the committed one, and why reconciling it is separate work.
chrisuthe
marked this pull request as ready for review
September 4, 2026 17:20
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The mark existed only as a 256×256 PNG, committed three times byte-identical, with no vector source. Nothing wired it to the Windows executable, the macOS bundle or the Linux window, so all three showed a generic icon.
One master, one generated set
packaging/icons/sendspin.svgis now the only hand-authored artwork of the app icon, andscripts/generate-icons.shrenders the committed set from it — the.ico, the.icnsand thehicolortheme.scripts/pack-icons.pywrites the.icoand.icnscontainers directly, so the generator needs noiconutiland runs on Linux as well as macOS.The set is committed rather than built: a plain
dotnet buildneeds the.ico, and a clean machine has no reason to own a rasterizer. Verified — withrsvg-convertand ImageMagick offPATH, the Windows head still builds.The SVG reproduces the old raster rather than redesigning it. The geometry was recovered by fitting the PNG, not by eye: a full-bleed disc, a radial gradient linear in distance from (89.6, 89.6) fitting to an RMS under 0.5/255 with no angular structure, and a centre hole at r = 42.7. Overlaying the two, 90% of pixels are within 1/255 and the difference is confined to two thin rings — the old raster had a ~4.5px soft edge from being resampled, while 50% alpha still lands at exactly r = 128. Same geometry; the vector render is the sharper of the two.
sendspin-menubar.svgis a second master and travels with this change because it needs the same generator. It is a different surface — a monochrome status glyph, not the app icon — and it is the only runtime behaviour change here.The CI guard hashes files, not a re-run
The new
iconsjob checkspackaging/icons/.source-hash, which records the SHA-256 of both masters, both scripts, and every generated file. The hashes are written at generation time and committed beside the files they describe, so the check asserts this set is what the generator last wrote from these masters — it catches a master edited without regenerating, an output hand-edited afterwards, and a regeneration only half committed.It deliberately does not regenerate and diff. That would assert that two builds of librsvg agree on edge antialiasing, which they need not, and would eventually fail whenever the runner image moved, for a reason no contributor could act on.
IconSetTestscovers what a hash cannot: that the containers are well formed. A truncated or single-size.icois still a valid file whose only symptom is a generic icon.Windows: one property, two symptoms
<ApplicationIcon>, conditioned on the Windows TFM for the same reason as the existingApplicationManifest, is the whole change. The compiler emits it asRT_GROUP_ICON32512 —IDI_APPLICATION. That is both the id Explorer, the taskbar and alt-tab read and the oneShellBalloonNotificationService'sExtractIconExcall was falling through on, so the notification balloons pick up the mark with no change to that file.Parsed out of the built executable:
PNG payloads at every size rather than BMP below 256: the head targets Win10 2004+, where the shell and
ExtractIconExhave read PNG entries since Vista.macOS
The
.icnsships as aBundleResourceandInfo.plistnames it inCFBundleIconFile; the existing bundle check in CI now asserts both, alongside the keys it already checked.NSWorkspace.icon(forFile:)on the built bundle returns the mark — the LaunchServices path Finder, the Dock and alt-tab all use.iconutilround-trips the generated.icnsinto the full ten-file iconset.The menu bar status item now uses the bundled silhouette, kept as
Template = trueso macOS keeps tinting it for light, dark and highlighted menu bars. Both existing fallbacks are preserved in order: themusic.noteSF Symbol, then the"Sendspin"title.Linux
The window carries
WM_CLASS=io.sendspin.clientviaX11PlatformOptions.WmClass, and the desktop entry names the same string inStartupWMClass— the pair a desktop matches a running window against. Every packaging path installs the wholehicolorsize range instead of only 256×256.The AppImage, deb and Flatpak paths now install the checked-in
packaging/io.sendspin.client.desktoprather than each writing its own inline copy, which is what let the icon basename drift. Two byte-identical duplicates of it are gone.Wayland is not fixed, and cannot be
This is an amendment to the acceptance criteria, not an omission. Avalonia's Wayland backend never sends
xdg_toplevel.set_app_id, so the compositor sees an empty app id and cannot match the window to the desktop file.StartupWMClassdoes not cover it — that key is X11-only. Verified against the shipped assemblies:Avalonia.Wayland12.1.2, the newest release, contains noapp_idreference at all, whileNWaylanddoes exposeXdgToplevel.SetAppId.The upstream API is
WaylandPlatformOptions.AppId— Avalonia#21783 asks for it, Avalonia#21982 implements it, and Avalonia's API review accepted it on 2026-08-28. It has not shipped. When it does, the Wayland arm ofConfigureWindowingtakes the same one-line.With(...)the X11 arm already has, which is what the comment there records. The gap is documented inREADME.mdanddocs/ARCHITECTURE.md;SENDSPIN_X11=1is the workaround meanwhile.No workaround is attempted. The toplevel is not on the window implementation — it is on a worker-thread object behind a proxy, so reaching it means marshalling a libwayland call onto that thread, timed against a surface commit the app cannot observe. A soft-failing version would be indistinguishable from the current behaviour.
Deduplication
The mark's raster is committed once. The Avalonia resource is a link, not a copy:
so
avares://Sendspin.Player/Assets/sendspin.pngstill resolves andTrayIconController.csis unchanged — which was the test of whether the centralization landed in the right place.MainWindowsetsIconto that same URI, so a broken link fails everyMainWindowShellTestscase at XAML load; that matters becauseTrayIconController.LoadIcon()deliberately swallows a missing asset into a warning and would not have shown the regression on its own.Two things I touched that the brief scoped out
scripts/build-appimage.shhad to change: deduplicating the icon made itsIcon=sendspinreference dangling, since no packaging path installs that basename any more, so shipping it untouched would have been a live bug. The out-of-scope item was reconciling the two AppImage builders, and that is untouched — it still duplicates the CI job, it just consumes canonical inputs now.scripts/build.shstill writes its own Flatpak manifest, which has already drifted from the committed one (it names runtime 23.08 against 25.08). Reconciling them is genuinely separate work — the committed manifest hardcodespublish/linux-x64, so pointing this at it would drop--runtime linux-arm64. A comment there now says so rather than leaving a reader to wonder.Verification
Green here: the full solution builds clean at Release with
TreatWarningsAsErrorsandEnforceCodeStyleInBuild, and each of the three heads builds individually../scripts/test.sh --configuration Release— 373 + 184 passed.This was built on macOS, so Windows and Linux desktop behaviour is evidenced by artifact contents rather than screenshots: the resource dump above, and the installed
hicolortree plus theWM_CLASS/StartupWMClasspair. Live testing on Windows and on a real Linux desktop needs to happen after release.Follow-ups, not fixed here
.icnsartwork is full-bleed, which is correct for Windows andhicolorbut off-HIG for the Dock on Big Sur and later, where artwork is expected inset within the canvas. Fixing it is an.icns-only inset in the generator, not a change to the master — but it is a design change, which this task puts out of scope.scripts/build.shandscripts/build-appimage.shstill fetchappimagetoolfromcontinuouswith no checksum, while the workflow pins a release and verifies its SHA-256 with a comment explaining why. Worth its own PR.