System Integration
D-Bus, notifications, file dialogs, secrets, search, media controls, background services, and hardware APIs — how a Linux app becomes part of the desktop instead of a window on top of it.
A window that opens is not an integrated app. Integration is what happens when the app uses the desktop’s file picker, appears in search, sends a notification the user can act on, remembers a password in the keyring, and yields to suspend without leaving a mess.
On Linux, most of that integration happens over D-Bus and XDG Desktop Portals. The toolkit wraps the common cases; the specs cover the rest.
The integration surface
Section titled “The integration surface”| Capability | Standard interface | Typical API |
|---|---|---|
| Application activation | org.freedesktop.Application |
Gtk.Application, QApplication |
| Notifications | org.freedesktop.Notifications |
Gio.Notification, KNotification |
| File dialogs | FileChooser portal | Gtk.FileDialog, Qt platform theme |
| Open files/URLs | OpenURI portal | Gtk.FileLauncher, Gio.AppInfo |
| Secrets | Secret Service | libsecret, QKeychain/KWallet |
| Screen capture | ScreenCast portal | PipeWire + portal |
| Global shortcuts | GlobalShortcuts portal | portal client |
| Search | GNOME Shell search provider | D-Bus service |
| Media controls | MPRIS | org.mpris.MediaPlayer2 |
| Background/autostart | Background portal | portal client |
| Settings | GSettings / dconf | Gio.Settings |
| Power/idle | Inhibit portal, UPower | Gtk.Application.inhibit, D-Bus |
| Hardware | UPower, BlueZ, NetworkManager, UDisks | D-Bus proxies |
Use the highest-level API the toolkit offers. It will choose the right transport, handle async, and keep the app working inside a sandbox.
Single instance and activation
Section titled “Single instance and activation”A desktop app should be a single instance with multiple windows — not five independent processes. Gtk.Application handles this through D-Bus: the first process owns the application bus name, later launches activate it. QApplication does not register a bus name by itself; use KDBusService(KDBusService::Unique) for KDE/Qt, or QLockFile/a custom D-Bus name for non-KDE Qt.
class NotesApplication(Adw.Application): def __init__(self): super().__init__( application_id="io.github.yourname.Notes", flags=Gio.ApplicationFlags.HANDLES_OPEN, ) self.connect("activate", self.on_activate) self.connect("open", self.on_open)
def on_activate(self, _app): self.present_window()
def on_open(self, _app, files, _n_files, _hint): window = self.present_window() for file in files: window.open_file(file)Set DBusActivatable=true in the desktop entry and ship a D-Bus service file if the app should start on demand:
[D-BUS Service]Name=io.github.yourname.NotesExec=/usr/bin/notes --gapplication-serviceGtk.Application implements org.freedesktop.Application for you. Do not invent a second activation protocol.
On Wayland, activation is also subject to focus-stealing prevention. Gtk.Application and Qt handle the activation token for you. If you use a custom toolkit, implement xdg_activation_v1 and pass the XDG_ACTIVATION_TOKEN from the launching process; Gio.AppInfo and Gtk.FileLauncher already do this. Do not use wmctrl, xdotool, or X11 activation hacks.
Notifications
Section titled “Notifications”Notifications are for events the user should know about while not looking at the app: an export finished, a sync failed, a message arrived. They are not a logging channel and not a way to advertise features.
notification = Gio.Notification.new("Export finished")notification.set_body("Your notes were exported to Documents.")notification.add_button("Open folder", "app.open-export")notification.set_default_action("app.open-export")notification.set_priority(Gio.NotificationPriority.NORMAL)app.send_notification("export-finished", notification)- Reuse a stable notification ID to replace an existing notification instead of stacking duplicates.
- Provide an action whenever there is something useful to do.
- Withdraw the notification when the task is cancelled or resolved.
- Never include secrets, tokens, full file paths, or personal data in the body.
- Respect the desktop’s “do not disturb” state; do not escalate priority to bypass it.
On GNOME, notifications are grouped by application. On KDE, they follow the host’s notification settings. Both are correct. Do not build a custom popup.
File dialogs and opening files
Section titled “File dialogs and opening files”Use the toolkit’s file dialog. It uses the FileChooser portal when sandboxed, remembers the last folder, and respects the desktop’s bookmarks.
dialog = Gtk.FileDialog(title="Open note")dialog.open(self, None, self.on_open_finished)
def on_open_finished(self, dialog, result): try: file = dialog.open_finish(result) except GLib.Error: return # user cancelled or portal denied self.load_note(file)For “open with the default app”, use Gtk.FileLauncher (GTK 4.10+) or Gio.AppInfo.launch_default_for_uri(). Do not shell out to xdg-open; it bypasses the sandbox and loses error handling.
For recent files, register with Gtk.RecentManager so the file appears in the desktop’s recent list. Do not keep a private list that only your app can see.
Secrets
Section titled “Secrets”Never store passwords, OAuth tokens, or API keys in a JSON/INI file. Use the Secret Service (libsecret on GNOME, KWallet on KDE with a compatible service). The secret is encrypted at rest and unlocked with the user’s login keyring.
import gigi.require_version("Secret", "1")from gi.repository import Secret
schema = Secret.Schema.new( "io.github.yourname.Notes", Secret.SchemaFlags.NONE, {"account": Secret.SchemaAttributeType.STRING},)
Secret.password_store_sync( schema, {"account": "sync"}, Secret.COLLECTION_DEFAULT, "Notes sync token", token, None,)- Use a stable schema name and attributes that identify the secret, not the user’s email address.
- Handle “keyring locked” and “user denied” as normal states, not crashes.
- Provide a way to forget the secret.
- In a Flatpak, the Secret Service is available through the portal; no extra filesystem permission is needed.
Search
Section titled “Search”If the app has searchable content, expose a GNOME Shell search provider. It is a D-Bus service implementing org.gnome.Shell.SearchProvider2:
GetInitialResultSet(terms)GetSubsearchResultSet(previous, terms)GetResultMetas(ids)ActivateResult(id, terms, timestamp)LaunchSearch(terms, timestamp)
Keep results fast: return IDs first, load metadata lazily, and never block the shell. A search provider is optional; a broken one is worse than none.
On KDE, the equivalent is KRunner runners. Do not build both unless you are targeting both desktops seriously.
Media controls with MPRIS
Section titled “Media controls with MPRIS”Media players should implement MPRIS (org.mpris.MediaPlayer2 and org.mpris.MediaPlayer2.Player). That gives the desktop media widget, lock-screen controls, keyboard media keys, and playerctl compatibility for free.
Minimum surface:
PlaybackStatus(Playing,Paused,Stopped)Metadata(title, artist, album, artwork URL, length)Play,Pause,PlayPause,Stop,Next,Previous,SeekCanPlay,CanPause,CanGoNext,CanGoPrevious,CanSeek
Emit PropertiesChanged when state changes. Do not poll the desktop; it will poll you.
Background work and autostart
Section titled “Background work and autostart”A GUI app should not keep a process alive after the last window closes unless it has a real background job. If it does:
- Use
Gtk.Application.hold()/release()to keep the application alive intentionally. - Use a systemd user service for long-running work, not a hand-rolled daemon.
- Use the Background portal for autostart inside a sandbox.
- Make the background state visible in the UI and easy to turn off.
[Unit]Description=Notes syncAfter=graphical-session.targetPartOf=graphical-session.target
[Service]ExecStart=/usr/bin/notes --syncRestart=on-failureStateDirectory=notesNoNewPrivileges=truePrivateTmp=trueProtectSystem=strict
[Install]WantedBy=default.targetsystemctl --user enable --now notes-sync.servicejournalctl --user -u notes-sync.service -fDo not daemonize manually, fork into the background, or write PID files. systemd already does that job. If the service needs the session environment, call systemctl --user import-environment from the session startup; do not assume a user unit inherits it.
MIME types and URL handlers
Section titled “MIME types and URL handlers”A desktop entry declares what the app can open and how it is launched:
- Add the types to
MimeType=and use%U(URLs) or%F(files) inExec. - For custom URL schemes, add
x-scheme-handler/<scheme>toMimeType=(Desktop Entry spec) and handle the URI inGio.Application’sopensignal. - Run
update-desktop-databaseafter installing the desktop file so the MIME cache sees it. - Set the default with
xdg-mime default io.github.yourname.Notes.desktop text/markdownand verify withxdg-mime query default. - For “Show in folder”, use
org.freedesktop.FileManager1.ShowItems/ShowItemPropertiesinstead of shelling out to a file manager.
Use Gio.AppInfo for “open with” behavior; it respects the user’s default applications and the sandbox.
Media, printing, and power
Section titled “Media, printing, and power”- Media: use GStreamer 1.28 with PipeWire.
gstreamer-vaapiis gone; use the VA-APIvaplugins for hardware decode/encode. Qt Multimedia uses PipeWire on Linux. - Printing: use
Gtk.PrintDialog/Gtk.PrintOperationor the Print portal; test against CUPS/IPP, including a printer that is offline. - Power: use
Gtk.Application.inhibitor the Inhibit portal for long work, and listen for logind’sPrepareForSleepbefore suspend. Release inhibitors as soon as the work finishes.
Hardware and system services
Section titled “Hardware and system services”Use D-Bus APIs instead of parsing /proc, /sys, or shelling out to command-line tools.
| Service | Interface | Use for |
|---|---|---|
| UPower | org.freedesktop.UPower |
Battery, suspend/resume, power profiles |
| BlueZ | org.bluez |
Bluetooth devices and adapters |
| NetworkManager | org.freedesktop.NetworkManager |
Connectivity, Wi-Fi, VPN state |
| UDisks2 | org.freedesktop.UDisks2 |
Disks, partitions, mounts |
| PipeWire | PipeWire API / GStreamer | Audio/video streams; there is no general org.pipewire D-Bus API |
| CUPS | IPP / libcups | Printing |
| GeoClue | org.freedesktop.GeoClue2 |
Location (with permission) |
In a sandbox, many of these are proxied by portals or unavailable by design. Check for the service, handle its absence, and explain the limitation to the user instead of crashing.
Settings and preferences
Section titled “Settings and preferences”Desktop-integrated preferences belong in GSettings, with a schema and an Adw.PreferencesPage or a Qt settings dialog. The desktop can then expose search, reset, and backup behavior.
- Group settings by task, not by implementation.
- Use the host’s switches, sliders, and combo rows.
- Apply changes immediately; do not require a “Save” button for preferences.
- Provide a “Reset to defaults” action.
- Never hide a critical setting behind a config file only.
Integration checklist
Section titled “Integration checklist”- ☐ The app is single-instance and handles file-open activation.
- ☐ The desktop entry has
DBusActivatable=truewhen appropriate. - ☐ Notifications are actionable, rate-limited, and contain no secrets.
- ☐ File dialogs use
Gtk.FileDialog/ the FileChooser portal. - ☐ Opening files/URLs uses
Gtk.FileLauncherorGio.AppInfo. - ☐ Secrets use the Secret Service, never plaintext.
- ☐ Recent files use
Gtk.RecentManager. - ☐ Search is exposed only if the app has searchable content.
- ☐ Media apps implement MPRIS.
- ☐ Background work uses systemd user units or the Background portal.
- ☐ Hardware state comes from D-Bus services, not
/procparsing. - ☐ File and URL handlers declare
MimeType=/%U, update the desktop database, and use the FileManager1 interface for “Show in folder”. - ☐ Settings are in GSettings with a schema and a reset path.
Sources
Section titled “Sources”- D-Bus
- GApplication
- Desktop Notifications
- XDG Desktop Portals
- Secret Service
- MPRIS
- systemd user units
- XDG activation
Next: Flatpak & Portals — the sandbox model and the mediation layer that makes integration safe.