Skip to content

feat: show the app icon on Wayland without AppImage integration - #689

Merged
Lolle2000la merged 4 commits into
v3.0from
feat/wayland-app-icon
Oct 11, 2026
Merged

Lolle2000la merged 4 commits into
v3.0from
feat/wayland-app-icon

Conversation

@Lolle2000la

@Lolle2000la Lolle2000la commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Problem

On Wayland the window icon is not a client property: compositors resolve it (and the app's display name / launcher grouping) through the desktop entry whose file name matches the toplevel's app_id. An AppImage only gets such an entry when a desktop-integration tool (appimaged, AppImageLauncher, Gear Lever, ...) has "installed" it, so an unintegrated AppImage always shows a generic icon even though it ships MediaSort.desktop and the icon files itself. The old app-id logic (appimagekit-<stem>.desktop / <stem>) only covered the naming schemes of some integration tools and can't help on its own.

The xdg-toplevel-icon-v1 protocol would solve this properly, but iced 0.14 pins winit 0.30.13, which cannot speak it (winit only gained support on its main branch, rust-windowing/winit#4288). Until iced moves to a newer winit, the standard cross-compositor route is to make the desktop-entry lookup succeed.

Solution

New crates/media-sort-gui/src/linux_desktop.rs, called once from main.rs to produce the window's application_id:

  1. Adopt a canonical entry. If a packaged or hand-written MediaSort.desktop (the canonical id) exists anywhere, it wins: its name is used as the app id and nothing is written.
  2. Adopt an integration entry. Otherwise it scans $XDG_DATA_HOME/applications and $XDG_DATA_DIRS/*/applications for a *.desktop file whose Exec/TryExec launches the running $APPIMAGE (or, when there is no AppImage, the running executable) - any integration tool, any file name; quotes, env prefixes and \s escapes are parsed and symlinked AppImages are resolved. Entries inside the running AppImage's own $APPDIR mount are ignored (the compositor cannot see the mount, so they must not count as "already registered"). If found, its file name is adopted as the app id and nothing is written.
  3. Self-provision when missing. Otherwise a hidden (NoDisplay=true) desktop entry plus the scalable MediaSort icon are written into $XDG_DATA_HOME, under a name unique to this module (applications/mediasort-wayland.desktop and icons/hicolor/scalable/apps/mediasort-wayland.svg).

The provisioned files are kept between runs - they are what gives the shell the icon, and removing them at exit would only churn icon/menu caches and break window grouping across a restart. Because their name is unique to this module, a self-provisioned file can never collide with or shadow a packaged, distro- or tool-managed MediaSort.desktop; a foreign file found at one of those paths is left untouched. They are removed again only when a canonical MediaSort.desktop appears (or when one was written under the canonical name by an earlier revision), so the packaged entry's icon and running state take over. Writes go through path_utils::atomic_write and are skipped when content is unchanged.

Nothing is provisioned on non-Wayland sessions: on X11 the icon is delivered by the window itself (window::Settings.icon), so only the app id is set there - to the canonical MediaSort, which matches the packaged and integration entries' StartupWMClass=MediaSort. This also gives cargo run and the standalone binary an icon on Wayland (without an AppImage, Exec points at the running executable).

Paths that cannot be represented portably (control characters, %, $, backticks) are skipped, so an invalid desktop entry is never written; spaces, quotes and backslashes are escaped for both the key-file and the shell parsing layers (verified against desktop-file-validate, GLib's loader and KDE's KConfig/KShell).

The embedded icon is packaging/linux/icon.svg itself - the scalable SVG the AppImage also ships - so it cannot drift from the packaged icon. GNOME (librsvg) and KDE (QtSvg) render theme SVGs natively (Breeze ships SVG-only icons), so no raster renditions are needed.

Testing

  • 26 unit tests: adoption by Exec/TryExec/symlink/env wrapper/plain executable/indented Exec, provisioning incl. the hidden-entry contents and the icon file, refresh when the executable moves, persistence and repair across runs, never overwriting a foreign entry or a user-replaced icon, removal when a canonical entry appears, canonical-name migration, quote/escape handling, rejection of unrepresentable characters, $XDG_DATA_DIRS parsing and the $APPDIR exclusion.
  • Exec escaping verified against desktop-file-validate, GLib's actual GDesktopAppInfo loader and KDE's KConfig/KShell.
  • Manually verified on KDE Wayland: WAYLAND_DEBUG=1 shows xdg_toplevel.set_app_id(...) and the provisioned files appear under $XDG_DATA_HOME; desktop-file-validate passes on the generated entry.
  • cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace.

Notes

  • The maintainer's existing hand-written ~/.local/share/applications/media_sort.desktop is adopted as-is when that AppImage runs (its Exec matches), so no duplicate hidden entry is created for it.
  • xdg-toplevel-icon-v1 is only partially implemented across compositors (KWin yes, Mutter/GNOME currently no), so even after iced ships a newer winit the desktop-entry route remains necessary for GNOME. When iced does, this module can be reduced to that protocol or removed - noted in AGENTS.md.

@Lolle2000la
Lolle2000la force-pushed the feat/wayland-app-icon branch 3 times, most recently from 42216dd to cf83da9 Compare October 11, 2026 19:11
Wayland compositors never read the window icon from the client; they
resolve a toplevel's icon through the desktop entry whose file name
matches the toplevel's app_id. An AppImage that was never integrated by
appimaged/AppImageLauncher/Gear Lever has no such entry, so the window
showed a generic icon even though the AppImage ships a desktop entry
and the icon files.

The new linux_desktop module scans the XDG application directories for
an entry whose Exec/TryExec launches the running $APPIMAGE and adopts
its file name as the app id (covering every integration tool, and
replacing the hardcoded appimagekit-<stem> guess). When nothing
matches, it writes a hidden (NoDisplay=true) MediaSort.desktop plus the
scalable MediaSort.svg into the user's XDG data directories, the same
locations integration tools use, so the icon resolves on any compositor
without adding a launcher entry.

Only files carrying the X-MediaSort-Provisioned marker are ever
rewritten or removed; packaged and hand-written entries always win (a
provisioned entry is even removed again when a packaged one appears,
since it would shadow it). Writes go through path_utils::atomic_write
and are skipped when content is unchanged.

Running instances coordinate through a shared flock on
$XDG_DATA_HOME/media-sort/desktop-integration.lock; the last instance
to exit removes the provisioned entry and icon again, so a cleanly
closed app leaves no integration behind. The cleanup is best-effort: a
crash or SIGKILL skips it and leaves the hidden files for the next run,
and the lock file itself stays behind.

The embedded icon is packaging/linux/icon.svg itself - the same
scalable icon the AppImage ships - so it cannot drift from it.
@Lolle2000la
Lolle2000la force-pushed the feat/wayland-app-icon branch from cf83da9 to 125108e Compare October 11, 2026 19:43
…ique id

Replace the provision-then-remove lifecycle (shared flock + cleanup on the
last exit) with a persistent desktop entry and icon named after a unique id.
Because the provisioned name can never collide with or shadow a packaged,
distro- or tool-managed MediaSort.desktop, keeping the files between runs is
safe, and it avoids per-launch icon/menu cache churn and a window group that
disappears across a restart.

- Provision applications/mediasort-wayland.desktop and
  icons/hicolor/scalable/apps/mediasort-wayland.svg (PROVISIONED_ID) instead of
  the canonical MediaSort name; the canonical id is used as the app id only
  when a real packaged/hand-written MediaSort.desktop exists.
- Drop IntegrationGuard/cleanup_provisioned; application_id() returns the id
  directly, matching what AGENTS.md already documented.
- Never overwrite a foreign file at the provisioned entry path; remove the
  provisioned files again only when a canonical entry appears, and migrate an
  entry written under the canonical name by an earlier revision.
- Ignore $APPDIR-bundled data dirs while scanning so an AppRun that prepends
  $APPDIR/usr/share to XDG_DATA_DIRS cannot silently disable provisioning.
- 22 unit tests; escaping re-verified against desktop-file-validate and GLib.
On X11 the window icon is delivered by the window itself
(window::Settings.icon), so the desktop-entry provisioning added nothing there
and only left a permanent footprint in the user's XDG data directories. Gate
the module on a Wayland session (WAYLAND_DISPLAY set); on other backends return
the canonical app id, which matches the packaged/Gear Lever
StartupWMClass=MediaSort used for X11 window grouping.
…reads

Follow-ups from review of the persistent desktop-entry integration:

- Adopt an existing desktop entry that launches the running executable even
  without an AppImage (a hand-written entry for a development build is now
  picked up instead of provisioning a second one).
- Compare existing files with a bounded read (read_capped_bytes), so a large or
  hostile file at the provisioned entry/icon path is never read whole.
- Write the icon before the entry, so a failed icon write cannot leave a marked
  entry pointing at a missing icon (which made the returned app id match
  neither the entry nor the icon).
- Tolerate leading whitespace before Exec=/TryExec=, as a key-file does.
- Tests for executable adoption, an indented Exec and a kept user-replaced icon
  (26 unit tests total).
@Lolle2000la
Lolle2000la merged commit 886c963 into v3.0 Oct 11, 2026
5 checks passed
@Lolle2000la
Lolle2000la deleted the feat/wayland-app-icon branch October 11, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant