Desktop integration
The app runs, but the desktop doesn’t treat it as one: its windows land under a generic icon, media keys do nothing, the tray icon never shows, a second launch opens a second copy, or double-clicking a file doesn’t open it. Each comes down to a name the desktop and the app have to agree on: a window class, a D-Bus name, a desktop-file field. Measure the name on the running app and write it into the package.
For agents
Windows not grouped under the app’s icon
The desktop matches a window to its launcher through StartupWMClass in the
desktop file (X11 WM_CLASS, Wayland app_id). A wrong value leaves windows
under a generic icon and the launcher looking idle.
- Measure it, on X11 with
XQueryTreeandXGetClassHintover all windows, or with the compositor’s client list (hyprctl clients -j). Electron main windows set only_NET_WM_NAME, so a scan that filters onXFetchNamemisses them. - Electron: the main window’s class is the app’s product name
(
edrlab-thoriumreader,min), not the binary name and not the app id. The name of the file the wrapper runs only names an invisible leader window. - Branded Chromium browsers ignore
CHROME_DESKTOP; the class is hardcoded (Brave Origin’s isbrave-origin). - Tauri and GTK apps without a GApplication id take the class from
argv[0]. Alaunchsymlink makes itlaunch; name the symlink after the app id and setStartupWMClassto the same (Flow). - Apps with a compiled GApplication id use that id (a Flutter template’s
com.example.<app>); keepStartupWMClassequal to it.
Media controls (MPRIS) missing
Players register org.mpris.MediaPlayer2.<name> on the session bus. Flatpak
lets an app own org.mpris.MediaPlayer2.<app-id>.* and nothing else, so any
other name is filtered and media keys and desktop widgets never see the player.
--log-session-bus shows Failed to get the ownership of org.mpris….
| Toolkit | Name it registers | Grant |
|---|---|---|
| Electron, Chromium | org.mpris.MediaPlayer2.chromium.instance2 (sandbox PID 2), only while media plays |
--own-name=org.mpris.MediaPlayer2.chromium.* (Folia) |
| WebKitGTK (Tauri) | org.mpris.MediaPlayer2.<id>.Sandboxed.instance-N, where <id> is the GApplication id, else the program name if it is a dotted id, else org.webkit.app-<hash> |
Name the launcher symlink after the app id; no grant needed (Flow) |
| Flutter (audio plugins) | org.mpris.MediaPlayer2.<app-id>.instance2 |
Covered by default; a narrower --own-name=…<app-id>.* is harmless (Vynody) |
- Test with sound playing. Chromium registers the name only during
playback, so an idle launch proves nothing. Start the app with
--remote-debugging-port, play a generated tone throughnew Audio(blobURL)over CDP, and watchbusctl --user list. - Cover art missing: Chromium writes the art to
$TMPDIRand hands the desktop afile://URL, and the sandbox’s/tmpis private. SetTMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID"in the wrapper, a path that is the same inside and outside; never grant/tmp.
Tray icon missing
Tray icons use the StatusNotifierItem protocol: the app owns a name and
registers it with org.kde.StatusNotifierWatcher. Every tray app needs
--talk-name=org.kde.StatusNotifierWatcher; what it owns depends on the
toolkit:
| Toolkit | Needs |
|---|---|
Tauri tray-icon |
libayatana-appindicator, which the GNOME runtime lacks: the ayatana-stack prebuilt, plus --filesystem=xdg-run/tray-icon:create for the icon files (AeroFTP). Without the library, tray-icon panics at start. |
Flutter tray_manager / desktop tray plugin |
--own-name=org.fluttertray.* (Vynody) |
| Avalonia (.NET) | Owns org.kde.StatusNotifierItem-2-0: --own-name=org.kde.* (XerahS) |
| Qt 6 (Enpass 6.12), Electron 45 and later, Chromium 155 and later | Register under their unique connection name; the watcher grant alone is enough |
| Wails v3 | Unique connection name; the watcher grant alone |
Check with --log-session-bus and busctl --user list | grep StatusNotifier.
A Tauri app that uses tray-icon only as a symbol from Tauri’s own ACL table
has no tray; look for TrayIconBuilder in its source before adding the stack.
A second launch starts a second copy
Single-instance apps own a well-known name on the session bus; a second launch finds it taken and hands its arguments over. If the package doesn’t grant that name, the first instance can’t own it, and every launch starts a full copy. Nothing logs an error.
- tauri-plugin-single-instance 2.4 and later owns
<tauri identifier>.SingleInstanceexactly; older versions usedorg.<identifier with . and - as _>.SingleInstance. Check the version in upstream’sCargo.lockand grant that name. - A compiled GApplication id different from the Flatpak id (a Flutter app
still on the template’s
com.example.<app>, a Wails app’sorg.wails.<name>):g_application_registeris refused and the app may exit silently. Grant--own-name=<compiled id>. - Test by launching twice: the second should exit at once, and
busctl --user listshows one owner.
Opening files from the file manager
- In the desktop file, put only
%f,%F,%uor%UinExec=. Flatpak adds--file-forwardingand the@@ … @@markers itself when it exports the file; writing them yourself makes the install fail (Invalid Exec argument @@u), and--file-forwardingisn’t afinish-argsoption. %Uhands the app afile:///run/user/…/doc/…URI and%Fa plain path. Many apps that parseargvthemselves only understand paths; test which one the app accepts.- Forwarded files arrive through the document portal, so no
--filesystemgrant is needed for them.
Apps that write desktop entries or autostart files
Some apps register a URL handler or a login item by writing a .desktop file
with Exec=/app/extra/…, which is a broken path on the host. Mask the
directories they write to, --nofilesystem=xdg-data/applications and
--nofilesystem=xdg-config/autostart, when the package otherwise grants home;
launch at login is a feature the sandbox can’t offer that way. A Wine prefix’s
installer can also drop shortcuts on the host’s desktop unless the prefix’s
desktop folder is redirected inside the sandbox.