# Linux Desktop Guide — Full Text > A practical field guide to building fast, accessible, deeply integrated native apps for the Linux desktops that matter, with a curated target matrix, a default stack for each target, and a shared quality bar. Canonical site: https://digitalesich.github.io/linux-desktop-guide/ --- # Fedora 45 KDE Plasma + Qt URL: https://digitalesich.github.io/linux-desktop-guide/targets/fedora-kde/ > The guide's upstream-current KDE reference target: Fedora 45 KDE Plasma, Qt 6, KDE Frameworks 6, Kirigami, Wayland, Flatpak, RPM, SELinux, and the quality gates for a KDE-native… This is the guide’s **upstream-current KDE reference target**. It is the right default when you want a Qt 6 + Kirigami app that follows the KDE HIG, uses Plasma’s integration points, and ships through Flatpak without asking users to change distribution. Platform guide This page covers the target-native decisions for Fedora + KDE Plasma: distribution, desktop, stack, HIG, packaging, integration, and validation. Shared rules live in the Linux-wide chapters and are linked, not repeated. If you ship one app across targets, start with [One App Across Linux Targets](/linux-desktop-guide/start/one-app-across-targets/). The target is deliberately current, not LTS. Fedora moves quickly, so the target assumes an annual upgrade cadence. The KDE 6.6 LTS series is supported longer by upstream, but this target follows the Fedora KDE edition and the current Plasma release. ## Target profile | Field | Value | | --- | --- | | Distribution | Fedora 45 KDE Plasma Desktop | | Desktop | KDE Plasma 6.7 or newer (check `plasmashell --version`; Fedora 45 may ship 6.7.x) | | Toolkit | Qt 6.11 + KDE Frameworks 6.30 or newer + Kirigami | | Languages | C++ and QML (primary). Python via PySide6 is possible, but the KDE HIG and examples target C++/QML. | | Display | Wayland. Plasma 6.8 retires X11 sessions; check the session options on the exact Fedora package. | | HIG | [KDE Human Interface Guidelines](https://develop.kde.org/hig/) | | Accessibility | QAccessible and Orca | | Target-native packaging | Flatpak / Flathub (preconfigured on Fedora); rpm for archive integration | | Cross-target packaging | Same Flatpak build | | Flatpak runtime | `org.kde.Platform//6.10`, `org.kde.Sdk//6.10` (the project baseline; verify the current Flathub branch) | | Support | Approximately 13 months | | Last checked | 2026-11-10 | The date is the evidence review date. Runtime validation on Fedora 45 is still pending; use `plasmashell --version`, the Fedora package, and the checks below on the exact release before you promise a version. ## Why this target - **Fedora KDE is a flagship edition.** It is not a second-class spin; Fedora ships it as a first-class desktop alongside Workstation. - **Flatpak and Flathub are ready.** Fedora Workstation, Silverblue, Kinoite, and the KDE edition ship Flatpak and configure Flathub through Third-Party Repositories. The target does not require a manual store setup. - **Qt and KDE Frameworks are current.** Qt 6.11 and KDE Frameworks 6.30+ give you the modern Qt Quick, Kirigami, KIO, KNotification, and KDBusService stack. - **The KDE stack has momentum.** Fedora KDE `countme` sample hits rose 25.8% between April and October 2026. Plasma 6.8 retires X11 sessions and adds accessibility and remote-desktop work. KDE e.V. and a public patron list back the project. - **The HIG is explicit.** KDE publishes a HIG and Kirigami patterns; a KDE-first app should use them instead of importing GNOME conventions. ## Trade-offs - **The support window is short.** Fedora releases are supported for about 13 months. Plan an annual upgrade and test the app on the next Fedora release before it lands. - **The exact Plasma version can lag the upstream announcement.** Fedora 45 may package Plasma 6.7.x even though Plasma 6.8 is current upstream. Check `plasmashell --version` and the Fedora package before you promise a version in your own docs. - **The Flatpak runtime can lag the distribution.** `org.kde.Platform//6.10` is the project baseline; the distro may have newer Qt. Pin the runtime in the manifest and test against it. - **RPM is the archive-native secondary.** Use it when you need Fedora archive integration or a system service; Flatpak remains the target default. - **KDE accessibility is improving, not finished.** QAccessible and Orca work, but test the main flows rather than assuming parity with GNOME. - **X11 is not the target.** Plasma 6.8 retires X11 sessions. Use Wayland and let XWayland handle legacy apps. ## Design contract: KDE Plasma The [KDE HIG](https://develop.kde.org/hig/) is the exhaustive reference. This contract is the decision layer for a KDE-first app; use it with the [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) chapter. Checked against the HIG on 2026-11-11. | Decision | Contract | | --- | --- | | **Default vs power** | **Simple by default, powerful when needed.** Make the first run and the primary task obvious; do not require configuration before the app is useful. Settings should support valid workflows, not merely toggle features. | | **Navigation** | For linear flows, use `Kirigami.PageStack` with back/forward actions. For five or fewer destinations, use `Kirigami.NavigationTabBar`; for more, use `Kirigami.GlobalDrawer`. On wide windows a sidebar or `Kirigami.PageRow` can show the structure; on narrow windows it becomes a drawer or the first page. Keep the launch view as the first destination. | | **Toolbar and menus** | Keep the main toolbar above the content on desktop and use page actions for contextual work. A hamburger menu holds a curated set of about 15 or fewer global actions; use a real menubar when there are more. Keep Quit and Minimize out of the hamburger menu. | | **Spacing and icons** | Use `Kirigami.Units.smallSpacing`, `mediumSpacing`, `largeSpacing`, and `gridUnit`; use `IconSizes` for icon sizes. Use Breeze icons and `Kirigami.Theme` colors; do not hardcode pixels or colors. | | **Settings** | Build configuration pages with `Kirigami.FormLayout`. Apply settings immediately, use progressive disclosure instead of an “Advanced” page, and do not use customization to avoid a design decision. | | **Status and errors** | Use `showPassiveNotification` for low-importance feedback, `Kirigami.InlineMessage` for persistent problems, and system notifications only for background events. Every error says what happened and what the user can do; avoid success-message noise. | | **Destructive actions** | Offer undo for reversible removal. Use a passive notification when losing the undo option is not catastrophic; use an inline message for destructive changes that need attention. Move files to the trash instead of deleting them immediately. | | **Keyboard and gesture** | Use `KStandardShortcut` for standard actions, keep every action visible in the UI or a menu, and make shortcuts assignable. Do not use the Meta key for app shortcuts; implement only two-finger gestures. | | **On other desktops** | Prioritize Plasma fidelity. A Qt/Kirigami app can run on GNOME through portals and Qt’s platform theme, but it remains a KDE-first app; do not sacrifice Plasma integration to imitate another desktop. | This is a decision summary, not a replacement for the HIG. For a pattern not listed here, use the HIG’s layout, navigation, status, icon, and accessibility pages rather than inventing a convention. ## Development environment Use the Fedora 45 KDE edition, or a Fedora 45 container for build and test. The container is not the desktop target; it is the reproducible build environment. Install `toolbox` first if your Fedora edition does not include it. ```bash toolbox create --distro fedora --release 45 toolbox enter fedora-toolbox-45 ``` Install the Qt/Kirigami development packages: ```bash sudo dnf5 install -y \ cmake ninja-build extra-cmake-modules \ qt6-qtbase-devel qt6-qtdeclarative-devel \ kf6-kirigami-devel kf6-ki18n-devel kf6-kconfig-devel \ kf6-kcoreaddons-devel kf6-kdbusaddons-devel ``` For a full KDE desktop inside a regular Fedora installation: ```bash sudo dnf5 install -y @kde-desktop-environment ``` For upstream KDE development, use `kdesrc-build` and the [KDE developer documentation](https://develop.kde.org/). For the app in this guide, the [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) chapter has the persistent C++ model, XDG storage, QML view, CMake, metadata, and test targets. It is source-reviewed in this revision; the Fedora 45 build and runtime checks below are still pending. Install the Flatpak runtime and SDK: ```bash flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak install --user flathub org.kde.Platform//6.10 org.kde.Sdk//6.10 ``` ## Fedora and KDE integration ### Portals and KIO Plasma’s portal backend is [`xdg-desktop-portal-kde`](https://invent.kde.org/plasma/xdg-desktop-portal-kde). It implements the file, settings, screenshot, and other interfaces used by Flatpak and sandboxed apps. Use `org.freedesktop.portal.*` through the toolkit or libportal; never bundle a portal or call the backend directly. `KIO` is the native file and network abstraction for a non-sandboxed KDE app. In a Flatpak, a file dialog goes through the portal; do not assume a `KIO` worker can see the host filesystem. ### KDE frameworks - **`KDBusService`** — single-instance behavior and activation. Use `KDBusService(KDBusService::Unique)`; do not add a second lock file or a private D-Bus name. - **`KNotification`** — Plasma notifications. Keep actions and urgency honest, and degrade gracefully when notifications are unavailable. - **`KIO`** — network and file access with KDE’s worker model. Use it for non-sandboxed apps; use portals in a Flatpak. - **KWallet** — use the KWallet framework for a non-sandboxed KDE app. In a Flatpak, prefer `org.freedesktop.portal.Secret` and the toolkit’s secret-service integration; do not talk to `org.kde.kwalletd6` directly unless the manifest grants it. - **`KConfig`** — application settings in the XDG config location. Do not write a private config format into `$HOME` directly. The [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) chapter has the code for these APIs; this section is the Fedora/KDE-specific checklist. ### Services and network - **systemd user units** — use `systemctl --user` and `~/.config/systemd/user/`. `loginctl enable-linger` is only for a service that must run without an active login; it changes the resource and security model, so do not enable it for a normal desktop app. - **firewalld** — only for a service that listens on the network. Open the smallest service or port, and prefer a loopback binding. A desktop app should not need an open port; if it does, document it and test the denial case. - **SELinux** — read [SELinux](/linux-desktop-guide/platform/security/#selinux) before shipping a system service. Do not ask users to disable it. - **Flatpak** — a sandboxed app should not install a system service. Use portals or a separate host service with an explicit interface. ### Fedora Atomic and Kinoite Fedora Kinoite is the Plasma-based Atomic desktop. The app path is Flatpak first: users install from Flathub, not by layering an RPM onto the host. `rpm-ostree install` is an administrator action that changes the host image and needs a reboot; it is not an app distribution channel. Test the Flatpak on Kinoite and keep the host immutable. Use [Toolbx](https://containertoolbx.org/) for the development environment; the target’s `toolbox` commands above are the supported path. For the native package, see [RPM packages](/linux-desktop-guide/platform/packaging/#rpm-packages). ## Golden path 1. [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) — the persistent C++/QML project, XDG storage, `QAbstractListModel`, app identity, `KDBusService`, KConfig, KNotification, KIO, CMake, tests, and accessibility. 2. [KDE Human Interface Guidelines](https://develop.kde.org/hig/) — the design language for a KDE-first app. 3. [AppStream & Metadata](/linux-desktop-guide/ship/appstream/) and [Flathub](/linux-desktop-guide/ship/flathub/) — the distribution path. 4. [Release Checklist](/linux-desktop-guide/ship/release-checklist/) — the final pass before users install. The first-app tutorial is GTK/Python. For this target, the Qt chapter is the starting project; run the Fedora 45 checks below before treating it as validated. Do not follow the GTK tutorial and then try to make it look like a KDE app. ## Validation checklist Run these on the exact Fedora 45 release before you call the target validated: - `plasmashell --version` and `rpm -q qt6-qtbase kf6-kirigami` match the matrix; record the versions. - `getenforce` reports `Enforcing`; do not validate on a permissive host. - A clean Toolbx build passes `cmake`, `ctest`, `qmllint`, and `qmlcachegen`. - The RPM builds in `mock -r fedora-45-x86_64` and passes `rpmlint`. - The Flatpak builds and runs with the manifest; install it on Fedora Kinoite as well. - Orca and Accerciser cover the main flows; the window works at 200% scaling and high contrast. - The RPM and Flatpak installs both launch, show the correct desktop entry, and pass AppStream validation. The current status is recorded, not implied: | Check | Status | | --- | --- | | Source review of the Qt chapter | 2026-11-11 | | Clean Toolbx build | Pending | | `ctest`, `qmllint`, `qmlcachegen` | Pending | | `mock` RPM build | Pending | | Flatpak install on Fedora Kinoite | Pending | | Orca/Accerciser and scaling checks | Pending | ## Quality gates Before you call the app ready on this target: - builds from a clean checkout with CMake and the Fedora 45 Qt/KF6 packages; - the `QAbstractListModel` and other C++ units pass under QtTest; - `qmllint` passes and the QML is compiled with `qmlcachegen`; - `desktop-file-validate` and `appstreamcli validate --no-net` pass; - the Flatpak builds and runs with the permissions in the manifest; - KDBusService provides single-instance behavior, and KNotification/KIO use the platform APIs; - Orca reads the main flows and Accerciser shows the expected accessible roles; - the window works at 200% scaling and with the high-contrast preference; - startup, input latency, and memory stay within the [performance budgets](/linux-desktop-guide/engineering/performance/); - the app follows the KDE HIG rather than a GNOME layout with KDE colors. ## Non-goals - Qt 5 or KDE Frameworks 5. - X11-only apps and X11-specific APIs. - Assuming the distro’s Qt version is the Flatpak runtime version. - Promising an LTS support window that Fedora does not provide. - Using GTK for a KDE-first app unless cross-platform is a hard requirement. ## Sources - [Fedora Linux 45 announcement](https://fedoramagazine.org/announcing-fedora-linux-45/) - [Fedora release lifecycle](https://docs.fedoraproject.org/en-US/releases/lifecycle/) - [Fedora KDE wiki](https://fedoraproject.org/wiki/KDE) - [KDE Plasma 6.8 announcement](https://kde.org/announcements/plasma/6/6.8.0/) - [KDE Frameworks 6.31.0](https://kde.org/announcements/frameworks/6/6.31.0/) - [KDE developer documentation](https://develop.kde.org/) - [KDE Human Interface Guidelines](https://develop.kde.org/hig/) - [KDE Frameworks API](https://api.kde.org/) - [xdg-desktop-portal-kde](https://invent.kde.org/plasma/xdg-desktop-portal-kde) - [Fedora SELinux Wiki](https://fedoraproject.org/wiki/SELinux) - [audit2allow(1)](https://man7.org/linux/man-pages/man1/audit2allow.1.html) - [firewalld documentation](https://firewalld.org/documentation/) - [Fedora Kinoite](https://docs.fedoraproject.org/en-US/fedora-kinoite/) - [Toolbx](https://containertoolbx.org/) - [Qt 6 documentation](https://doc.qt.io/qt-6/) - [QAccessible](https://doc.qt.io/qt-6/qaccessible.html) - [Flathub setup for Fedora](https://flathub.org/en/setup/Fedora) - [Fedora countme totals](https://data-analysis.fedoraproject.org/csv-reports/countme/totals.csv) Next: [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) — build the persistent C++/QML project for this target. --- # How Targets Are Chosen URL: https://digitalesich.github.io/linux-desktop-guide/targets/selection/ > The evidence-based method behind the guide's platform targets: adoption, momentum, developer environment, platform quality, longevity, and coverage. A vendor-controlled platform can define one screen, one toolkit, one design language, one store, and one supported environment. Linux cannot. There is no platform owner who can make that decision for the whole ecosystem, and a guide that pretends otherwise produces the usual result: a list of every distribution and every toolkit, with no defensible default. This guide takes the other road. It defines a **small set of development targets** and makes the choice for the reader. A target is not a distribution preference. It is a combination of: - distribution and release; - desktop and HIG; - toolkit and language bindings; - display server and accessibility stack; - packaging and update channel; - a reproducible development environment; - quality gates and explicit non-goals. The selection is evidence-based. Popularity shows where users are. Momentum shows where the ecosystem is going. Developer environment and platform quality show whether a target is actually good to build on. Longevity shows whether it will still be viable in two years. ## Criteria | Criterion | Weight | Question | Evidence | | --- | --- | --- | --- | | **Adoption / popularity** | 25% | Where are the users? | Steam Hardware Survey, Fedora `countme`, Debian popcon, Arch `pkgstats`, DistroWatch only as a weak interest proxy | | **Momentum** | 25% | Is the platform growing and maintained? | Releases, contributor and review activity, foundation or corporate backing, roadmap, upstream adoption, security response | | **Developer environment** | 20% | Can a developer reproduce a good setup? | Official containers, VMs and CI images; toolchain and SDK freshness; documentation; reproducible builds | | **Platform quality** | 20% | Does it meet the Native Standard? | Wayland, portals, Flatpak/AppStream, accessibility, HIG/toolkit maturity, sandboxing, security | | **Longevity / support** | 10% | Will it still be viable in two years? | Support window, LTS/stable policy, upgrade path, governance and funding | Coverage and role are **gates**, not weights. A high score does not automatically make a target a reference target. ## Evidence rules - Every metric gets a primary-source URL, a check date, and a confidence note. - Steam is opt-in, self-reported, gaming-skewed, and split across distribution versions. It is a proxy, not market share. - Fedora `countme` is an opt-out, sampled repository-request signal. It includes containers, cloud images, and downstream editions. Raw hits are not users. - Debian popcon and Arch `pkgstats` are opt-in and platform-specific. They measure package presence, not active sessions or distro share. - DistroWatch counts page hits. DistroWatch itself says the numbers do not measure usage or quality. It is included only as a weak interest proxy. - Missing data is written as **not verified**. It is not estimated. - Scores are an editorial synthesis of the cited evidence, not a measurement. The rubric makes the judgment explicit; it does not turn it into physics. ## Scoring Each criterion is scored from 0 to 3: | Score | Meaning | | --- | --- | | 0 | Absent, contradicted, or unverified | | 1 | Weak or ad hoc | | 2 | Adequate and documented | | 3 | Strong and independently verifiable | The weighted score is a sorting aid. The final matrix also has to satisfy the coverage gates and assign a role. ## Stack scores | Stack | Adoption | Momentum | Developer environment | Platform quality | Longevity | Weighted | Role | | --- | --: | --: | --: | --: | --: | --: | --- | | **GNOME + GTK 4 / libadwaita** | 3 | 3 | 3 | 3 | 3 | **3.00** | Reference | | **KDE Plasma + Qt 6 / Kirigami** | 3 | 3 | 3 | 3 | 3 | **3.00** | Reference | | COSMIC + libcosmic | 1 | 3 | 2 | 1 | 1 | 1.70 | Watch | | Cinnamon + GTK 3 | 2 | 2 | 1 | 1 | 2 | 1.60 | Watch | | Xfce + GTK 3 | 1 | 1 | 2 | 1 | 1 | 1.20 | Watch | GNOME and KDE are the two reference stacks because they combine mature HIGs, Wayland, portals, accessibility, developer documentation, and a large enough app ecosystem. COSMIC is promising but its accessibility support is still marked experimental. Cinnamon and Xfce remain on GTK 3 and do not meet the Wayland-native reference standard. ## Distribution scores | Distribution | Adoption | Momentum | Developer environment | Platform quality | Longevity | Weighted | Decision | | --- | --: | --: | --: | --: | --: | --: | --- | | **Ubuntu** | 3 | 2 | 3 | 3 | 3 | **2.75** | Reference: GNOME LTS | | **Fedora** | 2 | 3 | 3 | 3 | 1 | **2.55** | Reference: KDE; Validation: GNOME | | **Debian** | 2 | 2 | 2 | 2 | 3 | **2.10** | Compatibility | | **Arch Linux** | 2 | 2 | 3 | 2 | 0 | **2.00** | Validation: rolling | | Linux Mint | 3 | 2 | 1 | 1 | 3 | 1.95 | Watch | | NixOS | 1 | 3 | 2 | 2 | 1 | 1.90 | Watch | | Pop!\_OS | 1 | 3 | 2 | 1 | 1 | 1.70 | Watch | | openSUSE Tumbleweed | 1 | 2 | 2 | 2 | 1 | 1.65 | Watch | | elementary OS | 1 | 2 | 1 | 2 | 1 | 1.45 | Watch | | Manjaro | 1 | 1 | 1 | 2 | 0 | 1.10 | Watch | The scores do not select the matrix by themselves. Ubuntu is the GNOME reference because it pairs a long LTS window with a Wayland-only GNOME session and a reproducible developer environment. Fedora is the KDE reference because it is the strongest upstream-current KDE edition, with Flatpak and Flathub ready out of the box. Fedora also serves as the GNOME validation target: the next version of the same stack family on a current, non-LTS base. Debian is the compatibility target because its value is longevity and stability, not the newest libraries. Arch is the rolling validation target because it carries the newest GTK and Qt packages and has an official container image, but it has no support promise. ## Coverage gates The matrix must contain: - at least one reference **GNOME/GTK** target; - at least one reference **KDE/Qt** target; - at least one **LTS or stable** target; - at least one **current or rolling** target; - at least one **deb-based** and one **rpm-based** distribution; - no target that lacks Wayland, portals, Flatpak, AppStream, or a maintained accessibility stack. These gates are enforced by `npm run check:targets`. ## Why not every distribution? **Linux Mint + Cinnamon.** Mint has some of the strongest interest and Steam proxies in the set, and its move to yearly releases is a serious maintenance signal. But Cinnamon is still a GTK 3 desktop, the reviewed Mint 22.3 material does not declare Wayland the default, and no dedicated Cinnamon SDK or official container was verified. A GTK 3/X11-first target would contradict the guide’s native standard. Mint becomes interesting when Cinnamon’s Wayland session is the default and the project publishes a supported developer environment. **Pop!\_OS + COSMIC.** COSMIC has real momentum: Epoch 1 shipped in December 2025, it is Wayland-native, and it is available on many distributions. But libcosmic marks its accessibility feature as experimental, the toolkit is young, there is no official Pop!\_OS container, and the support end is not verified. It is a watch-list candidate, not a reference target. **NixOS.** NixOS has exceptional reproducibility and a strong contributor base. But its support window is short, its adoption proxies are weak, and Flatpak is optional. It is a promising future target for a reproducible-build workflow, not a mainstream reference. **openSUSE Tumbleweed.** A healthy rolling KDE and GNOME base with OBS and openQA. But the reviewed adoption proxies did not show a distinct signal and the GTK/Qt versions were not verified. Fedora and Arch already cover the current/rolling roles. **Manjaro and elementary OS.** Manjaro is Arch-based rolling with commercial backing, but it adds no coverage beyond Arch and has no verified official container or SDK. elementary OS has a distinct HIG and AppCenter, but niche adoption, no official container, and an unverified support end. Neither is a reference target for the general native standard. **Xfce.** Xfce 4.20 still defaults to X11, its Wayland support is experimental, it is built on GTK 3, and its release cadence is irregular. It is outside the Wayland-native reference standard. ## What would change the decision The matrix is not permanent. A target can move roles or leave the list when the evidence changes: - Cinnamon makes Wayland the default and publishes a supported SDK/container → Mint becomes a compatibility candidate. - COSMIC’s accessibility feature becomes stable and Pop!\_OS publishes a container → COSMIC becomes a validation candidate. - NixOS extends its support window or becomes a first-class Flatpak host → NixOS becomes a reproducibility candidate. - A reference distribution loses maintenance, drops Wayland, or fails the quality gates → it is removed or demoted. - A new distribution shows stronger evidence across the criteria → it enters the evaluation. ## Review - Evidence checked: **10 November 2026** - Next review due: **10 May 2027**, or the next Ubuntu/Fedora release, whichever comes first. - The machine-readable matrix is `src/data/targets.json`; `npm run check:targets` validates dates, scores, sources, roles, and coverage. ## Sources - [Ubuntu release cycle](https://ubuntu.com/about/release-cycle) - [Ubuntu 26.04 LTS release notes](https://documentation.ubuntu.com/release-notes/26.04/) - [Fedora Linux 45 announcement](https://fedoramagazine.org/announcing-fedora-linux-45/) - [Fedora release lifecycle](https://docs.fedoraproject.org/en-US/releases/lifecycle/) - [Debian releases](https://www.debian.org/releases/) - [Arch Linux about](https://archlinux.org/about/) - [GNOME 51 release notes](https://release.gnome.org/51/) - [KDE Plasma 6.8 announcement](https://kde.org/announcements/plasma/6/6.8.0/) - [KDE Frameworks 6.31.0](https://kde.org/announcements/frameworks/6/6.31.0/) - [Flathub setup](https://flathub.org/en/setup) - [AppStream documentation](https://www.freedesktop.org/software/appstream/docs/) - [Steam Hardware Survey, October 2026](https://store.steampowered.com/hwsurvey/) - [Fedora countme totals](https://data-analysis.fedoraproject.org/csv-reports/countme/totals.csv) - [Debian Popularity Contest](https://popcon.debian.org/) - [Arch pkgstats: desktop environments](https://pkgstats.archlinux.de/fun/Desktop%20Environments/current) - [DistroWatch page hit ranking](https://distrowatch.com/dwres.php?resource=popularity) — page-interest proxy only - [Debian reproducible builds](https://tests.reproducible-builds.org/debian/) - [NixOS reproducible builds](https://reproducible.nixos.org/) Next: [Platform Targets](/linux-desktop-guide/targets/) — the matrix that this method selects, with roles and the default stack for each target. --- # Platform Targets URL: https://digitalesich.github.io/linux-desktop-guide/targets/ > The curated targets this guide supports, the default stack for each, and the evidence behind the choice. A **target** is the environment the guide makes decisions for: a distribution release, a desktop, a toolkit, a HIG, a display server, a packaging path, and a quality gate. It is not a list of every distribution that can run Linux software. The guide supports a small matrix on purpose. A developer who picks a target gets a default stack, a design language, a supported environment, and a test path instead of a menu of mutually incompatible options. The choice is derived from evidence; the [selection method](/linux-desktop-guide/targets/selection/) explains the criteria, weights, and sources. ## The matrix | Target | Role | Stack | Release model | Support | Evidence | Why | | --- | --- | --- | --- | --- | --: | --- | | **[Ubuntu 26.04 LTS + GNOME](/linux-desktop-guide/targets/ubuntu-gnome/)** | Reference | GTK 4.22 + libadwaita 1.9 | LTS | 5 years standard; 10 with Pro; 15 with Legacy | 2.75 | Stable GNOME reference with the longest support window and a Wayland-only session | | **[Fedora 45 KDE Plasma + Qt](/linux-desktop-guide/targets/fedora-kde/)** | Reference | Qt 6.11 + KDE Frameworks 6.30+ + Kirigami | Current | ~13 months | 2.55 | Upstream-current KDE reference with Flatpak and Flathub ready out of the box | | **Fedora 45 Workstation + GNOME** | Validation | GTK 4.24 + libadwaita 1.10 | Current | ~13 months | 2.55 | Catches GTK, libadwaita, and GNOME drift before the next Ubuntu LTS | | **Arch Linux (rolling)** | Validation | GTK 4.24 / Qt 6.12 | Rolling | No EOL; no support promise | 2.00 | Newest upstream packages and an official container; catches breakage early | | **Debian 13 (trixie)** | Compatibility | GTK 4.18 + libadwaita 1.7; Qt 6.8 | Stable | EOL 2028; LTS 2030; ELTS 2035 | 2.10 | Conservative base with long support; use Flatpak when you need a newer stack | Scores are an editorial synthesis of the evidence, not a measurement. The role and the coverage gates decide the final matrix; see [How Targets Are Chosen](/linux-desktop-guide/targets/selection/). The matrix chooses the environment. The platform guides for the two reference targets describe what is native there: stack, HIG, packaging, integration, and validation. Shared standards live in the Linux-wide chapters. If you ship one app across targets, use [One App Across Linux Targets](/linux-desktop-guide/start/one-app-across-targets/). ## The reference targets ### Ubuntu 26.04 LTS + GNOME The stable GNOME target. Ubuntu 26.04 is an LTS with a Wayland-only GNOME session, GTK 4.22, libadwaita 1.9, AT-SPI/Orca, and a long support window. It is the right default when you want a predictable GNOME app that will still be supported in five years. The trade-off is deliberate: Ubuntu is not the fastest-moving GNOME distribution. If you need the newest GTK or libadwaita, build and test on the Fedora validation target and ship the app through Flatpak with a runtime that carries the newer stack. [Read the full Ubuntu + GNOME target profile →](/linux-desktop-guide/targets/ubuntu-gnome/) ### Fedora 45 KDE Plasma + Qt The upstream-current KDE target. Fedora KDE is a flagship edition, the target uses Wayland (Plasma 6.8 removes the X11 session, so check the exact Fedora package), Qt 6 and KDE Frameworks are current, and Flatpak plus Flathub are configured out of the box. It is the right default when you want a KDE-native app with Kirigami and the Plasma HIG. The trade-off is support duration: Fedora releases are supported for roughly 13 months, so the target assumes an annual upgrade cadence. The KDE stack is also less mature than GNOME on some accessibility edges, but Plasma 6.8 and KDE Frameworks 6.30+ are a serious, actively maintained baseline. [Read the full Fedora + KDE target profile →](/linux-desktop-guide/targets/fedora-kde/) ## Validation targets **Fedora 45 Workstation + GNOME 51.** The next GNOME stack after Ubuntu: GNOME 51 with GTK 4.24 and libadwaita 1.10, against Ubuntu’s GNOME 50, GTK 4.22, and libadwaita 1.9. It tells you whether a GTK or libadwaita change breaks before the next Ubuntu LTS. It is not a second full documentation path; it is the place to run the test suite and the packaging smoke test. **Arch Linux (rolling).** The newest GTK and Qt packages, an official container image, and a large package ecosystem. It is the early-warning system for upstream breakage. It has no support promise, so never present it as your app’s supported baseline. ## Compatibility target **Debian 13 (trixie).** A stable base with long support, reproducible-build infrastructure, and older GTK/Qt libraries. It is the target for conservative users and for apps that must run on a stable distribution without Flatpak. When you need a newer GTK or Qt than Debian ships, use a Flatpak runtime instead of requiring a backport. ## The default stack | Target | Default toolkit | HIG | Platform default | Do not | | --- | --- | --- | --- | --- | | Ubuntu + GNOME | GTK 4 + libadwaita | GNOME HIG | Snap (`core26`) and deb; Flatpak is the cross-target channel | Start a Qt app for a GNOME-first product unless cross-platform is a hard requirement | | Fedora + KDE | Qt 6 + Kirigami | KDE HIG | Flatpak/Flathub; rpm secondary | Start a GTK app for a KDE-first product unless cross-platform is a hard requirement | | Debian | The target stack, usually through Flatpak | GNOME or KDE HIG | deb; Flatpak for newer stacks | Assume Debian’s GTK/Qt versions are the app runtime | | Arch | Either stack, for validation | The chosen desktop’s HIG | Flatpak; pacman for development | Promise Arch as a supported baseline | “Platform default” is the channel native to that target. For an app that supports both reference targets, Flatpak/Flathub is the cross-target default; Snap and RPM are optional native additions. See [One App Across Linux Targets](/linux-desktop-guide/start/one-app-across-targets/). The point is not that one toolkit is universally better. The point is that a GNOME-first app should feel like a GNOME app, and a KDE-first app should feel like a KDE app. A cross-platform product can use one toolkit and accept that it will be a guest on the other desktop; that is a deliberate trade-off, not a default. ## Evaluated but not selected These are real candidates with real strengths. They are not in the matrix because they do not yet add the coverage or meet the gates. | Candidate | Why it is not a reference target | | --- | --- | | **Linux Mint + Cinnamon** | Strong interest and Steam proxies, but Cinnamon is GTK 3, the reviewed release materials do not declare Wayland the default, and no dedicated SDK or official container was verified. Re-evaluate when the Wayland session and developer tooling mature. | | **Pop!\_OS + COSMIC** | Wayland-native and shipping widely, but libcosmic marks accessibility as experimental, the toolkit is young, there is no official Pop!\_OS container, and the support end is unverified. | | **NixOS** | Exceptional reproducibility and contributor activity, but a short support window, weak adoption proxies, and optional Flatpak. A future reproducibility target. | | **openSUSE Tumbleweed** | A healthy rolling KDE/GNOME base with OBS and openQA, but no distinct adoption signal in the reviewed proxies and unverified toolkit versions. Fedora and Arch cover the current/rolling roles. | | **Manjaro** | Arch-based and commercially backed, but no verified official container or SDK and no coverage beyond Arch. | | **elementary OS** | A distinct HIG and AppCenter, but niche adoption, no official container verified, and an unverified support end. | | **Xfce** | X11 by default, experimental Wayland, GTK 3, and an irregular cadence. Outside the Wayland-native reference standard. | ## What this means for your app 1. **Pick one target first.** Choose the target whose users and design language match your product. Do not start with a toolkit and then look for a desktop. 2. **Use the target’s HIG.** GTK apps should follow the GNOME HIG; Qt apps should follow the KDE HIG. A cross-desktop app can share code, but it still needs to respect the host. 3. **Choose the packaging scope before the channel.** If you support one target, start with its native path: Snap or deb on Ubuntu, Flatpak or RPM on Fedora. If you support both reference targets, ship Flatpak as the common upstream channel; add native channels only when they earn their maintenance cost. 4. **Test on the validation targets.** Fedora GNOME catches upstream GNOME drift; Arch catches upstream GTK/Qt drift. Neither is a support promise. 5. **Do not promise every distribution.** State the supported targets and the compatibility target. A clear support matrix is more trustworthy than a vague “works on Linux”. ## Review - Matrix checked: **10 November 2026** - Next review due: **10 May 2027**, or the next Ubuntu/Fedora release, whichever comes first. - The date is the evidence check date. Runtime validation: **Ubuntu 26.04.1 only**; Fedora, Debian, and Arch were checked through release notes, lifecycle pages, and package indexes. - The machine-readable matrix is `src/data/targets.json`; `npm run check:targets` validates roles, scores, sources, and coverage. ## Sources - [How Targets Are Chosen](/linux-desktop-guide/targets/selection/) — criteria, weights, and the full evidence discussion - [Ubuntu release cycle](https://ubuntu.com/about/release-cycle) - [Ubuntu 26.04 LTS release notes](https://documentation.ubuntu.com/release-notes/26.04/) - [Fedora Linux 45 announcement](https://fedoramagazine.org/announcing-fedora-linux-45/) - [Fedora release lifecycle](https://docs.fedoraproject.org/en-US/releases/lifecycle/) - [Debian releases](https://www.debian.org/releases/) - [Arch Linux about](https://archlinux.org/about/) - [GNOME 51 release notes](https://release.gnome.org/51/) - [KDE Plasma 6.8 announcement](https://kde.org/announcements/plasma/6/6.8.0/) - [Flathub setup](https://flathub.org/en/setup) Next: [Ubuntu 26.04 LTS + GNOME](/linux-desktop-guide/targets/ubuntu-gnome/) — the stable GNOME reference target, its development environment, and its quality gates. --- # Ubuntu 26.04 LTS + GNOME URL: https://digitalesich.github.io/linux-desktop-guide/targets/ubuntu-gnome/ > The guide's stable GNOME reference target: Ubuntu 26.04 LTS, GNOME 50, GTK 4.22, libadwaita 1.9, Wayland, native Snap/deb packaging, cross-target Flatpak, and the quality gates… This is the guide’s **stable GNOME reference target**. It is the right default when you want a GTK 4 + libadwaita app that feels at home on Ubuntu, follows the GNOME HIG, and remains supportable for years. Platform guide This page covers the target-native decisions for Ubuntu + GNOME: distribution, desktop, stack, HIG, packaging, integration, and validation. Shared rules live in the Linux-wide chapters and are linked, not repeated. If you ship one app across targets, start with [One App Across Linux Targets](/linux-desktop-guide/start/one-app-across-targets/). The target is deliberately conservative. Ubuntu 26.04 is an LTS, not the fastest-moving GNOME distribution. If you need the newest GTK or libadwaita before the next LTS, build on the Fedora validation target and ship the app through a Flatpak runtime with the newer stack. ## Target profile | Field | Value | | --- | --- | | Distribution | Ubuntu 26.04 LTS “Resolute Raccoon” | | Desktop | GNOME 50 | | Toolkit | GTK 4.22 + libadwaita 1.9 | | Languages | C, Vala, Python (PyGObject), Rust (gtk-rs) | | Display | Wayland. GNOME on Xorg was removed in Ubuntu 25.10; XWayland remains for legacy X11 apps. | | HIG | [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/) | | Accessibility | AT-SPI and Orca | | Target-native packaging | Snap (`core26`) for third-party apps; deb for archive and system integration | | Cross-target packaging | Flatpak / Flathub (not preinstalled; not Canonical-supported) | | Flatpak runtime | `org.gnome.Platform//50`, `org.gnome.Sdk//50` | | Support | 5 years standard; 10 with Ubuntu Pro; 15 with the Legacy add-on | | Last validated | 2026-11-10 (Ubuntu 26.04.1) | ## Why this target - **It is an LTS.** Five years of standard support, ten with Ubuntu Pro, and a predictable upgrade path. That is the longest support window in the guide’s matrix. - **It is Wayland-only.** The Ubuntu GNOME session dropped GNOME on Xorg in 25.10. There is no X11 fallback to design around; XWayland is for legacy apps, not for your app’s architecture. - **The stack is coherent.** GTK 4.22 and libadwaita 1.9 are the toolkit and design system the desktop itself uses. AT-SPI, Orca, portals, and AppStream are part of the platform, not add-ons. - **The developer environment is reproducible.** Ubuntu publishes an official OCI image and cloud images, and the toolchain is documented in the [Ubuntu for Developers](https://ubuntu.com/developers/docs/) docs. - **The adoption evidence is strong enough.** Ubuntu is the LTS reference for the GNOME stack, with a long support policy and a large install base. The Steam proxy is version-split and gaming-skewed; it is not treated as market share. ## Trade-offs - **Snap and deb are the native paths.** Ubuntu supports Snap and deb first-class. Use Snap when you want the App Center’s confined, auto-updating path; use deb when you need archive or system integration. Validate the desktop entry, AppStream metadata, and permissions on the same target. - **Flatpak is the cross-target channel.** Flatpak is not preinstalled and Canonical does not support it as an Ubuntu package format. It is still the right common channel when one app must also run on Fedora and other targets; document the setup separately and do not imply that it is part of a stock Ubuntu installation. - **GTK 4.22 is not the newest GTK.** Upstream moves faster. Test against the Fedora validation target when you use a newer API, and pin the Flatpak runtime that carries it. - **Ubuntu Pro is optional.** The baseline records the Pro window because it affects support planning; your app must not require Pro. ## Design contract: GNOME The [GNOME HIG](https://developer.gnome.org/hig/) is the exhaustive reference. This contract is the decision layer for an Ubuntu/GNOME app; use it with the [GTK & libadwaita](/linux-desktop-guide/engineering/gtk/) chapter. Checked against the HIG on 2026-11-11. | Decision | Contract | | --- | --- | | **Navigation** | Prefer one window. Use `Adw.ViewStack` with `Adw.ViewSwitcher` for a few equivalent views, and a sidebar when there are too many. Keep hierarchies to one level of depth; avoid stacking windows for core content. | | **Window chrome** | Use `Adw.ApplicationWindow` and `Adw.HeaderBar`; let the compositor draw the window controls. Do not draw a custom titlebar or custom window buttons. | | **Dialogs** | Use `Adw.AlertDialog` for alerts and confirmations, and `Adw.Dialog` for actions. Parent them to the window, focus the first control, place Cancel before the affirmative button, and bind `Esc` to cancel. Prefer undo over confirmation for reversible destruction. | | **Feedback** | Use `Adw.Toast` for non-critical confirmation and undo, `Adw.Banner` for persistent problems, and `Adw.StatusPage` for empty states. Use notifications only for events that happen while the app is in the background. | | **Spacing and type** | Use libadwaita/GTK spacing, style classes, and the system font; do not hardcode colors, pixel spacing, or a private theme. Respect the system accent, light/dark preference, and text scale. | | **Icons** | Use symbolic icons for controls and a full-color app icon; install icons in hicolor and follow the icon naming specification. | | **Keyboard and focus** | Bind shortcuts to `Gio.Action` accelerators, support the standard shortcuts, show focus clearly, and keep a logical tab order. Dialogs trap focus and restore it to the trigger when closed. | | **Scale and settings** | Test 100–200% scaling, light/dark/high contrast, reduced motion, and large text. Layouts adapt; they do not assume a fixed window size. | | **On KDE** | A libadwaita app remains a GNOME app on Plasma. Use the KDE portal backend and file dialogs, but do not claim that it follows the Plasma HIG; state the primary desktop and the fallback in the app’s support notes. | This is a decision summary, not a replacement for the HIG. For a pattern not listed here, use the HIG’s navigation, container, feedback, and reference sections rather than inventing a convention. ## Development environment Use the Ubuntu 26.04 OCI image or a 26.04 cloud image. The container is for building and testing; the desktop target is Ubuntu 26.04 with GNOME 50. Do not substitute a newer Ubuntu release and call it the LTS target. ```bash podman run --rm -it -v "$PWD:/src:Z" -w /src ubuntu:26.04 bash ``` Install the toolkit, build tools, and packaging validators: ```bash apt update apt install -y \ build-essential git pkg-config meson ninja-build \ python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 \ libgtk-4-dev libadwaita-1-dev \ flatpak flatpak-builder desktop-file-utils appstream ``` Install the Flatpak runtime and SDK that match the target: ```bash flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak install --user flathub org.gnome.Platform//50 org.gnome.Sdk//50 ``` The runtime branch is part of the target. If you move to a newer GNOME runtime, record it as a target change and re-run the quality gates. ## Golden path 1. [Build Your First App](/linux-desktop-guide/start/first-app/) — the complete GTK 4 + libadwaita tutorial with XDG storage, AppStream, Meson, and Flatpak. 2. [GTK & libadwaita](/linux-desktop-guide/engineering/gtk/) — application structure, Blueprint, actions, models, settings, async work, testing, and accessibility. 3. [Ubuntu Platform](/linux-desktop-guide/platform/ubuntu/) — release support, Snap, deb, Ubuntu Pro, and the Ubuntu-specific services. 4. [AppStream & Metadata](/linux-desktop-guide/ship/appstream/) and [Flathub](/linux-desktop-guide/ship/flathub/) — the distribution path. 5. [Release Checklist](/linux-desktop-guide/ship/release-checklist/) — the final pass before users install. ## Quality gates Before you call the app ready on this target: - builds from a clean checkout with the target toolchain; - the model and storage tests pass without a display; - `desktop-file-validate` and `appstreamcli validate --no-net` pass; - the Flatpak builds and runs with the permissions in the manifest; - Orca reads the main flows and Accerciser shows the expected accessible roles; - the window works at 200% text scaling and in high-contrast mode; - startup, input latency, and memory stay within the [performance budgets](/linux-desktop-guide/engineering/performance/); - the app has empty, loading, and error states, and a keyboard path for every action. ## Non-goals - X11-only apps and X11-specific APIs. XWayland is for legacy software, not a target architecture. - GTK 3. The reference stack is GTK 4 + libadwaita. - Ubuntu releases older than 26.04 LTS. - Requiring Ubuntu Pro or a Snap-only distribution path. - Treating the Steam or DistroWatch proxies as a user count. ## Sources - [Ubuntu release cycle](https://ubuntu.com/about/release-cycle) - [Ubuntu 26.04 LTS release notes](https://documentation.ubuntu.com/release-notes/26.04/) - [Ubuntu Desktop documentation](https://documentation.ubuntu.com/desktop/en/latest/) - [Ubuntu for Developers](https://ubuntu.com/developers/docs/) - [GNOME 50 release notes](https://release.gnome.org/50/) - [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/) - [GTK 4 documentation](https://docs.gtk.org/gtk4/) - [libadwaita documentation](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/) - [Flathub setup for Ubuntu](https://flathub.org/en/setup/Ubuntu) - [AT-SPI](https://gitlab.gnome.org/GNOME/at-spi2-core) and [Orca](https://gitlab.gnome.org/GNOME/orca) - [Ubuntu 25.10 drops GNOME on Xorg](https://discourse.ubuntu.com/t/ubuntu-25-10-drops-support-for-gnome-on-xorg/62538) Next: [Fedora 45 KDE + Qt](/linux-desktop-guide/targets/fedora-kde/) — the other reference target, and the place to see what a KDE-first stack looks like. --- # Build Your First App URL: https://digitalesich.github.io/linux-desktop-guide/start/first-app/ > A complete, minimal GTK 4 + libadwaita app for the Ubuntu 26.04 LTS + GNOME target — with adaptive layout, XDG storage, keyboard shortcuts, AppStream metadata, and a Flatpak… This tutorial builds **Notes**, a small but real GTK 4 + libadwaita application. It is deliberately incomplete as a product and complete as a _platform citizen_: adaptive layout, XDG storage, a keyboard shortcut, a proper about dialog, AppStream metadata, and a sandboxed Flatpak build with no unnecessary permissions. The code is Python to keep the example readable. The structure maps directly to C, Rust, or Vala; the toolkit behaves the same. Choosing KDE? This tutorial builds the GNOME target with GTK 4 + libadwaita. If you chose [Fedora 45 KDE + Qt](/linux-desktop-guide/targets/fedora-kde/), start with the [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) chapter instead; it contains the C++/QML project and build path for that target. Why Flatpak in this tutorial? This tutorial uses Flatpak because it is the guide’s cross-target channel: the same manifest style works on the Fedora KDE target and other distributions. On Ubuntu, Snap is the native third-party path and deb is the archive path. Flatpak is not preinstalled on Ubuntu and is not Canonical-supported. If you ship Ubuntu only, evaluate Snap first. See [One App Across Linux Targets](/linux-desktop-guide/start/one-app-across-targets/). What you need [Ubuntu 26.04 LTS](/linux-desktop-guide/reference/baseline/) (or a recent Fedora/Arch with GTK 4.22 and libadwaita 1.9), `python3-gi`, `gir1.2-gtk-4.0`, `gir1.2-adw-1`, `meson`, `flatpak`, and `flatpak-builder`. ## 1\. Install the toolchain ```bash sudo apt install python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 \ meson ninja-build flatpak flatpak-builder \ desktop-file-utils appstream ``` Install the GNOME runtime and SDK used by the manifest: ```bash flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak install --user flathub org.gnome.Platform//50 org.gnome.Sdk//50 ``` The runtime branch is documented in [Flatpak runtimes](https://docs.flatpak.org/en/latest/available-runtimes.html). ## 2\. Create the project ```text notes/ ├── data/ │ ├── io.github.yourname.Notes.desktop │ ├── io.github.yourname.Notes.metainfo.xml │ └── io.github.yourname.Notes.svg ├── src/ │ └── main.py ├── io.github.yourname.Notes.yml └── meson.build ``` The application ID is a reverse-DNS identifier. Use a domain you control, or a code-hosting ID such as `io.github.yourname.Notes`. It must match the desktop file, the metainfo file, the icon name, and the Flatpak ID. ## 3\. Write the application Create `src/main.py`: ```python #!/usr/bin/env python3 import json import pathlib import sys import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") from gi.repository import Adw, Gio, GLib, Gtk APP_ID = "io.github.yourname.Notes" class NotesApplication(Adw.Application): def __init__(self): super().__init__(application_id=APP_ID) self.connect("activate", self.on_activate) self.set_accels_for_action("win.new-note", ["n"]) def on_activate(self, _app): window = NotesWindow(application=self) window.present() class NotesWindow(Adw.ApplicationWindow): def __init__(self, **kwargs): super().__init__(**kwargs) self.set_title("Notes") self.set_default_size(980, 680) self.data_file = pathlib.Path(GLib.get_user_data_dir()) / "notes" / "notes.json" self.notes = self.load_notes() self.rows = {} # Window actions ------------------------------------------------- new_action = Gio.SimpleAction.new("new-note", None) new_action.connect("activate", self.on_new_note) self.add_action(new_action) about_action = Gio.SimpleAction.new("about", None) about_action.connect("activate", self.on_about) self.add_action(about_action) # Header bar ----------------------------------------------------- header = Adw.HeaderBar() new_button = Gtk.Button(icon_name="list-add-symbolic") new_button.set_tooltip_text("New note (Ctrl+N)") new_button.update_property([Gtk.AccessibleProperty.LABEL], ["New note"]) new_button.set_action_name("win.new-note") new_button.add_css_class("suggested-action") header.pack_start(new_button) menu = Gio.Menu() menu.append("About Notes", "win.about") menu_button = Gtk.MenuButton(icon_name="open-menu-symbolic", menu_model=menu) menu_button.set_tooltip_text("Main menu") menu_button.update_property([Gtk.AccessibleProperty.LABEL], ["Main menu"]) header.pack_end(menu_button) # Sidebar -------------------------------------------------------- self.listbox = Gtk.ListBox() self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE) self.listbox.add_css_class("navigation-sidebar") self.listbox.connect("row-selected", self.on_row_selected) sidebar_scroller = Gtk.ScrolledWindow() sidebar_scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) sidebar_scroller.set_child(self.listbox) sidebar_scroller.set_vexpand(True) # Content -------------------------------------------------------- self.title_label = Gtk.Label(xalign=0) self.title_label.add_css_class("title-2") self.body_label = Gtk.Label(xalign=0, wrap=True, selectable=True) self.body_label.add_css_class("body") content_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) content_box.set_margin_top(30) content_box.set_margin_bottom(30) content_box.set_margin_start(36) content_box.set_margin_end(36) content_box.append(self.title_label) content_box.append(self.body_label) content_scroller = Gtk.ScrolledWindow() content_scroller.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) content_scroller.set_child(content_box) content_scroller.set_vexpand(True) empty = Adw.StatusPage() empty.set_icon_name("document-edit-symbolic") empty.set_title("No note selected") empty.set_description("Select a note or create a new one with Ctrl+N.") empty.set_vexpand(True) self.stack = Gtk.Stack() self.stack.set_transition_type(Gtk.StackTransitionType.CROSSFADE) self.stack.add_named(empty, "empty") self.stack.add_named(content_scroller, "editor") self.stack.set_visible_child_name("empty") # Adaptive split view ------------------------------------------- split = Adw.OverlaySplitView() split.set_sidebar(sidebar_scroller) split.set_content(self.stack) split.set_min_sidebar_width(240) split.set_max_sidebar_width(320) split.set_show_sidebar(True) breakpoint = Adw.Breakpoint.new( Adw.BreakpointCondition.parse("max-width: 600sp") ) breakpoint.add_setter(split, "collapsed", True) self.add_breakpoint(breakpoint) # Layout --------------------------------------------------------- toolbar = Adw.ToolbarView() toolbar.add_top_bar(header) toolbar.set_content(split) self.set_content(toolbar) self.refresh() self.connect("close-request", self.on_close_request) # Data --------------------------------------------------------------- def load_notes(self): try: data = json.loads(self.data_file.read_text(encoding="utf-8")) if isinstance(data, list): return data except (FileNotFoundError, json.JSONDecodeError): pass return [ { "title": "Welcome to Notes", "body": "This note lives in your XDG data directory.\n\n" "It survives restarts, works inside a Flatpak sandbox, " "and never writes next to the application binary.", }, { "title": "Keyboard first", "body": "Press Ctrl+N to create a note. Every action in a " "native app should be reachable without a mouse.", }, ] def save_notes(self): self.data_file.parent.mkdir(parents=True, exist_ok=True) temp = self.data_file.with_suffix(".tmp") temp.write_text(json.dumps(self.notes, indent=2, ensure_ascii=False), encoding="utf-8") temp.replace(self.data_file) def refresh(self): self.listbox.remove_all() self.rows.clear() for index, note in enumerate(self.notes): row = Adw.ActionRow(title=note["title"], subtitle=note["body"].splitlines()[0]) row.set_subtitle_lines(1) row.set_activatable(True) self.listbox.append(row) self.rows[row] = index first = self.listbox.get_row_at_index(0) if first is not None: self.listbox.select_row(first) # Signal handlers --------------------------------------------------- def on_row_selected(self, _listbox, row): if row is None or row not in self.rows: return note = self.notes[self.rows[row]] self.title_label.set_text(note["title"]) self.body_label.set_text(note["body"]) self.stack.set_visible_child_name("editor") def on_new_note(self, _action, _param): self.notes.insert(0, {"title": "Untitled note", "body": "Start writing…"}) self.save_notes() self.refresh() def on_about(self, _action, _param): about = Adw.AboutDialog( application_name="Notes", application_icon=APP_ID, developer_name="Your Name", version="0.1.0", developers=["Your Name "], copyright="© 2026 Your Name", license_type=Gtk.License.GPL_3_0, website="https://example.org/notes", issue_url="https://example.org/notes/issues", ) about.present(self) def on_close_request(self, _window): self.save_notes() return False if __name__ == "__main__": app = NotesApplication() if "--quit-after-startup" in sys.argv: GLib.timeout_add(800, app.quit) app.run(None) ``` Run it locally: ```bash python3 src/main.py ``` You should get a window with a sidebar, two notes, an adaptive split view, and a working `Ctrl+N` shortcut. Close and reopen it: the notes persist in `~/.local/share/notes/notes.json`. The `--quit-after-startup` flag is a smoke-test hook: it opens the window, waits for the first frame, then exits. It is not a user-facing feature. ## 4\. Add the desktop entry Create `data/io.github.yourname.Notes.desktop`: ```ini [Desktop Entry] Name=Notes GenericName=Note Taking Comment=Write and organize notes Exec=notes Icon=io.github.yourname.Notes Terminal=false Type=Application Categories=Office; Keywords=notes;writing;markdown; StartupNotify=true StartupWMClass=io.github.yourname.Notes ``` Validate it: ```bash desktop-file-validate data/io.github.yourname.Notes.desktop ``` D-Bus activation and single-instance behavior are covered in [System Integration](/linux-desktop-guide/platform/integration/#single-instance-and-activation). ## 5\. Add AppStream metadata Create `data/io.github.yourname.Notes.metainfo.xml`: ```xml io.github.yourname.Notes Notes Write and organize notes CC0-1.0 GPL-3.0-or-later

Notes is a small, fast note-taking app for the Linux desktop with a calm, keyboard-first interface.

It stores your notes locally, works offline, and integrates with the desktop through standard XDG interfaces.

io.github.yourname.Notes.desktop https://example.org/notes Office notes writing markdown Your Name
``` Validate it: ```bash appstreamcli validate --no-net data/io.github.yourname.Notes.metainfo.xml ``` A software center needs more than this to look good — screenshots, categories, keywords, release notes. Those are covered in [AppStream & Metadata](/linux-desktop-guide/ship/appstream/). ## 6\. Add an icon Install an SVG icon as `data/io.github.yourname.Notes.svg`. It must be a square, scalable icon that reads at 16 px and at 256 px. For the tutorial, a simple placeholder is fine; do not ship a generic toolkit logo. ```xml ``` ## 7\. Add the Meson build Create `meson.build`: ```meson project('notes', version: '0.1.0', meson_version: '>=1.3.0') bindir = get_option('bindir') datadir = get_option('datadir') configure_file( input: 'src/main.py', output: 'notes', copy: true, install: true, install_dir: bindir, install_mode: 'rwxr-xr-x', ) install_data('data/io.github.yourname.Notes.desktop', install_dir: datadir / 'applications') install_data('data/io.github.yourname.Notes.metainfo.xml', install_dir: datadir / 'metainfo') install_data('data/io.github.yourname.Notes.svg', install_dir: datadir / 'icons' / 'hicolor' / 'scalable' / 'apps') ``` Test the install into a staging directory: ```bash meson setup build --prefix=/tmp/notes-prefix meson install -C build find /tmp/notes-prefix -type f ``` ## 8\. Package it as a Flatpak Create `io.github.yourname.Notes.yml`: ```yaml id: io.github.yourname.Notes runtime: org.gnome.Platform runtime-version: '50' sdk: org.gnome.Sdk command: notes finish-args: - --share=ipc - --socket=wayland - --socket=fallback-x11 - --device=dri modules: - name: notes buildsystem: meson sources: - type: dir path: . ``` `type: dir` is for local development only. A Flathub submission uses a `git` source pinned to a tag/commit or an `archive` source with a checksum. Build and install it for your user: ```bash flatpak-builder --user --install --force-clean build-dir io.github.yourname.Notes.yml flatpak run io.github.yourname.Notes ``` Inspect the sandbox: ```bash flatpak info --show-permissions io.github.yourname.Notes flatpak run --command=sh io.github.yourname.Notes ``` You now have an app that: - launches from the desktop and the app grid; - stores data inside its sandbox at `~/.var/app/io.github.yourname.Notes/data/notes/notes.json`; - uses Wayland and falls back to X11 only when needed; - has no access to your home directory, network, or devices beyond the GPU; - uses the GNOME runtime, so the GTK version is the same on every distribution. ## 9\. What to change before shipping This tutorial is intentionally minimal. A real app needs: - **A real data model.** JSON is fine for a demo; use SQLite for search, migrations, and concurrent access. - **Editing.** A `Gtk.TextView` with undo/redo, autosave, and conflict handling. - **Search.** A `Gtk.SearchEntry`, a filtered list model, and a keyboard shortcut. - **Settings.** A GSettings schema with window size, font, and sort order. - **Tests.** Unit tests for storage, UI tests for keyboard flows, and a packaging smoke test. - **Screenshots and release notes.** Required for a good software-center listing. - **A license, privacy policy, and security contact.** Trust is part of the product. - **A Flathub submission.** See [Flathub](/linux-desktop-guide/ship/flathub/) for the review requirements. The full example is available in the `examples/notes` directory of this guide’s repository. Copy it, rename the app ID, and start replacing the placeholder logic. ## Sources - [GTK 4 documentation](https://docs.gtk.org/gtk4/) - [libadwaita 1.9](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/) - [Flatpak documentation](https://docs.flatpak.org/) - [AppStream documentation](https://www.freedesktop.org/software/appstream/docs/) - [Desktop Entry specification](https://specifications.freedesktop.org/desktop-entry-spec/latest/) - [Meson](https://mesonbuild.com/) Next: [The Native Standard](/linux-desktop-guide/start/native-standard/) — the ten rules that separate a working app from one that belongs. --- # Choose Your Target and Stack URL: https://digitalesich.github.io/linux-desktop-guide/start/choose-stack/ > Choose a platform target first, then the default stack for that target. A practical comparison of GTK 4 + libadwaita, Qt 6 + Kirigami, Rust, Python, and web tech, with… There is no single “Linux SDK”. There are several mature stacks, each with a different center of gravity. The right choice is the one your team can maintain for five years, on the desktop your users actually run. GTK and Qt are both excellent. Rust is a strong language choice for new GTK apps, but it is not a separate platform default; the target still decides the toolkit and HIG. Web technology can be the right answer when the product is genuinely web-shaped — and the wrong answer when it is used to avoid learning the platform. The guide does not pretend there is one stack for every Linux desktop. It pairs each **reference target** with a default stack, a HIG, and a packaging path. Choose the target first; the stack follows. Decision guide, not a platform guide This page compares toolkits, languages, and HIG choices. Target-native setup, packaging, and validation live in the Platform Guides. If you ship one app across targets, use [One App Across Linux Targets](/linux-desktop-guide/start/one-app-across-targets/). ## Choose a target first | Target | Default stack | HIG | Start | | --- | --- | --- | --- | | [Ubuntu 26.04 LTS + GNOME](/linux-desktop-guide/targets/ubuntu-gnome/) | GTK 4 + libadwaita | GNOME HIG | [GTK & libadwaita](/linux-desktop-guide/engineering/gtk/) | | [Fedora 45 KDE + Qt](/linux-desktop-guide/targets/fedora-kde/) | Qt 6 + Kirigami + KDE Frameworks | KDE HIG | [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) | | [Debian 13](/linux-desktop-guide/targets/#compatibility-target) | The target stack; deb is native, Flatpak for newer stacks | GNOME or KDE HIG | Use the target’s HIG | | [Arch Linux](/linux-desktop-guide/targets/#validation-targets) | Either stack, for validation | The chosen desktop’s HIG | Validation only, no support promise | If you have not chosen a target, start with [Platform Targets](/linux-desktop-guide/targets/). If your product is genuinely cross-distro, pick one primary target, ship Flatpak as the common upstream channel, and test on the validation targets. See [One App Across Linux Targets](/linux-desktop-guide/start/one-app-across-targets/). Do not build a different UI for every distribution. ## At a glance | If this is true… | Start with… | | --- | --- | | You are building for GNOME/Ubuntu first and want the most native result | **GTK 4 + libadwaita**, in C, Rust, Python, or Vala | | You are building for KDE Plasma first, or need C++/QML depth | **Qt 6 + Kirigami** | | You want memory safety, performance, and a growing GNOME ecosystem | **Rust + gtk-rs + libadwaita-rs** or **Relm4** | | You have a small utility and a Python team | **Python + PyGObject + libadwaita** | | You need a cross-platform app and already have a web team | A maintained web or custom-render runtime; apply the quality signals and do the native integration work | | You are building a game or a custom-rendered canvas | **SDL3**, **Godot**, or **Qt Quick** | | You need a single binary for many distros with minimal runtime — a special case, not a reference default | **Rust + Iced/Slint**, or **Qt** with static deployment; verify licensing, accessibility, and desktop integration | The guide has one default **per target**, not one universal default: > **GNOME target:** **[GTK 4.22 + libadwaita 1.9](/linux-desktop-guide/reference/baseline/) + Blueprint + Meson**, written in C, Rust, Python, or Vala. Target-native packaging: Snap (`core26`) or deb. Cross-target packaging: Flatpak/Flathub. > **KDE target:** **Qt 6.11 + Kirigami + KDE Frameworks 6.30+ + CMake**, written in C++ and QML. Target-native packaging: Flatpak/Flathub; archive-native secondary: RPM. Cross-target packaging: the same Flatpak build. GTK is the direct path to GNOME-native behavior, and it is what Ubuntu 26.04 LTS ships. Qt/Kirigami is the direct path to KDE-native behavior, and it is what the Fedora KDE reference target ships. A cross-distro product can share code, but it still has to choose which HIG it follows first. ## The comparison ### GTK 4 + libadwaita **What it is:** GTK is the widget toolkit behind GNOME. libadwaita adds the GNOME design system: headerbars, adaptive split views, view switchers, preferences pages, dialogs, toasts, and the Adwaita style. **Strengths** - The most direct path to a GNOME-native look and feel. - Excellent accessibility (AT-SPI), input method, and Wayland integration. - Modern rendering through GSK; GTK 4.22 includes native SVG rendering with `GtkSvg`. - libadwaita 1.9 adds `Adw.Sidebar`, `Adw.ViewSwitcherSidebar`, and automatic reduced-motion support. - Strong language options: C, Rust, Python, Vala, JavaScript (GJS). - Blueprint makes UI declarative and reviewable. **Trade-offs** - libadwaita is intentionally GNOME-opinionated. On KDE it will look like a GNOME app, not a Plasma app. - The C API is verbose; Rust and Python wrappers reduce the pain but add their own friction. - GTK’s major-version transitions are infrequent but significant. Budget for them. - Custom design systems require restraint; fighting Adwaita usually looks worse than adopting it. **Choose it when:** GNOME/Ubuntu is your primary audience, you want a direct path to native behavior, or you are building an app for the GNOME ecosystem. ### Qt 6 + Kirigami **What it is:** Qt is a mature C++/QML application framework. Kirigami is KDE’s adaptive UI framework built on Qt Quick. The guide’s KDE reference target is [Fedora 45 KDE Plasma](/linux-desktop-guide/targets/fedora-kde/); [Kubuntu 26.04](https://kubuntu.org/news/kubuntu-26-04-release-notes/) is a valid Ubuntu-based KDE check outside the matrix, but it is not a selected target or a support promise. **Strengths** - Excellent performance and a very mature tooling chain (Qt Creator, QML profiler, GammaRay). - QML is productive for highly animated, touch-friendly, adaptive interfaces. - Kirigami gives you KDE-native navigation, cards, sheets, and responsive page stacks. - Strong cross-platform story across desktop, mobile, and embedded targets. - C++ and Python bindings; Qt Widgets remains a solid choice for dense desktop UIs. **Trade-offs** - Two UI paradigms: Qt Widgets (classic desktop) and Qt Quick/QML (declarative, GPU-rendered). Mixing them has costs. - QML performance depends on discipline; JavaScript bindings and oversized scenes can burn CPU. - KDE integration is excellent, but a Qt app on GNOME does not automatically inherit GNOME’s HIG. - Licensing requires attention (LGPL/GPL/commercial) depending on how you link. **Choose it when:** KDE is your primary desktop, you need C++/QML depth, or you want one codebase across desktop and embedded. ### Rust toolkits Rust is no longer experimental on the Linux desktop. GNOME apps such as Loupe and Resources are written in Rust, and COSMIC is built on Rust. | Option | Model | Best for | Notes | | --- | --- | --- | --- | | **gtk-rs + libadwaita-rs** | Bindings to GTK/libadwaita | GNOME-native apps | Most native; follows GTK’s object model closely | | **Relm4** | Component/message architecture on gtk-rs | GNOME apps with complex state | Ergonomic, opinionated, good docs | | **Iced** | Elm-inspired, custom renderer | Cross-platform tools and games-adjacent UIs | Not GNOME-native; smaller ecosystem | | **Slint** | Declarative UI, custom renderer | Embedded and cross-platform products | Commercial licensing for some uses | | **libcosmic** | COSMIC desktop toolkit | COSMIC-native apps | Young but active; tied to System76’s ecosystem | **Strengths:** memory safety, fearless concurrency, strong packaging story (`cargo` + vendored sources), excellent performance. **Trade-offs:** longer compile times, a smaller pool of desktop UI contributors, and GTK objects are not `Send` — the main-thread rule still applies. **Choose it when:** you want memory safety without giving up native GTK, or you are building a new app with a team that already knows Rust. ### Python + PyGObject **What it is:** Python bindings to GTK/libadwaita (and the rest of GObject). **Strengths:** the fastest way to prototype a native GTK app; excellent for internal tools and small utilities; no compile step. **Trade-offs:** startup time and memory are worse than C/Rust; packaging a Python runtime into Flatpak adds size; type safety is weak; distribution to users who do not have the right Python environment is painful. **Choose it when:** the app is small, the team is Python-first, or you are prototyping. For a widely distributed app, budget for packaging and startup work. ### Vala and C **Vala** compiles to C and gives you GObject syntax with less boilerplate. It is used by elementary and some GNOME apps. The ecosystem is smaller than Rust’s, and tooling is less active. **C** remains the lingua franca of GTK and the Linux desktop. It is stable, portable, and well documented. It is also manual: memory management, error handling, and string handling are on you. Use C for libraries and small, long-lived tools; prefer Rust for new application code unless your team is already fluent. ### Web and custom-render runtimes Web technology is legitimate when the product is genuinely document- or web-shaped, or when the team’s velocity depends on it. It is a poor default for a small native utility, and it does not remove the need to integrate with the desktop. If you choose a web or custom-render runtime, evaluate it with the same [quality signals](/linux-desktop-guide/reference/quality-signals/) as any other dependency: - **Open source and license:** can you inspect, patch, and redistribute it? - **Maintenance:** active releases, security process, and a clear support window. - **Accessibility:** does it expose AT-SPI semantics, keyboard navigation, and input-method support, or will you rebuild them? - **Sandboxing:** does it work inside Flatpak/Snap and use portals for files, notifications, and secrets? - **Packaging:** can you build a reproducible artifact and validate its metadata? - **Desktop integration:** does it support Wayland, fractional scaling, dark mode, and the host’s file dialogs? - **Runtime weight:** startup time, memory, battery, and install size are product costs, not just engineering metrics. A named runtime is an example, not an endorsement. If a project fails the quality signals, do not recommend it just because it is popular. If you choose this path, do the native work anyway: a proper `.desktop` file, AppStream metadata, portal-based file dialogs, keyboard shortcuts, dark-mode support, accessibility labels, and a tray-free primary workflow. A web app that respects the desktop is better than a native app that does not. ## Language and toolkit are separate decisions You can write GTK apps in C, Rust, Python, Vala, or JavaScript. You can write Qt apps in C++, Python, or QML/JavaScript. Choose the toolkit for its **platform fit and widget model**; choose the language for **your team, safety requirements, and packaging constraints**. A useful rule: - **Toolkit** determines how native the app can feel. - **Language** determines how expensive it is to maintain. - **Packaging** determines whether users can actually run it. Do not let a language preference force you into a toolkit that fights the desktop, and do not let a toolkit force you into a language your team cannot staff. ## Target toolchains The two reference targets have their own setup paths. Ubuntu publishes supported setup and version-availability guides for Python, Go, Rust, GCC, Clang, .NET, Java, and Zig. Use them as the source of truth when you need to know what the LTS ships, or when you need a supported install path: - [Ubuntu + GNOME target](/linux-desktop-guide/targets/ubuntu-gnome/) — the target environment and golden path. - [Fedora + KDE target](/linux-desktop-guide/targets/fedora-kde/) — the target environment and golden path. - [Ubuntu for Developers](https://ubuntu.com/developers/docs/) — overview and tutorials. - [Toolchains](https://ubuntu.com/developers/docs/toolchains/) — per-language setup and first-program guides. - [Toolchain availability](https://ubuntu.com/developers/docs/reference/availability/) — what each Ubuntu release provides. - [IDEs](https://ubuntu.com/developers/docs/reference/ides/) — supported editors and install commands. For GTK apps, use the archive toolchain when you link against system libraries. For compilers or language runtimes that must be newer than the archive, use a project-local toolchain or a snap and pin the version in CI. Do not mix an archive library with a newer runtime ABI without testing the combination. ## A decision walkthrough 1. **Which desktop is primary?** GNOME/Ubuntu → GTK/libadwaita. KDE → Qt/Kirigami. Both equally → choose one primary HIG and toolkit, then document the other as a guest; do not average the HIGs or assume Qt is the automatic compromise. 2. **Does the app need a custom renderer?** Games, CAD, video editors, and canvas-first tools may prefer SDL3, Qt Quick, or a custom GPU stack. Form-and-list apps should use native widgets. 3. **Is the team already fluent in a stack?** A well-maintained Qt app beats a poorly maintained GTK app. Maintenance is a feature. 4. **How will it ship?** If you support one target, start with its native channel: Snap or deb on Ubuntu, Flatpak or RPM on Fedora. If you support both reference targets, Flatpak is the common upstream build; add native channels only when they earn their maintenance cost. See [One App Across Linux Targets](/linux-desktop-guide/start/one-app-across-targets/). 5. **What is the accessibility story?** Native widgets give you AT-SPI semantics for free. Custom-drawn UI does not. Budget accordingly. 6. **What is the five-year plan?** Check the toolkit’s release cadence, major-version policy, and the health of its bindings using the [quality signals](/linux-desktop-guide/reference/quality-signals/). ## Versions to target in 2026 The validated version set for the GNOME target lives in [Baseline](/linux-desktop-guide/reference/baseline/); the full matrix lives in [Platform Targets](/linux-desktop-guide/targets/). The targets record the distribution, toolkit, support window, and review date. The Flatpak runtime is a separate cross-target detail; it is not the host toolkit version. Use the targets as the reference for examples and screenshots; define a separate minimum-version policy when you support an older release. Do not target the newest API by default A Flatpak app can bundle a newer runtime, but a deb must work with the libraries in the archive. If you ship both, target the oldest library version you support and use compile-time version checks (`GTK_CHECK_VERSION`, `QT_VERSION_CHECK`) or feature flags for newer APIs. ## Sources - [Platform Targets](/linux-desktop-guide/targets/) - [How Targets Are Chosen](/linux-desktop-guide/targets/selection/) - [Baseline](/linux-desktop-guide/reference/baseline/) - [GTK 4 documentation](https://docs.gtk.org/gtk4/) - [libadwaita 1.9](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/) - [Qt 6.10 release](https://www.qt.io/blog/qt-6.10-released) - [KDE Frameworks 6.24](https://kde.org/announcements/frameworks/6/6.24.0/) - [gtk-rs book](https://gtk-rs.org/gtk4-rs/stable/latest/book/) - [Relm4 book](https://relm4.org/book/stable/) - [Quality Signals](/linux-desktop-guide/reference/quality-signals/) Next: [Build Your First App](/linux-desktop-guide/start/first-app/) for the GNOME path, or [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) for the KDE path. If you have not chosen a target yet, start with [Platform Targets](/linux-desktop-guide/targets/). --- # One App Across Linux Targets URL: https://digitalesich.github.io/linux-desktop-guide/start/one-app-across-targets/ > How to ship one Linux app across Ubuntu, Fedora, Debian, and Arch without pretending one toolkit, HIG, or package channel is native everywhere. A single codebase can run on every target in the matrix. It cannot be equally native on GNOME and KDE at the same time. The Linux-wide approach is a shared foundation plus one primary desktop, with explicit guest behavior everywhere else. This page connects the guide’s three layers. Use it after you have chosen a target and before you promise one app for every Linux desktop. ## The three layers | Layer | Decides | Where it lives | | --- | --- | --- | | **Linux-wide foundation** | Standards, architecture, accessibility, security, testing, and release criteria that apply across targets | [Native Standard](/linux-desktop-guide/start/native-standard/), [Wayland](/linux-desktop-guide/platform/wayland/), [Design Principles](/linux-desktop-guide/design/principles/), [Architecture](/linux-desktop-guide/engineering/architecture/), [Quality Bar](/linux-desktop-guide/ship/quality-bar/) | | **Platform guide** | Distribution, desktop, toolkit, HIG, native packaging, integration, and validation for one target | [Platform Targets](/linux-desktop-guide/targets/) and the target profiles | | **Cross-target recipe** | Primary HIG and toolkit, shared code, common package, test matrix, and support statement | This page | The platform guide is the target-native truth. The Linux-wide foundation is toolkit-neutral. This page connects them; it does not replace either. ## Pick a primary desktop There is no neutral answer. A GTK 4 + libadwaita app follows the GNOME HIG; on Plasma it is a GNOME app using the host’s portals and file dialogs. A Qt 6 + Kirigami app follows the KDE HIG; on GNOME it is a KDE app using the host’s integration points. | If the app is for… | Primary stack | Primary HIG | Guest behavior | | --- | --- | --- | --- | | GNOME and Ubuntu users | GTK 4 + libadwaita | GNOME | Use KDE portals and host file dialogs; do not claim Plasma HIG compliance | | KDE and Fedora users | Qt 6 + Kirigami | KDE Plasma | Use GNOME portals and host file dialogs; do not claim GNOME HIG compliance | | Both equally | Choose one as primary | The chosen HIG | Document the other desktop as a guest; do not average the HIGs | “Both equally” is not a stack choice. It is a product decision that will make one desktop secondary. State that in the support notes. ## What can be shared Share the parts that do not encode a desktop’s identity: - the domain model and data format; - XDG storage, atomic writes, migrations, and recovery; - domain and storage tests; - AppStream metadata, desktop-entry fields, app ID, and icon; - the security model, permissions, and portal use; - CI, release process, and update policy; - user documentation and error-message structure. Keep these target-specific: - navigation and window chrome; - toolkit widgets and accessibility wiring; - HIG-specific dialogs, settings, and feedback; - native package metadata and integration points; - screenshots and store copy that show the primary desktop. ## Package once, adapt where needed Target-native packaging and cross-target packaging are different decisions. Choose the scope first. | Scope | Default | Why | | --- | --- | --- | | **Ubuntu only** | Snap (`core26`) for third-party apps; deb for archive and system integration | Snap is preinstalled and App Center-integrated; deb is archive-supported | | **Fedora only** | Flatpak / Flathub | Preconfigured, sandboxed, AppStream, human review | | **Both reference targets** | Flatpak / Flathub | One artifact, one runtime, one store across Ubuntu and Fedora | | **Debian or Arch** | Flatpak | Compatibility and validation; the host libraries are not the app runtime | For a cross-target app, Flatpak is the upstream default. Add Snap or RPM only when the audience or a required integration justifies the extra release work. Do not call Flatpak the Ubuntu native default: Ubuntu does not preinstall it and Canonical does not support it as an Ubuntu package format. The full decision table and recipes are in [Packaging & Distribution](/linux-desktop-guide/platform/packaging/). ## Test the claim A support statement is only as good as the targets you actually run. - Build and run on the primary target with its native stack. - Install the declared package on a clean environment. - Run the core task: create, edit, save, reopen, and recover from a failure. - On the other reference desktop, check portals, file dialogs, notifications, scaling, high contrast, and keyboard access. - Run the validation target checks for Fedora GNOME or Arch when the app uses newer toolkit APIs. - Test install, update, uninstall, and data preservation on the declared channel. - Record the target, date, version, and evidence in the release notes. A website build or link check is not app validation. See [Coherent reference development paths](/linux-desktop-guide/start/native-standard/#how-this-guide-defines-a-complete-path). ## Declare the support scope State the supported targets and the primary desktop in the README, the AppStream metadata, and the release notes: - **Supported:** Ubuntu 26.04 LTS + GNOME and Fedora 45 KDE Plasma. - **Primary:** GNOME; Plasma is supported as a guest. - **Package:** Flatpak/Flathub for both; no Snap or RPM yet. - **Known limits:** no Plasma-native navigation; file dialogs use the host portal. “Works on Linux” is not a support statement. The [target matrix](/linux-desktop-guide/targets/) defines the environments; the [Quality Bar](/linux-desktop-guide/ship/quality-bar/#release-criteria) defines what blocks a release. ## What not to do - **Do not average the HIGs.** A window that borrows GNOME navigation and Plasma toolbars feels accidental on both. - **Do not ship every channel.** Each channel adds build, review, test, and support work. - **Do not treat the Flatpak runtime as the host toolkit.** The app runs on the runtime you pin; the host may have a different GTK or Qt. - **Do not promise native behavior you did not test.** A Qt app on GNOME is a guest, and so is a GTK app on Plasma. - **Do not expect a separate tutorial for every target.** Two reference paths and short validation checks are enough. Next: [Platform Targets](/linux-desktop-guide/targets/) to choose the environment, then the platform guide for the target you picked. --- # The Native Standard URL: https://digitalesich.github.io/linux-desktop-guide/start/native-standard/ > Ten rules for building Linux desktop apps that are fast, accessible, well-integrated, and trustworthy — in any toolkit. Platforms do not feel native because of a color palette. They feel native because the software keeps a set of promises consistently enough that users stop thinking about the software and start thinking about their task. This is the standard this guide uses. It is toolkit-neutral: GTK, Qt, Rust, and even a web runtime can meet it. In practice, teams usually have three or four weak rules rather than all ten. Fixing those is often the difference between “a Linux build” and “a Linux app”. ## Purpose of the guide This guide gives developers a curated, evidence-backed path to high-quality apps for a defined set of Linux desktop targets. It explains how the parts work together, from the first useful task to an installable, safely updatable release. Apps should be straightforward to use, accessible, responsive, protective of user data, and maintainable over time. That is the standard the guide works toward, not a claim that every current path has already passed it. The [target matrix](/linux-desktop-guide/targets/) defines the supported environments, and the [Quality Bar](/linux-desktop-guide/ship/quality-bar/) defines the evidence required before an app is called release-ready. ## How this guide defines a complete path A complete path is a connected sequence, not a collection of snippets: **project setup → a useful core task → persistence and recovery → desktop integration → packaging → installation → updates.** - **Outcome first.** Each chapter answers the developer decision or user task it serves. Optional integrations are included only when the app’s purpose needs them. - **Examples must fit together.** File names, app IDs, build dependencies, manifests, and source versions must agree across the path. An illustrative fragment is labeled as illustrative; it is not evidence that the path works. - **Verification is specific.** The guide distinguishes source-reviewed facts, successful builds, and runtime-tested behavior. A website build or link check is not app validation. - **The host is not the runtime.** Distribution packages, the build SDK, and the Flatpak runtime can carry different toolkit versions. State each version where it matters. - **The primary HIG wins.** A GNOME app follows the GNOME HIG on GNOME and degrades gracefully elsewhere; a KDE app follows the KDE HIG on Plasma. Shared standards do not make the desktops identical. - **One supported channel can be enough.** A well-maintained Flatpak is a complete distribution answer for an app that declares it; additional formats are optional, not a quality score. ## 1\. Respect the compositor **Why.** Wayland is the security and performance boundary on Ubuntu 26.04. The compositor owns input, screen capture, window placement, and scaling. Apps that reach around it break under tiling, fractional scaling, screen sharing, and input methods. **Do** - Use GTK 4 or Qt 6, which speak Wayland natively. - Use XDG Desktop Portals for screen capture, file access, notifications, and global shortcuts. - Support fractional scaling and per-monitor DPI with vector assets and toolkit layouts. - Let the compositor place windows; save size, not absolute position. **Avoid** - X11-only APIs, `xdotool`, `XGrabKey`, `XTest`, and `_NET_WM_*` assumptions. - Forcing window positions, always-on-top, or global hotkeys without a portal. - Capturing the screen by reading the framebuffer. ## 2\. Start fast, stay responsive **Why.** A desktop app is judged in the first second. If the window takes three seconds to appear or the UI freezes while loading, no amount of visual polish recovers the impression. **Do** - Show the first window as early as possible; defer non-critical work until after the first frame. - Keep all UI work on the main thread and move I/O, parsing, and network work to workers. - Use asynchronous APIs and cancellation (`Gio` async, Qt’s thread pool, Rust’s async runtime bridged to the GLib main loop). - Cache expensive computations and invalidate deliberately. **Avoid** - Blocking the main loop with `sleep`, synchronous network calls, or large file reads. - Loading every plugin, thumbnail, or database row at startup. - Fake splash screens that hide a slow architecture. **Budget.** A focused app should cold-start in under a second on the oldest hardware you support. A complex app should show a usable window in under two seconds. Input latency should stay under 50 ms; scrolling should hold 60 fps. ## 3\. Use the platform’s widgets before inventing your own **Why.** Native widgets bring keyboard behavior, accessibility semantics, input methods, high-contrast support, and theme integration for free. Custom-drawn controls bring all of those responsibilities to you. **Do** - Use `Adw.HeaderBar`, `Adw.OverlaySplitView`, `Adw.ViewStack`, `Adw.PreferencesPage`, `Gtk.ListView`, `Gtk.FileDialog`, and the rest of the toolkit’s vocabulary. - Extend a widget with composition or a subclass when the behavior is genuinely new. - Use the toolkit’s spacing, typography, and icon conventions. **Avoid** - Reimplementing buttons, switches, tabs, or file pickers “for brand reasons”. - Drawing text with a canvas when a label would do. - Mixing three icon styles in one window. ## 4\. Follow the host’s HIG where it matters **Why.** GNOME, KDE, and elementary have different ideas about navigation, density, and dialogs. An app that looks GNOME-native on KDE is not automatically native there; it is a GNOME app running on KDE. **Do** - Pick a primary desktop and follow its HIG closely. - On secondary desktops, use the toolkit’s platform integration and avoid hardcoding one desktop’s assets. - Keep window controls, file dialogs, notifications, and menus consistent with the host. - Put brand expression in the icon, accent, empty states, and content — not in the window frame. **Avoid** - Copying another platform’s chrome. - Forcing a dark theme when the user chose light. - Custom titlebars that break tiling, gestures, or accessibility. ## 5\. Make it keyboard-first **Why.** Linux users expect to drive applications from the keyboard. Keyboard access is also the foundation of screen-reader support and power-user speed. **Do** - Support the standard shortcuts: `Ctrl+N`, `Ctrl+O`, `Ctrl+S`, `Ctrl+W`, `Ctrl+Q`, `Ctrl+Z`, `Ctrl+Shift+Z`, `Ctrl+F`, `F1`, `Esc`. - Give every action a discoverable menu item and, where useful, a shortcut. - Show focus clearly and keep a logical tab order. - Make dialogs dismissible with `Esc` and confirm with `Enter` where appropriate. **Avoid** - Mouse-only interactions, hover-only controls, and hidden gestures. - Overriding system shortcuts or grabbing keys globally. - Keyboard traps in dialogs or custom widgets. ## 6\. Integrate through standards **Why.** The desktop already has a notification system, a file picker, a secrets store, a search index, a media controller, and a settings UI. Reimplementing them is wasted work and usually feels foreign. **Do** - Use `GNotification` / `KNotification` for background events, with actions. - Use `Gtk.FileDialog` / the FileChooser portal, and `Gio.File` for file operations. - Use the Secret Service (libsecret) for credentials. - Provide MPRIS for media playback and a search provider only if the app has searchable content. - Expose a single-instance D-Bus application and handle file-open requests. **Avoid** - Custom notification popups. - Storing passwords in a JSON file. - Opening a file by shelling out to `xdg-open` when a portal or `Gio.AppInfo` is available. - Tray-only workflows. ## 7\. Ask for the least privilege **Why.** Sandboxing is only as good as the permissions an app requests. A note-taking app with home-directory access is a note-taking app that can read your SSH keys. **Do** - Start with no static permissions and add only what a feature requires. - Use portals for files, screen capture, camera, location, USB, and printing. - Explain permission requests in context, before the system dialog appears. - Test the app with permissions denied and provide a useful fallback. **Avoid** - `--filesystem=host`, `--filesystem=home`, or `--socket=system-bus` by default. - Running a root daemon for something a user service can do. - Disabling the sandbox because packaging is easier. ## 8\. Store data where it belongs **Why.** Backups, resets, migrations, and sandboxes all depend on the XDG directory layout. An app that writes to `~/.myapp` or next to its binary is invisible to the system and hostile to multi-user setups. **Do** - Use `$XDG_CONFIG_HOME` for preferences, `$XDG_DATA_HOME` for user data, `$XDG_STATE_HOME` for logs and session state, `$XDG_CACHE_HOME` for regenerable data, and `$XDG_RUNTIME_DIR` for sockets and locks. - Write atomically: temp file in the same directory, `fsync` if durability matters, then rename. - Version your data and migrate forward. - Respect `$XDG_DATA_DIRS` for system-wide assets. **Avoid** - Hardcoded `~/.config` or `~/.local/share` paths. - Writing logs or caches into the data directory. - Assuming the home directory is writable, or that the app is the only writer. ## 9\. Ship reproducibly **Why.** Users install software from many channels. Trust comes from being able to rebuild what they run, verify what changed, and update without surprises. **Do** - Build from source in a clean environment; pin dependencies and runtimes. - Publish valid AppStream metadata, a desktop file, icons, screenshots, and release notes. - Use Flatpak/Flathub for upstream distribution; add deb/Snap only when you can maintain them. - Sign releases and document the update path. **Avoid** - Binary blobs in the manifest or repository. - Self-updaters inside a sandbox. - Version numbers that do not match the metadata. - “Works on my machine” release builds. ## 10\. Be a good citizen **Why.** The desktop is shared. An app that spams notifications, holds a DBus name forever, ignores suspend, or leaves daemons behind makes every other app worse. **Do** - Use a single application instance and release resources when idle. - Respect the user’s reduced-motion, contrast, font-size, and power settings. - Clean up temporary files, sockets, and locks. - Provide a way to reset settings and export data. - Write a `SECURITY.md` and respond to reports. **Avoid** - Polling in the background for no reason. - Autostarting without an explicit user choice. - Registering global shortcuts you do not need. - Leaving a process running after the last window closes unless the app is genuinely a background service. ## The standard in one sentence > A native Linux app renders through the compositor, integrates through standards, stores data in XDG locations, preserves that data through failure and updates, asks for the least privilege, starts fast, stays responsive, works from the keyboard, and ships reproducibly. If you can defend that sentence in a code review, you are building native software. ## How to use this standard - Use it as a design review checklist before the first release. - Use it as a code review lens for porting work. - Use it to push back on shortcuts that will cost more later. - Use the [Quality Bar](/linux-desktop-guide/ship/quality-bar/) to score an app across its seven dimensions. ## Sources - [Baseline](/linux-desktop-guide/reference/baseline/) - [Quality Signals](/linux-desktop-guide/reference/quality-signals/) - [Wayland](https://wayland.freedesktop.org/) - [XDG specifications](https://specifications.freedesktop.org/) - [XDG Desktop Portals](https://flatpak.github.io/xdg-desktop-portal/docs/) - [AppStream](https://www.freedesktop.org/software/appstream/docs/) - [WCAG 2.2](https://www.w3.org/TR/WCAG22/) Next: [Ubuntu Platform](/linux-desktop-guide/platform/ubuntu/) — the release, toolchains, and packaging paths the standard runs on. --- # Why Linux Is Different URL: https://digitalesich.github.io/linux-desktop-guide/start/why-linux/ > A model for building software across GNOME, KDE Plasma, and every Linux desktop that shares the same contracts. Linux is not a product with a vendor at the top. It is an ecosystem held together by open standards, several strong desktop communities, and users who expect to remain in control. That changes the job. You are not targeting one framework. You are targeting a **contract**: a small set of protocols, file formats, and behavioral expectations that let your app work across GNOME, KDE Plasma, and every distribution that ships them. Once you accept that, the apparent fragmentation becomes an advantage. You can choose the toolkit that fits your product. You can ship directly to users. You can integrate as deeply as the platform allows — without asking a gatekeeper for permission. ## The stack, from glass to app A Linux desktop app sits on a layered stack. Each layer has a stable interface, and each one can change independently. | Layer | What it provides | What your app should use | | --- | --- | --- | | Kernel & drivers | Hardware, input, power, networking | Standard APIs; never talk to devices directly unless you must | | Display | Wayland compositor, rendering, input, scaling | Wayland protocols through your toolkit; XWayland only for legacy | | Desktop services | Notifications, files, secrets, screen capture, settings | D-Bus and XDG Desktop Portals | | Toolkit | Widgets, layout, accessibility, theming | GTK 4 + libadwaita, Qt 6 + Kirigami, or another maintained toolkit | | Your app | Product logic, data, presentation | Platform conventions plus your own brand | The important line is not between “Linux” and “your app”. It is between **stable contracts** and **implementation details**. Wayland, D-Bus, XDG directory layout, AppStream, and the Desktop Entry specification are contracts. A particular compositor’s private protocol, a distro’s patched library, or an undocumented D-Bus interface is an implementation detail. Build on the former; treat the latter as optional. The practical test If a feature only works on one desktop, one display server, or one packaging format, it is not your baseline. It is an enhancement. Make the baseline excellent first. ## Where each concern lives A Linux desktop is assembled from a small number of stable layers. Knowing where each concern lives is more useful than memorizing a single framework. | Concern | Where it lives | | --- | --- | | UI toolkit | GTK 4 + libadwaita, Qt 6 + Kirigami, or another maintained toolkit | | Design authority | GNOME HIG, KDE HIG, elementary HIG, and distribution guidance | | Windowing | Wayland compositors such as Mutter, KWin, and wlroots-based compositors | | Integration | D-Bus, XDG Desktop Portals, and freedesktop specifications | | Distribution | Flathub, deb, Snap, AppImage, and distribution repositories | | Sandbox | Flatpak or Snap confinement plus portals | | Updates | Flatpak/OSTree, Snap, the package manager, or a documented app updater | | User control | Explicit, visible, and often scriptable | The last row is the philosophical difference. On Linux, the user can replace the desktop, the theme, the file manager, the compositor, and the package source. Your app is a guest in an environment it does not own. The best Linux apps behave like good guests: they adapt, they ask before taking, and they leave things as they found them. ## The four contracts Everything in this guide hangs off four contracts. They are also the four sections of the platform part of this book. If a term is unfamiliar, the [Glossary](/linux-desktop-guide/reference/glossary/) defines the platform and toolkit vocabulary used throughout. ### 1\. Display: render through the compositor The [Ubuntu Desktop session on 26.04 LTS](https://documentation.ubuntu.com/release-notes/26.04/) is Wayland-only; XWayland still runs X11 applications. The compositor owns the screen, input, and the security boundary. Your app renders its own surfaces and talks to the compositor through protocols. It does not position windows at absolute screen coordinates, synthesize input, or capture the screen behind the compositor’s back. This is not a restriction to work around. It is what makes fractional scaling, per-monitor DPI, input methods, and secure screen sharing possible. → [Wayland & Display](/linux-desktop-guide/platform/wayland/) ### 2\. Desktop: integrate through standards Notifications, file dialogs, secrets, screen capture, global shortcuts, and opening URLs all have standard interfaces. On a modern Linux desktop, most of them are mediated by **XDG Desktop Portals**. Your app asks for a capability; the desktop decides how to provide it and, when necessary, asks the user. → [System Integration](/linux-desktop-guide/platform/integration/) and [Flatpak & Portals](/linux-desktop-guide/platform/flatpak-portals/) ### 3\. Data: follow the XDG layout There is no single per-user application-support directory. Configuration, data, state, cache, and runtime files have separate homes defined by the [XDG Base Directory specification](https://specifications.freedesktop.org/basedir-spec/latest/). Writing to the right place is not bureaucracy; it is what makes backup, reset, migration, and sandboxing work. → [The XDG Contract](/linux-desktop-guide/platform/xdg/) ### 4\. Distribution: ship reproducibly Users can install software from a software center, a package manager, a single file, or a Git checkout. Each channel has different expectations. The common denominator is trust: a reproducible build, valid metadata, minimal permissions, and an update path that does not require the user to reinstall. → [Packaging & Distribution](/linux-desktop-guide/platform/packaging/) ## What “native” means here “Native” is not a theme, a font, or a titlebar shape. It is a set of observable behaviors. - **Predictable.** Standard shortcuts work. Files open where the user expects. Settings persist. The app starts even when the network is down. - **Responsive.** The first window appears quickly. The main loop never blocks. Scrolling and typing stay smooth under load. - **Integrated.** The app uses the host’s file dialogs, notifications, secrets store, and settings. It appears in search, the software center, and the task switcher with the right name and icon. - **Accessible.** Every action is reachable by keyboard and exposed to assistive technologies. Text scales. Contrast holds. Motion can be reduced. - **Honest.** Permissions match the feature set. Metadata matches the binary. Errors say what happened and what to do next. A native app can be written in C, Rust, Python, C++, or TypeScript. It can use GTK, Qt, or a custom renderer. What makes it native is that it respects the contracts above. ## Anti-patterns that break the illusion These are the most common ways a ported or hastily built app stops feeling native. | Anti-pattern | Why it fails | Do this instead | | --- | --- | --- | | Hardcoding `~/.myapp` | Breaks backups, sandboxing, and multi-user setups | Use XDG directories | | Requiring `sudo` at runtime | Violates least privilege and desktop expectations | Use a user service or a portal | | Bundling a full web runtime for a form | Wastes memory, battery, and startup time | Use the native toolkit unless web tech is essential | | Drawing a custom titlebar | Fights tiling, gestures, and accessibility | Use the toolkit’s headerbar/window frame | | Ignoring the file chooser portal | Breaks in Flatpak and Snap | Use `Gtk.FileDialog` or the portal API | | Polling every second | Wastes battery | Use event-driven APIs and D-Bus signals | | Shipping a tray-only app | Many desktops do not expose a tray | Provide a real window and an optional status icon | | Writing logs with secrets | Leaks credentials into journald and support bundles | Redact, rotate, and use structured logging | | Auto-updating inside a sandbox | Bypasses the store and breaks permissions | Let Flatpak/Snap/the package manager update | ## The Linux advantage The Linux model is a bet on shared ownership. - **You can ship without an OS-vendor gatekeeper.** Flathub, a PPA, a `.deb`, or a tarball are all legitimate paths, though stores still review submissions. - **You can integrate deeper.** D-Bus, portals, and the XDG specifications expose capabilities the desktop already provides. - **You can support older hardware.** A well-written GTK or Qt app can run on older laptops that still meet your baseline, provided the graphics stack and drivers are supported. - **You can be inspected.** Users and distributors can read the source, verify the build, and patch the package. - **You can choose your stack.** The ecosystem rewards maintenance and good citizenship. The cost is that you must take standards seriously. The reward is software that belongs on the desktop rather than merely running on it. ## Sources - [Ubuntu 26.04 release notes](https://documentation.ubuntu.com/release-notes/26.04/) - [Ubuntu release cycle](https://ubuntu.com/about/release-cycle) - [Wayland](https://wayland.freedesktop.org/) - [XDG specifications](https://specifications.freedesktop.org/) - [XDG Desktop Portals](https://flatpak.github.io/xdg-desktop-portal/docs/) - [AppStream](https://www.freedesktop.org/software/appstream/docs/) - [Flatpak](https://docs.flatpak.org/) Next: [Choose Your Stack](/linux-desktop-guide/start/choose-stack/) — a practical comparison of the toolkits and languages you can build on. --- # Flatpak & Portals URL: https://digitalesich.github.io/linux-desktop-guide/platform/flatpak-portals/ > How the Flatpak sandbox works, which permissions are acceptable, and how XDG Desktop Portals let a sandboxed app use files, secrets, screen capture, and notifications without… Flatpak is the upstream-friendly way to ship a Linux desktop app across distributions. It bundles a runtime, runs the app in a sandbox, and mediates access to the host through **XDG Desktop Portals**. Ubuntu also ships Snap, and `.deb` packages remain the native system format. They are not mutually exclusive. A serious app can ship Flatpak for upstream users, a deb for the Ubuntu archive, and a Snap for the Ubuntu App Center. The important part is that the sandboxed build behaves like a first-class app, not a degraded one. ## The model ```text ┌─────────────────────────────────────────────────────────────┐ │ Flatpak app sandbox │ │ │ │ your app ── runtime (GTK/Qt, libraries) ── app data │ │ │ │ │ └──────────────┐ │ └─────────────────────┼───────────────────────────────────────┘ │ D-Bus / portal frontend ┌─────────────────────▼───────────────────────────────────────┐ │ xdg-desktop-portal │ │ FileChooser · OpenURI · ScreenCast · Secret · Notification │ │ GlobalShortcuts · Background · Camera · Print · … │ └─────────────────────┬───────────────────────────────────────┘ │ ┌─────────────────────▼───────────────────────────────────────┐ │ Desktop services: compositor, file manager, keyring, … │ └─────────────────────────────────────────────────────────────┘ ``` - **Runtime:** a shared base (GNOME 50, KDE 6.10, Freedesktop) with the toolkit and common libraries. It is updated independently of your app. - **Sandbox:** bubblewrap namespaces restrict filesystem, network, devices, and D-Bus. - **Portals:** a D-Bus API that asks the desktop to perform a privileged operation on the app’s behalf. The desktop shows the UI and enforces policy. - **Permissions:** declared statically in the manifest (`finish-args`) or granted dynamically through portals. The goal is simple: **the app should need almost no static permissions**. ## Portals you will actually use | Portal | Interface | Use | | --- | --- | --- | | File chooser | `org.freedesktop.portal.FileChooser` | Open/save files chosen by the user | | Open URI | `org.freedesktop.portal.OpenURI` | Open a file or URL in the default app | | Notification | `org.freedesktop.portal.Notification` | Send desktop notifications | | Secret | [`org.freedesktop.portal.Secret`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html) | Per-app master secret for app-local encrypted storage; libsecret uses it transparently | | ScreenCast | `org.freedesktop.portal.ScreenCast` | Record/share the screen or a window | | Screenshot | `org.freedesktop.portal.Screenshot` | Take a screenshot | | GlobalShortcuts | `org.freedesktop.portal.GlobalShortcuts` | User-approved global hotkeys | | Background | `org.freedesktop.portal.Background` | Autostart/background execution | | Camera | `org.freedesktop.portal.Camera` | Camera access | | Location | `org.freedesktop.portal.Location` | Geolocation | | Print | `org.freedesktop.portal.Print` | Printing | | USB | [`org.freedesktop.portal.Usb`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Usb.html) | USB device access | | Settings | [`org.freedesktop.portal.Settings`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html) | Read color scheme, accent, contrast, and reduced-motion preference; fonts come from the toolkit | Most GTK apps do not call the portal directly. `Gtk.FileDialog`, `Gtk.PrintDialog`, `Gio.Notification`, and libsecret use the portal when the app is sandboxed. That is exactly what you want: the same code works inside and outside Flatpak. Test outside the sandbox too Portals are available on the host as well. For GTK 4, set [`GDK_DEBUG=portals`](https://docs.gtk.org/gtk4/running.html) while developing to force portal-backed dialogs and catch permission/UI differences before packaging. (`GTK_USE_PORTAL` is a GTK 3-era compatibility switch.) ## A minimal manifest ```yaml id: io.github.yourname.Notes runtime: org.gnome.Platform runtime-version: '50' sdk: org.gnome.Sdk command: notes finish-args: # Display - --socket=wayland - --socket=fallback-x11 - --share=ipc - --device=dri # No network, no home directory, no session bus. # File access goes through the FileChooser portal. # fallback-x11 only exposes X11 when Wayland is unavailable; # use --socket=x11 instead if you must run under XWayland on Wayland. modules: - name: notes buildsystem: meson sources: - type: dir path: . ``` The `finish-args` above are a good starting point for a local-first document app. Every additional line should be justified by a feature that cannot use a portal. `type: dir` is a local-development source. For a Flathub submission, replace it with a `git` source pinned to a tag/commit or an `archive` source with a checksum; Flathub does not accept the application source code inside the submission pull request. ### Permission cheat sheet | Permission | Grants | Verdict | | --- | --- | --- | | `--socket=wayland` | Wayland display | Required | | [`--socket=fallback-x11`](https://docs.flatpak.org/en/latest/sandbox-permissions.html) | X11 only when Wayland is unavailable | Acceptable for systems without Wayland; does nothing on a Wayland session | | `--share=ipc` | Shared memory with X11 | Usually needed with X11/fallback X11 | | `--device=dri` | GPU rendering | Needed for hardware acceleration | | `--share=network` | Full network access | Only if the app needs it; no portal equivalent for arbitrary sockets | | `--filesystem=xdg-documents` | All documents | Prefer the FileChooser portal | | `--filesystem=home` | Entire home directory | Avoid | | `--filesystem=host` | Entire host filesystem | Avoid | | `--socket=session-bus` | Full session bus | Avoid; use `--talk-name=` for specific services | | `--socket=system-bus` | Full system bus | Avoid; use a portal or a specific `--system-talk-name=` | | `--device=all` | All devices | Avoid; use specific devices or portals | | `--allow=devel` | Debugging ptrace etc. | Development only | `--talk-name=org.freedesktop.secrets` is sometimes used for direct keyring access. Prefer the Secret portal/libsecret path; it is designed for sandboxes. ## Building and running ```bash # Ubuntu does not ship Flatpak by default; install it and add Flathub first sudo apt install flatpak flatpak-builder flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo # Install the runtime and SDK flatpak install --user flathub org.gnome.Platform//50 org.gnome.Sdk//50 # Build and install for the current user flatpak-builder --user --install --force-clean build-dir io.github.yourname.Notes.yml # Run flatpak run io.github.yourname.Notes # Inspect permissions flatpak info --show-permissions io.github.yourname.Notes flatpak permissions ``` ### Debugging inside the sandbox ```bash # Shell inside the app sandbox flatpak run --command=sh io.github.yourname.Notes # Run a one-off command in the build environment flatpak-builder --run build-dir io.github.yourname.Notes.yml sh # Temporary overrides for debugging only flatpak override --user --filesystem=/tmp io.github.yourname.Notes flatpak override --user --share=network io.github.yourname.Notes # Remove overrides again flatpak override --user --reset io.github.yourname.Notes ``` Do not ship a manifest that relies on user overrides. If a feature only works after `flatpak override`, the app is not sandbox-compatible yet. ## What “sandbox-compatible” actually requires 1. **No hardcoded host paths.** Use `Gtk.FileDialog` and `Gio.File`; the path you receive may be a portal document ID. 2. **No writing outside XDG dirs.** The app’s data directory is writable; everything else is not. 3. **No shelling out to host binaries.** A sandbox may not have `xdg-open`, `nmcli`, or `pactl`. Use D-Bus/portals. 4. **No assuming a session bus name exists.** Check, handle failure, and explain. 5. **No self-updater.** Let Flatpak update the app. 6. **No bundling system libraries.** Use the runtime; add modules only when necessary. 7. **No silent network access.** If the app needs the network, declare it and tell the user why. ## Runtime lifecycle and permission persistence A runtime is a dependency with its own support window. Track the [`runtime-version`](https://docs.flatpak.org/en/latest/available-runtimes.html) branch, rebase before the runtime reaches end-of-life, run `flatpak update`, and remove unused runtimes with `flatpak uninstall --unused`. Do not assume an Ubuntu LTS support window applies to a GNOME or KDE runtime. Portal grants are persistent user decisions, not manifest settings. Inspect them with `flatpak permissions`, set or reset them with `flatpak permission-set`/`flatpak permission-reset`, and use `--persist=` only for app-specific paths that genuinely cannot go through a portal. Users can also review portal permissions in the desktop’s app settings. ## Snap and portals Snap is not Flatpak, but Ubuntu 26.04 improves the same portal path for confined snaps. Snap applications can use [XDG Desktop Portals](https://flatpak.github.io/xdg-desktop-portal/docs/) for files, camera, notifications, USB, and other resources, and users can manage Snap portal permissions in GNOME Settings. If you ship a Snap, test the portal flows and the `snap connections` output; do not assume a Snap interface can replace a portal for a user-chosen file or device. ## Flatpak vs Snap vs deb | Dimension | Flatpak | Snap | deb | | --- | --- | --- | --- | | Cross-distro | Excellent | Good (snapd on many distros) | Debian/Ubuntu family | | Sandbox | bubblewrap + portals | AppArmor + interfaces | Traditional, optional hardening | | Runtime | Shared runtime, frequent updates | Shared base snap plus bundled dependencies | System libraries | | Store | Flathub | Snap Store | Ubuntu archive/PPA | | System integration | Portals; some host limits | Interfaces; good Ubuntu integration | Full host access | | Best for | Upstream apps, Flathub | Ubuntu App Center, server tools | Distro packages, system components | | Review burden | Flathub review + lint | Store review | Archive review | For a new upstream desktop app that needs one build across distributions, **Flatpak/Flathub is a strong option**, but it is not part of a stock Ubuntu installation and is not Canonical’s supported path. Add a deb when you want Ubuntu archive integration; add a Snap when the App Center is a priority. The right combination depends on your audience and maintenance capacity. ## Common mistakes | Mistake | Consequence | Fix | | --- | --- | --- | | `--filesystem=home` | App can read SSH keys, browser data, everything | Use FileChooser portal | | Writing to `/usr` or next to the binary | Read-only sandbox; update wipes it | Use XDG data dirs | | Calling `xdg-open` | Binary may not exist in the sandbox | `Gtk.FileLauncher` / OpenURI portal | | Bundling GTK inside the app | Huge, stale, conflicts with runtime | Use `org.gnome.Platform` | | No `app-id` match | Desktop file, icon, D-Bus name diverge | One ID everywhere | | Network permission by default | Unnecessary attack surface | Add only when needed | | Self-updater | Breaks sandbox and store updates | Remove it | | Missing metainfo | App invisible or rejected in Flathub | Add AppStream metadata | ## Flathub requirements worth knowing early Flathub is curated. Before you submit: - The app must be functional, have a meaningful development history, and provide real user value. - The application ID must be reverse-DNS and match the metainfo and desktop file. - The app must build entirely from source with no network access during the build. - Static permissions must be minimal; portals are mandatory where a suitable portal exists. - English localization is required for the UI, metadata, and desktop entry. - The manifest must pass `flatpak-builder-lint`. - The license must be declared and installed. - Screenshots, release notes, and a valid AppStream file are required. The full process is covered in [Flathub](/linux-desktop-guide/ship/flathub/). ## Sources - [xdg-desktop-portal](https://flatpak.github.io/xdg-desktop-portal/docs/) - [Secret portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Secret.html) - [Usb portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Usb.html) - [Settings portal](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.Settings.html) - [Flatpak sandbox permissions](https://docs.flatpak.org/en/latest/sandbox-permissions.html) - [Flatpak runtimes](https://docs.flatpak.org/en/latest/available-runtimes.html) - [Flathub requirements](https://docs.flathub.org/docs/for-app-authors/requirements) Next: [Packaging & Distribution](/linux-desktop-guide/platform/packaging/) — how the formats differ, and how to ship each one without losing your sanity. --- # Packaging & Distribution URL: https://digitalesich.github.io/linux-desktop-guide/platform/packaging/ > How to package a Linux desktop app: Flatpak, deb, RPM, Snap, and AppImage compared — with the files, commands, and release practices for installation and updates. On Linux, packaging is not a final step. It is part of the architecture. A platform-native build and a cross-target build solve different problems, and the same source tree should not be forced into every channel. You do not have to ship every format. You do have to choose deliberately, because each one changes how the app is built, sandboxed, updated, and supported. ## Two packaging decisions Packaging answers two different questions: | Decision | Question | Examples | | --- | --- | --- | | **Platform-native** | What does this target’s platform support and expect? | Ubuntu: Snap, deb. Fedora: Flatpak, RPM. Debian: deb, Flatpak. Arch: Flatpak, pacman. | | **Cross-target** | What single artifact can carry the app across the reference targets? | Flatpak/Flathub. | Choose the scope before the channel. A platform-native default is not a universal default; a cross-target channel is not the Ubuntu native default. ## Choose the channel Ubuntu documents two supported package formats: **deb** and **Snap**. Flatpak is not installed by default and is not Canonical’s supported path, but it is a mature, open-source, cross-distribution channel. Start with the scope: one target or several. ### Ubuntu-supported paths | Channel | Best for | Updates | Sandbox | Effort | | --- | --- | --- | --- | --- | | **deb / PPA** | Ubuntu/Debian users, archive integration | apt, unattended-upgrades | No (unless hardened) | Medium–high | | **Snap** | Ubuntu App Center, confined apps | snapd, automatic refresh | Yes, AppArmor | Medium | | **Distro archive** | System components, long-lived packages | Distro updates | Varies | High | Ubuntu’s [App Center recommends snaps](https://ubuntu.com/desktop/docs/en/26.04/explanation/snap-and-deb-packages) for third-party apps because they are sandboxed and update independently of the release. It also manages debs from the Ubuntu archive and can install third-party debs, but third-party debs are discouraged: they have full access to the user’s resources and may add their own repositories. ### Cross-distro and alternative paths | Channel | Best for | Updates | Sandbox | Effort | | --- | --- | --- | --- | --- | | **RPM** | Fedora/RHEL/openSUSE users, archive integration | dnf, dnf-automatic | No (unless hardened) | High | | **Flatpak / Flathub** | Upstream desktop apps, cross-distro; not shipped by Ubuntu | OSTree deltas | Yes, portals | Medium | | **AppImage** | Portable demos, closed beta, legacy | Manual or app-specific | No | Low | | **Source tarball / self-hosted repo** | Developers, packagers, special audiences | User/distro or self-hosted | Varies | Low–high | A pragmatic release matrix: 1. **If you support one target, start with its native path.** A deb in a PPA or the archive for apt integration; a Snap for the App Center and confined auto-updates; an RPM for Fedora archive integration; Flatpak/Flathub on Fedora. 2. **If you support both reference targets, use Flatpak/Flathub as the common upstream build.** It gives you one artifact and one store across Ubuntu and Fedora. Document the extra setup: Ubuntu does not ship Flatpak or Flathub by default. 3. **Add Snap or RPM only when the audience or a required integration justifies the work.** Do not add a native channel just to mirror the cross-target build. 4. **Add AppImage** only for portable demos or users who cannot install packages. 5. **Keep a source tarball** for packagers and developers. 6. **Do not ship every channel by default.** Every channel is a maintenance, testing, and support commitment. Ubuntu’s own guidance Ubuntu supports **deb** and **Snap** as first-class package formats. The App Center recommends snaps for third-party apps and supports debs from the Ubuntu archive; third-party debs are possible but discouraged. Flatpak is not part of a stock Ubuntu installation and is not Canonical’s supported path, but it remains a legitimate open-source cross-distribution channel for upstream apps. See [Ubuntu Platform](/linux-desktop-guide/platform/ubuntu/#packaging-paths) for the full decision table and primary sources. Quality signals Whichever channel you choose, check that the runtime, toolkit, and store are actively maintained. The [quality signals](/linux-desktop-guide/reference/quality-signals/) page lists observable metrics for release recency, activity, bus factor, responsiveness, security, release engineering, governance, standards, and adoption. ## Flatpak Flatpak is covered in depth in [Flatpak & Portals](/linux-desktop-guide/platform/flatpak-portals/). It is the cross-target default when one app supports both reference targets. It is not the Ubuntu native default: Snap and deb are the target-native paths there, and Ubuntu does not install Flatpak or Flathub by default. If you choose this channel, set it up explicitly: ```bash sudo apt install flatpak flatpak-builder flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo ``` The App Center does not manage Flatpak apps. Users who want Flatpak install it themselves or use a distribution that ships it. For local development, `type: dir` sources are fine. For a Flathub submission, replace them with a `git` source pinned to a tag/commit or an `archive` source with a checksum; Flathub does not accept the app source code inside the submission PR. The packaging-specific workflow is: ```bash flatpak-builder --user --install --force-clean build-dir io.github.yourname.Notes.yml flatpak-builder --repo=/tmp/notes-repo --force-clean build-dir io.github.yourname.Notes.yml flatpak build-update-repo /tmp/notes-repo ``` For a self-hosted repository, publish the OSTree repo and a `.flatpakref`: ```ini [Flatpak Ref] Title=Notes Name=io.github.yourname.Notes Branch=stable Url=https://example.org/flatpak/repo GPGKey=... IsRuntime=false ``` For public distribution, use Flathub instead of running your own repository. It gives you a store listing, delta updates, verification, and a build service. Flatpak runtimes have their own support windows. Track the `runtime-version` branch, rebase before the runtime reaches end-of-life, run `flatpak update`, and remove unused runtimes with `flatpak uninstall --unused`. Do not assume that an Ubuntu LTS support window applies to a GNOME or KDE runtime. Do not self-update inside Flatpak The sandbox cannot replace its own deployment. Ship updates through Flathub/Flatpak. An in-app updater will either fail or bypass the permission model. ## deb packages A Debian package is a directory tree plus metadata. The [Ubuntu packaging guide](https://ubuntu.com/project/docs/) and the [new-package guide](https://ubuntu.com/project/docs/contributors/new-package/create-a-new-package/) cover the archive process; the example below is the minimal local layout. The minimum source package layout is: ```text notes/ ├── debian/ │ ├── changelog │ ├── control │ ├── copyright │ ├── install │ └── rules ├── meson.build └── src/ ``` ### `debian/control` ```text Source: notes Section: utils Priority: optional Maintainer: Your Name Build-Depends: debhelper-compat (= 13), meson, ninja-build, python3, libgtk-4-dev, libadwaita-1-dev Standards-Version: 4.7.4.0 Rules-Requires-Root: no Homepage: https://example.org/notes Package: notes Architecture: all Depends: ${misc:Depends}, python3, python3-gi, gir1.2-gtk-4.0, gir1.2-adw-1 Description: small note-taking app for the Linux desktop Notes is a local-first note-taking application with a native GTK 4 and libadwaita interface. ``` Use `Architecture: any` if you ship compiled code. For a Python app, `all` is correct, but the dependencies must be available in every supported release. If they are not, build a Flatpak instead of bundling a private Python runtime in `/opt`. ### `debian/rules` ```make #!/usr/bin/make -f %: dh $@ ``` Make it executable: `chmod +x debian/rules`. Because this is an `Architecture: all` Python package, the build produces `notes_0.1.0-1_all.changes`. Use the matching file for `lintian`, or use `../*.changes` so the command works for either architecture. ### `debian/changelog` ```text notes (0.1.0-1) resolute; urgency=medium * Initial release. -- Your Name Tue, 10 Nov 2026 12:00:00 +0100 ``` The distribution (`resolute` for Ubuntu 26.04) and the version must match the target archive. ### Build and validate ```bash sudo apt install debhelper devscripts lintian dpkg-buildpackage -b -us -uc lintian ../notes_0.1.0-1_all.changes ``` For a clean build that matches the archive, use `sbuild` or `pbuilder` rather than your desktop environment. A package that only builds on your machine is not a package. ### PPA ```bash debuild -S -sa dput ppa:yourname/notes ../notes_0.1.0-1_source.changes ``` A PPA is a good stepping stone. It is not a substitute for the Ubuntu archive, and users still need to add it manually. Follow [Build packages in a PPA](https://ubuntu.com/project/docs/contributors/bug-fix/build-packages-in-a-ppa/) for the upload and signing flow. ### What goes in the package | Asset | Path | | --- | --- | | Binary/script | `/usr/bin/notes` | | Desktop entry | `/usr/share/applications/io.github.yourname.Notes.desktop` | | Metainfo | `/usr/share/metainfo/io.github.yourname.Notes.metainfo.xml` | | Icon | `/usr/share/icons/hicolor/scalable/apps/io.github.yourname.Notes.svg` | | GSettings schema | `/usr/share/glib-2.0/schemas/` | | Translations | `/usr/share/locale/` | | License | `/usr/share/doc/notes/copyright` | Do not install into `/opt` unless the app is genuinely self-contained and you understand the trade-offs with themes, icons, and desktop integration. ## RPM packages RPM is the native package format on Fedora, RHEL, and openSUSE. On the Fedora KDE target it is the **archive-native secondary** channel: Flatpak/Flathub is the target default because it carries the current Qt/KDE runtime, while RPM is for apps that need archive integration, a system service, or a Fedora-only audience. State the support scope; do not present both as equal defaults. The [Fedora Packaging Guidelines](https://docs.fedoraproject.org/en-US/packaging-guidelines/) are the contract. A spec file describes the source, build, install, files, and changelog; it is not a shell script. The skeleton below is illustrative — replace the URL and app ID, and build it in `mock` before claiming Fedora support: ```spec Name: notes Version: 0.1.0 Release: 1%{?dist} Summary: Small note-taking app for the Linux desktop License: MIT URL: https://example.org/notes Source0: %{url}/archive/v%{version}/%{name}-%{version}.tar.gz BuildRequires: cmake BuildRequires: ninja-build BuildRequires: extra-cmake-modules BuildRequires: qt6-qtbase-devel BuildRequires: qt6-qtdeclarative-devel BuildRequires: kf6-kirigami-devel BuildRequires: kf6-ki18n-devel BuildRequires: kf6-kconfig-devel BuildRequires: kf6-kcoreaddons-devel BuildRequires: kf6-kdbusaddons-devel BuildRequires: desktop-file-utils BuildRequires: appstream Requires: hicolor-icon-theme %description Notes is a local-first note-taking application with a native KDE interface. %prep %autosetup %build %cmake -G Ninja %cmake_build %install %cmake_install %check desktop-file-validate %{buildroot}%{_datadir}/applications/io.github.yourname.Notes.desktop appstreamcli validate --no-net %{buildroot}%{_datadir}/metainfo/io.github.yourname.Notes.metainfo.xml %files %license LICENSE %{_bindir}/notes %{_datadir}/applications/io.github.yourname.Notes.desktop %{_datadir}/metainfo/io.github.yourname.Notes.metainfo.xml %{_datadir}/icons/hicolor/scalable/apps/io.github.yourname.Notes.svg %changelog * Wed Nov 11 2026 Your Name - 0.1.0-1 - Initial package ``` The `%license LICENSE` line requires a license file in the source tree; add one that matches the metainfo’s `project_license`. Every `find_package` component in the Qt example needs a matching `BuildRequires`; check the list when you add KIO, KCrash, or Notifications. The commands separate a local build from a clean build: ```bash sudo dnf5 install rpmdevtools mock rpmlint fedpkg rpmdev-setuptree cp notes.spec ~/rpmbuild/SPECS/ spectool -g -R ~/rpmbuild/SPECS/notes.spec rpmbuild -ba ~/rpmbuild/SPECS/notes.spec mock -r fedora-45-x86_64 --rebuild ~/rpmbuild/SRPMS/notes-0.1.0-1.fc45.src.rpm rpmlint ~/rpmbuild/SPECS/notes.spec ~/rpmbuild/RPMS/x86_64/notes-0.1.0-1.fc45.x86_64.rpm ``` `rpmbuild` proves the spec parses on your machine; `mock` builds in a clean Fedora chroot and is the check that matters. `rpmlint` catches metadata and file-layout problems. For a package destined for Fedora, use `fedpkg lint` in the dist-git checkout and follow the review process. For a package that is not in Fedora, [COPR](https://docs.copr.fedorainfracloud.org/user_documentation.html) provides a build service and repository; it is not a substitute for Fedora review. Do not install into `/opt` and do not bundle a private Qt runtime. Use the distribution’s Qt/KF6 packages for an RPM, and the Flatpak runtime for a Flatpak; mixing the two produces an app that is neither reproducible nor supportable. ## Snap A Snap is a squashfs image with an AppArmor confinement profile and a `snapcraft.yaml` recipe. [`core26`](https://ubuntu.com/docs/snapcraft/stable/reference/bases/) is the recommended base for Ubuntu 26.04. ```yaml name: notes base: core26 version: '0.1.0' summary: Small note-taking app for the Linux desktop description: | Notes is a local-first note-taking application with a native GTK 4 and libadwaita interface. grade: stable confinement: strict apps: notes: command: usr/bin/notes extensions: [gnome] # Add `plugs: [gsettings]` only if the app reads or writes host GSettings. parts: python-runtime: plugin: nil stage-packages: - libpython3.14-minimal - libpython3.14-stdlib - python3-minimal - python3.14-minimal - python3.14-venv notes: after: [python-runtime] plugin: meson source: . meson-parameters: - --prefix=/usr build-packages: - python3 - python3-gi - gir1.2-gtk-4.0 - gir1.2-adw-1 stage-packages: - python3-gi - gir1.2-gtk-4.0 - gir1.2-adw-1 ``` ```bash snapcraft pack sudo snap install --dangerous notes_0.1.0_amd64.snap snap run notes snapcraft lint ``` core26 and Python `core26` does not bundle Python. A strictly confined Python snap must stage its own runtime, as the `python-runtime` part above does. The [GNOME extension](https://ubuntu.com/docs/snapcraft/stable/how-to/extensions/use-the-gnome-extension/) supports `core26` from Snapcraft 9.2 onward and no longer adds the `gsettings` plug automatically; add it only when the app needs host settings. - `command` is relative to the snap root. With `--prefix=/usr`, the binary is `usr/bin/notes`, not `bin/notes`. - `extensions: [gnome]` pulls in the GNOME content snaps and desktop helpers. - Keep `plugs` minimal. Prefer portals and specific interfaces; `home`, `network`, and the legacy `unity7` interface are broad and should not be defaults. - `confinement: strict` is the right default. `classic` is legitimate for tools that genuinely need broad host access, but it is reviewed manually and should not be the first choice. - The Snap Store review checks metadata, confinement, and policy. Expect iteration. Snap’s automatic refresh is a feature, not a bug. Do not add a self-updater; test the app across a refresh, including with unsaved work. For the full build and review flow, use the [Snapcraft documentation](https://ubuntu.com/docs/snapcraft/stable/) and the [Craft a snap](https://ubuntu.com/docs/snapcraft/stable/tutorials/craft-a-snap/) tutorial. Read the current [interfaces reference](https://snapcraft.io/docs/interfaces) and [XDG desktop portals guidance](https://snapcraft.io/docs/xdg-desktop-portals). For automation, use the [snapd REST API](https://snapcraft.io/docs/snapd-api) rather than shelling out to `snap`. ## OCI images with Rockcraft If the deliverable is a service or cloud workload rather than a desktop app, build it as an OCI image with [Rockcraft](https://github.com/canonical/rockcraft). Rocks are minimal, Ubuntu-based images with a supported build path. Do not use them as a desktop packaging format. ## AppImage AppImage is a single executable file that mounts a squashfs image. It is useful for demos, portable tools, and users who cannot install packages, but it has no sandbox, no repository, and no standard update mechanism. ```bash # Example with linuxdeploy and the AppImage plugin linuxdeploy --appdir AppDir --executable build/notes \ --desktop-file data/io.github.yourname.Notes.desktop \ --icon-file data/io.github.yourname.Notes.svg linuxdeploy-plugin-appimage --appdir AppDir ``` If you ship an AppImage: - Include an `AppRun` that sets `LD_LIBRARY_PATH` correctly. - Provide a `.desktop` file and icon inside the image. - Publish a checksum and a GPG signature. - Document that updates are manual. - Do not treat it as your only distribution channel. ## Versioning and release notes Use semantic versioning for the app (`MAJOR.MINOR.PATCH`) and a distribution-specific revision suffix (`0.1.0-1`) for deb packages. The AppStream `` section should match the version users see. ```xml

First public release.

  • Local-first notes with adaptive sidebar.
  • Keyboard-first navigation and search.
``` Release notes are user-facing. Write them for users, not for the commit log. Mention breaking changes, migration steps, and known issues. ## CI: build every channel from one tag A release pipeline should produce: - a Flatpak build that passes `flatpak-builder-lint`; - a deb source and binary package that passes `lintian`; - an RPM built in `mock` on Fedora 45 that passes `rpmlint`; - a Snap that installs and launches on a clean VM; - an AppImage (optional) with a checksum; - a source tarball with a signed tag. ```yaml name: release on: push: tags: ['v*'] jobs: flatpak: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v4 - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: bundle: notes.flatpak manifest-path: io.github.yourname.Notes.yml cache-key: flatpak-builder-${{ hashFiles('**/*.yml', '**/*.json', '**/Cargo.lock', '**/Cargo.toml') }} deb: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v4 - run: sudo apt-get update - run: sudo apt-get install -y devscripts equivs lintian - run: sudo mk-build-deps -i -y debian/control - run: dpkg-buildpackage -b -us -uc - run: lintian ../*.changes snap: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v4 - uses: snapcore/action-build@v1 id: build - run: sudo snap install --dangerous ${{ steps.build.outputs.snap }} rpm: runs-on: ubuntu-26.04 container: fedora:45 steps: - uses: actions/checkout@v4 - run: dnf5 install -y rpmdevtools rpmlint - run: rpmdev-setuptree - run: cp notes.spec ~/rpmbuild/SPECS/ - run: spectool -g -R ~/rpmbuild/SPECS/notes.spec - run: rpmbuild -ba ~/rpmbuild/SPECS/notes.spec - run: rpmlint ~/rpmbuild/SPECS/notes.spec ~/rpmbuild/RPMS/*/*.rpm ``` `mock` is the stronger clean-chroot check. Run it on a Fedora 45 host or VM; the container job is a fast spec/build check, not a substitute for a clean chroot. Pin the runner image and the runtime. A release that depends on “whatever was latest” is not reproducible. ## Updates | Channel | Update mechanism | What the app should do | | --- | --- | --- | | Flatpak | OSTree deltas via Flathub; runtime branch has its own EOL | Rebase before runtime EOL; do not self-update | | Snap | snapd automatic refresh | Nothing; handle restart gracefully | | deb | apt / unattended-upgrades | Nothing; do not self-update | | RPM | dnf / dnf-automatic | Nothing; do not self-update | | AppImage | Manual | Check for updates only with consent; link to download | Never replace your own binary from inside the app. It breaks package ownership, signatures, and sandboxing. If the app needs a newer version of itself, tell the user and link to the store. ## Packaging checklist - ☐ One application ID across desktop file, metainfo, icon, D-Bus, and Flatpak. - ☐ The app builds from a clean checkout with no network access. - ☐ The desktop file passes `desktop-file-validate`. - ☐ The metainfo passes `appstreamcli validate --no-net`. - ☐ The Flatpak passes `flatpak-builder-lint`. - ☐ The deb passes `lintian`. - ☐ The RPM builds in `mock` on Fedora 45 and passes `rpmlint`. - ☐ The Snap uses `core26`, installs and launches on a clean VM with `strict` confinement, and passes `snapcraft lint`. - ☐ The app launches with no home directory access (Flatpak) and degrades gracefully. - ☐ The release states whether it is platform-native or cross-target, and does not present a cross-target channel as the target-native default. - ☐ The channel choice passes the [quality signals](/linux-desktop-guide/reference/quality-signals/) check; Flatpak setup is documented separately from stock Ubuntu. - ☐ The upgrade path from the previous version is tested. - ☐ Release notes and screenshots are current. - ☐ The license and copyright are installed. - ☐ The uninstall path removes app data only with consent. ## Sources - [Snapcraft bases](https://ubuntu.com/docs/snapcraft/stable/reference/bases/) - [Core24 to core26](https://ubuntu.com/docs/snapcraft/stable/how-to/change-bases/change-from-core24-to-core26/) - [Fedora Packaging Guidelines](https://docs.fedoraproject.org/en-US/packaging-guidelines/) - [RPM Packaging Guide](https://rpm-packaging-guide.github.io/) - [Fedora Developer: RPM packaging](https://developer.fedoraproject.org/deployment/rpm/about.html) - [COPR user documentation](https://docs.copr.fedorainfracloud.org/user_documentation.html) - [GNOME extension](https://ubuntu.com/docs/snapcraft/stable/how-to/extensions/use-the-gnome-extension/) - [Flathub requirements](https://docs.flathub.org/docs/for-app-authors/requirements) - [Debian Policy](https://www.debian.org/doc/debian-policy/) - [AppImage documentation](https://docs.appimage.org/) - [snapd REST API](https://snapcraft.io/docs/snapd-api) Next: [Security & Privacy](/linux-desktop-guide/platform/security/) — the threat model behind the sandbox, secrets, and permissions. --- # Security & Privacy URL: https://digitalesich.github.io/linux-desktop-guide/platform/security/ > A practical threat model for Linux desktop apps: sandboxing, AppArmor and SELinux, least privilege, secrets, safe file handling, network security, privacy-respecting telemetry,… Desktop apps handle the user’s documents, credentials, messages, and sometimes their entire screen. A security bug is not an edge case; it is a breach of the trust that makes the app installable in the first place. Linux gives you strong primitives — a sandbox, portals, the Secret Service, signed packages — but they only help if the app uses them. This page is the minimum bar, not a complete security review. ## The threat model Assume: - **Files are untrusted.** Documents, images, archives, and project files can be malicious. - **The network is hostile.** Wi-Fi, proxies, captive portals, and DNS can be controlled. - **Other apps are not trustworthy.** A sandboxed app should not be able to read another app’s data. - **The user can make mistakes.** Design for recovery, not blame. - **The supply chain can be compromised.** Dependencies, build runners, and update channels matter. - **The app will have bugs.** Limit the blast radius. The goal is not to eliminate risk. It is to make the common failure survivable. ## Least privilege Start with the smallest set of capabilities that makes the app useful, then add one at a time with a justification. | Capability | Default | When to add | | --- | --- | --- | | Filesystem access | None (portals only) | Only for a feature that cannot use a portal | | Network | None | Only if the app genuinely needs it | | Camera/microphone | None | Only with an explicit feature and a visible indicator | | Location | None | Only with a clear user benefit and a portal prompt | | Session bus | None | Only for specific `--talk-name=` services | | System bus | None | Almost never; use a portal or a user service | | Devices | GPU only if needed | Specific device classes only | In Flatpak, this is expressed in `finish-args`. In Snap, it is `plugs`. In a deb, it is your own responsibility: do not run as root, do not install setuid helpers, and do not grant broad filesystem access by default. ## Sandboxing is not a cage you escape A common anti-pattern is to ship a Flatpak with `--filesystem=home` because “the app needs to open files”. That defeats the sandbox and teaches users to distrust permissions. Use the **FileChooser** portal for files the user chooses, `--persist=` for a small number of app-specific paths if the portal is not enough, and `--filesystem=xdg-run/...` for sockets that genuinely belong to the app. If a feature cannot work in a sandbox, the answer is usually one of: - redesign the feature around a portal; - split the privileged part into a small, audited helper; - ship outside the sandbox with a clear explanation and a narrower audience. Do not silently disable the sandbox. ## Ubuntu security services Ubuntu layers four services on top of the sandbox model. They are covered in more detail in [Ubuntu Platform](/linux-desktop-guide/platform/ubuntu/#security-and-ubuntu-pro). | Service | Use it for | Do not | | --- | --- | --- | | **AppArmor** | Confining system services and sandboxed apps | Disable a profile to make a feature work | | **Polkit** | Named privileged actions with desktop authentication | Run `sudo` or `pkexec` from a GUI app | | **fwupd / LVFS** | Firmware updates and device status | Write firmware directly | | **Ubuntu Pro** | ESM, Livepatch, FIPS, CIS for users who subscribe | Require Pro for core app functionality | ### Permission prompting and Security Center Ubuntu 26.04 includes an experimental permission-prompting system on top of AppArmor 4.0. It is snap-focused: users enable it in **Security Center**, and prompts can grant one-time or persistent access to files, camera, microphone, and similar resources. Flatpak apps use XDG Desktop Portals instead, and debs are managed through AppArmor and the app’s own privilege model. Do not build a feature that depends on prompting being enabled. Use portals and the smallest Snap interfaces you need, handle denial as a normal state, and test with prompting both on and off. The user’s decision is authoritative; never retry a denied prompt in a loop. ### AppArmor AppArmor is the default mandatory-access-control layer on Ubuntu. It mediates filesystem access, capabilities, network sockets, D-Bus, and IPC for confined programs. If you ship a system service, include an AppArmor profile and test it: ```bash aa-status sudo journalctl -fx -k --grep=apparmor ``` Desktop apps should rely on the Flatpak or Snap sandbox rather than asking users to install a custom profile. ### Polkit Polkit is the correct way to request a privileged operation from a desktop session. Define a named action with a clear message, then let the desktop’s authentication agent prompt the user. Do not shell out to `sudo`, and do not run `pkexec` from a GUI process; both bypass the desktop’s policy and break in sandboxes. ### fwupd and Ubuntu Pro fwupd handles firmware updates through the Linux Vendor Firmware Service (LVFS). Integrate with it or link to the firmware updater; never flash firmware yourself. Ubuntu Pro adds ESM, Livepatch, FIPS, and CIS hardening. It is optional: ```bash pro status sudo pro enable esm-apps sudo pro enable livepatch ``` Detect whether a Pro service is enabled before exposing it in the UI, and keep the app fully usable when it is absent. ## Fedora security services Fedora enables SELinux by default. The `targeted` policy confines selected system services; many desktop user applications still run unconfined. That scoping detail matters: SELinux is not the app sandbox, and a denial is not a reason to disable it. ### SELinux Check the mode before changing anything: ```bash getenforce sestatus ``` Never ship instructions to set `SELINUX=permissive` or `SELINUX=disabled`. If the app or its service is denied, read the audit log: ```bash sudo ausearch -m AVC,USER_AVC -ts recent sudo ausearch -c notes -m AVC -ts recent ``` `audit2allow` turns those denials into candidate rules: ```bash sudo ausearch -m AVC -ts recent | audit2allow -m notes_local ``` Treat the output as a diagnostic, not as policy. Generated rules often allow more than the app needs. For a local test, review the generated `.te`, build the module with the SELinux development Makefile, and load it with `semodule`; do not ship generated policy without a confined-domain design and review. | Scope | What confines it | What to do | | --- | --- | --- | | Desktop user app | Usually unconfined under `targeted`; Flatpak is the sandbox when packaged as a Flatpak | Use portals and a Flatpak sandbox; do not add a custom policy just to make a file dialog work | | System service | SELinux domain, file contexts, and D-Bus policy | Package the policy with the service; test denials before release | | Flatpak app | bubblewrap, seccomp, and XDG Desktop Portals | Request the smallest portal or permission; do not use SELinux as a substitute for a sandbox | File contexts belong to the package, not to a user’s manual setup. These commands are for local testing; replace `notes_exec_t` with a type from your policy: ```bash sudo semanage fcontext -a -t notes_exec_t '/usr/libexec/notes' sudo restorecon -Rv /usr/libexec/notes ``` For a Fedora package, put the contexts and the domain in the SELinux policy package and follow the Fedora packaging rules. A `%post` script that runs `semanage` is not a substitute for a policy. ## Secrets Never store credentials in plaintext. Use the Secret Service (libsecret on GNOME, KWallet on KDE with a compatible backend), the Secret portal in a sandbox, or the platform’s keyring API. ```python import gi gi.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, ) secret = Secret.password_lookup_sync(schema, {"account": "sync"}, None) ``` Rules: - Use a stable schema and non-personal attributes. - Handle a locked or unavailable keyring without crashing. - Provide a “Forget” action that deletes the secret. - Do not log tokens, even at debug level. - Do not put secrets in command-line arguments; other users can read `/proc`. - Do not put secrets in environment variables that are inherited by child processes unless necessary. ## Safe file handling Files are the most common attack surface. Treat every path, filename, and file format as untrusted. - **Write atomically.** Create a temporary file in the same directory, write, `fsync` if durability matters, then rename. Never write directly to the user’s file. - **Respect symlinks.** Do not follow symlinks out of a directory you own; use `O_NOFOLLOW` where appropriate. - **Check permissions.** Secrets and private data should be `0600`; directories `0700`. - **Limit sizes.** A 10 GB image or a 2 million-row JSON file should fail gracefully, not exhaust memory. - **Validate formats.** Use a parser with a security track record; fuzz it; reject malformed input early. - **Use `Glycin` for untrusted images.** It is designed to decode untrusted image data in a sandboxed process. - **Do not extract archives blindly.** Guard against path traversal (`../`), absolute paths, and zip bombs. - **Use the Trash.** `Gio.File.trash()` instead of permanent deletion. ```python import os import tempfile from pathlib import Path def write_atomic(path: Path, data: bytes) -> None: path.parent.mkdir(parents=True, exist_ok=True) fd, tmp = tempfile.mkstemp(dir=path.parent, prefix=f".{path.name}.") try: with os.fdopen(fd, "wb") as handle: handle.write(data) handle.flush() os.fsync(handle.fileno()) os.replace(tmp, path) finally: if os.path.exists(tmp): os.unlink(tmp) ``` ## Network security - Use HTTPS for everything. Do not implement “fallback to HTTP”. - Use the system trust store. Do not bundle a private CA unless the app is enterprise-managed. - Let the toolkit or a mature library handle TLS (`Gio.TlsConnection`, Qt Network, rustls, OpenSSL). Do not roll your own. - Respect system proxy settings and `Gio.NetworkMonitor` for connectivity changes. - Set timeouts and cancellation on every request. A hung request is a hung UI if you await it on the main thread. - Validate certificates and hostnames. Do not add “ignore certificate errors” to production. - Beware of SSRF if the app fetches user-provided URLs: block localhost, link-local, and metadata endpoints unless explicitly required. - Treat downloaded content as untrusted: parse it in a sandbox or a separate process where possible. Certificate pinning is rarely appropriate on Linux: certificates rotate, proxies exist, and users may legitimately inspect their own traffic. Use the system trust store unless you have a specific, documented threat model. ## Input validation and memory safety - Prefer memory-safe languages (Rust, Python, managed runtimes) for parsers and network-facing code. - In C/C++, use bounded APIs, check integer overflows, and run static analysis (`clang-tidy`, `scan-build`). - Fuzz parsers and protocol handlers. A crash on malformed input is a bug; a memory corruption is a vulnerability. - Treat command-line arguments, environment variables, D-Bus messages, and portal responses as untrusted. - Never pass untrusted strings to a shell. Use `GSubprocess` with an argument vector, not `system()` or `popen()`. ## Privacy Privacy is a product decision, not a legal footnote. - **Collect the minimum.** If a feature does not need a data point, do not collect it. - **Opt in, not out.** Telemetry, crash reporting, and analytics require explicit consent. The default is off. - **Be specific.** Tell users what is collected, why, where it goes, and how long it is retained. - **Local-first by default.** Sync and cloud features should be optional and clearly labeled. - **No unique identifiers** unless necessary. If you need one, rotate it and document it. - **Redact logs and crash reports.** Remove paths, usernames, tokens, document contents, and IP addresses. - **Respect the desktop’s privacy settings.** Do not infer location or identity from unrelated data. - **Keep system telemetry separate.** Ubuntu Insights is opt-in system telemetry; it is not consent for your app’s analytics. Ask separately and never bundle the two. - **Make deletion real.** “Delete account” must delete the data, not just hide it. If you add telemetry, publish the schema and a sample payload. Users should be able to audit what is sent. ## Updates and supply chain - Ship through a package manager or store with signed updates (Flatpak/OSTree, Snap assertions, apt signatures). - Do not self-update inside a sandbox. - Pin dependencies in CI and review lockfile changes. - Use a clean build environment; do not publish artifacts built on a developer laptop with unknown state. - Sign tags and release artifacts. Publish checksums. - Monitor your dependencies for CVEs; have a patch cadence. - Never `curl | sh` in an install script. Never ask users to disable security features to install your app. ## Vulnerability disclosure Ship a `SECURITY.md` with: - a private contact (email or a security advisory channel); - supported versions; - a response window; - a commitment to coordinated disclosure; - a credit policy, if you want one. When a report arrives: reproduce, contain, patch, release, notify. Do not argue with the reporter in public. Do not silently fix a critical issue without a CVE when one is warranted. ## Security checklist - ☐ The app runs without root and without setuid helpers. - ☐ Flatpak/Snap permissions are minimal; portals are used where available. - ☐ Secrets are in the Secret Service, not in config files or logs. - ☐ File writes are atomic; symlinks and traversal are handled. - ☐ Untrusted images are decoded with Glycin or an equivalent sandbox. - ☐ Network requests use TLS, timeouts, and cancellation. - ☐ No shell interpolation of untrusted data. - ☐ Crash reports and telemetry are opt-in and redacted; app telemetry is separate from Ubuntu Insights. - ☐ Snap/Flatpak permission prompting and denial paths are tested. - ☐ Updates come from a signed channel; no self-updater in a sandbox. - ☐ AppArmor profiles are shipped for system services and tested with `aa-status`. - ☐ Privileged operations use a named Polkit action, not `sudo` or `pkexec`. - ☐ Firmware updates go through fwupd/LVFS; the app never flashes firmware directly. - ☐ Ubuntu Pro is optional; the app works without ESM, Livepatch, FIPS, or CIS. - ☐ `SECURITY.md` exists and is current. - ☐ Dependencies are pinned and monitored. ## Sources - [AppArmor](https://apparmor.net/) - [Fedora SELinux Wiki](https://fedoraproject.org/wiki/SELinux) - [Fedora SELinux troubleshooting](https://docs.fedoraproject.org/en-US/quick-docs/selinux-troubleshooting/) - [audit2allow(1)](https://man7.org/linux/man-pages/man1/audit2allow.1.html) - [Polkit](https://www.freedesktop.org/software/polkit/docs/latest/) - [fwupd / LVFS](https://fwupd.org/) - [Ubuntu Pro](https://ubuntu.com/pro) - [Permissions prompting](https://discourse.ubuntu.com/t/permissions-prompting-a-deep-dive/81785) - [Glycin](https://gitlab.gnome.org/GNOME/glycin) Next: [Design Principles](/linux-desktop-guide/design/principles/) — how to make the app feel calm, fast, and native across Linux desktops. --- # System Integration URL: https://digitalesich.github.io/linux-desktop-guide/platform/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… 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 | 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. Ubuntu defaults On Ubuntu, these interfaces are provided by specific system services: NetworkManager, PipeWire/WirePlumber, UPower, BlueZ, CUPS, UDisks2, and Polkit. The [Ubuntu Platform](/linux-desktop-guide/platform/ubuntu/#system-services) page maps each service to its supported API and tells you when an app should use a portal instead of talking to the service directly. ## 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. ```python 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: ```ini [D-BUS Service] Name=io.github.yourname.Notes Exec=/usr/bin/notes --gapplication-service ``` [`Gtk.Application`](https://docs.gtk.org/gio/class.Application.html) 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`](https://wayland.app/protocols/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 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. ```python 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 Use the toolkit’s file dialog. It uses the FileChooser portal when sandboxed, remembers the last folder, and respects the desktop’s bookmarks. ```python 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 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. ```python import gi gi.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 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 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`, `Seek` - `CanPlay`, `CanPause`, `CanGoNext`, `CanGoPrevious`, `CanSeek` Emit `PropertiesChanged` when state changes. Do not poll the desktop; it will poll you. ## 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. ```ini [Unit] Description=Notes sync After=graphical-session.target PartOf=graphical-session.target [Service] ExecStart=/usr/bin/notes --sync Restart=on-failure StateDirectory=notes NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict [Install] WantedBy=default.target ``` ```bash systemctl --user enable --now notes-sync.service journalctl --user -u notes-sync.service -f ``` Do 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 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) in `Exec`. - For custom URL schemes, add `x-scheme-handler/` to `MimeType=` ([Desktop Entry spec](https://specifications.freedesktop.org/desktop-entry-spec/latest/)) and handle the URI in `Gio.Application`’s `open` signal. - Run `update-desktop-database` after installing the desktop file so the MIME cache sees it. - Set the default with `xdg-mime default io.github.yourname.Notes.desktop text/markdown` and verify with `xdg-mime query default`. - For “Show in folder”, use `org.freedesktop.FileManager1.ShowItems`/`ShowItemProperties` instead 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 - **Media:** use GStreamer 1.28 with PipeWire. `gstreamer-vaapi` is gone; use the VA-API `va` plugins for hardware decode/encode. Qt Multimedia uses PipeWire on Linux. - **Printing:** use `Gtk.PrintDialog`/`Gtk.PrintOperation` or the Print portal; test against CUPS/IPP, including a printer that is offline. - **Power:** use `Gtk.Application.inhibit` or the Inhibit portal for long work, and listen for logind’s `PrepareForSleep` before suspend. Release inhibitors as soon as the work finishes. ## 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](https://docs.pipewire.org/) / GStreamer | Audio/video streams; there is no general `org.pipewire` D-Bus API | | CUPS | [IPP / libcups](https://openprinting.github.io/cups/) | 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 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 - ☐ The app is single-instance and handles file-open activation. - ☐ The desktop entry has `DBusActivatable=true` when appropriate. - ☐ Notifications are actionable, rate-limited, and contain no secrets. - ☐ File dialogs use `Gtk.FileDialog` / the FileChooser portal. - ☐ Opening files/URLs uses `Gtk.FileLauncher` or `Gio.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 `/proc` parsing. - ☐ 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 - [D-Bus](https://www.freedesktop.org/wiki/Software/dbus/) - [GApplication](https://docs.gtk.org/gio/class.Application.html) - [Desktop Notifications](https://specifications.freedesktop.org/notification-spec/latest/) - [XDG Desktop Portals](https://flatpak.github.io/xdg-desktop-portal/docs/) - [Secret Service](https://specifications.freedesktop.org/secret-service-spec/latest/) - [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/) - [systemd user units](https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html) - [XDG activation](https://wayland.app/protocols/xdg-activation-v1) Next: [Flatpak & Portals](/linux-desktop-guide/platform/flatpak-portals/) — the sandbox model and the mediation layer that makes integration safe. --- # The XDG Contract URL: https://digitalesich.github.io/linux-desktop-guide/platform/xdg/ > Where Linux apps put configuration, data, state, caches, desktop entries, icons, MIME types, and autostart files — and why the XDG specs are the closest thing the desktop has to… Linux has no single per-user application-support directory. It has a small set of environment variables and directory conventions maintained by [freedesktop.org](https://specifications.freedesktop.org/). They are boring, underspecified in places, and absolutely essential. If your app writes to the right place, backups include it, resets remove it, sandboxes map it, and users can find it. If it writes next to the binary, into `~/.myapp`, or into the current working directory, all of that breaks — often silently, often only after a user upgrades. ## The base directories | Variable | Default | Put here | | --- | --- | --- | | `XDG_CONFIG_HOME` | `~/.config` | User preferences and configuration | | `XDG_DATA_HOME` | `~/.local/share` | User data that should be backed up | | `XDG_STATE_HOME` | `~/.local/state` | Logs, session state, recent history | | `XDG_CACHE_HOME` | `~/.cache` | Regenerable data; safe to delete | | `XDG_RUNTIME_DIR` | `/run/user/$UID` | Sockets, locks, pipes; cleared on logout | | `XDG_DATA_DIRS` | `/usr/local/share:/usr/share` | System-wide data | | `XDG_CONFIG_DIRS` | `/etc/xdg` | System-wide configuration defaults | The `XDG_RUNTIME_DIR` is created by `pam_systemd` with mode `0700`. Do not create it yourself; check that it exists and fall back to a private temporary directory only if necessary. Never hardcode the defaults `~/.config` is the default, not a guarantee. Users, containers, test harnesses, and Flatpak all change these variables. Use `GLib.get_user_config_dir()`, `GLib.get_user_data_dir()`, `GLib.get_user_cache_dir()`, `GLib.get_user_state_dir()`, or the equivalent Qt/XDG library. ### A healthy app directory ```text ~/.config/notes/ └── settings.ini # small, human-editable preferences ~/.local/share/notes/ ├── notes.db # user data └── templates/ # user-visible assets ~/.local/state/notes/ ├── session.json # window layout, last opened note └── logs/ └── notes.log # rotated, no secrets ~/.cache/notes/ └── thumbnails/ # regenerable, safe to delete ``` If the app is sandboxed, these paths are remapped inside the sandbox. A Flatpak app’s `~/.local/share/notes` becomes `~/.var/app/io.github.yourname.Notes/data/notes`, and the host paths are not visible unless the user granted them. That is the point. ## Configuration: GSettings or a file? Use **GSettings** when the settings should integrate with the desktop, be discoverable, and be schema-validated. Use a simple config file when the settings are app-private, highly dynamic, or need to be edited outside the app. | Approach | Best for | Notes | | --- | --- | --- | | GSettings / dconf | Desktop-integrated preferences | Schema XML, `glib-compile-schemas`, `Gio.Settings` | | INI/TOML/JSON | App-private config, portable builds | Write atomically; validate; version the format | | SQLite | Structured data, search, migrations | Use WAL mode; handle corruption | | Keyfile (`GKeyFile`) | Small, human-readable config | GLib-native, no extra dependency | Do not mix all four. Pick one primary store and document the migration path. ### GSettings example ```xml 980 Window width 680 Window height 'modified' Sort order for notes ``` ```python self.settings = Gio.Settings.new(APP_ID) # On startup: default-width/default-height are the initial size. self.settings.bind("window-width", self, "default-width", Gio.SettingsBindFlags.DEFAULT) self.settings.bind("window-height", self, "default-height", Gio.SettingsBindFlags.DEFAULT) # On close: save the actual size, not the initial size. def on_close_request(self, _window): self.settings.set_int("window-width", self.get_width()) self.settings.set_int("window-height", self.get_height()) return False ``` `default-width`/`default-height` are not updated when the user resizes the window; save `get_width()`/`get_height()` on close and restore them with `set_default_size()` or the bindings above. Install the schema to `$datadir/glib-2.0/schemas/` and run `glib-compile-schemas`. For local development, set `GSETTINGS_SCHEMA_DIR` to the directory containing your compiled schema. ## Desktop entries Every graphical app needs a `.desktop` file. It is how the app appears in the app grid, the software center, and the file manager’s “Open With” menu. ```ini [Desktop Entry] Name=Notes GenericName=Note Taking Comment=Write and organize notes Exec=notes %U Icon=io.github.yourname.Notes Terminal=false Type=Application Categories=Office; MimeType=text/markdown;text/plain; Keywords=notes;writing;markdown; StartupNotify=true StartupWMClass=notes DBusActivatable=true ``` | Field | Why it matters | | --- | --- | | `Exec` | Must match the installed binary; `%U` passes files/URLs | | `Icon` | Must match the installed icon name, not a file path | | `Categories` | Used by menus and software centers; follow the spec | | `MimeType` | Declares what the app can open; use `xdg-mime` to verify | | `StartupNotify` | Lets the compositor show a launch indicator | | `StartupWMClass` | Matches the window to the desktop entry; GTK derives `WM_CLASS` from the program name unless you set it, so test with `xprop WM_CLASS` | | `DBusActivatable` | Lets the desktop start the app through D-Bus; ship a matching service file with `--gapplication-service` | | `NoDisplay` | Hides helper entries from menus | | `OnlyShowIn` / `NotShowIn` | Restricts to specific desktops; use sparingly | Validate it: ```bash desktop-file-validate io.github.yourname.Notes.desktop ``` Do not put shell pipelines or environment-specific paths in `Exec`. If you need environment variables, use a wrapper script or `Exec=env FOO=bar notes`. Keep the command simple and consistent with the installed binary; `%U` passes files or URLs. ## MIME types The shared MIME-info database tells the desktop how to identify files and which app should open them. For common formats (`text/plain`, `image/png`, `application/pdf`), you only need to declare `MimeType=` in the desktop file. For a custom format, install an XML file under `$datadir/mime/packages/` and run `update-mime-database`: ```xml Notes document ``` ```bash sudo update-mime-database /usr/share/mime xdg-mime query default application/x-notes-document ``` Prefer an existing MIME type when one fits. A custom type is a commitment: you need an icon, a description, and a fallback for apps that do not know it. ## Icons Install icons into the hicolor theme: ```text /usr/share/icons/hicolor/scalable/apps/io.github.yourname.Notes.svg /usr/share/icons/hicolor/256x256/apps/io.github.yourname.Notes.png /usr/share/icons/hicolor/symbolic/apps/io.github.yourname.Notes-symbolic.svg ``` - The name must match the desktop entry’s `Icon=` value. - Provide a scalable SVG and at least a 256×256 PNG fallback. - A symbolic variant is useful for menus and buttons; keep it monochrome and legible at 16 px. - Do not install icons into an app-private directory unless the desktop entry points to an absolute path (which is fragile). ## Autostart Autostart entries live in `$XDG_CONFIG_HOME/autostart/`. They are normal desktop files. A user can disable one by creating an override with `Hidden=true`; do not ship `Hidden=true` in the app’s own autostart file, because that disables it for everyone. ```ini [Desktop Entry] Type=Application Name=Notes Exec=notes --background Icon=io.github.yourname.Notes X-GNOME-Autostart-enabled=true NoDisplay=true ``` Autostart is a user choice, not a default. If the app needs to run in the background, offer an explicit switch in settings. In a sandbox, use the **Background** portal rather than writing to the host autostart directory. ## Trash, recent files, and user directories - **Trash:** use `Gio.File.trash()` or the Trash portal. Never permanently delete a user file without an explicit confirmation. - **Recent files:** use `Gtk.RecentManager` on GTK or `KRecentFilesAction` on KDE so the file appears in the desktop’s recent list. Do not maintain a private recent-files list. - **User directories:** use `g_get_user_special_dir()` or `xdg-user-dir` instead of assuming `~/Documents`. The localized paths are defined in `~/.config/user-dirs.dirs`, but the API is the stable interface. ## System-wide installation | Asset | Install to | | --- | --- | | Executable | `$prefix/bin` | | Desktop entry | `$prefix/share/applications` | | AppStream metainfo | `$prefix/share/metainfo` | | Icons | `$prefix/share/icons/hicolor/...` | | GSettings schema | `$prefix/share/glib-2.0/schemas` | | MIME package | `$prefix/share/mime/packages` | | D-Bus service file | `$prefix/share/dbus-1/services` | | systemd user unit | `$prefix/lib/systemd/user` | | Translations | `$prefix/share/locale` | | Documentation | `$prefix/share/doc/$app-id` | | License | `$prefix/share/doc//copyright` | Use your build system’s install directories (`get_option('datadir')` in Meson, `GNUInstallDirs` in CMake) instead of hardcoding `/usr/share`. ## Migration and compatibility Apps evolve. When the storage format changes: 1. Read the old format if it exists. 2. Migrate in memory or into a new file. 3. Write the new format atomically. 4. Keep a backup until the migration succeeds. 5. Never delete the old data on first launch without a fallback. When moving from `~/.myapp` to XDG paths, look for the old directory once, copy the data, and leave a note in the log. Do not silently abandon the user’s history. ## Validation checklist - ☐ No hardcoded `~/.config`, `~/.local/share`, or `~/.myapp` paths. - ☐ Config, data, state, cache, and runtime files are separated. - ☐ Writes are atomic; partial writes cannot corrupt data. - ☐ The desktop file passes `desktop-file-validate`. - ☐ The metainfo file passes `appstreamcli validate`. - ☐ The icon name matches the desktop entry and the app ID. - ☐ The app declares the MIME types it can open. - ☐ Autostart is opt-in and uses the Background portal when sandboxed. - ☐ Recent files and trash use the desktop APIs. - ☐ A reset removes config and state but not user data without confirmation. ## Sources - [XDG Base Directory](https://specifications.freedesktop.org/basedir-spec/latest/) - [Desktop Entry](https://specifications.freedesktop.org/desktop-entry-spec/latest/) - [Shared MIME-info](https://specifications.freedesktop.org/shared-mime-info-spec/latest/) - [Icon Theme](https://specifications.freedesktop.org/icon-theme-spec/latest/) - [Autostart](https://specifications.freedesktop.org/autostart-spec/latest/) - [XDG User Dirs](https://www.freedesktop.org/wiki/Software/xdg-user-dirs/) - [Debian Policy](https://www.debian.org/doc/debian-policy/) - [GSettings](https://docs.gtk.org/gio/class.Settings.html) Next: [System Integration](/linux-desktop-guide/platform/integration/) — D-Bus, notifications, secrets, search, and the services that make an app feel part of the desktop. --- # Ubuntu Platform URL: https://digitalesich.github.io/linux-desktop-guide/platform/ubuntu/ > Release support, toolchains, packaging paths, system services, security, and Ubuntu Pro for desktop developers. Ubuntu 26.04 LTS is the guide’s GNOME reference target, but it is not a single SDK. It is an LTS release with a supported toolchain set, two first-class package formats, a set of privileged system services, and a security-maintenance model that extends beyond the standard support window. This page is the map. It tells you which Ubuntu sources to trust, which packaging path fits which audience, and which system APIs an app should use instead of shelling out to a command-line tool. The exact validated component versions are recorded in [Baseline](/linux-desktop-guide/reference/baseline/). The other Platform pages go deeper on Wayland, XDG, integration, Flatpak, packaging, and security. ## What changed in Ubuntu 26.04 - The Ubuntu Desktop session is **Wayland-only**. XWayland still runs X11 applications, but there is no Xorg GNOME session. - **GNOME 50** ships Reduced Motion, improved Orca, fractional scaling on by default, better VRR/HDR, and a more accessible remote desktop. - The **App Center** manages snaps and debs from the Ubuntu archive, and can install third-party debs (with a warning). Snaps remain the recommended third-party path. - **Permission prompting** is experimental and snap-focused; users enable it in **Security Center** and manage portal permissions in Settings. - **Snap base `core26`** is the recommended base. It no longer bundles Python, so Python snaps must add a runtime explicitly. - **.NET 10** and **OpenJDK 25** are the current archive defaults; **Zig 0.15.2** is available in the archive. - **Glycin** now handles sandboxed image decoding for many apps, **GStreamer 1.28** is the media stack with **VA-API**, and **Sysprof** is installed by default. - **NVIDIA + Wayland** is supported; use the drivers installed by `ubuntu-drivers` and test the Wayland session. Official ARM64 desktop images are available for VMs, ACPI/EFI hardware, and Snapdragon devices. - **Ubuntu Insights** is opt-in system telemetry; keep app telemetry separate and opt-in. ## Release and support Target an LTS release unless you have a reason not to. LTS releases are what users and organizations run, and they are what the Ubuntu archive and Snap bases are built around. Flatpak runtimes follow their own Freedesktop, GNOME, and KDE release schedules; they do not track Ubuntu LTS. | Release | Type | Standard support | Ubuntu Pro | Legacy add-on | | --- | --- | --- | --- | --- | | **26.04 LTS** (Resolute Raccoon) | LTS | May 2031 | May 2036 | May 2041 | | **24.04 LTS** (Noble Numbat) | LTS | May 2029 | May 2034 | May 2039 | | **22.04 LTS** (Jammy Jellyfish) | LTS | May 2027 | May 2032 | May 2037 | | **26.10** (Stonking Stingray) | Interim | Jul 2027 | — | — | The 26.04 release notes say the standard support window ends in April 2031, while the release-cycle table says May 2031. This guide uses the release-cycle table and records the discrepancy rather than hiding it. - **LTS** is the baseline for app development, CI, and support statements. - **Interim releases** are for testing new kernels, toolchains, and desktop features. Do not make them your only supported target. - **Ubuntu Pro** is optional. Apps must work on a stock Ubuntu installation without it. - **Legacy add-on** is an extended-support option for organizations; it is not an app requirement. Sources: [Ubuntu Desktop for developers](https://ubuntu.com/desktop/developers), [Ubuntu release cycle](https://ubuntu.com/about/release-cycle), [Ubuntu 26.04 release notes](https://documentation.ubuntu.com/release-notes/26.04/). ## Toolchains Ubuntu documents a supported toolchain set with installation guides, version availability, and first-program tutorials. Prefer the archive package when you need system integration and ABI stability. Prefer a snap when you need an isolated, cross-distribution tool. Use an upstream installer only when you need a version or feature that Ubuntu does not ship. | Toolchain | Install path | Ubuntu documentation | | --- | --- | --- | | Python | `apt install python3` / `uv` for project environments | [Python on Ubuntu](https://ubuntu.com/developers/docs/toolchains/python/) | | Go | `apt install golang-go` or the official tarball | [Go on Ubuntu](https://ubuntu.com/developers/docs/toolchains/go/) | | Rust | `apt install rustc cargo` or `rustup` for current stable | [Rust on Ubuntu](https://ubuntu.com/developers/docs/toolchains/rust/) | | GCC | `apt install build-essential` | [GCC on Ubuntu](https://ubuntu.com/developers/docs/toolchains/gcc/) | | Clang/LLVM | `apt install clang lld` | [Clang on Ubuntu](https://ubuntu.com/developers/docs/toolchains/clang/) | | .NET | `apt install dotnet10` (26.04 default); .NET 8/9 via the backports PPA or the .NET snap | [.NET on Ubuntu](https://ubuntu.com/developers/docs/toolchains/dotnet/) | | Java | `apt install openjdk-25-jdk` (26.04 default); `openjdk-21-jdk` for older compatibility | [Java on Ubuntu](https://ubuntu.com/developers/docs/toolchains/java/) | | Zig | `apt install zig` (0.15.2); snap or upstream tarball only for a newer version | [Zig on Ubuntu](https://ubuntu.com/developers/docs/toolchains/zig/) | Guidance: - Use the archive toolchain for code that links against system libraries and ships as a deb. - Use a snap or a project-local toolchain for compilers and language runtimes that must be newer than the archive. - Pin the toolchain version in CI. “Whatever is installed on the runner” is not a version policy. - Check the [toolchain availability reference](https://ubuntu.com/developers/docs/reference/availability/) before promising support for an older release. - IDE setup and installation commands are collected in the [IDE reference](https://ubuntu.com/developers/docs/reference/ides/). ## Packaging paths Ubuntu supports two first-class package formats: **deb** and **snap**. Flatpak remains the cross-distribution path covered by this guide. They solve different problems. | Path | Best for | Updates | Sandbox | Trade-off | | --- | --- | --- | --- | --- | | **deb + PPA** | Ubuntu archive integration, system tools, libraries | apt, unattended-upgrades | No | Full system access; tied to release | | **Snap** | Third-party desktop apps in the Ubuntu App Center | snapd, automatic refresh | Yes, AppArmor | Larger install; confinement interfaces | | **Flatpak / Flathub** | Cross-distro upstream desktop apps; not installed by Ubuntu by default | OSTree deltas | Yes, portals | Separate runtime model; the user or upstream must install Flatpak and add Flathub | | **OCI / Rockcraft** | Services and cloud workloads | Container registry | Container | Not a desktop app format | Ubuntu’s own guidance recommends snaps for third-party apps in the App Center because they are sandboxed and updated independently of the release. The App Center also manages debs from the Ubuntu archive and can install third-party debs, but third-party debs are discouraged: they have full access to the user’s resources and may add their own repositories. Use a deb when you need archive/system integration or are maintaining an Ubuntu package; use a Snap when you want the App Center’s confined, auto-updating path. Flatpak is a different case. Ubuntu does not ship Flatpak or the Flathub remote by default, and Canonical does not list it as a supported Ubuntu package format. It is still a mature, open-source, cross-distribution channel that many users install deliberately. If you target those users, treat Flatpak as a first-class build target and document the `flatpak`/`flatpak-builder` install and Flathub setup; do not imply that it is part of a stock Ubuntu installation. ### deb and PPA ```bash sudo add-apt-repository ppa:yourname/notes sudo apt update sudo apt install notes ``` - Follow the [Ubuntu packaging guide](https://ubuntu.com/project/docs/) and the [new-package guide](https://ubuntu.com/project/docs/contributors/new-package/create-a-new-package/). - Build packages in a clean chroot; validate with `lintian`. - Use a PPA for upstream deb distribution; use the Ubuntu archive only when you can maintain the package for the release lifecycle. - See [Build packages in a PPA](https://ubuntu.com/project/docs/contributors/bug-fix/build-packages-in-a-ppa/). ### Snap and Snapcraft ```bash snapcraft init snapcraft pack sudo snap install --dangerous ./notes_0.1.0_amd64.snap snap connections notes ``` - `confinement: strict` is the default for a new app. `classic` is for tools that genuinely need full system access and is reviewed manually. - Declare the smallest set of [interfaces](https://snapcraft.io/docs/interfaces) you need. - Use XDG Desktop Portals inside the snap where possible; the Snap documentation has a dedicated [XDG desktop portals](https://snapcraft.io/docs/xdg-desktop-portals) page. - Build with [Snapcraft](https://ubuntu.com/docs/snapcraft/stable/) and follow the [Craft a snap](https://ubuntu.com/docs/snapcraft/stable/tutorials/craft-a-snap/) tutorial. - For snapd automation, use the [REST API](https://snapcraft.io/docs/snapd-api), not the `snap` CLI from inside a confined app. ### Flatpak Flatpak is covered in [Flatpak & Portals](/linux-desktop-guide/platform/flatpak-portals/) and [Flathub](/linux-desktop-guide/ship/flathub/). Keep the distinction clear: - **Ubuntu App Center** recommends snaps for third-party desktop apps and also manages debs from the Ubuntu archive; third-party debs are supported but discouraged. - **Flathub** is the vendor-neutral upstream store for Flatpak apps across distributions. It is not part of a stock Ubuntu installation, so document the Flatpak setup separately. - Shipping both is legitimate if you can maintain both. Do not treat one as a drop-in replacement for the other. ### OCI images and Rockcraft Use [Rockcraft](https://github.com/canonical/rockcraft) when the deliverable is a container image, not a desktop app. Rocks are Ubuntu-based, minimal OCI images with a supported build path. They are not a way to package a GUI application for the desktop. ## System services Ubuntu’s desktop services expose stable D-Bus APIs. Use the API, not the command-line tool, and use a portal when one exists. The table is intentionally opinionated: it tells you what an app should normally do, not everything the service can do. | Service | API | App guidance | Primary documentation | | --- | --- | --- | --- | | **systemd / journald** | `org.freedesktop.systemd1`, journal fields | Use user units for background work; log to stderr/journald; never manage system units from an app | [systemd](https://systemd.io/), [journald](https://www.freedesktop.org/software/systemd/man/latest/systemd-journald.service.html) | | **NetworkManager** | `org.freedesktop.NetworkManager`, libnm | Observe connectivity; do not reconfigure the network unless the app is a network manager | [NetworkManager developers](https://networkmanager.dev/docs/developers/) | | **PipeWire** | PipeWire API, GStreamer | Play and record through PipeWire; do not open raw ALSA devices | [PipeWire docs](https://docs.pipewire.org/) | | **WirePlumber** | WirePlumber library, `wpctl` | Let the session manager route devices; do not override policies | [WirePlumber docs](https://pipewire.pages.freedesktop.org/wireplumber/) | | **UPower** | `org.freedesktop.UPower` | Read battery and power state; react to suspend/resume; do not poll | [UPower docs](https://upower.freedesktop.org/docs/) | | **BlueZ** | `org.bluez` | Use BlueZ profiles and D-Bus; do not talk to raw HCI | [BlueZ D-Bus API](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc) | | **CUPS** | IPP, libcups | Use `Gtk.PrintDialog` or the Print portal; do not manage printers | [OpenPrinting CUPS](https://openprinting.github.io/cups/) | | **UDisks2** | `org.freedesktop.UDisks2` | Use GIO/GVfs for files; only disk utilities should use UDisks directly | [UDisks2 API](https://storaged.org/udisks/docs/) | | **AppArmor** | Kernel LSM, `aa-*` tools | Ship a profile for system services; desktop apps rely on the Flatpak/Snap sandbox | [AppArmor docs](https://apparmor.net/) | | **Polkit** | `org.freedesktop.PolicyKit1` | Never run `sudo` from an app; define a Polkit action for privileged operations | [Polkit docs](https://www.freedesktop.org/software/polkit/docs/latest/) | | **fwupd** | `org.freedesktop.fwupd` | Do not flash firmware yourself; integrate with fwupd/LVFS or link to the firmware updater | [fwupd / LVFS](https://fwupd.org/) | | **PackageKit** | `org.freedesktop.PackageKit` | Do not install packages from an app; use the software center/PackageKit | [PackageKit](https://www.freedesktop.org/software/PackageKit/) | | **snapd** | REST API on `/run/snapd.socket` | Do not manage snaps from inside a snap; let snapd/the store update them | [snapd REST API](https://snapcraft.io/docs/snapd-api) | | **AT-SPI** | D-Bus accessibility interfaces | Use native widgets and `Gtk.Accessible`; do not draw inaccessible controls | [Ubuntu accessibility stack](https://documentation.ubuntu.com/desktop/en/latest/explanation/accessibility-stack/) | | **Security Center / permission prompting** | snapd prompting API, AppArmor | Use portals and minimal Snap interfaces; users manage experimental prompts in Security Center | [Permissions prompting deep dive](https://discourse.ubuntu.com/t/permissions-prompting-a-deep-dive/81785) | Two rules cover most of the table: 1. **Use the highest-level API that fits.** A file dialog, print dialog, notification, or secret belongs in a portal or toolkit API, not in a direct service call. 2. **Do not shell out to system tools from a sandbox.** `nmcli`, `udisksctl`, `snap`, and `fwupdmgr` may be absent or restricted. Use the D-Bus API or a portal. ## Security and Ubuntu Pro Ubuntu’s security model combines AppArmor, Polkit, signed packages, and optional Ubuntu Pro services. - **AppArmor** confines services and sandboxed apps with per-application profiles. If you ship a system service, provide a profile and test it with `aa-status` and `journalctl -k --grep=apparmor`. - **Polkit** mediates privileged operations. Define a named action instead of asking users to run `sudo`; the desktop can then present a proper authentication dialog. - **fwupd** handles firmware updates through the LVFS. Apps should link to the firmware updater or expose status, not write firmware directly. - **Ubuntu Pro** adds ESM, Livepatch, FIPS, CIS hardening, and extended support. It is preinstalled as the `pro` client: ```bash pro status sudo pro attach sudo pro enable esm-apps sudo pro enable livepatch pro fix CVE-2025-12345 ``` Apps must not require Pro. If your app exposes security status, detect whether a service is enabled and explain the benefit without blocking core functionality. See [Ubuntu Pro Client](https://documentation.ubuntu.com/pro-client/en/latest/) and [Ubuntu Pro](https://ubuntu.com/pro). ## Accessibility on Ubuntu Ubuntu Desktop’s accessibility stack is built on AT-SPI over D-Bus. GTK 4 exposes semantics through `Gtk.Accessible`; GTK 3 uses ATK. Orca is the default screen reader, with BRLTTY for Braille and Speech Dispatcher/eSpeak for speech. - Use GTK or Qt widgets that expose AT-SPI semantics. - Test with Orca and the accessibility inspector, not only with an automated checker. - Custom-rendered or web-rendered UI does not inherit AT-SPI semantics automatically; test it with Orca and budget for explicit accessibility work. - Follow [Accessibility](/linux-desktop-guide/design/accessibility/) for the product-level checklist. Sources: [Ubuntu Desktop accessibility stack](https://documentation.ubuntu.com/desktop/en/latest/explanation/accessibility-stack/), [AT-SPI D-Bus interfaces](https://documentation.ubuntu.com/desktop/en/latest/reference/accessibility/dbus/). ## Where to get help - [Ubuntu for Developers](https://ubuntu.com/developers/docs/) — toolchains, IDEs, packaging. - [Ubuntu Desktop documentation](https://documentation.ubuntu.com/desktop/en/latest/) — desktop behavior, accessibility, snap/deb policy. - [Ubuntu Discourse](https://discourse.ubuntu.com/) — platform and desktop discussion. - [Ubuntu Desktop Matrix](https://matrix.to/#/#desktop-dev:ubuntu.com) — desktop development chat. - [Ask Ubuntu](https://askubuntu.com/) — user-facing questions. - [Launchpad](https://launchpad.net/) — bugs, PPAs, and Ubuntu packages. ## Ubuntu platform checklist - ☐ Target an LTS release and state the support window. - ☐ Pin the toolchain version used in CI and packaging. - ☐ Choose deb, snap, Flatpak, or OCI deliberately; do not ship all four by default. - ☐ Use portals or toolkit APIs for files, printing, notifications, secrets, and screen capture. - ☐ Use D-Bus APIs or the snapd REST API instead of shelling out to `nmcli`, `udisksctl`, `snap`, or `fwupdmgr`. - ☐ State whether the app ships through Ubuntu-supported deb/Snap or an alternative channel; document the Flatpak setup separately if you use it. - ☐ Test the Wayland-only Ubuntu Desktop session and XWayland only for legacy X11 apps. - ☐ Never require `sudo` at runtime; define a Polkit action if privileged work is unavoidable. - ☐ Do not require Ubuntu Pro; detect optional services and degrade gracefully. - ☐ Test with Orca and the Ubuntu accessibility stack. - ☐ Validate the desktop file, AppStream metadata, and package with the Ubuntu tooling. - ☐ Test on a stock Ubuntu installation, not only on your development machine. ## See also - [Wayland & Display](/linux-desktop-guide/platform/wayland/) - [The XDG Contract](/linux-desktop-guide/platform/xdg/) - [System Integration](/linux-desktop-guide/platform/integration/) - [Flatpak & Portals](/linux-desktop-guide/platform/flatpak-portals/) - [Packaging & Distribution](/linux-desktop-guide/platform/packaging/) - [Security & Privacy](/linux-desktop-guide/platform/security/) - [Ubuntu + GNOME target](/linux-desktop-guide/targets/ubuntu-gnome/) — the full target profile, development environment, and quality gates. - [Glossary](/linux-desktop-guide/reference/glossary/) — definitions for the platform and toolkit terms used here. Next: [Wayland & Display](/linux-desktop-guide/platform/wayland/) — how the compositor, input, and scaling model change what the app can do. --- # Wayland & Display URL: https://digitalesich.github.io/linux-desktop-guide/platform/wayland/ > How Wayland changes windowing, input, scaling, screen capture, and security — and what a native Linux app must do instead of reaching for X11. Ubuntu 26.04 LTS ships a Wayland-only GNOME session. XWayland still runs X11 applications, but the native path is Wayland, and the assumptions that made X11 apps work are often exactly the assumptions that break under Wayland. Wayland is not “X11 with a different socket”. It is a different security model. The compositor is the display server, the window manager, the input router, and the policy layer. Clients render their own buffers and describe their surfaces; they do not get to inspect or control the rest of the session. That model is what makes fractional scaling, per-monitor DPI, input methods, and secure screen sharing tractable and secure. It is also why apps that rely on global coordinates, synthetic input, or framebuffer capture stop working. ## The architecture in one diagram ```text ┌──────────────────────────────────────────────────────────────┐ │ Your application │ │ GTK 4 / Qt 6 ── renders buffers ── describes surfaces │ └───────────────────────────┬──────────────────────────────────┘ │ Wayland protocol ┌───────────────────────────▼──────────────────────────────────┐ │ Compositor (Mutter, KWin…) │ │ window placement · input routing · scaling · focus · policy │ └───────────────┬──────────────────────────────┬───────────────┘ │ │ ┌───────▼────────┐ ┌────────▼─────────┐ │ XWayland │ │ Portals / D-Bus │ │ legacy X11 │ │ screen capture │ └────────────────┘ └──────────────────┘ ``` Your app talks to the compositor through the toolkit. It talks to desktop services through D-Bus and XDG Desktop Portals. It should not talk to the kernel’s input devices, the DRM framebuffer, or another app’s window. ## What changes for application developers | Concern | X11 habit | Wayland-native approach | | --- | --- | --- | | Window position | `XMoveWindow` to absolute coordinates | Let the compositor place it; save size, not position | | Screen capture | `XGetImage` / framebuffer grab | ScreenCast portal + PipeWire | | Global shortcuts | `XGrabKey` | GlobalShortcuts portal, or app-local shortcuts | | Input injection | `XTest` | Not allowed; design an accessible in-app path | | Clipboard | X selection ownership | `Gdk.Clipboard`; asynchronous, portal-mediated where needed | | Scaling | Integer scale factor | Fractional scale protocol; toolkit handles it | | Decorations | Reparenting window manager | Client-side decorations by default; `xdg-decoration` negotiation | | Focus stealing | `XRaiseWindow` / `_NET_ACTIVE_WINDOW` | Request activation through the toolkit; compositor decides | | Idle inhibition | X screensaver extension | `org.freedesktop.portal.Inhibit` / `Gtk.Application.inhibit` | | Accessibility | AT-SPI over X | AT-SPI over D-Bus; toolkit exposes semantics | ## The rules that matter ### Do not position windows Wayland deliberately gives clients no global coordinate system. `Gtk.Window` has no “move to 1920, 400”. The compositor may be tiled, may have multiple monitors with different scales, or may place the window on a workspace you cannot see. **Do:** save and restore window _size_, maximized state, and fullscreen state. Let the compositor choose the monitor and position. If the app needs a specific monitor, use the toolkit’s monitor APIs for sizing and fullscreen, not absolute coordinates. **Avoid:** `move()`, `set_position()`, or X11 `XMoveWindow` calls. They either do nothing or behave inconsistently. ### Use client-side decorations, but do not reinvent them GTK 4 and Qt 6 draw client-side decorations by default. A `Adw.HeaderBar` or a Qt window frame participates in tiling, dragging, resizing, and accessibility. A custom titlebar that draws its own drag logic usually breaks at least one of those. **Do:** use `Adw.ApplicationWindow` + `Adw.HeaderBar`, or `QMainWindow` with the platform theme. If you need a custom header, keep the toolkit’s window controls and drag handling. **Avoid:** hiding the system close button, drawing fake window controls, or implementing window dragging with raw pointer events. ### Handle fractional scaling Fractional scaling is enabled by default on Ubuntu 26.04. A 125% or 150% session is normal. Toolkit layouts should use logical pixels and vector assets; the compositor and toolkit handle the rest. **Do** - Use SVG icons and scalable UI assets. - Test at 100%, 125%, 150%, 200%, and with two monitors at different scales. - Use toolkit spacing and font scaling rather than hardcoded pixel sizes. - Let text wrap and windows resize; do not assume a minimum width that only fits your development monitor. **Avoid** - Raster assets at a single size. - Fixed-size windows that clip at 200%. - Manual scale-factor math in application code. GTK 4 handles fractional scaling through the compositor’s scale protocol. If you draw custom content with `Gtk.Snapshot`, use the widget’s logical coordinates and let the toolkit scale the surface. ### Screen capture goes through the portal An app that needs to record the screen, share a window, or take a screenshot uses the **ScreenCast** or **Screenshot** portal. The desktop shows a picker and a permission prompt. The app receives a PipeWire stream, not a framebuffer. **Do** - Use `org.freedesktop.portal.ScreenCast` for recording and sharing. - Use `org.freedesktop.portal.Screenshot` for one-shot screenshots. - Handle the case where the user cancels or denies permission. - Stop the stream when the app is done; do not leave a recorder running. **Avoid** - X11 screenshot APIs. - Reading `/dev/dri` or the framebuffer directly. - Recording without a visible indicator. ### Global shortcuts need consent A global shortcut is a key grab that works while another app has focus. Under Wayland, that is a privileged capability. Use the **GlobalShortcuts** portal, which lets the desktop present the shortcut and lets the user change or revoke it. For shortcuts that only work while your window is focused, use the toolkit’s shortcut system (`Gtk.ShortcutController`, `QShortcut`). That needs no portal and should be your default. ### Input methods are not optional Wayland routes text input through the `text-input` and `input-method` protocols. GTK and Qt implement this, including Compose sequences, dead keys, and CJK input methods. **Do:** use standard text widgets and let the toolkit handle input. Test with at least one non-Latin input method and with Compose sequences. **Avoid:** intercepting raw key events to implement text entry, or assuming a US keyboard layout. If you implement a custom text editor, integrate with `Gtk.IMContext` (GTK) or `QInputMethod` (Qt). ### Clipboard and primary selection are asynchronous `Gdk.Clipboard` reads and writes asynchronously. The clipboard may be owned by another process, and the data may arrive after a round trip. The primary selection (middle-click paste) is a separate clipboard on Linux. **Do:** use `Gdk.Display.get_clipboard()` and `get_primary_clipboard()`, handle `read_text_async`/`read_value_async`, and provide a sensible fallback when the clipboard is empty. **Avoid:** assuming the clipboard is synchronously available, or clearing it on exit. On Wayland, the clipboard survives the source app only if the compositor supports it; do not build features that depend on the source staying alive. ### Activation and single-instance behavior Wayland restricts focus stealing. An app cannot raise itself over another app without the compositor’s consent. `Gtk.Application` and `QApplication` use the `org.freedesktop.Application` D-Bus interface to activate an existing instance and pass files or URIs. **Do:** use `Gtk.Application` with a stable application ID, implement `open` for file arguments, and let the toolkit handle activation. Use `GApplication`’s `HANDLES_OPEN` flag when the app opens documents. **Avoid:** launching a second process and hoping the compositor focuses it. Avoid `wmctrl`, `xdotool`, or X11 activation hacks. ### Idle inhibition and suspend Long-running work (video export, a download, a presentation) may need to prevent idle suspend. Use the **Inhibit** portal or the toolkit API: ```python cookie = app.inhibit( window, Gtk.ApplicationInhibitFlags.IDLE | Gtk.ApplicationInhibitFlags.SUSPEND, "Exporting video", ) # ... later app.uninhibit(cookie) ``` Release the inhibitor as soon as the work finishes. An app that holds an inhibitor forever is a battery bug. ## A minimal Wayland checklist - ☐ The app starts on a Wayland session with no X11-specific code path. - ☐ Window size and maximized state persist; position does not. - ☐ The UI works at 100%, 125%, 150%, and 200% scaling. - ☐ Icons are SVG or symbolic; no blurry raster assets. - ☐ File dialogs use `Gtk.FileDialog` or the portal. - ☐ Screen capture uses the ScreenCast portal. - ☐ Global shortcuts use the portal or are app-local. - ☐ Text input works with an IME and with Compose. - ☐ The app is single-instance and handles file-open activation. - ☐ The app does not hold idle inhibitors after work completes. - ☐ Accessibility works with Orca on Wayland. ## Debugging Wayland | Tool | Use | | --- | --- | | `WAYLAND_DEBUG=1` | Log the protocol messages between the app and compositor | | `GTK_DEBUG=interactive` | Inspect the widget tree, layout, and render nodes | | `gtk4-demo` / `gtk4-widget-factory` | Compare behavior with canonical GTK apps | | Mutter Devkit | Test nested GNOME Shell with simulated monitors and scaling | | `weston --backend=headless` | Run a headless Wayland compositor for tests | | `wev` | Inspect input events on Wayland | | `wayland-info` | List globals and protocol support (`wayland-utils`) | For GNOME-specific behavior, Mutter Devkit in GNOME 50 can simulate HiDPI, fractional scaling, multiple monitors, and clipboard sharing. It is the fastest way to test layout without physical hardware. ## Common pitfalls | Symptom | Likely cause | Fix | | --- | --- | --- | | Window appears at a random position | App calls `move()` | Remove absolute positioning; save size only | | Blurry UI at 125% | Raster assets or manual scaling | Use SVG and toolkit layouts | | Screen share shows a black window | App uses X11 capture | Use the ScreenCast portal | | Global shortcut does nothing | X11 key grab | Use GlobalShortcuts portal or app-local shortcut | | Text input duplicates characters | Custom key handling | Use the toolkit’s text widgets and IM context | | File dialog does not open in Flatpak | Direct filesystem access | Use `Gtk.FileDialog` / FileChooser portal | | App steals focus | X11 activation hack | Use `Gtk.Application` activation | | Clipboard empty after copy | Asynchronous read not awaited | Use async clipboard APIs and handle failure | ## Sources - [Wayland](https://wayland.freedesktop.org/) - [XDG Desktop Portals](https://flatpak.github.io/xdg-desktop-portal/docs/) - [GTK Wayland](https://docs.gtk.org/gtk4/wayland.html) - [Mutter](https://gitlab.gnome.org/GNOME/mutter) - [wayland-utils](https://gitlab.freedesktop.org/wayland/wayland-utils) - [Ubuntu 26.04 release notes](https://documentation.ubuntu.com/release-notes/26.04/) Next: [The XDG Contract](/linux-desktop-guide/platform/xdg/) — where apps put their data, and why it matters. --- # Accessibility URL: https://digitalesich.github.io/linux-desktop-guide/design/accessibility/ > Keyboard access, screen-reader semantics, contrast, text scaling, reduced motion, and inclusive forms — the technical and legal baseline for Linux desktop apps. Accessibility is not a feature for a minority of users. It is the quality of the interface under constraints: no mouse, small screen, low vision, motor impairment, cognitive load, bright sunlight, or a noisy train. Most people benefit from accessible design at some point, and many rely on it permanently. It is also a legal requirement. The **European Accessibility Act** applies to many digital products and services from 28 June 2025. [WCAG 2.2 AA](https://www.w3.org/TR/WCAG22/) is the common reference; [EN 301 549](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/04.01.01_60/en_301549v040101p.pdf) is the European harmonized standard; Section 508 applies in the United States. If you ship to the public, accessibility is part of compliance, not a nice-to-have. ## The Linux accessibility stack | Layer | What it does | | --- | --- | | **AT-SPI** | D-Bus protocol that exposes the UI tree, roles, states, and actions | | **Toolkit** | GTK 4 and Qt 6 expose accessibility semantics automatically for native widgets | | **Orca** | GNOME screen reader; reads the AT-SPI tree and drives the app | | **Accerciser** | Inspector for the accessibility tree | | **Desktop settings** | Large text, high contrast, reduced motion, sticky keys, screen reader | If you use native widgets, you get most of this for free. If you draw your own controls, you must implement the semantics yourself — and that is usually a bad trade. On Ubuntu, the stack is documented in the [Ubuntu Desktop accessibility guide](https://documentation.ubuntu.com/desktop/en/latest/explanation/accessibility-stack/). GTK 4 apps expose semantics through `Gtk.Accessible`; GTK 3 uses ATK. Orca is the default screen reader, with BRLTTY for Braille and Speech Dispatcher/eSpeak for speech. Custom-rendered or web-rendered UI does not inherit AT-SPI semantics automatically; test it with Orca and budget for explicit accessibility work. The [AT-SPI D-Bus interfaces](https://documentation.ubuntu.com/desktop/en/latest/reference/accessibility/dbus/) are the reference for custom assistive tooling. ## The baseline: WCAG 2.2 AA The four principles are **perceivable, operable, understandable, and robust**. For desktop apps, the practical checklist is: - Every action is operable with a keyboard. - Every control has an accessible name and role. - Focus is visible and follows a logical order. - Text and UI components meet contrast requirements. - Content reflows at 200% zoom/text scaling without loss of function. - Motion can be reduced. - Errors are identified and described. - Forms have labels, instructions, and error messages. - Media has captions/transcripts where applicable. - The app does not rely on color alone. WCAG 2.2 adds several criteria that matter for desktop apps: **2.4.11 Focus Not Obscured (Minimum)**, **2.5.7 Dragging Movements**, **2.5.8 Target Size (Minimum)**, **3.2.6 Consistent Help**, **3.3.7 Redundant Entry**, and **3.3.8 Accessible Authentication (Minimum)**. Treat them as part of the AA baseline, not as optional polish. EN 301 549 v4.1.1 adopts WCAG 2.2, but procurement documents may still cite v3.2.1 (WCAG 2.1 AA). Check which harmonised standard your market or customer requires. ## Semantics: use native widgets A `Gtk.Button` already exposes a button role, a label, a pressed state, and keyboard activation. A custom `Gtk.DrawingArea` with a click handler exposes none of that. - Use native widgets for buttons, entries, lists, tabs, menus, dialogs, and switches. - If you must build a custom widget, subclass an existing accessible widget or implement `Gtk.Accessible` correctly. - Give icon-only buttons an accessible label. - Link labels to their controls with mnemonics. - Expose state changes (expanded, selected, busy) through properties, not just visuals. ```python button = Gtk.Button(icon_name="list-add-symbolic") button.set_tooltip_text("New note") button.update_property([Gtk.AccessibleProperty.LABEL], ["New note"]) ``` ```python label = Gtk.Label(label="_Name", use_underline=True) label.set_mnemonic_widget(name_entry) ``` Tooltips are not a substitute for accessible names. A screen reader may not read a tooltip, and a keyboard user may never hover. ## Keyboard access - Every interactive element is reachable with `Tab` / `Shift+Tab`. - Every action has a keyboard path, even if it is only through a menu. - Dialogs trap focus and restore it on close. - `Esc` closes the current layer. - `Enter` activates the default action; `Space` toggles the focused control. - Arrow keys navigate lists, trees, tabs, and radio groups according to platform conventions. - No keyboard traps. If focus enters a custom widget, it can leave. - Shortcuts do not conflict with the desktop or screen reader. Test by unplugging the mouse. Then test with Orca running. ## Focus visibility - Keep the toolkit’s focus ring, or replace it with something equally visible. - Do not use `outline: none` without an alternative. - Focus should be visible in light, dark, and high-contrast modes. - The focused element should be scrolled into view. - When a dialog closes, focus returns to the control that opened it. ## Contrast and color | Element | Minimum contrast | | --- | --- | | Body text | 4.5:1 | | Large text (18 pt / 14 pt bold) | 3:1 | | UI components and boundaries | 3:1 | | Focus indicator | 3:1 against adjacent colors | - Use semantic colors from the toolkit; they are tested against the theme. - Do not use color as the only indicator of state. Add an icon, label, or pattern. - Check hover, pressed, disabled, selected, and error states. - Test in high-contrast mode; custom colors often fail there. - Do not put text over busy images without a solid or blurred scrim. ## Text scaling and zoom Users can increase the system font size or set a display scale of 200%. The app must remain usable. - Use wrapping labels, flexible layouts, and scrollable content. - Avoid fixed heights for text containers. - Do not truncate essential text; use ellipsis only for secondary information. - Test at 200% text scaling and 200% display scaling. - Keep target sizes usable when text grows. - Do not disable zoom in a document viewer; support `Ctrl++`/`Ctrl+-`. ## Reduced motion - Respect the system’s reduced-motion setting. - libadwaita 1.9 applies it to most widgets automatically; custom animations must check. - Replace large transitions with instant state changes or short fades. - Do not autoplay video or looping animations. - Provide a way to pause or stop motion in media. ```python from gi.repository import Adw animations_enabled = Adw.get_enable_animations(self) ``` libadwaita widgets honor the system setting. GTK 4.22 core widgets and custom animations still need verification; do not assume every animation is disabled automatically. ## Forms and errors - Every field has a visible label, not just a placeholder. - Required fields are marked in text, not only with an asterisk. - Errors are announced and associated with the field. - Error messages say how to fix the problem. - Do not clear the form on error. - Support autofill and password managers where appropriate. - Group related fields with a fieldset-like container and a heading. ## Media - Provide captions for video with speech. - Provide transcripts for audio. - Provide audio descriptions for visual information that is not in the dialogue. - Do not autoplay media with sound. - Ensure media controls are keyboard accessible and labeled. - Respect reduced motion and autoplay settings. ## Testing 1. **Keyboard-only:** complete the main tasks with no mouse. 2. **Orca:** enable the screen reader and walk through the app. `orca --replace` starts it; `Super+Alt+S` toggles it on GNOME. Listen for missing labels, duplicate announcements, and focus jumps. 3. **Accerciser:** inspect roles, names, states, and relations. Fix anything that reads as “unknown” or “panel”. 4. **High contrast:** switch the desktop to high contrast and check every custom style. 5. **Large text:** set 200% text scaling and 200% display scaling; check clipping and overflow. 6. **Reduced motion:** disable animations and verify the app remains understandable. 7. **Automation:** use `dogtail` or AT-SPI scripts for regression tests on critical flows. Test with real assistive technology Automated accessibility checks catch missing labels. They do not catch a confusing reading order or an announcement that fires three times. Budget time with Orca and, if possible, a screen-reader user. ## Common failures | Failure | Fix | | --- | --- | | Icon-only button with no label | Add `Gtk.AccessibleProperty.LABEL` | | Placeholder used as a label | Add a persistent visible label | | Custom toggle with no state | Use `Gtk.Switch` / `Gtk.ToggleButton` or expose state | | Focus ring removed | Restore or replace it | | Fixed-height rows clip large text | Use natural height and wrapping | | Color-only error state | Add an icon and text | | Dialog focus escapes to the background | Use a modal dialog and trap focus | | Animation ignores reduced motion | Check the setting before animating | | Drag-only reordering | Add move up/down actions | | Canvas-drawn chart with no text alternative | Provide a table or description | ## Legal and organizational checklist - ☐ Accessibility is in the definition of done, not a post-release audit. - ☐ WCAG 2.2 AA is the target for public-facing apps. - ☐ Keyboard and screen-reader tests are part of release QA. - ☐ The accessibility statement names known limitations and a contact. - ☐ Product documentation lists accessibility features and is available in an accessible format; see [User Documentation](/linux-desktop-guide/ship/documentation/). - ☐ Third-party components are checked for accessibility. - ☐ The team has access to Orca, Accerciser, and a high-contrast environment. - ☐ Accessibility bugs are prioritized like other user-facing bugs. ## Sources - [WCAG 2.2](https://www.w3.org/TR/WCAG22/) - [EN 301 549 v4.1.1](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/04.01.01_60/en_301549v040101p.pdf) - [European Accessibility Act](https://commission.europa.eu/strategy-and-policy/policies/justice-and-fundamental-rights/disability/european-accessibility-act-eaa_en) - [Ubuntu accessibility stack](https://documentation.ubuntu.com/desktop/en/latest/explanation/accessibility-stack/) - [AT-SPI](https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/) - [libadwaita get\_enable\_animations](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/func.get_enable_animations.html) Next: [Localization](/linux-desktop-guide/design/localization/) — making the app work in every language, locale, and writing direction. --- # Design Principles URL: https://digitalesich.github.io/linux-desktop-guide/design/principles/ > How to design Linux desktop apps that feel calm, fast, and native across GNOME, KDE, and the wider ecosystem — without sanding off your product's character. Linux design is plural. GNOME, KDE Plasma, and elementary publish human-interface guidelines; COSMIC publishes its own design guidance. Each has a coherent language, but they do not agree on every pattern. There is no single authority that can settle every question, so the app has to make a choice: **pick a primary desktop, follow its conventions deeply, and adapt gracefully elsewhere.** That is not a limitation. It is what lets a Linux app feel like it belongs in more than one place without feeling imported from another platform. The [Platform Targets](/linux-desktop-guide/targets/) page chooses the default HIG for each target; this chapter explains the principles behind that choice. ## The principles ### 1\. Clarity before cleverness The user should understand what the window does, what is selected, and what happens next without reading documentation. Use plain labels, visible state, and a clear hierarchy. - One primary action per view. - Destructive actions are visually distinct and confirmable. - Icons are supported by labels when the meaning is not universal. - Empty states explain the next step. ### 2\. Calm by default Desktop apps are long-lived. The interface should be quiet enough to live in for hours. - Avoid constant animation, pulsing badges, and attention-seeking color. - Use motion to explain a change, not to decorate it. - Keep chrome minimal; let content carry the visual weight. - Do not use modals for information that can be inline. ### 3\. Content is the interface The best GNOME apps feel like a frame around the user’s work. The headerbar, sidebar, and toolbar should recede when the user is reading, writing, or editing. - Reduce persistent toolbars to the actions that matter in context. - Use progressive disclosure for advanced settings. - Let the window resize and the layout adapt; do not force a fixed canvas. - Prefer a document-first layout over a dashboard of widgets. ### 4\. Keyboard-first, pointer-optional Every action should be reachable without a mouse. This is not a power-user feature; it is the baseline for accessibility and speed. - Standard shortcuts work. - Focus is visible and moves logically. - Menus expose shortcuts. - Dialogs can be completed and dismissed from the keyboard. - Search is one shortcut away. ### 5\. Consistency with the host Use the host’s widgets, spacing, icons, and interaction patterns. A custom control is a debt: it must reimplement focus, hover, pressed, disabled, high-contrast, RTL, and accessibility behavior. - Use `Adw` widgets on GNOME, Kirigami on KDE. - Follow the host’s HIG for navigation and dialogs. - Respect the system accent, dark mode, font, and text scale. - Do not ship a theme that overrides the desktop. ### 6\. Direct manipulation When the user can act on an object, let them act on the object. Drag to reorder, click to select, type to rename, right-click for context. - Provide visible affordances, not hidden gestures. - Support undo for reversible actions. - Keep selection stable across refreshes. - Do not require a modal dialog for a one-field edit. ### 7\. Feedback is immediate Every action should produce a visible response within 100 ms. If the work takes longer, show progress and allow cancellation. - Buttons show a pressed state. - Long operations show progress, not a frozen window. - Success is quiet; failure is specific. - Toasts confirm non-critical actions; notifications cover background events. - Errors say what happened, why, and what to do next. ### 8\. Forgiving by design Users make mistakes, and so do apps. Design for recovery. - Undo instead of confirmation for reversible actions. - Confirm only destructive, irreversible actions. - Autosave and restore sessions. - Never lose user input because of a crash or a closed dialog. - Provide a reset path for settings and a export path for data. ### 9\. Performance is a design property A beautiful interface that stutters is not beautiful. Startup time, scroll smoothness, and input latency are part of the experience. - Keep the first paint fast. - Avoid layout thrash and huge widget trees. - Use skeleton states instead of blank windows. - Test on the oldest hardware you support. ### 10\. Respect the user’s settings The desktop is configurable because users have preferences. Honor them. - Light/dark mode and accent color. - Font size and text scaling. - Reduced motion. - High contrast. - Language and locale. - Power-saving mode. An app that ignores these settings is not native, no matter how it looks. ## Pick a primary desktop | Desktop | Design language | Toolkit | HIG | | --- | --- | --- | --- | | GNOME | Calm, content-first, headerbars, Adwaita | GTK 4 + libadwaita | [GNOME HIG](https://developer.gnome.org/hig/) | | KDE Plasma | Dense, configurable, Kirigami | Qt 6 + Kirigami | [KDE HIG](https://develop.kde.org/hig/) | | elementary OS | Minimal, structured, AppCenter-first | GTK 4 + Granite | [elementary HIG](https://docs.elementary.io/hig/) | | COSMIC | Modular, Rust-native | libcosmic | [COSMIC design](https://system76.com/cosmic) | **Pick one as your primary.** Follow its navigation, spacing, and dialog conventions. On other desktops, use the toolkit’s platform integration, avoid hardcoded assets, and accept that the app will feel like a guest — because it is one. The reference targets carry a concise decision layer in their profiles: [GNOME design contract](/linux-desktop-guide/targets/ubuntu-gnome/#design-contract-gnome) and [KDE Plasma design contract](/linux-desktop-guide/targets/fedora-kde/#design-contract-kde-plasma). The canonical HIGs remain the exhaustive reference; these contracts translate them into the choices an app has to make. They are summaries, not copies of the upstream guidelines. ## Where brand belongs Brand expression is not banned. It just has a small, high-leverage surface area: | Good places for brand | Bad places for brand | | --- | --- | | App icon | Window controls | | Accent color (within system limits) | File dialogs | | Empty states and illustrations | Notifications | | Onboarding copy | Menus and shortcuts | | About dialog | Titlebar layout | | Marketing site and screenshots | System tray behavior | The fastest way to make a Linux app feel foreign is to import another platform’s chrome. The fastest way to make it feel distinctive is a strong icon, a thoughtful empty state, and a clear voice. ## A design process that fits Linux 1. **Write the user’s task in one sentence.** “Capture a thought before it disappears.” Not “note management”. 2. **Sketch the smallest flow.** One window, one primary action, one escape hatch. 3. **Choose the platform pattern.** Sidebar, split view, stack, tabs, or dialog. Use the HIG, not a generic design-gallery mockup. 4. **Prototype in the real toolkit.** GTK Builder/Blueprint or Qt Quick. A prototype that cannot resize is not a prototype. 5. **Test at three sizes.** Narrow (360 px), medium (720 px), wide (1280 px). Test at 200% scale. 6. **Test with the keyboard only.** Then with Orca. Then with reduced motion and high contrast. 7. **Remove one thing.** The first design is usually too busy. 8. **Ship, observe, iterate.** Watch where users hesitate; fix the flow, not the color. ## What to avoid | Anti-pattern | Why it hurts | | --- | --- | | Hamburger menu for primary navigation | Hides the app’s structure; poor discoverability | | Custom titlebar with fake window controls | Breaks tiling, gestures, and platform expectations | | Modal dialog for every action | Interrupts flow; stacks up; hard to undo | | Tiny icon-only buttons without tooltips | Inaccessible and ambiguous | | Animation on every state change | Feels slow; ignores reduced-motion settings | | Fixed window size | Breaks on small screens and at 200% scaling | | Dark mode as an afterthought | Half the UI becomes unreadable | | “Are you sure?” for reversible actions | Trains users to click through dialogs | | Hover-only controls | Unusable with keyboard and touch | | Custom file picker | Breaks sandboxing, bookmarks, and recent files | ## Sources - [GNOME HIG](https://developer.gnome.org/hig/) - [KDE HIG](https://develop.kde.org/hig/) - [elementary HIG](https://docs.elementary.io/hig/) - [COSMIC design](https://system76.com/cosmic) - [WCAG 2.2](https://www.w3.org/TR/WCAG22/) Next: [Visual Language](/linux-desktop-guide/design/visual-language/) — typography, color, spacing, icons, and motion that adapt to the host. --- # Interaction & Feedback URL: https://digitalesich.github.io/linux-desktop-guide/design/interaction/ > Keyboard shortcuts, focus, pointer targets, undo, search, toasts, errors, loading states, and motion — the small behaviors that make a Linux app feel responsive and trustworthy. Users do not experience your architecture. They experience what happens when they press a key, click a button, or wait for a file to open. Interaction design is the discipline of making those moments predictable, fast, and reversible. ## Keyboard first Every action must be reachable without a pointer. This is a baseline for accessibility, a productivity feature for power users, and a quality signal for everyone else. ### Standard shortcuts | Shortcut | Action | | --- | --- | | `Ctrl+N` | New | | `Ctrl+O` | Open | | `Ctrl+S` | Save | | `Ctrl+Shift+S` | Save as | | `Ctrl+W` | Close window | | `Ctrl+Q` | Quit application | | `Ctrl+Z` | Undo | | `Ctrl+Shift+Z` / `Ctrl+Y` | Redo | | `Ctrl+X` / `Ctrl+C` / `Ctrl+V` | Cut / copy / paste | | `Ctrl+A` | Select all | | `Ctrl+F` | Find / search | | `Ctrl+K` | Command palette (optional) | | `F1` | Help / shortcuts | | `Esc` | Cancel, close, leave fullscreen | | `F11` | Fullscreen | | `Ctrl++` / `Ctrl+-` / `Ctrl+0` | Zoom in / out / reset | Do not invent a different shortcut for a standard action. Do not override a system shortcut. If a shortcut conflicts with the desktop, let the user change it or choose a different one. ### Actions, not key handlers Bind shortcuts to `Gio.Action` objects, not raw key events. Actions can be discovered by menus, the shortcuts dialog, and accessibility tools. ```python action = Gio.SimpleAction.new("new-note", None) action.connect("activate", self.on_new_note) self.add_action(action) app.set_accels_for_action("win.new-note", ["n"]) ``` For app-wide actions, use `app.`; for window actions, use `win.`. Add the action to the window or application accordingly. ### Shortcuts dialog Expose the full shortcut list in a searchable dialog. libadwaita provides `Adw.ShortcutsDialog`; use it instead of a hand-written help page. ## Focus Focus is the keyboard user’s cursor. If it is invisible or unpredictable, the app is unusable without a mouse. - Show a clear focus ring. Do not remove it for aesthetics. - Keep a logical tab order that follows the visual layout. - Move focus to the first meaningful control when a view opens. - Restore focus to the trigger when a dialog closes. - Trap focus inside modal dialogs; `Esc` closes them. - Do not put focus on a destructive button by default. - Make custom widgets focusable and expose their state. ```python self.search_entry.grab_focus() ``` Test by tabbing through the entire app with the mouse untouched. Then test with a screen reader. ## Pointer and touch targets - Minimum target: 24×24 px (WCAG 2.2). Comfortable: 32×32 px or larger. - Provide hover and pressed states; the cursor should change over interactive elements. - Right-click opens a context menu for the object under the pointer. - Drag and drop should have a keyboard-accessible alternative. - Do not rely on hover for essential information; touch and keyboard users cannot hover. - Support two-finger scrolling and kinetic scrolling where appropriate. - Avoid swipe-only navigation on the desktop; provide buttons and shortcuts. ## Feedback Every action needs a response within 100 ms. If the work takes longer, show progress. | Duration | Feedback | | --- | --- | | < 100 ms | Immediate visual state change | | 100 ms–1 s | Inline spinner or subtle progress | | \> 1 s | Determinate progress, cancel button, and a description | | Background | Notification or a status indicator in the app | ### Toasts Use `Adw.Toast` for non-critical confirmations and undo affordances. ```python toast = Adw.Toast.new("Note deleted") toast.set_button_label("Undo") toast.connect("button-clicked", self.on_undo_delete) self.toast_overlay.add_toast(toast) ``` Toasts disappear automatically. Do not use them for errors that require action; use an inline banner or a dialog. ### Notifications Use notifications for events that happen while the user is not looking at the app. They should be actionable, rate-limited, and free of secrets. See [System Integration](/linux-desktop-guide/platform/integration/#notifications). ## Undo and destructive actions - Prefer undo over confirmation for reversible actions. - Confirm only destructive, irreversible actions (“Delete 12 notes permanently?”). - Use a clear verb on the destructive button (“Delete”, not “OK”). - Keep an undo stack for editing operations, with a visible history if the app is complex. - Autosave drafts and restore them after a crash. `Gtk.TextBuffer` has built-in undo for text editing. For app-level operations, implement a command stack with clear descriptions. ## Search Search is a first-class interaction, not a filter box bolted onto a list. - `Ctrl+F` focuses search; `Esc` clears or closes it. - Search incrementally, with a debounce of 100–200 ms. - Show a “no results” state with a way to clear the query. - Highlight matches in the results, not just in the detail view. - Preserve the query when navigating into a result and back. - For large datasets, search the model, not the rendered widgets. ```python self.search_entry.connect("search-changed", self.on_search_changed) def on_search_changed(self, entry): query = entry.get_text().strip().lower() self.filter_model.set_filter( Gtk.CustomFilter.new(lambda item: query in item.title.lower()) ) ``` `Gtk.FilterListModel` has a `filter` property, not a `set_filter_func()` method; `Gtk.CustomFilter` wraps the match function. If the app has searchable content that should appear in the desktop overview, add a GNOME Shell search provider. Do not make the shell wait on a slow network call. ## Command palette A command palette (`Ctrl+K`) is optional, but useful for apps with many actions. It should: - list actions, not just navigation; - show shortcuts next to actions; - search by synonyms and translated labels; - never be the only way to reach an action; - be fast and keyboard-only. ## Motion Motion should explain a change, not decorate it. - Use 150–250 ms for most transitions. - Animate transform and opacity, not layout. - Respect reduced motion. If the user disabled animations, do not animate; show the final state. - Do not animate on every keystroke or scroll event. - Avoid parallax, bouncing, and attention-seeking loops. ## Sound and haptics Desktop apps should be silent by default. Sound is appropriate for media apps, games, and explicit user-enabled alerts. Haptics are not a desktop primitive; do not depend on them. If you add sound: - Provide a setting to disable it. - Use it for meaningful events, not every click. - Never use sound as the only feedback. ## Errors An error message should answer three questions: 1. **What happened?** “Could not save the note.” 2. **Why?** “The disk is full.” 3. **What can I do?** “Free some space, then try again.” Rules: - Use plain language; put the technical detail behind “Details”. - Offer a retry action when retrying can work. - Never show a raw stack trace as the primary message. - Do not blame the user. - Log the technical detail with a correlation ID; do not include secrets. - For destructive or data-loss errors, preserve the user’s work and offer export. ## Loading - Show a skeleton for content that has a known shape; a spinner for unknown work. - Avoid layout shift when content arrives; reserve space. - Cancel work when the user navigates away. - Do not block the entire window for a small background task. - Show determinate progress when you can estimate it; indeterminate when you cannot. - Keep the UI responsive during long operations; the main loop is sacred. ## Interaction checklist - ☐ Every action is reachable by keyboard. - ☐ Standard shortcuts work; custom shortcuts are documented. - ☐ Focus is visible, logical, and restored after dialogs. - ☐ Target sizes are at least 24×24 px. - ☐ Hover is not required for essential information. - ☐ Actions respond within 100 ms; long work has progress and cancel. - ☐ Reversible actions use undo; destructive actions confirm. - ☐ Search is incremental and has a no-results state. - ☐ Errors explain what, why, and what to do next. - ☐ Reduced motion is respected. - ☐ Sound is optional and off by default. - ☐ Notifications are actionable and rate-limited. ## Sources - [GNOME HIG](https://developer.gnome.org/hig/) - [KDE HIG](https://develop.kde.org/hig/) - [Adw.ShortcutsDialog](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/class.ShortcutsDialog.html) - [Gtk.CustomFilter](https://docs.gtk.org/gtk4/class.CustomFilter.html) - [Gtk.FilterListModel](https://docs.gtk.org/gtk4/class.FilterListModel.html) Next: [Accessibility](/linux-desktop-guide/design/accessibility/) — the legal, ethical, and technical baseline for software that works for everyone. --- # Layout & Navigation URL: https://digitalesich.github.io/linux-desktop-guide/design/layout/ > Adaptive layouts, navigation patterns, dialogs, empty states, and settings pages for Linux desktop apps — with GNOME/libadwaita and KDE/Kirigami patterns. Layout is where “native” becomes visible. A Linux app can use the right widgets and still feel wrong if the navigation model fights the desktop, the window does not adapt, or the settings page looks like a web form. Start with the **content model**, not the widgets. What is the user looking at? Is it a list, a document, a canvas, a timeline, or a set of independent tools? The answer determines the navigation pattern. ## Pick the pattern | Pattern | Use when | GNOME/libadwaita | KDE/Kirigami | | --- | --- | --- | --- | | Single view | One task, one document | `Adw.ApplicationWindow` + content | `Kirigami.Page` | | Master–detail | A list and a selected item | `Adw.NavigationSplitView` | `Kirigami.PageRow` | | Collapsible sidebar | Many sections, content-first | `Adw.OverlaySplitView` + `Adw.Sidebar` | `Kirigami.GlobalDrawer` | | View switcher | 2–5 peer views | `Adw.ViewStack` + `Adw.ViewSwitcher` | `Kirigami.PageRow` / tabs | | Tabs | Multiple open documents | `Adw.TabView` + `Adw.TabBar` | `QTabWidget` / `Kirigami.PageRow` | | Focused task | Modal editing or confirmation | `Adw.Dialog` / `Adw.AlertDialog` | `Kirigami.Dialog` / `Kirigami.PromptDialog` | | Settings | Preferences | `Adw.PreferencesPage` / `Adw.PreferencesDialog` | `KConfigDialog` / `Kirigami.ScrollablePage` | ### Master–detail The canonical desktop layout: a list on the left, content on the right. On narrow windows, the list becomes the first page and the content slides in. ```text Wide: [ sidebar ][ detail ] Narrow: [ list ] → [ detail ] → back ``` Use `Adw.NavigationSplitView` when the user navigates _into_ an item and expects a back button. Use `Adw.OverlaySplitView` when the sidebar is a persistent navigation surface that can collapse into an overlay. ### Sidebars and view switchers - A **sidebar** is for navigation between sections that persist. Use `Adw.Sidebar` (libadwaita 1.9) or a `Gtk.ListBox` with the `navigation-sidebar` style class. - A **view switcher** is for 2–5 peer views inside the same context. Use `Adw.ViewSwitcher` in the headerbar, not a row of custom tabs. - A **tab view** is for multiple open documents. Use `Adw.TabView`; do not reimplement tabs with buttons. If the sidebar contains more than ~10 items without grouping, the information architecture is probably wrong. Group with sections and labels. ## Make it adaptive Test at three widths: | Width | Typical layout | | --- | --- | | < 600 px | Single pane; navigation pushes a new page | | 600–900 px | Collapsed sidebar or two panes with narrow content | | \> 900 px | Sidebar + content, or three panes | Use `Adw.Breakpoint` to change layout properties at a width threshold. In Blueprint: ```text Adw.ApplicationWindow { content: Adw.ToolbarView { [top] Adw.HeaderBar {} content: Adw.OverlaySplitView split { sidebar: Gtk.ScrolledWindow { child: Gtk.ListBox { ... }; }; content: Gtk.Stack { ... }; }; }; Adw.Breakpoint { condition ("max-width: 600sp") setters { split.collapsed: true; } } } ``` In Python: ```python breakpoint = Adw.Breakpoint.new( Adw.BreakpointCondition.parse("max-width: 600sp") ) breakpoint.add_setter(split, "collapsed", True) self.add_breakpoint(breakpoint) ``` Do not use breakpoints for everything. Prefer widgets that provide the adaptive primitives (`Adw.OverlaySplitView`, `Adw.NavigationSplitView`, `Adw.ViewStack`), and pair them with a breakpoint when they need to collapse or change structure. ## Window size and state Wayland does not let apps position windows. Save the size, maximized state, and fullscreen state; let the compositor decide where the window goes. ```python self.settings = Gio.Settings.new("io.github.yourname.Notes") # Restore the initial size. self.set_default_size(self.settings.get_int("window-width"), self.settings.get_int("window-height")) # Save the actual size when the window closes. def on_close_request(self, _window): self.settings.set_int("window-width", self.get_width()) self.settings.set_int("window-height", self.get_height()) return False ``` `default-width`/`default-height` bindings only set the initial size; they do not track a user resize. Provide a sensible default size for the first launch, and a minimum size that still works at 200% scaling. Do not force a size that only fits your monitor. ## Empty, loading, and error states Every view that can be empty needs three states: | State | What to show | | --- | --- | | Empty | What this view is for, why it is empty, and one primary action | | Loading | A skeleton or spinner that does not shift the layout; cancel if long | | Error | What failed, why, and what the user can do; a retry action | Use `Adw.StatusPage` for empty and error states. It gives you the right spacing, icon size, and typography. ```python page = Adw.StatusPage() page.set_icon_name("document-edit-symbolic") page.set_title("No notes yet") page.set_description("Create your first note to get started.") button = Gtk.Button(label="New note") button.add_css_class("suggested-action") page.set_child(button) ``` Do not use a blank white window with a tiny “No data” label. The empty state is the first impression of the feature. ## Settings Settings are part of the UI, not a config file with a window around it. - Group by task: “Appearance”, “Editing”, “Sync”, “Privacy”. - Use `Adw.PreferencesPage` with `Adw.PreferencesGroup` and rows (`Adw.SwitchRow`, `Adw.EntryRow`, `Adw.SpinRow`, `Adw.ComboRow`). - Apply changes immediately; no “Save” button. - Provide search if there are more than ~15 settings. - Use a separate dialog for window-specific settings; use the app’s preferences for global settings. - Provide “Reset to defaults” and a way to export/import settings. On KDE, use `KConfigDialog` or a Kirigami settings page with the same grouping principles. ## Dialogs Use a dialog when the user must finish a focused task before returning, or when a decision is destructive and irreversible. - `Adw.AlertDialog` for confirmations and simple choices. - `Adw.Dialog` for a small form or a focused task. - `Adw.PreferencesDialog` for settings that belong to a window. - Avoid dialogs that merely display information; use inline UI. - Do not stack dialogs. If a dialog opens another dialog, reconsider the flow. - Destructive actions use a clear verb (“Delete note”), not “OK”. - The escape route is always available (`Esc`), and the primary action is reachable with `Enter`. ## Onboarding Keep it to one screen or none. The app should be usable immediately; onboarding explains the _why_, not the _how_. - Show the core value in one sentence. - Offer one primary action (“Create your first note”). - Do not require an account, a tour, or a permission before the user sees value. - Explain permissions in context, when the feature is used. - Make onboarding dismissible and re-openable from Help/About. ## Menus and discoverability - Put primary actions in the headerbar as buttons with labels or well-known icons. - Put secondary actions in the primary menu (the hamburger or gear), grouped with separators. - Show shortcuts in menu items. - Use `Gtk.PopoverMenu` / `Adw.HeaderBar` conventions; do not build a custom dropdown. - Do not hide the only way to do something in a context menu. Context menus are accelerators, not the primary interface. ## Kirigami notes If you are building for KDE: - Use `Kirigami.ApplicationWindow` and `Kirigami.Page`. - `Kirigami.PageRow` gives you adaptive master–detail with a back button on narrow screens. - `Kirigami.OverlaySheet` is the mobile-friendly alternative to a dialog; use it for quick actions, not for long forms. - Use `Kirigami.Units.gridUnit` for spacing instead of hardcoded pixels. - Use `Kirigami.Theme` colors; respect the system color scheme. - Use Breeze icons and `Kirigami.Icon` for consistency. ## Layout checklist - ☐ The navigation model matches the content: list/detail, sections, documents, or a single task. - ☐ The app works at 360 px, 720 px, and 1280 px widths. - ☐ The sidebar collapses or becomes a navigation page on narrow windows. - ☐ Window size and maximized state persist; position does not. - ☐ Empty, loading, and error states exist for every dynamic view. - ☐ Settings use `Adw.PreferencesPage` / `KConfigDialog`, grouped by task. - ☐ Dialogs are focused, dismissible, and never stacked. - ☐ Onboarding is one screen and skippable. - ☐ Primary actions are visible; secondary actions are in a menu with shortcuts. - ☐ The layout works at 200% text and display scaling. ## Sources - [GNOME HIG](https://developer.gnome.org/hig/) - [Adw.OverlaySplitView](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/class.OverlaySplitView.html) - [Adw.NavigationSplitView](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/class.NavigationSplitView.html) - [Adw.Breakpoint](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/class.Breakpoint.html) - [Gtk.Window default-width](https://docs.gtk.org/gtk4/property.Window.default-width.html) Next: [Interaction & Feedback](/linux-desktop-guide/design/interaction/) — keyboard, focus, undo, search, and the small behaviors that make an app feel alive. --- # Localization URL: https://digitalesich.github.io/linux-desktop-guide/design/localization/ > Internationalization and localization for Linux desktop apps: gettext, Qt Linguist, plurals, dates, RTL layouts, fonts, metadata translation, and a workflow that keeps… An app that only works in English is an app that excludes most of the world. Localization is not a late-stage translation pass; it is a set of decisions about strings, layout, formatting, and fonts that you make while writing the first screen. The Linux ecosystem has strong translation infrastructure: [gettext](https://www.gnu.org/software/gettext/manual/), Weblate, GNOME’s Damned Lies, KDE’s Lokalize, and distribution translation teams. Use it instead of inventing a JSON translation format. ## Internationalization vs localization - **Internationalization (i18n):** making the code capable of supporting multiple languages and locales. No hardcoded strings, no concatenation, no fixed date formats. - **Localization (l10n):** providing the actual translations, formats, and cultural adaptations for a locale. You do i18n once; translators do l10n continuously. ## Strings ### Mark every user-facing string **C / GLib:** ```c #include label = gtk_label_new(_("Open note")); ``` **Python:** ```python import gettext gettext.bindtextdomain("notes", "/usr/share/locale") gettext.textdomain("notes") _ = gettext.gettext ngettext = gettext.ngettext label.set_text(_("Open note")) ``` Inside a Flatpak, install `.mo` files under `$FLATPAK_DEST/share/locale/` and keep the same domain name. **Qt / C++:** ```cpp auto *label = new QLabel(tr("Open note")); ``` **QML:** ```qml Label { text: qsTr("Open note") } ``` **Rust:** Use `gettext-rs` with the same `.po` workflow, or Fluent for message-heavy apps. Do not hardcode English strings in Rust macros that bypass extraction. ### Do not concatenate Translators need the full sentence, not fragments. ```python # Bad: word order differs across languages message = _("Deleted") + " " + str(count) + " " + _("notes") # Good: one translatable string with a placeholder message = ngettext("Deleted %d note", "Deleted %d notes", count) % count ``` ### Use placeholders, not string interpolation ```python # Good label = _("Welcome back, {name}").format(name=user.name) ``` Keep placeholders named and documented. Do not change them without updating translators. ### Plurals Use the platform’s plural API. Plural rules differ across languages; a simple `count == 1` check is wrong for many locales. ```python ngettext("%d note", "%d notes", count) ``` ```c ngettext("%d note", "%d notes", count) ``` Qt: `tr("%n note(s)", "", count)`. QML: `qsTr("%n note(s)", "", count)`. ### Context Words like “Open” (verb) and “Open” (adjective), or “File” (noun) and “File” (verb), need context. Use `pgettext()` / `QT_TRANSLATE_NOOP` with a context string. ```c _("Open") // ambiguous pgettext("verb", "Open") // clear to the translator ``` ### Translator comments Add comments for ambiguous strings, placeholders, and UI constraints. ```c // TRANSLATORS: Title of the dialog that appears when a note is deleted. _("Delete note?") ``` ## Build integration ### Meson + gettext ```python subdir('po') ``` ```python i18n = import('i18n') i18n.gettext('notes', preset: 'glib') ``` `i18n.gettext()` resolves `POTFILES.in` and `LINGUAS` relative to the directory that calls it, so keep the call in `po/meson.build` or move the files to the root. ```text src/main.py ``` ```text de es fr pt_BR ``` ```bash # Extract strings into a POT template xgettext --from-code=UTF-8 --add-comments=TRANSLATORS -o po/notes.pot src/*.py # Merge the template into existing translations msgmerge --update po/de.po po/notes.pot # Compile for testing msgfmt po/de.po -o po/de.mo ``` Install `.mo` files to `$prefix/share/locale//LC_MESSAGES/notes.mo`. ### Qt Use `lupdate` to extract `tr()` strings into `.ts` files, translate them in [Qt Linguist](https://doc.qt.io/qt-6/qtlinguist-index.html), and `lrelease` them into `.qm` files. With CMake, `qt_add_translations()` wires the build and install steps. Load the `.qm` with `QTranslator` before creating the UI, or install them under `$prefix/share//translations/`. ## Dates, numbers, and currency Never format dates and numbers by hand. - **GLib:** `GDateTime` with locale-aware format strings; use `g_date_time_format()` for display, ISO 8601 for storage. - **Qt:** `QLocale` for dates, numbers, currency, and measurement units. - **Python:** `datetime` + `locale` or `babel`; prefer `babel` for explicit locale control. - **Rust:** ICU4X for full CLDR data; `chrono`/`time` alone do not provide complete locale-aware formatting. Store timestamps in UTC (or with an explicit offset) and convert for display. Do not assume the first day of the week is Sunday or Monday; read the locale. ## Layout and RTL Right-to-left languages (Arabic, Hebrew, Persian, Urdu) require more than mirrored text. - Use start/end layout APIs (`set_margin_start`, `set_margin_end`) instead of left/right. - Let the toolkit mirror the layout automatically. - Mirror directional icons (arrows, back/forward) but not universal symbols (play, check, warning). - Test with a generated RTL locale (for example `LANG=ar_EG.UTF-8`) and a full RTL desktop session; `locale -a` shows what the machine actually has. - Avoid absolute positioning and hardcoded x-coordinates. - Check that progress bars, sliders, and timelines make sense in RTL. GTK and Qt handle most mirroring when the locale is RTL. Your job is to avoid assumptions that defeat it. ## Fonts and text rendering - Use the system UI font; it has fallbacks for the user’s language. - Do not hardcode a font family that lacks CJK, Arabic, or Devanagari glyphs. - Test with long German compounds, Japanese, Arabic, and Hindi. - Do not use fixed-width labels for translated text. - Allow text to wrap; avoid truncating essential information. - Check line height for scripts with tall glyphs and diacritics. Ubuntu ships Noto fonts for broad coverage. If you bundle a font for branding, pair it with the system fallback rather than replacing the UI font entirely. ## Input methods and bidirectional text Wayland routes text input through the `text-input` and `input-method` protocols. Do not hardcode a single input-method framework: - Use the toolkit’s text widgets and let the compositor’s input method handle Compose, dead keys, and CJK input. - On Wayland, do not set `GTK_IM_MODULE` or `QT_IM_MODULE`; the compositor and portal select the input method. On X11, the user’s session decides. - Test with IBus and Fcitx5, at least one CJK input method, and Compose sequences. - For user-supplied bidirectional text, wrap the embedded value in Unicode isolate characters (U+2068 FSI and U+2069 PDI) or let the toolkit’s bidi-aware widget handle it. - Do not assume the UI direction equals the direction of user content. ## Metadata and desktop entry Translate user-visible metadata, not just the UI. ```ini Name=Notes Name[de]=Notizen Comment=Write and organize notes Comment[de]=Notizen schreiben und organisieren Keywords=notes;writing; Keywords[de]=Notizen;Schreiben; ``` ```xml Notes Notizen Write and organize notes Notizen schreiben und organisieren ``` AppStream supports localized names, summaries, descriptions, and screenshots. Software centers show the user’s language when available. ## Translation workflow 1. **Extract** strings into a `.pot` template on every change. 2. **Translate** in a platform that supports context, plurals, and comments (Weblate, Damned Lies, Lokalize, Crowdin). 3. **Review** by a native speaker, especially for UI-critical strings. 4. **Test** with the translated build, not just the `.po` file. 5. **Freeze** strings before a release; give translators a deadline. 6. **Credit** translators in the about dialog or release notes. Do not machine-translate the UI and ship it unreviewed. Flathub explicitly rejects low-quality machine translation, and users notice. ## Testing localization - Run with `LANG=de_DE.UTF-8`, `LANG=ar_EG.UTF-8`, `LANG=ja_JP.UTF-8`. - Use pseudo-localization (replace letters with accented variants and expand strings by 30%) to catch truncation. - Test plural forms with 0, 1, 2, 5, 11, 21, and 100. - Test date, time, number, and currency formats. - Test RTL layout and mirrored icons. - Test with a larger system font and 200% scaling. - Check the desktop entry and software-center listing in the translated locale. ## Localization checklist - ☐ No user-facing string is hardcoded outside the translation system. - ☐ No string concatenation for sentences. - ☐ Plurals use `ngettext` / `tr("%n")`. - ☐ Translators have context and comments for ambiguous strings. - ☐ Dates, numbers, and currency use locale APIs. - ☐ Layout uses start/end, not left/right. - ☐ Icons are mirrored where appropriate. - ☐ The UI works with long translations and 200% text scaling. - ☐ Desktop entry and AppStream metadata are localized. - ☐ English is complete; additional languages are reviewed. - ☐ The app runs in an RTL locale. - ☐ Translators are credited. ## Sources - [GNU gettext](https://www.gnu.org/software/gettext/manual/) - [Meson i18n module](https://mesonbuild.com/i18n-module.html) - [Qt Linguist](https://doc.qt.io/qt-6/qtlinguist-index.html) - [ICU4X](https://icu4x.unicode.org/) - [Flathub localisation policy](https://docs.flathub.org/docs/for-app-authors/requirements#localisation-policy) - [AppStream metadata](https://www.freedesktop.org/software/appstream/docs/chap-Metadata.html) Next: [Architecture](/linux-desktop-guide/engineering/architecture/) — how to structure the code so the UI stays fast and the app stays maintainable. --- # Visual Language URL: https://digitalesich.github.io/linux-desktop-guide/design/visual-language/ > Typography, color, spacing, icons, shape, and motion for Linux desktop apps that adapt to GNOME, KDE, and the user's accessibility settings. A Linux app does not need a unique visual language to be distinctive. It needs a **coherent** one that sits inside the host’s language. The fastest route to “native” is to use the platform’s type scale, spacing, icons, and semantic colors, then spend your design budget on the parts the platform does not provide: your content, your empty states, your icon. This page uses GTK 4 + libadwaita as the reference because it is the Ubuntu default. The principles translate to Qt/Kirigami with `Kirigami.Units`, `Kirigami.Theme`, and Breeze icons. ## Typography Use the system UI font. Do not ship a custom font for body text unless the product is a reading or writing app where typography is the feature. | Role | libadwaita class | Typical use | | --- | --- | --- | | Page title | `.title-1` | Main window or empty-state headline | | Section title | `.title-2` | Dialog titles, large content headers | | Subsection | `.title-3` / `.title-4` | Card headings, grouped content | | Heading | `.heading` | Settings group headings | | Body | `.body` | Default text | | Caption | `.caption` | Metadata, timestamps, helper text | | Monospace | `.monospace` | Code, IDs, logs | | Dimmed | `.dim-label` | Secondary information | Rules: - Keep body text at the system size. If you need emphasis, use weight or the heading classes, not a larger font. - Line length: 45–75 characters for prose; wider for tables and code. - Line height: the toolkit default is tuned for the font; do not compress it. - Use `Gtk.Label` with `wrap=True` and `xalign=0` for multi-line text; avoid fixed heights. - Do not use Pango markup for layout. Use it for inline emphasis only, and keep the plain text accessible. - Test at 200% text scaling and with long translations (German is a good stress test). Don’t hardcode font sizes A user who sets a larger system font expects every app to follow. If you must use a custom size, express it relative to the system font and test the result at 100%, 150%, and 200%. ## Color Use semantic colors from the toolkit, not hex values. | Need | libadwaita token | Notes | | --- | --- | --- | | Window background | `@window_bg_color` | Adapts to light/dark | | Window text | `@window_fg_color` | Meets contrast in both schemes | | Card background | `@card_bg_color` | Layered surfaces | | Accent background | `@accent_bg_color` | Buttons, selection | | Accent text on accent | `@accent_fg_color` | Do not guess | | Accent as text | `@accent_color` | Links, highlights | | Destructive | `@destructive_bg_color` / `@destructive_color` | Use sparingly | | Success/warning | `@success_color` / `@warning_color` | Semantic, not decorative | The system accent color is a user preference. Respect it. A brand accent can be used inside illustrations or the app icon; it should not repaint every button. Contrast: - Body text: at least 4.5:1 against its background. - Large text and UI boundaries: at least 3:1. - Never use color as the only signal. Pair it with an icon, label, or pattern. - Test in light, dark, and high-contrast modes. - Do not place text on a photographic background without a scrim. ## Spacing and layout Use a 6 px base grid: 6, 12, 18, 24, 36, 48. This matches Adwaita’s rhythm and keeps layouts calm. | Space | Use | | --- | --- | | 6 px | Icon-to-label, tight inline gaps | | 12 px | Related controls, list row padding | | 18 px | Between groups in a card | | 24 px | Page margins, section spacing | | 36–48 px | Major section breaks, empty states | Prefer the toolkit’s spacing constants: ```python box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12) box.set_margin_top(24) box.set_margin_bottom(24) box.set_margin_start(24) box.set_margin_end(24) ``` Avoid fixed widths for text containers. Let labels wrap, let lists scroll, and let the window resize. ## Icons - Use **symbolic** icons for UI controls. They inherit the text color and work in light, dark, and high-contrast themes. - Use full-color icons for app icons and illustrative content. - Install app icons into `hicolor` at multiple sizes; provide an SVG. - Keep one icon style. Mixing GNOME symbolic icons with colorful KDE icons looks accidental. - Icon-only buttons need a tooltip and an accessible label. - Do not use an icon to replace a label when the meaning is ambiguous. ```python button = Gtk.Button(icon_name="list-add-symbolic") button.set_tooltip_text("New note") button.update_property([Gtk.AccessibleProperty.LABEL], ["New note"]) ``` ## Shape and elevation Adwaita uses modest corner radii: 6 px for controls, 12 px for cards and dialogs, and fully rounded pills for status badges. Pick a small set and use it consistently. - Use borders and background layering before shadows. - Shadows should suggest elevation, not decoration. A popover can have a shadow; a list row should not. - Avoid glassmorphism, neon glows, and heavy gradients. They date quickly and often fail contrast. - In dark mode, use a subtle border instead of a bright shadow. ## Motion Motion explains cause and effect. It should be fast enough to feel responsive and subtle enough to ignore. | Interaction | Duration | Easing | | --- | --- | --- | | Button press | 100–150 ms | ease-out | | View transition | 200–250 ms | ease-in-out | | Dialog enter | 200 ms | ease-out | | Toast | 150 ms in, 250 ms out | ease-out | Rules: - Animate transform and opacity, not layout properties. - Do not animate more than one thing at a time unless they are part of the same transition. - Respect reduced motion. libadwaita widgets handle the system setting; GTK 4.22 core widgets and custom animations must be checked explicitly with `Adw.get_enable_animations(widget)`. - Never use motion to hide latency. Fix the latency or show progress. ```python from gi.repository import Adw if Adw.get_enable_animations(widget): widget.add_css_class("pulse") else: widget.remove_css_class("pulse") ``` ```css .pulse { transition: opacity 180ms ease-out; } ``` GTK CSS does not support the web `prefers-reduced-motion` media query; use the libadwaita API or `Gtk.Settings` to decide whether to add the class. ## Imagery and empty states - Use illustrations to explain a state, not to fill space. - Provide light and dark variants, or use symbolic imagery that adapts. - Empty states should say what the view is for, why it is empty, and what to do next. - Screenshots in AppStream metadata should show the real app, in both light and dark mode, without fake window chrome. ## Brand expression | Surface | Guidance | | --- | --- | | App icon | The strongest brand surface. Make it legible at 16 px. | | Accent | Use the system accent for controls; use your brand color in illustrations and the icon. | | Empty states | A short, human sentence and a clear action. | | Onboarding | One screen, not a carousel. Explain the value and the first step. | | About dialog | Name, version, developers, license, website. No marketing wall. | | Window chrome | Do not brand it. Use the host’s headerbar and controls. | ## GTK CSS: use it sparingly GTK CSS is for small adjustments, not for rebuilding a theme. Prefer libadwaita’s style classes and widget properties. ```css .app-badge { background-color: alpha(@accent_bg_color, 0.15); color: @accent_color; border-radius: 6px; padding: 2px 8px; font-size: 0.85em; font-weight: 600; } ``` ```python badge = Gtk.Label(label="Beta") badge.add_css_class("app-badge") ``` Avoid: - Targeting internal widget nodes (`.button > box > label`) — they change. - Overriding `background` or `color` on broad selectors (`window`, `button`, `entry`). - Shipping a full theme. It will break on the next libadwaita release. - Using `!important` to win a specificity fight. ## Visual checklist - ☐ System font, system text size, no fixed font sizes for body text. - ☐ Semantic colors, no hardcoded hex values in widgets. - ☐ System accent respected; brand color confined to icon/illustrations. - ☐ Light, dark, and high-contrast modes tested. - ☐ Spacing on a 6 px grid; consistent radii. - ☐ Symbolic icons for controls; one icon style. - ☐ Icon-only buttons have tooltips and accessible labels. - ☐ Reduced motion respected. - ☐ Layout works at 200% text scaling and 200% display scaling. - ☐ Empty states explain the next step. - ☐ Screenshots show the real app in both color schemes. ## Sources - [GNOME HIG](https://developer.gnome.org/hig/) - [libadwaita CSS variables](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/css-variables.html) - [libadwaita style classes](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/style-classes.html) - [GTK CSS overview](https://docs.gtk.org/gtk4/css-overview.html) - [Adw.get\_enable\_animations](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/func.get_enable_animations.html) Next: [Layout & Navigation](/linux-desktop-guide/design/layout/) — choosing the right adaptive structure for your app. --- # Architecture URL: https://digitalesich.github.io/linux-desktop-guide/engineering/architecture/ > A maintainable structure for Linux desktop apps: the main loop, state, async work, storage, D-Bus, and testing seams that keep the UI fast and the codebase understandable. Desktop apps fail in predictable ways: the main loop blocks, state is duplicated across widgets, storage is coupled to the UI, and the whole thing becomes untestable. The toolkit is not the problem. The structure is. This page describes an architecture that works for GTK, Qt, and Rust apps of any size. It is deliberately boring: separate the UI from the application logic, keep one source of truth, and make the boundaries testable. ## The main loop is sacred Every desktop toolkit has an event loop on the main thread. GTK, Qt, and the GLib main context all share the same rule: **do not block it**. Blocking the main loop means no redraws, no input, no animations, and an unresponsive window. A synchronous network call, a large file read, a database query, or a `sleep` on the main thread is a bug even if it “usually finishes quickly”. ```text main thread: input → state change → layout → paint → input … worker thread: I/O, parsing, network, compression main thread: receive result → update state → repaint ``` Rules: - UI objects are touched only on the main thread. - Long work runs on a worker, a thread pool, or an async runtime bridged to the main loop. - Results are delivered back to the main thread. - Every long operation is cancellable. - Progress is reported, not guessed. ### GTK / GLib - Use `Gio` async APIs (`load_contents_async`, `read_async`, `GTask`). - Use `GLib.idle_add` / `GLib.MainContext.invoke_full` to return to the main thread. - Use `GCancellable` for cancellation. - Never call `time.sleep()` or a blocking socket on the main thread. ### Qt - Use `QtConcurrent`, `QThreadPool`, or `QNetworkAccessManager` (async by design). - Use signals/slots to deliver results to the GUI thread. - Never touch `QWidget` from a worker thread. - Use `QFutureWatcher` to observe async work. ### Rust - `gtk-rs` objects are not `Send`; keep them on the main thread. - Use `glib::MainContext::spawn_local` for UI-bound async work. - Bridge Tokio with `glib` carefully; do not run a Tokio runtime on the GTK main thread. - Use channels to send results back to the UI. ## Layers A practical structure: ```text src/ ├── main.py / main.rs / main.cpp # entry point, application lifecycle ├── application/ # use cases, commands, state │ ├── app.py │ └── state.py ├── domain/ # pure models and rules │ ├── note.py │ └── note_repository.py # interface ├── infrastructure/ # files, SQLite, network, D-Bus │ ├── sqlite_repository.py │ └── sync_client.py └── ui/ # widgets, windows, view models ├── window.py └── preferences.py ``` The dependency rule: **domain does not import UI or infrastructure**. The UI depends on application/domain interfaces; infrastructure implements them. This keeps the core testable without a display server. For a small app, you do not need four packages. You do need the _seams_: a repository interface, a clock, a network client, and a settings object that can be faked in tests. ## State Have one source of truth. Widgets render state; they do not own it. - Keep a model (dataclass, GObject, struct) for the app’s data. - Derive UI from the model; do not copy fields into labels and forget to update them. - Use observable properties or a small store to notify views. - Keep transient UI state (selection, focus, scroll) separate from domain state. - Make invalid states unrepresentable where practical. ### GTK - GObject properties + `GBinding` for simple cases. - `Gio.ListStore` + `Gtk.ListView` for collections. - `Gtk.FilterListModel` / `Gtk.SortListModel` for derived views. - A small view-model object for complex screens. ### Qt - `QAbstractItemModel` for lists and tables. - `Q_PROPERTY` + signals for observable state. - QML bindings for simple derivation; avoid JavaScript logic in bindings. ### Rust - Relm4’s message/update model, or a plain struct with explicit `update` methods. - Avoid `Rc>` webs; prefer ownership plus messages. - Use `Arc>` only across threads, not as a default. ## Async and cancellation Every operation that can take longer than a frame should be cancellable. ```python self.cancellable = Gio.Cancellable() def load(self): file = Gio.File.new_for_path(self.path) file.load_contents_async(self.cancellable, self.on_loaded) def on_loaded(self, file, result): try: ok, contents, _etag = file.load_contents_finish(result) except GLib.Error as error: if error.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED): return self.show_error(error) return self.apply(contents) def cancel(self): self.cancellable.cancel() ``` Cancel when the view closes, the user navigates away, or a newer request supersedes the old one. Do not let stale results overwrite fresh state. ## Storage | Data | Store | Why | | --- | --- | --- | | Preferences | GSettings | Desktop integration, schema validation | | Small app state | Keyfile/JSON | Human-readable, simple | | Structured/user data | SQLite | Queries, migrations, concurrency | | Large blobs | Files in XDG data dir | Streaming, backup | | Secrets | Secret Service | Encrypted, user-controlled | | Logs | journald or XDG state | Structured, rotated | Rules: - Write atomically. - Version the schema and migrate forward. - Use transactions for multi-step writes. - Handle disk full, permission denied, and corrupt data. - Never store secrets in the same database as ordinary data without encryption. - Back up before a destructive migration. ## Single instance and IPC `Gtk.Application` handles single-instance behavior and file-open activation through D-Bus. `QApplication` does not register a bus name; use `KDBusService(KDBusService::Unique)` for KDE/Qt, or `QLockFile`/a custom D-Bus name for non-KDE Qt. If the app needs a custom service: - Own a well-known D-Bus name. - Export a small, versioned interface. - Validate all inputs. - Return errors, not crashes. - Document the interface for other apps. Do not use a local socket if D-Bus already solves the problem. Do not use the session bus for high-frequency data; use a direct pipe or shared memory. ## Error boundaries Errors should be handled at the layer that can do something about them: - **Infrastructure** returns typed errors (`Result`, exceptions, `GError`). - **Application** decides whether to retry, fall back, or surface. - **UI** presents a human message and a next step. - **Logging** records the technical detail with context. Never swallow an error silently. Never show a raw exception to the user. Never use a crash as control flow. ## Trust boundaries and observability - Treat files, images, D-Bus messages, portal responses, and command-line arguments as untrusted input. - Decode untrusted images with Glycin; reserve `GtkSvg` for trusted app resources. - Log through structured journald fields or a correlation ID so a user report can be tied to a specific operation. - Keep secrets out of logs, crash reports, and command-line arguments. ## Testing seams Design for tests before you need them: - Inject the clock, filesystem root, network client, and settings. - Keep domain logic pure. - Make the UI constructible with a fake application object. - Separate “what to do” from “how to draw it”. - Provide a headless mode for integration tests. If a class is hard to test, it usually has too many responsibilities or talks to the world directly. ## A small example ```python from dataclasses import dataclass @dataclass(frozen=True) class Note: id: str title: str body: str # domain/note_repository.py from typing import Protocol class NoteRepository(Protocol): def list(self) -> list[Note]: ... def save(self, note: Note) -> None: ... def delete(self, note_id: str) -> None: ... # application/notes.py class NotesService: def __init__(self, repository: NoteRepository): self.repository = repository def create(self, title: str, body: str) -> Note: note = Note(id=new_id(), title=title.strip() or "Untitled", body=body) self.repository.save(note) return note ``` The UI calls `NotesService`; tests use an in-memory `NoteRepository`. The database is an implementation detail. ## Architectural anti-patterns | Anti-pattern | Cost | | --- | --- | | Business logic in signal handlers | Untestable, duplicated, hard to reason about | | Widgets as the model | State drifts; refresh bugs | | Global mutable state | Hidden coupling; test interference | | Blocking the main loop | Frozen UI, watchdog kills, bad reviews | | One giant window class | 3,000 lines, merge conflicts, no reuse | | Premature abstraction | Layers with no purpose, more code to maintain | | Custom IPC when D-Bus exists | Reinvented discovery, auth, and lifecycle | | Silent exception handling | Data loss with no diagnosis | ## Sources - [GApplication](https://docs.gtk.org/gio/class.Application.html) - [KDBusService](https://api.kde.org/kdbusservice.html) - [systemd user units](https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html) - [GLib main loop](https://docs.gtk.org/glib/main-loop.html) Next: [GTK & libadwaita](/linux-desktop-guide/engineering/gtk/) — the deep dive into the Ubuntu-native toolkit. --- # GTK & libadwaita URL: https://digitalesich.github.io/linux-desktop-guide/engineering/gtk/ > A practical deep dive into GTK 4.22 and libadwaita 1.9: project structure, Blueprint UI, adaptive widgets, actions, models, settings, async work, testing, accessibility, and… GTK 4 is the toolkit behind GNOME and the default native stack on Ubuntu 26.04 LTS. libadwaita adds the GNOME design system: headerbars, adaptive split views, view switchers, preferences pages, dialogs, toasts, and the Adwaita style. The combination is opinionated, and that is the point. If you are building for GNOME/Ubuntu, it gives you a direct path to native behavior: accessibility, input methods, Wayland, portals, and a coherent visual language. Versions in Ubuntu 26.04 GTK 4.22, libadwaita 1.9, GNOME 50. GTK 4 is the stable line; GTK 3 is maintenance-only. Target GTK 4.22 on Ubuntu 26.04. Use `GTK_CHECK_VERSION` in C or the binding’s feature flags when you also support older releases; API availability is a compile-time property, not a runtime guard. ## Project skeleton The [Ubuntu + GNOME target](/linux-desktop-guide/targets/ubuntu-gnome/) is the reference environment for this chapter. The [first-app tutorial](/linux-desktop-guide/start/first-app/) builds the complete GTK 4 + libadwaita project, and `examples/notes` in the guide repository is the runnable version. The structure is deliberately small: ```text notes/ ├── data/ │ ├── io.github.yourname.Notes.desktop │ ├── io.github.yourname.Notes.metainfo.xml │ └── io.github.yourname.Notes.svg ├── src/ │ └── main.py ├── io.github.yourname.Notes.yml └── meson.build ``` Keep the application object, window, and data layer separate before the file grows. A useful split for a larger app is `src/main.py` (application and actions), `src/window.py` (widgets and bindings), and `src/model.py` (data and pure functions). The Blueprint section below adds `src/window.blp` and a GResource when you want declarative UI. ## The application skeleton Every GTK app starts with an application object and a window. Use `Adw.Application`, not a bare `Gtk.Application`, so libadwaita’s style and helpers are initialized. ```python import gi gi.require_version("Gtk", "4.0") gi.require_version("Adw", "1") from gi.repository import Adw, Gio, GLib, Gtk class NotesApp(Adw.Application): def __init__(self): super().__init__( application_id="io.github.yourname.Notes", flags=Gio.ApplicationFlags.DEFAULT_FLAGS, ) self.connect("activate", self.on_activate) about_action = Gio.SimpleAction.new("about", None) about_action.connect("activate", self.on_about) self.add_action(about_action) def on_activate(self, _app): window = NotesWindow(application=self) window.present() def on_about(self, _action, _param): about = Adw.AboutDialog( application_name="Notes", application_icon="io.github.yourname.Notes", version="0.1.0", ) about.present(self.active_window) if __name__ == "__main__": NotesApp().run(None) ``` `Gtk.Application` handles single-instance behavior, D-Bus activation, and the main loop. Do not call `Gtk.init()` manually or create a second `Gtk.Application`. ## Blueprint: declarative UI Blueprint is a readable UI description language that compiles to GTK `.ui` files. It is easier to review than XML and avoids building large widget trees in code. ```text using Gtk 4.0; using Adw 1; menu main_menu { section { item { label: _("About Notes"); action: "app.about"; } } } template $NotesWindow: Adw.ApplicationWindow { title: _("Notes"); default-width: 980; default-height: 680; content: Adw.ToolbarView { [top] Adw.HeaderBar { [start] Gtk.Button { icon-name: "list-add-symbolic"; tooltip-text: _("New note"); action-name: "win.new-note"; styles ["suggested-action"] } [end] Gtk.MenuButton { icon-name: "open-menu-symbolic"; menu-model: main_menu; } } content: Adw.OverlaySplitView split { sidebar: Gtk.ScrolledWindow { child: Gtk.ListBox note_list { styles ["navigation-sidebar"] }; }; content: Gtk.Label content_label { label: _("Select a note"); styles ["dim-label"] }; }; }; } ``` Compile it: ```bash blueprint-compiler compile src/window.blp --output src/window.ui ``` The Blueprint snippet above is compile-checked with `blueprint-compiler` 0.19. The Python snippets are illustrative; adapt imports and application wiring to your project. Bundle the `.ui` file into a GResource so it is available at runtime: ```xml window.ui ``` ```bash glib-compile-resources src/notes.gresource.xml \ --sourcedir=src --target=src/notes.gresource ``` Then bind the template in Python: ```python @Gtk.Template(resource_path="/io/github/yourname/Notes/window.ui") class NotesWindow(Adw.ApplicationWindow): __gtype_name__ = "NotesWindow" note_list = Gtk.Template.Child() content_label = Gtk.Template.Child() def __init__(self, **kwargs): super().__init__(**kwargs) self.note_list.connect("row-selected", self.on_row_selected) ``` For Meson, compile Blueprint and GResource as custom targets. Keep the `.blp` as the source of truth; check in the generated `.ui` only if your build system needs it. ## The widget vocabulary | Need | Widget | | --- | --- | | Window with headerbar | `Adw.ApplicationWindow` + `Adw.HeaderBar` | | Toolbar + content | `Adw.ToolbarView` | | Collapsible sidebar | `Adw.OverlaySplitView` | | Master–detail navigation | `Adw.NavigationSplitView` | | Peer views | `Adw.ViewStack` + `Adw.ViewSwitcher` | | Sidebar navigation | `Adw.Sidebar` + `Adw.ViewSwitcherSidebar` (1.9), or `Gtk.ListBox` | | Tabs | `Adw.TabView` + `Adw.TabBar` | | Settings | `Adw.PreferencesPage` + rows | | Dialog | `Adw.Dialog`, `Adw.AlertDialog` | | Toast | `Adw.ToastOverlay` + `Adw.Toast` | | Shortcuts help | `Adw.ShortcutsDialog` (1.8) | | Wrapping toolbar | `Adw.WrapBox` (1.7) | | Empty/error state | `Adw.StatusPage` | | Inline banner | `Adw.Banner` | | List with model | `Gtk.ListView` + `Gio.ListStore` | | Search | `Gtk.SearchEntry` + `Gtk.FilterListModel` | | Form row | `Adw.EntryRow`, `Adw.SwitchRow`, `Adw.SpinRow`, `Adw.ComboRow` | | Progress | `Gtk.ProgressBar`, `Adw.Spinner` | Prefer these over custom compositions. They carry spacing, accessibility, keyboard behavior, and theme integration. ## Actions and shortcuts Use `Gio.Action` for commands. Actions can be bound to buttons, menu items, and shortcuts, and they show up in the shortcuts dialog. ```python new_action = Gio.SimpleAction.new("new-note", None) new_action.connect("activate", self.on_new_note) self.add_action(new_action) app.set_accels_for_action("win.new-note", ["n"]) ``` ```text Gtk.Button { icon-name: "list-add-symbolic"; action-name: "win.new-note"; } ``` Use `app.` for application-wide actions (quit, about, preferences) and `win.` for window-scoped actions. Actions do not automatically appear in a shortcuts dialog. Provide an `Adw.ShortcutsDialog` (libadwaita 1.8+) or the `shortcuts_dialog` resource that `Adw.Application` loads, so the shortcuts are discoverable. ## Lists and models Do not build thousands of widgets. Use a model and a `Gtk.ListView` with a factory. ```python store = Gio.ListStore.new(NoteObject) for note in notes: store.append(NoteObject(note)) selection = Gtk.SingleSelection.new(store) factory = Gtk.SignalListItemFactory() factory.connect("setup", lambda _factory, item: item.set_child(Gtk.Label(xalign=0))) factory.connect("bind", lambda _factory, item: item.get_child().set_text(item.get_item().title)) list_view = Gtk.ListView.new(selection, factory) ``` For filtering and sorting, wrap the model: ```python filter_model = Gtk.FilterListModel.new(store, filter) sort_model = Gtk.SortListModel.new(filter_model, sorter) selection = Gtk.SingleSelection.new(sort_model) ``` A `Gtk.SignalListItemFactory` binds a row widget to an item. Reuse rows; do not create a new widget per item on every scroll. ## Settings Use GSettings for preferences that belong to the desktop. Define a schema, install it, and bind properties. ```python self.settings = Gio.Settings.new("io.github.yourname.Notes") # On startup. self.set_default_size(self.settings.get_int("window-width"), self.settings.get_int("window-height")) # On close. def on_close_request(self, _window): self.settings.set_int("window-width", self.get_width()) self.settings.set_int("window-height", self.get_height()) return False ``` `default-width`/`default-height` are initial values; they do not track a user resize. Build the preferences UI with `Adw.PreferencesPage`: ```text Adw.PreferencesPage { Adw.PreferencesGroup { title: _("Appearance"); Adw.ComboRow color_scheme { title: _("Color scheme"); model: Gtk.StringList { strings ["Follow system", "Light", "Dark"] }; } } } ``` ```python def on_color_scheme_changed(row, _pspec): schemes = [ Adw.ColorScheme.DEFAULT, Adw.ColorScheme.PREFER_LIGHT, Adw.ColorScheme.PREFER_DARK, ] Adw.StyleManager.get_default().set_color_scheme(schemes[row.get_selected()]) ``` Apply changes immediately. Provide a reset action. Do not write config files behind the user’s back. ## Async work GTK runs on the GLib main loop. Use async I/O and return to the main thread. ```python def load_note(self, path): self.cancellable = Gio.Cancellable() file = Gio.File.new_for_path(path) file.load_contents_async(self.cancellable, self.on_loaded) def on_loaded(self, file, result): try: ok, contents, _etag = file.load_contents_finish(result) except GLib.Error as error: if not error.matches(Gio.io_error_quark(), Gio.IOErrorEnum.CANCELLED): self.show_error(error) return self.apply_contents(contents) ``` For CPU-bound work, use a worker thread and deliver the result with `GLib.idle_add`. Do not touch GTK objects from the worker. ## Custom widgets Subclass a widget when the behavior is genuinely new. Compose existing widgets when it is not. - Override `do_measure` and `do_snapshot` for custom drawing (GTK 4, not `size_allocate`/`draw`). - Implement `Gtk.Accessible` semantics: role, label, state. - Respect the style context and high-contrast mode. - Test at 200% scaling and with RTL. - Prefer `Gtk.DrawingArea` + a model for charts, not a wall of labels. ## CSS and resources Use libadwaita’s named colors and style classes. Custom CSS should be small. ```python from gi.repository import Gdk provider = Gtk.CssProvider() provider.load_from_resource("/io/github/yourname/Notes/style.css") Gtk.StyleContext.add_provider_for_display( Gdk.Display.get_default(), provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, ) ``` ```css .app-badge { background-color: alpha(@accent_bg_color, 0.15); color: @accent_color; border-radius: 6px; padding: 2px 8px; font-weight: 600; } ``` Do not target internal widget nodes or ship a full theme. libadwaita 1.9 deprecates the autoloaded `style-dark.css`, `style-hc.css`, and `style-hc-dark.css`; use `style.css` with `@media (prefers-color-scheme: dark)` and `prefers-contrast` instead. ## SVG and image decoding GTK 4.22 includes [`Gtk.Svg`](https://docs.gtk.org/gtk4/class.Svg.html), an in-process SVG paintable for **trusted** assets such as app icons and resources. It supports SVG animations and integrates with GSK. ```python svg = Gtk.Svg.new_from_resource("/io/github/yourname/Notes/icon.svg") picture = Gtk.Picture.new_for_paintable(svg) ``` `Gtk.Svg` is not a sandbox for arbitrary user files. Decode untrusted SVG or image data with [Glycin](https://gitlab.gnome.org/GNOME/glycin), which runs the parser in a sandboxed process. `Gtk.SvgWidget` is a 4.24-only convenience widget; Ubuntu 26.04’s GTK 4.22 does not provide it. ## Testing Test the model without a display, then test the window with a display. A pure function is the cheapest test and catches the logic that tends to break during refactors. ```python def normalize_title(value: str) -> str: return value.strip() ``` ```python from model import normalize_title def test_title_is_trimmed(): assert normalize_title(" Buy milk ") == "Buy milk" def test_title_can_be_empty(): assert normalize_title(" ") == "" ``` Run the model tests from the project root so `src` is importable: ```bash python3 -m pip install --user pytest PYTHONPATH=src python3 -m pytest ``` For the UI layer, validate the files before launching the app and test the keyboard flow with a real accessibility stack: ```bash gtk4-builder-tool validate src/window.ui blueprint-compiler compile src/window.blp --output /tmp/window.ui desktop-file-validate data/io.github.yourname.Notes.desktop appstreamcli validate --no-net data/io.github.yourname.Notes.metainfo.xml ``` A widget test needs a display. On a headless runner, start a throwaway compositor (for example `weston --headless`) or use the distribution’s GNOME test harness; do not switch to X11 just to run tests. For end-to-end checks, drive the app through AT-SPI with [Dogtail](https://gitlab.com/dogtail/dogtail) or a similar client. The [Testing](/linux-desktop-guide/engineering/testing/) chapter covers the full matrix. ## Accessibility GTK exposes roles, names, and states for native widgets. The failures happen in custom widgets, icon-only buttons, and dialogs that move focus unexpectedly. ```python button = Gtk.Button(icon_name="list-add-symbolic") button.update_property([Gtk.AccessibleProperty.LABEL], ["New note"]) button.update_property([Gtk.AccessibleProperty.DESCRIPTION], ["Create a new note"]) ``` - Use `Gtk.AccessibleRole` and `Gtk.AccessibleProperty` for custom widgets; do not rely on a tooltip as the accessible name. - Keep a logical focus order and restore focus when a dialog closes. - Register actions with `Gtk.Application`; they are keyboard-accessible and appear in `Adw.ShortcutsDialog`. - Test with Orca and [Accerciser](https://gitlab.gnome.org/GNOME/accerciser), at 200% text scaling and in high-contrast mode. - Do not encode meaning in color alone; pair it with text, an icon, or a state. ## Debugging | Tool | Use | | --- | --- | | `GTK_DEBUG=interactive` | Open the GTK Inspector | | `GTK_DEBUG=actions` | Log action activation | | `GTK_DEBUG=layout` | Show layout diagnostics | | `GTK_DEBUG=builder` | Catch deprecated builder patterns | | `GTK_DEBUG=help` | List all debug options | | `gtk4-widget-factory` | Exercise every widget and state | | `gtk4-demo` | Reference implementations | | `GSK_RENDERER=help` | List renderer options | Use the Inspector to check CSS, layout, accessibility, and render nodes. It is the fastest way to find why a widget is the wrong size. ## Common pitfalls | Pitfall | Fix | | --- | --- | | `Gtk.init()` called manually | Let `Gtk.Application` initialize GTK | | Window never appears | Call `present()` after creating it | | Widget not visible | GTK 4 widgets are visible by default; check parent and size | | `pack_start` on a `Gtk.Box` | Use `append()` / `prepend()` in GTK 4 | | Blocking the main loop | Use async APIs or a worker | | Touching widgets from a thread | Return to the main thread first | | CSS targets internals | Use public style classes and named colors | | Hardcoded font sizes | Use `.title-*`, `.heading`, `.body`, `.caption` | | Missing accessible label | Set `Gtk.AccessibleProperty.LABEL` | | Custom titlebar breaks tiling | Use `Adw.HeaderBar` and toolkit window controls | | Rebuilding rows on scroll | Use `Gtk.ListView` + a factory | ## Sources - [GTK 4 documentation](https://docs.gtk.org/gtk4/) - [libadwaita 1.9 documentation](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/1.9/) - [GtkSvg](https://docs.gtk.org/gtk4/class.Svg.html) - [Glycin](https://gitlab.gnome.org/GNOME/glycin) - [Blueprint](https://gnome.pages.gitlab.gnome.org/blueprint-compiler/) - [Gtk.SignalListItemFactory](https://docs.gtk.org/gtk4/class.SignalListItemFactory.html) Next: [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) — the KDE-native stack with the same architectural discipline. --- # Performance URL: https://digitalesich.github.io/linux-desktop-guide/engineering/performance/ > Startup budgets, main-loop discipline, rendering, memory, battery, and a profiling workflow with Sysprof, perf, heaptrack, and GTK Inspector. Performance is not a benchmark score. It is the feeling that the app responds before you finish the thought. It is also measurable, budgetable, and regressable — which means it belongs in engineering, not in a late optimization sprint. Ubuntu 26.04 ships **Sysprof** as a default system utility. Use it. A profile of the real app on the oldest hardware you support is more useful than guesswork. ## Budgets Set budgets before optimizing. A focused desktop app should be able to defend these on a mid-range laptop from the last five years. | Metric | Target | How to measure | | --- | --- | --- | | Cold start to first window | < 1 s (focused app), < 2 s (complex app) | Sysprof, startup marks | | Time to interactive | < 500 ms after first frame | Manual + profile | | Input latency | < 50 ms | `libinput debug-events`, `wev`, Sysprof timestamps | | Frame time during scroll | < 16.7 ms (60 fps) | Sysprof, GTK Inspector | | Idle CPU | < 1% | `top`, `powertop`, Sysprof | | Resident memory | < 200 MB (document app), < 500 MB (complex) | `/usr/bin/time -v`, heaptrack | | Idle wakeups | Near zero | `powertop`, `perf` | | Install size | < 20 MB app payload (Flatpak runtime shared) | `du -sh` | These are not universal laws. A video editor and a notes app have different budgets. The point is to write the budget down and test it. ## Measure before you optimize ### Sysprof [Sysprof](https://apps.gnome.org/Sysprof/) records CPU samples, marks, and (with perf integration) kernel events. It is the default on Ubuntu 26.04 and understands GLib/GTK apps. 1. Start Sysprof. 2. Launch the app and perform the slow action. 3. Stop the recording. 4. Inspect the call tree, flame graph, and marks. 5. Fix the top bottleneck, not the most interesting one. Add marks around startup phases and long operations so the profile tells a story: “open database”, “load notes”, “first paint”. ### perf ```bash perf record -g ./build/notes perf report ``` `perf` is excellent for CPU hotspots and scheduler behavior. It needs symbols; build with debug info (`-g`) for profiling, and strip for release. ### heaptrack ```bash heaptrack ./build/notes heaptrack_gui heaptrack.notes.*.gz ``` Use it for allocation counts, peak memory, and leaks. A slow app is often an allocation-heavy app. ### GTK Inspector ```bash GTK_DEBUG=interactive ./build/notes ``` Inspect the widget tree, CSS, layout, render nodes, and accessibility. Use the [Recorder tab](https://developer.gnome.org/documentation/tools/inspector.html) for render nodes and clip regions; the **Objects** tab shows properties, actions, CSS nodes, and accessibility. ### Startup tracing For startup work, capture a trace instead of guessing: ```bash sysprof-cli -- ./build/notes GDK_DEBUG=frame-timings ./build/notes ``` `perf` may require `perf_event_paranoid` to be lowered for your user; do that deliberately on a development machine, not on a shared production host. ### Other tools | Tool | Use | | --- | --- | | `strace -c` | Syscall overhead, file I/O patterns | | `ltrace` | Library calls (rarely needed) | | `valgrind --tool=callgrind` | Instruction-level profiles (slow) | | `massif` | Heap profiles | | `hyperfine` | Microbenchmarks and startup loops | | `cargo bloat` | Rust binary size contributors | | `powertop` | Idle wakeups and power | | `wev` | Wayland input events | | `GSK_RENDERER=help` | List GTK renderers | | `GDK_DEBUG=help` | List GDK debug options | ## Startup The first second is mostly about **what you do before the first frame**. - Create the window and show a usable shell before loading data. - Defer non-critical work: thumbnails, indexes, plugin discovery, update checks. - Do not open every database, parse every file, or enumerate the home directory at startup. - Keep imports/initialization small. Python apps pay for imports; Rust apps pay for static initialization; C apps pay for plugin loading. - Use `Gtk.Application`’s `startup` for global setup and `activate` for the window. - Avoid synchronous network calls; show cached data first. - Use a skeleton layout that matches the final content to avoid layout shift. ```python def on_activate(self, _app): window = NotesWindow(application=self) window.present() GLib.idle_add(self.load_secondary_content, priority=GLib.PRIORITY_DEFAULT_IDLE) ``` For a Python app, measure import time: ```bash python3 -X importtime src/main.py 2> importtime.log ``` If a library costs 300 ms and is only needed for an optional feature, import it lazily. ## The main loop A responsive app is an app that never blocks the main loop. - Move file I/O, network, parsing, compression, and database queries off the main thread. - Use async APIs with cancellation. - Chunk large work into idle callbacks so input can be processed between chunks. - Avoid `sleep`, synchronous `subprocess`, and blocking locks on the main thread. - Do not update the UI once per item in a large loop; batch and coalesce. ```python def process_batch(self, items, offset=0, batch=100): for item in items[offset:offset + batch]: self.process(item) next_offset = offset + batch if next_offset < len(items): GLib.idle_add(self.process_batch, items, next_offset, batch) ``` Measure input latency, not just CPU time. A fast function that runs 60 times per second on the main thread is still a problem. ## Rendering GTK 4 renders through GSK, usually on the GPU. Most rendering problems are caused by the widget tree, not the renderer. - Keep the widget tree shallow. Deep nesting costs layout and paint. - Use `Gtk.ListView` with recycled rows for long lists; do not build thousands of widgets. - Avoid CSS selectors that match huge subtrees; keep custom CSS small. - Do not animate layout properties (width, height, margin); animate transform and opacity. - Hide or destroy off-screen content; do not keep a full second UI in memory. - Use `Gtk.Picture` with `can_shrink` and appropriately sized textures for images. - Avoid decoding a 6000×4000 image just to show a 64×64 thumbnail. - Test with `GSK_RENDERER=gl`, `ngl`, `vulkan`, and `cairo` if you support multiple renderers; do not force one in production without a reason. - Use [`Gtk.GraphicsOffload`](https://docs.gtk.org/gtk4/class.GraphicsOffload.html) for fullscreen video or other surfaces that should bypass GSK composition, and test `GDK_DEBUG=gl-disable` to catch fallback behavior. If scrolling stutters, check: 1. Row count and widget depth. 2. Image decoding on the main thread. 3. CSS complexity. 4. Layout invalidation on every frame. 5. A synchronous operation triggered by scroll. ## Memory - GObject is reference-counted; cycles leak. Use weak references in signal handlers and callbacks. - Rust `Rc` cycles leak the same way; use `Weak`. - Python references keep GObject wrappers alive; disconnect signals when destroying widgets. - Bound caches: thumbnails, decoded images, search results, undo history. - Release large buffers when a view closes. - Use `GdkTexture` and `Gtk.Picture` instead of keeping `GdkPixbuf` copies. - Use [`GMemoryMonitor`](https://docs.gtk.org/gio/iface.MemoryMonitor.html) low-memory warnings to drop caches and bound memory. The kernel OOM killer sends `SIGKILL`; the app cannot catch it or “fail gracefully” after the kill. - Decode untrusted images with Glycin, which also limits resource use. ## Battery and background work - Prefer event-driven APIs over polling. D-Bus signals, `Gio.NetworkMonitor`, and file monitors are cheaper than a 1-second timer. - Use the longest acceptable timer interval; coalesce updates. - Stop animations and timers when the window is not visible. - Release idle inhibitors when work finishes. - Use `Gtk.Application.inhibit` or the Inhibit portal, and test with power-profiles-daemon in balanced, power-saver, and performance modes. - Do not hold a GPU context or a wake lock for a hidden window. - Test suspend/resume: the app should reconnect, refresh, and not spin. - A Wayland compositor restart normally terminates client connections. Persist state continuously and restore it on the next launch; do not promise a live reconnect unless your toolkit and compositor explicitly support it. `powertop` and Sysprof can show wakeups and CPU residency. A “0% CPU” app that wakes 100 times per second is still draining the battery. ## A profiling workflow 1. **Write the budget.** “Cold start under 1 s on a 2021 ThinkPad.” 2. **Reproduce.** Use the same hardware, session type, scaling, and data set. 3. **Capture.** Sysprof for CPU and marks; heaptrack for memory; GTK Inspector for layout. 4. **Find the top cost.** Ignore the rest until the top is fixed. 5. **Fix one thing.** Measure again. Do not bundle five changes. 6. **Add a regression check.** A startup benchmark, a frame-time budget, or a memory ceiling in CI. 7. **Repeat.** Performance work is a loop, not a milestone. ## CI and release gates - Track binary/package size on every release. - Run a startup benchmark on a fixed runner with a `--quit-after-startup` flag. - Fail the build on a large regression (for example, > 20% startup time). - Keep a performance profile attached to release notes for major versions. - Test on the oldest supported hardware before declaring a release “fast”. ```bash hyperfine --warmup 3 --runs 20 \ './build/notes --quit-after-startup' ``` Implement `--quit-after-startup` in the app: show the window, wait for the first frame, then exit. It is a test hook, not a user feature. ## Common wins | Problem | Fix | | --- | --- | | Slow cold start | Defer plugins, indexes, thumbnails; lazy imports | | UI freezes on open | Async file/network I/O; cancellable tasks | | Scroll stutter | `Gtk.ListView`, fewer widgets, smaller images | | High idle CPU | Remove polling; use D-Bus signals | | Memory grows over time | Bound caches; break GObject/Rc cycles | | Slow search | Search the model, debounce, index in a worker | | Battery drain | Fewer wakeups; stop timers when hidden | | Slow first paint | Show the shell before loading data | | Large package | Strip binaries, share runtime, remove unused assets | ## Sources - [Sysprof](https://apps.gnome.org/Sysprof/) - [GTK Inspector](https://developer.gnome.org/documentation/tools/inspector.html) - [GMemoryMonitor](https://docs.gtk.org/gio/iface.MemoryMonitor.html) - [Gtk.GraphicsOffload](https://docs.gtk.org/gtk4/class.GraphicsOffload.html) - [perf](https://perf.wiki.kernel.org/) - [heaptrack](https://github.com/KDE/heaptrack) Next: [Testing](/linux-desktop-guide/engineering/testing/) — how to prove the app works before users do. --- # Qt & Kirigami URL: https://digitalesich.github.io/linux-desktop-guide/engineering/qt/ > Building native KDE apps with Qt 6.10+ and Kirigami: a persistent C++/QML project, XDG storage, QAbstractListModel, CMake, KConfig, KNotification, KIO, testing, accessibility,… Qt is the other major Linux desktop stack. It is mature, widely used, cross-platform, and backed by a deep tooling ecosystem. **Kirigami** is KDE’s adaptive UI framework built on Qt Quick; it gives you the navigation, sheets, cards, and responsive layout that make an app feel at home on Plasma. If your primary audience is KDE, Qt + Kirigami is a direct path to native behavior. If you need one codebase across desktop, mobile, and embedded targets, Qt is often the pragmatic choice — provided you still do the platform integration work. Two reference baselines Ubuntu 26.04 ships Qt 6.10.2, KDE Frameworks 6.24, and Plasma 6.6. Fedora 45 KDE, the reference environment for this chapter, ships Qt 6.11, KDE Frameworks 6.30+, and Plasma 6.7 or newer. The example uses APIs available in both: treat Qt 6.10/KF 6.24 as the compatibility floor and test on Fedora for the current stack. Use `QT_VERSION_CHECK` when you also support older releases; API availability is compile-time, not a runtime guard. Qt 6.8 LTS is commercially maintained; open-source users should track their distribution’s Qt or the latest stable. Verification status Source review: 2026-11-11. Runtime verification: pending on Fedora 45. This chapter is source-reviewed and illustrative in this revision: it has not been compiled or run on Fedora 45. The preparation environment did not include Qt 6 or KDE Frameworks development packages, Toolbx, or `mock`. The code is complete enough to build; run the commands in the [Fedora + KDE validation checklist](/linux-desktop-guide/targets/fedora-kde/#validation-checklist) on the exact target before treating the path as validated. API references are in [Sources](#sources). ## QML or Qt Widgets? | Dimension | Qt Widgets | Qt Quick / QML | | --- | --- | --- | | Look | Classic desktop, dense | Fluid, animated, touch-friendly | | Performance | Excellent for forms/tables | Excellent with a disciplined scene | | Adaptability | Manual layouts | Layouts, states, transitions, `PageRow` | | Learning curve | C++ widgets | QML + JavaScript + C++ backend | | Accessibility | Mature | Improving; requires explicit labels | | Best for | Utilities, IDEs, data-heavy tools | Media, touch, animated, adaptive apps | Do not mix the two paradigms in one window unless you have a specific reason. A QML app with a C++ backend is the modern KDE pattern; Qt Widgets remains a solid choice for dense desktop tools. ## Project skeleton The [Fedora + KDE target](/linux-desktop-guide/targets/fedora-kde/) is the reference environment for this chapter. The app keeps the data and persistence in C++, the view in QML, and the file format in a small store that can be tested without a window: ```text notes/ ├── CMakeLists.txt ├── LICENSE ├── src/ │ ├── main.cpp │ ├── Main.qml │ ├── NotesModel.h │ ├── NotesModel.cpp │ ├── NotesStore.h │ └── NotesStore.cpp ├── tests/ │ ├── tst_notesmodel.cpp │ └── tst_notesstore.cpp ├── data/ │ ├── io.github.yourname.Notes.desktop │ ├── io.github.yourname.Notes.metainfo.xml │ └── io.github.yourname.Notes.svg └── io.github.yourname.Notes.yml ``` The desktop entry, metainfo, icon, Flatpak ID, and `setDesktopFileName()` all use `io.github.yourname.Notes`. The binary stays `notes`. That separation is normal: the app ID identifies the desktop application, while the executable name identifies the process. Add a `LICENSE` file that matches the metainfo’s `project_license`. ## A minimal Kirigami app ### The model `QAbstractListModel` is the boundary between C++ data and QML. Persistence is a separate `NotesStore`, so the file format can be tested without a display. ```cpp #pragma once #include #include struct Note { QString title; QString body; }; class NotesStore { public: explicit NotesStore(QString filePath = {}); QString filePath() const; bool exists() const; QList load(QString *error = nullptr) const; bool save(const QList ¬es, QString *error = nullptr) const; private: QString m_filePath; }; ``` ```cpp #include "NotesStore.h" #include #include #include #include #include #include #include #include #include #include namespace { constexpr int kSchemaVersion = 1; } NotesStore::NotesStore(QString filePath) : m_filePath(std::move(filePath)) { if (m_filePath.isEmpty()) { const QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); m_filePath = dir + QStringLiteral("/notes.json"); } } QString NotesStore::filePath() const { return m_filePath; } bool NotesStore::exists() const { return QFileInfo::exists(m_filePath); } QList NotesStore::load(QString *error) const { QFile file(m_filePath); if (!file.exists()) { return {}; } if (!file.open(QIODevice::ReadOnly)) { if (error) *error = file.errorString(); return {}; } QJsonParseError parseError; const QJsonDocument document = QJsonDocument::fromJson(file.readAll(), &parseError); if (parseError.error != QJsonParseError::NoError || !document.isObject()) { if (error) *error = parseError.errorString(); return {}; } const QJsonObject root = document.object(); if (root.value(QStringLiteral("version")).toInt() != kSchemaVersion) { if (error) *error = QStringLiteral("Unsupported notes file version."); return {}; } QList notes; const QJsonArray array = root.value(QStringLiteral("notes")).toArray(); for (const QJsonValue &value : array) { const QJsonObject object = value.toObject(); notes.append({ object.value(QStringLiteral("title")).toString(), object.value(QStringLiteral("body")).toString(), }); } return notes; } bool NotesStore::save(const QList ¬es, QString *error) const { QJsonArray array; for (const Note ¬e : notes) { array.append(QJsonObject{ {QStringLiteral("title"), note.title}, {QStringLiteral("body"), note.body}, }); } const QJsonObject root{ {QStringLiteral("version"), kSchemaVersion}, {QStringLiteral("notes"), array}, }; QDir().mkpath(QFileInfo(m_filePath).absolutePath()); QSaveFile file(m_filePath); if (!file.open(QIODevice::WriteOnly)) { if (error) *error = file.errorString(); return false; } const QByteArray data = QJsonDocument(root).toJson(QJsonDocument::Indented); if (file.write(data) != data.size()) { if (error) *error = file.errorString(); file.cancelWriting(); return false; } if (!file.commit()) { if (error) *error = file.errorString(); return false; } return true; } ``` The default path is `QStandardPaths::AppDataLocation + "/notes.json"`. With `applicationName` set to `notes`, that is `$XDG_DATA_HOME/notes/notes.json`; inside the Flatpak sandbox it is `~/.var/app/io.github.yourname.Notes/data/notes/notes.json`. `QSaveFile` writes to a temporary file and commits atomically, so a crash cannot leave a half-written notes file. `kSchemaVersion` is the migration hook. When the format changes, read the old version, convert it, write the new file atomically, and keep the old file until the migration succeeds. The rules are in [The XDG Contract](/linux-desktop-guide/platform/xdg/#migration-and-compatibility). ```cpp #pragma once #include #include #include #include #include #include #include "NotesStore.h" class NotesModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QString lastError READ lastError NOTIFY errorChanged) public: enum Role { TitleRole = Qt::UserRole + 1, BodyRole, }; explicit NotesModel(QObject *parent = nullptr); explicit NotesModel(NotesStore store, QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; QVariant data(const QModelIndex &index, int role) const override; QHash roleNames() const override; QString lastError() const; Q_INVOKABLE QString titleAt(int row) const; Q_INVOKABLE QString bodyAt(int row) const; Q_INVOKABLE void addNote(const QString &title, const QString &body); Q_INVOKABLE void updateNote(int row, const QString &title, const QString &body); Q_INVOKABLE void removeNote(int row); Q_INVOKABLE bool save(); Q_INVOKABLE void reload(); signals: void errorChanged(const QString &message); private: void setError(const QString &message); NotesStore m_store; QList m_notes; QString m_lastError; }; ``` ```cpp #include "NotesModel.h" #include NotesModel::NotesModel(QObject *parent) : NotesModel(NotesStore(), parent) { } NotesModel::NotesModel(NotesStore store, QObject *parent) : QAbstractListModel(parent) , m_store(std::move(store)) { QString error; m_notes = m_store.load(&error); if (!error.isEmpty()) { setError(error); return; } if (m_notes.isEmpty() && !m_store.exists()) { m_notes = { {tr("Welcome"), tr("This note is stored in your XDG data directory and survives restarts.")}, {tr("Keyboard first"), tr("Press Ctrl+N to create a note. Every action should be reachable without a mouse.")}, }; save(); } } int NotesModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : m_notes.size(); } QVariant NotesModel::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() < 0 || index.row() >= m_notes.size()) { return {}; } const Note ¬e = m_notes.at(index.row()); switch (role) { case TitleRole: return note.title; case BodyRole: return note.body; default: return {}; } } QHash NotesModel::roleNames() const { return { {TitleRole, "title"}, {BodyRole, "body"}, }; } QString NotesModel::lastError() const { return m_lastError; } QString NotesModel::titleAt(int row) const { return row >= 0 && row < m_notes.size() ? m_notes.at(row).title : QString(); } QString NotesModel::bodyAt(int row) const { return row >= 0 && row < m_notes.size() ? m_notes.at(row).body : QString(); } void NotesModel::addNote(const QString &title, const QString &body) { beginInsertRows(QModelIndex(), 0, 0); m_notes.prepend({title, body}); endInsertRows(); save(); } void NotesModel::updateNote(int row, const QString &title, const QString &body) { if (row < 0 || row >= m_notes.size()) { return; } m_notes[row] = {title, body}; const QModelIndex changed = index(row, 0); emit dataChanged(changed, changed, {TitleRole, BodyRole}); save(); } void NotesModel::removeNote(int row) { if (row < 0 || row >= m_notes.size()) { return; } beginRemoveRows(QModelIndex(), row, row); m_notes.removeAt(row); endRemoveRows(); save(); } bool NotesModel::save() { QString error; if (!m_store.save(m_notes, &error)) { setError(error); return false; } setError(QString()); return true; } void NotesModel::reload() { QString error; const QList notes = m_store.load(&error); if (!error.isEmpty()) { setError(error); return; } beginResetModel(); m_notes = notes; endResetModel(); setError(QString()); } void NotesModel::setError(const QString &message) { if (m_lastError == message) { return; } m_lastError = message; emit errorChanged(m_lastError); } ``` ### The application entry point Set the application identity, install the QML translation context, and activate the existing window when a second launch arrives. ```cpp #include #include #include #include #include #include #include #include #include #include #include "NotesModel.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QCoreApplication::setApplicationName(QStringLiteral("notes")); QGuiApplication::setDesktopFileName(QStringLiteral("io.github.yourname.Notes")); QQuickStyle::setStyle(QStringLiteral("org.kde.desktop")); KLocalizedString::setApplicationDomain("notes"); KAboutData about( QStringLiteral("notes"), i18n("Notes"), QStringLiteral("0.1.0"), i18n("A small note-taking app"), KAboutLicense::GPL_V3, i18n("© 2026 Your Name"), QString(), QStringLiteral("https://example.org/notes")); KAboutData::setApplicationData(about); KDBusService service(KDBusService::Unique); NotesModel notesModel; QQmlApplicationEngine engine; auto *context = new KLocalizedContext(&engine); context->setTranslationDomain(QStringLiteral("notes")); engine.rootContext()->setContextObject(context); engine.rootContext()->setContextProperty(QStringLiteral("notesModel"), ¬esModel); QObject::connect(&service, &KDBusService::activateRequested, &engine, [&engine](const QStringList &, const QString &) { const auto windows = engine.rootObjects(); if (windows.isEmpty()) { return; } if (auto *window = qobject_cast(windows.first())) { window->show(); window->raise(); window->requestActivate(); } }); engine.loadFromModule("Notes", "Main"); if (engine.rootObjects().isEmpty()) { return -1; } return app.exec(); } ``` ### The QML view The list page and the editor page share the model. The add, save, and delete actions call invokable methods; QML never mutates the C++ list directly. A storage failure appears as an inline message instead of a silent loss. ```qml import QtQuick import QtQuick.Controls as Controls import QtQuick.Layouts import org.kde.kirigami as Kirigami Kirigami.ApplicationWindow { id: root title: i18n("Notes") width: 980 height: 680 pageStack.initialPage: Kirigami.Page { title: i18n("Notes") actions: [ Kirigami.Action { text: i18n("New note") icon.name: "list-add" shortcut: StandardKey.New onTriggered: { notesModel.addNote(i18n("Untitled note"), QString()) root.pageStack.push(detailPage, { noteIndex: 0 }) } } ] ColumnLayout { anchors.fill: parent spacing: 0 Kirigami.InlineMessage { Layout.fillWidth: true visible: notesModel.lastError.length > 0 text: notesModel.lastError type: Kirigami.MessageType.Error } ListView { Layout.fillWidth: true Layout.fillHeight: true clip: true model: notesModel delegate: Controls.ItemDelegate { required property int index required property string title width: ListView.view.width text: title onClicked: root.pageStack.push(detailPage, { noteIndex: index }) } } } } Component { id: detailPage Kirigami.ScrollablePage { id: editor property int noteIndex: -1 title: i18n("Edit note") actions: [ Kirigami.Action { text: i18n("Delete") icon.name: "edit-delete" onTriggered: { notesModel.removeNote(editor.noteIndex) root.pageStack.pop() } }, Kirigami.Action { text: i18n("Save") icon.name: "document-save" shortcut: StandardKey.Save onTriggered: { notesModel.updateNote(editor.noteIndex, titleField.text, bodyField.text) root.pageStack.pop() } } ] ColumnLayout { anchors.fill: parent Controls.TextField { id: titleField Layout.fillWidth: true text: notesModel.titleAt(editor.noteIndex) placeholderText: i18n("Title") } Controls.TextArea { id: bodyField Layout.fillWidth: true Layout.fillHeight: true text: notesModel.bodyAt(editor.noteIndex) placeholderText: i18n("Write a note…") wrapMode: TextEdit.Wrap } } } } } ``` ### The build The QML module URI and the `loadFromModule("Notes", "Main")` call must agree. The file is `Main.qml`, not `main.qml`. The test targets use `Qt6::Test` and run without a display. ```cmake cmake_minimum_required(VERSION 3.21) project(notes VERSION 0.1.0 LANGUAGES CXX) include(GNUInstallDirs) include(CTest) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) find_package(Qt6 6.10 REQUIRED COMPONENTS Quick QuickControls2 Gui DBus Core Test) find_package(KF6 REQUIRED COMPONENTS Kirigami I18n Config CoreAddons DBusAddons) qt_add_executable(notes src/main.cpp src/NotesModel.cpp src/NotesModel.h src/NotesStore.cpp src/NotesStore.h ) qt_add_qml_module(notes URI Notes VERSION 1.0 QML_FILES src/Main.qml ) target_link_libraries(notes PRIVATE Qt6::Quick Qt6::QuickControls2 Qt6::Gui Qt6::DBus Qt6::Core KF6::Kirigami KF6::I18n KF6::ConfigCore KF6::CoreAddons KF6::DBusAddons ) install(TARGETS notes RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) install(FILES data/io.github.yourname.Notes.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) install(FILES data/io.github.yourname.Notes.metainfo.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) install(FILES data/io.github.yourname.Notes.svg DESTINATION ${CMAKE_INSTALL_DATADIR}/icons/hicolor/scalable/apps) if(BUILD_TESTING) qt_add_executable(tst_notesstore tests/tst_notesstore.cpp src/NotesStore.cpp src/NotesStore.h ) target_include_directories(tst_notesstore PRIVATE src) target_link_libraries(tst_notesstore PRIVATE Qt6::Test Qt6::Core) add_test(NAME notesstore COMMAND tst_notesstore) qt_add_executable(tst_notesmodel tests/tst_notesmodel.cpp src/NotesModel.cpp src/NotesModel.h src/NotesStore.cpp src/NotesStore.h ) target_include_directories(tst_notesmodel PRIVATE src) target_link_libraries(tst_notesmodel PRIVATE Qt6::Test Qt6::Core) add_test(NAME notesmodel COMMAND tst_notesmodel) endif() ``` Build it: ```bash cmake -S . -B build -G Ninja cmake --build build ./build/notes ``` ## Kirigami patterns | Need | Pattern | | --- | --- | | Master–detail | `Kirigami.PageRow` | | Quick action sheet | `Kirigami.OverlaySheet` | | Dialog | `Kirigami.Dialog` / `Kirigami.PromptDialog` | | Settings | `Kirigami.ScrollablePage` + `Kirigami.FormLayout` | | Cards | `Kirigami.AbstractCard` / `Kirigami.Card` | | Context menu | `Controls.Menu` | | Adaptive toolbar | `Kirigami.Action` + page actions | `Kirigami.PageRow` is the key adaptive primitive: on wide windows it shows the list and detail side by side; on narrow windows it pushes the detail page with a back button. ## Spacing, colors, and units Use `Kirigami.Units` and `Kirigami.Theme` instead of hardcoded pixels and colors. ```qml import org.kde.kirigami as Kirigami Controls.Label { font.pixelSize: Kirigami.Units.gridUnit color: Kirigami.Theme.textColor padding: Kirigami.Units.smallSpacing } ``` - `Kirigami.Units.gridUnit` scales with the user’s font size. - `Kirigami.Theme` adapts to the color scheme and platform. - Use Breeze icons (`icon.name: "document-open"`) for KDE consistency. - Do not hardcode `width: 800` for content; let layouts expand. - Test at 100%, 125%, 150%, and 200% scaling. ## Settings with KConfig KDE apps use KConfig for settings. Define a schema (`.kcfg`) and generate a configuration class, or use `KConfigGroup` directly for small apps. ```cpp KSharedConfigPtr config = KSharedConfig::openConfig(); KConfigGroup general(config, QStringLiteral("General")); int width = general.readEntry("windowWidth", 980); general.writeEntry("windowWidth", width); config->sync(); ``` For settings UI, use `KConfigDialog` with `.kcfg`\-generated widgets, or a Kirigami form bound to the config object. Add `KF6::ConfigWidgets` to CMake when you use `KConfigDialog`. Apply changes immediately and provide a reset action. For non-KDE deployments, `QSettings` is the portable equivalent. Do not write INI files by hand if the platform already has a settings system. ## Notifications and integration - **[KNotification](https://api.kde.org/knotification.html)** for desktop notifications, with actions and urgency. - **KIO** for network-transparent file access, trash, and remote URLs. Do not assume every `QUrl` is a local file. - **KWallet** / Secret Service for credentials. - **[KDBusService](https://api.kde.org/kdbusservice.html)** for single-instance behavior and activation. `QApplication` alone does not register a D-Bus name; use `KDBusService(KDBusService::Unique)` for KDE/Qt, or `QLockFile`/a custom D-Bus name for non-KDE Qt. - **MPRIS** via Qt DBus for media apps. - **XDG portals** for file dialogs, screen capture, and global shortcuts, especially in Flatpak. ```cpp KNotification *notification = new KNotification(QStringLiteral("export-finished")); notification->setText(i18n("Export finished")); KNotificationAction *openAction = notification->addAction(i18n("Open folder")); connect(openAction, &KNotificationAction::activated, this, &Window::openExportFolder); notification->sendEvent(); ``` KDE notifications also need a `.notifyrc` file that declares the event, its icon, and its actions. Install it to `knotifications6/` and make sure the application name or `setComponentName()` matches. Add `KF6::Notifications` to CMake when you use `KNotification`. ## Qt 6.10: high contrast, vector, and models [Qt 6.10](https://www.qt.io/blog/qt-6.10-released) adds accessibility and UI features that are relevant on Ubuntu 26.04: - **High contrast:** [`QStyleHints::accessibility()->contrastPreference()`](https://www.qt.io/blog/high-contrast-mode-in-qt-6.10) reports the platform preference, and the Fusion/Basic styles strengthen control outlines when it is enabled. - **Animated vector graphics:** `VectorImage` renders animated SVG and Lottie in the Qt Quick scene graph; use `svgtoqml`/`lottietoqml` for precompiled assets. Treat user-supplied vector files as untrusted. - **Models and controls:** `SearchField`, `QRangeModel`, `Synchronizer` (tech preview), `TreeModel`, `SortFilterProxyModel`, and `delegateModelAccess` reduce C++/QML boilerplate. - **QML performance:** compile QML ahead of time (`qmlcachegen`), run `qmllint`, inspect the scene graph with `QSG_INFO=1`, and choose the render loop deliberately (`QSG_RENDER_LOOP`). - **KDE integration:** call `KCrash::initialize()` early, use `KDBusService` for activation, and keep KIO jobs asynchronous. - **Wayland, scaling, and input:** test 125/150/200%, set the high-DPI rounding policy deliberately, and do not set `QT_IM_MODULE` on Wayland; let the compositor’s input method handle text. `KDE Frameworks 6.24` requires Qt 6.8 or newer. Add KIO/KCrash components only when the app actually uses them; the CMake example above keeps the dependency set to what the example needs. ## Build and package The CMake file above builds the executable, compiles `Main.qml` into the `Notes` module, registers the two tests, and installs the binary, desktop entry, metainfo, and icon. Build and test it with: ```bash cmake -S . -B build -G Ninja cmake --build build ctest --test-dir build --output-on-failure qmllint src/Main.qml ./build/notes ``` ### Desktop entry and metainfo The desktop entry and metainfo use the same app ID as `setDesktopFileName()`. The binary stays `notes`; `StartupWMClass` matches the process class on X11/XWayland. ```ini [Desktop Entry] Name=Notes Comment=A small note-taking app Exec=notes Icon=io.github.yourname.Notes Terminal=false Type=Application Categories=Utility;TextEditor; StartupWMClass=notes ``` ```xml io.github.yourname.Notes Notes A small note-taking app

Notes is a small local-first note-taking app for KDE Plasma.

CC0-1.0 GPL-3.0-or-later io.github.yourname.Notes.desktop https://example.org/notes Your Name notes Utility TextEditor

Initial release.

``` Use [AppStream & Metadata](/linux-desktop-guide/ship/appstream/) for screenshots, release notes, and validation. The icon is `data/io.github.yourname.Notes.svg`, installed into `hicolor/scalable/apps`. ### Flatpak Flatpak uses the KDE runtime: ```yaml id: io.github.yourname.Notes runtime: org.kde.Platform runtime-version: '6.10' sdk: org.kde.Sdk command: notes finish-args: - --socket=wayland - --socket=fallback-x11 - --share=ipc - --device=dri modules: - name: notes buildsystem: cmake-ninja sources: - type: dir path: . ``` The KDE runtime includes Kirigami and the KDE Frameworks, so you do not need to bundle them. For the Fedora RPM, use the spec in [Packaging & Distribution](/linux-desktop-guide/platform/packaging/#rpm-packages); every `find_package` component needs a matching `BuildRequires`. ## Async and I/O QML bindings run on the GUI thread. Move blocking file and network work to KIO jobs or a worker, then deliver results through signals. ```cpp #include #include auto *job = KIO::copy(source, destination, KIO::Overwrite); connect(job, &KJob::result, this, [](KJob *job) { if (job->error()) { qWarning() << "copy failed:" << job->errorString(); } }); ``` - KIO handles local files, trash, and remote URLs; add `KF6::KIO` to CMake when you use it. - For CPU-bound work, use `QtConcurrent::run` with a `QFutureWatcher`, or a `QThread`; never touch QML objects from the worker. - Prefer `QUrl` and KIO over raw `QFile` paths for user-selected files. Under Flatpak, KIO uses the FileChooser portal. - Cancel long jobs when the page closes; connect the page’s `Component.onDestruction` or the job’s cancel signal. ## Accessibility ```qml Controls.Button { text: i18n("Delete") Accessible.name: text Accessible.description: i18n("Delete the selected note") } ``` - Set `Accessible.name`, `Accessible.description`, and `Accessible.role` on custom QML items; native controls already expose semantics. - Keep keyboard navigation logical: `focus: true`, a visible focus ring, and actions instead of ad-hoc `Keys.onPressed` handlers. - Test with Orca and Accerciser. Qt 6.10 exposes the platform contrast preference through `QStyleHints::accessibility()->contrastPreference()`. - Do not rely on color alone; pair it with text or an icon. - Treat `Kirigami.Settings.isMobile` as a layout hint, not a reason to remove desktop affordances. ## Testing Both test targets are already registered in the CMake file above. They need no display. The store test uses a temporary directory; the model test starts from a valid empty file so it does not depend on the welcome notes. ```cpp #include #include #include #include "NotesStore.h" class NotesStoreTest : public QObject { Q_OBJECT private slots: void roundTripsNotes(); void reportsInvalidJson(); }; void NotesStoreTest::roundTripsNotes() { QTemporaryDir dir; QVERIFY(dir.isValid()); NotesStore store(dir.filePath(QStringLiteral("notes.json"))); const QList notes{ {QStringLiteral("One"), QStringLiteral("First body")}, {QStringLiteral("Two"), QStringLiteral("Second body")}, }; QString error; QVERIFY2(store.save(notes, &error), qPrintable(error)); const QList loaded = store.load(&error); QVERIFY2(error.isEmpty(), qPrintable(error)); QCOMPARE(loaded.size(), 2); QCOMPARE(loaded.at(0).title, QStringLiteral("One")); QCOMPARE(loaded.at(1).body, QStringLiteral("Second body")); } void NotesStoreTest::reportsInvalidJson() { QTemporaryDir dir; QVERIFY(dir.isValid()); QFile file(dir.filePath(QStringLiteral("notes.json"))); QVERIFY(file.open(QIODevice::WriteOnly)); file.write("{not json"); file.close(); NotesStore store(file.fileName()); QString error; const QList loaded = store.load(&error); QVERIFY(loaded.isEmpty()); QVERIFY(!error.isEmpty()); } QTEST_GUILESS_MAIN(NotesStoreTest) #include "tst_notesstore.moc" ``` ```cpp #include #include #include "NotesModel.h" class NotesModelTest : public QObject { Q_OBJECT private slots: void addsUpdatesAndRemoves(); }; void NotesModelTest::addsUpdatesAndRemoves() { QTemporaryDir dir; QVERIFY(dir.isValid()); NotesStore store(dir.filePath(QStringLiteral("notes.json"))); QString error; QVERIFY2(store.save(QList(), &error), qPrintable(error)); NotesModel model(store); QCOMPARE(model.rowCount(), 0); QVERIFY(model.lastError().isEmpty()); model.addNote(QStringLiteral("Buy milk"), QString()); QCOMPARE(model.rowCount(), 1); QCOMPARE(model.titleAt(0), QStringLiteral("Buy milk")); model.updateNote(0, QStringLiteral("Buy oat milk"), QStringLiteral("From the shop")); QCOMPARE(model.titleAt(0), QStringLiteral("Buy oat milk")); QCOMPARE(model.bodyAt(0), QStringLiteral("From the shop")); model.removeNote(0); QCOMPARE(model.rowCount(), 0); } QTEST_GUILESS_MAIN(NotesModelTest) #include "tst_notesmodel.moc" ``` Run `ctest --test-dir build --output-on-failure`. For QML components use `qmltestrunner`; for headless runs set `QT_QPA_PLATFORM=offscreen` and wrap session-bus tests in `dbus-run-session`. `GammaRay` inspects the live scene; `QT_LOGGING_RULES` narrows logs. ## Common pitfalls | Pitfall | Fix | | --- | --- | | Mixing Qt 5 and Qt 6 | Build against one Qt major version; use the distro’s Qt 6 | | Hardcoded pixel sizes | Use `Kirigami.Units` | | Hardcoded colors | Use `Kirigami.Theme` | | JavaScript logic in bindings | Move it to C++ or a JS function with clear inputs | | QML scene too deep | Flatten; use `Loader` and `ListView` | | Touching QWidgets from a worker | Use signals/slots to the GUI thread | | Missing app ID | `QGuiApplication::setDesktopFileName()` and matching desktop/metainfo IDs | | Notes stay only in memory | Use `NotesStore` with `QSaveFile`; load on startup and save on every mutation | | QML `i18n()` is empty | Install `KLocalizedContext` before `loadFromModule()` | | No high-DPI support | Qt 6 handles it; test and avoid manual scaling | | File access outside the sandbox | Use KIO/portals and XDG paths | | No accessible names on custom items | Set `Accessible.name` and role | ## Sources - [Qt 6.10 release](https://www.qt.io/blog/qt-6.10-released) - [Qt 6.10 high contrast](https://www.qt.io/blog/high-contrast-mode-in-qt-6.10) - [Qt 6 documentation](https://doc.qt.io/qt-6/) - [KDE Frameworks 6.24](https://kde.org/announcements/frameworks/6/6.24.0/) - [KNotifications](https://api.kde.org/knotification.html) - [KDBusService](https://api.kde.org/kdbusservice.html) - [GNUInstallDirs](https://cmake.org/cmake/help/latest/module/GNUInstallDirs.html) Next: [Rust on the Desktop](/linux-desktop-guide/engineering/rust/) — memory safety without giving up native GTK. --- # Reliability & Logging URL: https://digitalesich.github.io/linux-desktop-guide/engineering/reliability/ > Designing for failure: logging, crash reports, recovery, and data integrity across the Linux desktop. Every app fails. The disk fills up, the network disappears, the keyring is locked, the compositor restarts, a portal is missing, and a file is corrupted. Reliability is not the absence of failure; it is the ability to fail visibly, recover gracefully, and explain what happened. This page is about the unglamorous engineering that decides whether users trust the app after the first bad day. ## Failure modes | Failure | What the user sees | What the app should do | | --- | --- | --- | | Disk full | Save fails | Keep the draft in memory, offer export, explain | | Permission denied | File cannot open | Explain which permission; suggest the portal | | Network down | Sync fails | Keep local data, retry with backoff, show offline state | | Keyring locked | Secret unavailable | Prompt to unlock; do not crash | | Portal missing | File dialog unavailable | Fall back to a clear explanation, not a dead button | | Corrupt config | App fails to start | Back up, reset to defaults, tell the user | | Corrupt database | Data unreadable | Restore backup, run integrity check, preserve the file | | Compositor restart | Window disappears/reappears | A Wayland compositor restart normally kills the client connection; persist state continuously and restore on the next launch | | Suspend/resume | Timers fire late | Recompute state, reconnect, refresh | | Out of memory | App killed | The kernel sends `SIGKILL`; use `GMemoryMonitor` warnings to drop caches and bound memory before exhaustion | | Dependency missing | Import/startup error | Ship the runtime; fail with a clear message | ## Error handling Handle errors at the layer that can do something about them. - **Infrastructure** returns typed errors with context. - **Application** decides: retry, fall back, or surface. - **UI** presents a human message and a next step. - **Logging** records the technical detail with a correlation ID. Do not swallow exceptions. Do not show a raw traceback as the primary message. Do not use a crash as control flow. ```python try: self.repository.save(note) except StorageFullError as error: self.show_error( title="Could not save the note", body="The disk is full. Free some space, then try again.", details=str(error), retry=self.save_current_note, ) ``` The “Details” expansion can show the technical error; the primary message must be understandable. ## Logging Logs are for diagnosis, not for users. They should be structured, leveled, rotated, and free of secrets. ### GLib / C ```c #define G_LOG_DOMAIN "notes" g_message("Opened database at %s", path); g_warning("Sync failed: %s", error->message); g_debug("Loaded %u notes", count); ``` GLib logs go to stderr, which systemd captures in the journal when the app is launched from a unit or desktop session. Use `g_log_structured()` if you need key-value fields. ### Python ```python import logging log = logging.getLogger("notes") log.info("opened database", extra={"path": path}) log.warning("sync failed", extra={"error": str(error)}) ``` Configure one handler at startup. Do not call `logging.basicConfig()` in library code. Send logs to stderr; let the session capture them. ### Qt ```cpp Q_DECLARE_LOGGING_CATEGORY(notes) Q_LOGGING_CATEGORY(notes, "notes") qCInfo(notes) << "Opened database" << path; qCWarning(notes) << "Sync failed" << error; ``` Use `QT_LOGGING_RULES="notes.debug=true"` to enable categories at runtime. ### Rust ```rust use tracing::{info, warn}; info!(path = %path.display(), "opened database"); warn!(error = %error, "sync failed"); ``` Initialize `tracing-subscriber` once, with a level from `RUST_LOG` or a `--debug` flag. Do not log secrets or full document contents. ### Log levels | Level | Use | | --- | --- | | Error | The operation failed and the user needs to know | | Warning | Something is degraded but recoverable | | Info | Lifecycle events: start, open, save, sync | | Debug | Diagnostic detail, off by default | | Trace | Very verbose, development only | Rate-limit repeated warnings. A broken network should not produce one log line per retry per second. ## journald On a systemd desktop, stderr is captured by the journal. Users and support can inspect it: ```bash journalctl --user -u notes.service -f journalctl --user --since "1 hour ago" | grep notes ``` For a GUI app launched from the desktop, the journal may show the app’s stderr under the session. Use `journalctl --user -b` and filter by the app name or a structured field. Do not write your own log rotation unless you have a specific need. If you do write a log file, put it in `XDG_STATE_HOME`, rotate it, and bound its size. ## systemd user units A GUI background service belongs in a user unit, not a hand-rolled daemon. Start from `graphical-session.target`, use `PartOf=graphical-session.target`, set `StateDirectory=`, and add basic hardening (`NoNewPrivileges=true`, `PrivateTmp=true`, `ProtectSystem=strict`). Test `systemctl --user daemon-reload`, `enable --now`, and `journalctl --user -u`. ## Crash reporting ### Local crash dumps Ubuntu uses `systemd-coredump`. Check for crashes: ```bash coredumpctl list notes coredumpctl info notes coredumpctl gdb notes ``` Ship with symbols or a separate debug package so the stack trace is useful. Do not strip everything and then ask users for a backtrace. ### Remote crash reporting Remote reporting must be **opt-in** and privacy-preserving. - Show a crash dialog with “Send report” and “Don’t send”. - Explain what is included (stack trace, app version, OS, GPU) and what is not (documents, paths, secrets). - Scrub paths and usernames. - Use a project-specific DSN/key; do not embed a shared secret. - Provide a way to inspect the payload before sending. - Honor the user’s choice permanently. Choose a crash backend by its data-retention policy, self-hosting option, SDK license, and redaction controls. The important part is consent and redaction, not the vendor name. ## Data integrity - Write files atomically: temp file in the same directory, `fsync`, rename, then `fsync` the parent directory. The directory sync is what makes the rename durable after a power loss. - Use SQLite transactions for multi-step writes. - Enable WAL mode for concurrent reads; handle `SQLITE_BUSY`. - Version the schema and migrate forward in a transaction. - Back up before a destructive migration. - Run `PRAGMA integrity_check` on startup or on demand; if it fails, preserve the file and offer recovery. - Never truncate a file before the replacement is durable. ```python with connection: version = connection.execute("PRAGMA user_version").fetchone()[0] if version < 2: connection.execute("ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0") connection.execute("PRAGMA user_version = 2") ``` ## Recovery Users should be able to recover from a bad state without losing everything. - **Autosave** drafts and restore them after a crash. - **Session restore** for open windows, tabs, and scroll positions. - **Safe mode** that starts with settings disabled when a plugin or config causes a crash. - **Reset settings** without deleting user data. - **Export data** in an open format before a destructive operation. - **Backup rotation** for the database and config. A “Reset” button that silently deletes documents is not recovery; it is data loss. ## Diagnostics Give support a way to collect the facts: ```bash notes --version notes --debug notes --doctor ``` `--doctor` can check: - app version, runtime, toolkit version; - session type (Wayland/X11) and desktop; - portal availability; - permissions (Flatpak/Snap); - config/data paths and writability; - database integrity; - recent errors (redacted). A diagnostic bundle should be a single file the user can review before sending. Never include documents or secrets by default. ## Resource cleanup - Cancel async work when a view closes. - Disconnect signal handlers when a widget is destroyed. - Release idle inhibitors, screen casts, and file locks. - Close database connections and flush writes on exit. - Remove temporary files and sockets. - Handle `SIGTERM` and session logout gracefully; do not block shutdown indefinitely. ## Reliability checklist - ☐ Every I/O operation has an error path. - ☐ The app can start with a missing config, corrupt cache, and read-only data dir. - ☐ Writes are atomic; migrations are transactional. - ☐ Logs are leveled, rate-limited, and free of secrets. - ☐ Crashes produce a useful local backtrace. - ☐ Remote crash reporting is opt-in and redacted. - ☐ Autosave and session restore exist for editable content. - ☐ There is a safe mode or settings reset. - ☐ `--version`, `--help`, and `--debug` work. - ☐ The app persists state across suspend/resume and compositor restart, and handles OOM warnings without assuming it can catch `SIGKILL`. - ☐ Background work is cancellable and cleaned up. - ☐ Support has a diagnostic bundle path. ## Sources - [systemd-coredump](https://www.freedesktop.org/software/systemd/man/latest/systemd-coredump.html) - [journald](https://www.freedesktop.org/software/systemd/man/latest/systemd-journald.service.html) - [GMemoryMonitor](https://docs.gtk.org/gio/iface.MemoryMonitor.html) - [SQLite WAL](https://www.sqlite.org/wal.html) - [SQLite integrity\_check](https://www.sqlite.org/pragma.html#pragma_integrity_check) - [systemd user units](https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html) Next: [AppStream & Metadata](/linux-desktop-guide/ship/appstream/) — how the software center sees your app. --- # Rust on the Desktop URL: https://digitalesich.github.io/linux-desktop-guide/engineering/rust/ > Rust desktop options compared — gtk-rs, Relm4, Iced, Slint, libcosmic — with a complete gtk-rs project, actions, async patterns, testing, accessibility, and Flatpak packaging. Rust is a strong choice for Linux desktop apps. GNOME’s Loupe and Resources are written in Rust, COSMIC is built on Rust, and the gtk-rs bindings are mature enough for production. You get memory safety, fearless concurrency, and a packaging story that fits Flatpak well. The [gtk-rs](https://gtk-rs.org/gtk4-rs/stable/latest/book/) and [Relm4](https://relm4.org/book/stable/) ecosystems are active; the trade-offs are real: longer compile times, a smaller pool of UI contributors, and a toolkit object model that does not map cleanly to Rust’s ownership rules. None of them are blockers if you choose the right layer. Ubuntu documents both the archive Rust toolchain and the `rustup` path, along with the versions available in each release. Use the [Ubuntu Rust guide](https://ubuntu.com/developers/docs/toolchains/rust/) when you need an install path, and the [version reference](https://ubuntu.com/developers/docs/reference/availability/rust/) before you promise support for an older LTS. ## The options | Stack | Model | Best for | Notes | | --- | --- | --- | --- | | **gtk-rs + libadwaita-rs** | Direct GTK bindings | GNOME-native apps | Most native; object model is GTK’s | | **Relm4** | Component/message architecture on gtk-rs | GNOME apps with complex state | Ergonomic; good docs and examples | | **Iced** | Elm-inspired, custom renderer | Cross-platform tools, games-adjacent UIs | Not GNOME-native; smaller ecosystem | | **Slint** | Declarative UI, custom renderer | Embedded and cross-platform products | Licensing depends on use | | **libcosmic** | COSMIC toolkit | COSMIC-native apps | Young, tied to System76’s ecosystem | For a GNOME/Ubuntu app, use **gtk-rs + libadwaita-rs**, or **Relm4** if you want a structured message loop. For a custom-rendered cross-platform tool, evaluate Iced or Slint before committing. ### Version and feature matrix | Crate | Version in the 2026 stack | Features to enable | | --- | --- | --- | | `gtk4` | 0.11.x | `v4_22` or `gnome_50` | | `libadwaita` | 0.9.x | `v1_9` | | `relm4` | 0.11.x | Match the gtk4 feature set | | `iced` | 0.14.x | Default renderer; evaluate accessibility yourself | | `slint` | 1.18.x | Check the license for your use | | `libcosmic` | git-pinned | Track the COSMIC release you target | Feature flags are compile-time. If you enable `v4_22`/`v1_9`, the binary will not run against an older GTK/libadwaita; use runtime checks only if you build separate compatibility paths. ## Project skeleton ```text notes/ ├── Cargo.toml ├── src/ │ ├── main.rs │ └── note.rs ├── data/ │ ├── io.github.yourname.Notes.desktop │ ├── io.github.yourname.Notes.metainfo.xml │ └── io.github.yourname.Notes.svg └── io.github.yourname.Notes.yml ``` Keep the domain logic in its own module so it can be tested without a display. `main.rs` owns the application and window; `note.rs` owns the data rules. ## A minimal gtk-rs app ```toml [package] name = "notes" version = "0.1.0" edition = "2024" [dependencies] gtk = { package = "gtk4", version = "0.11", features = ["v4_22"] } libadwaita = { version = "0.9", features = ["v1_9"] } ``` ```rust pub fn normalize_title(value: &str) -> String { value.trim().to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn trims_surrounding_whitespace() { assert_eq!(normalize_title(" Buy milk "), "Buy milk"); } } ``` ```rust mod note; use adw::prelude::*; use gtk::{gio, glib}; fn main() -> glib::ExitCode { let app = adw::Application::builder() .application_id("io.github.yourname.Notes") .build(); app.connect_activate(build_ui); app.run() } fn build_ui(app: &adw::Application) { let window = adw::ApplicationWindow::builder() .application(app) .title("Notes") .default_width(980) .default_height(680) .build(); let header = adw::HeaderBar::new(); let add_button = gtk::Button::builder() .icon_name("list-add-symbolic") .tooltip_text("New note") .action_name("win.new-note") .build(); add_button.add_css_class("suggested-action"); header.pack_start(&add_button); let list = gtk::ListBox::new(); list.set_selection_mode(gtk::SelectionMode::Single); list.add_css_class("boxed-list"); let scroller = gtk::ScrolledWindow::builder() .hscrollbar_policy(gtk::PolicyType::Never) .vexpand(true) .child(&list) .build(); let toolbar = adw::ToolbarView::new(); toolbar.add_top_bar(&header); toolbar.set_content(Some(&scroller)); window.set_content(Some(&toolbar)); let new_note = gio::SimpleAction::new("new-note", None); new_note.connect_activate(glib::clone!( #[weak] list, move |_, _| { let row = adw::ActionRow::builder() .title(note::normalize_title("Untitled note")) .build(); list.append(&row); } )); window.add_action(&new_note); app.set_accels_for_action("win.new-note", &["n"]); window.present(); } ``` This app keeps notes in memory. The [first-app tutorial](/linux-desktop-guide/start/first-app/) shows the XDG persistence, atomic-write, and Flatpak parts; replace the in-memory list with `gio::File` or SQLite when you add storage. The Rust example is illustrative: build it against the crate versions in the table above and the GTK/libadwaita development packages for your distribution. ## State and actions The window owns the `gio::SimpleAction`; the button activates it, and the action appends to the list. That is the smallest useful boundary: the widget tree does not own the data, and a test can exercise the data rule without a window. For a larger app, move the state into a struct and pass messages instead of mutating widgets from several callbacks. Relm4’s component model below is one way to structure that. ## Testing `cargo test` runs the `note` module’s unit test without a display. Add more tests around the pure data layer before reaching for a UI test: ```bash cargo test cargo clippy --all-targets -- -D warnings cargo fmt --check ``` For the UI, drive the app through AT-SPI with [Dogtail](https://gitlab.com/dogtail/dogtail), or use the GTK test harness under a throwaway compositor. Run session-bus tests inside `dbus-run-session`; keep `weston --headless` or the distribution’s headless GNOME session in CI. `cargo llvm-cov` adds coverage, and the [Testing](/linux-desktop-guide/engineering/testing/) chapter covers the full matrix. ## Accessibility gtk-rs exposes GTK’s accessibility semantics for native widgets. Set labels on icon-only controls and custom widgets: ```rust use gtk::prelude::*; let button = gtk::Button::builder() .icon_name("list-add-symbolic") .build(); button.update_property(&[ gtk::accessible::Property::Label("New note"), gtk::accessible::Property::Description("Create a new note"), ]); ``` Test with Orca and Accerciser, at 200% text scaling and in high-contrast mode. Do not treat a tooltip as the accessible name; use `gtk::accessible::Property::Label` and, for custom widgets, an explicit `AccessibleRole`. ## Ownership and the GTK object model GTK objects are reference-counted and not `Send`. Rust’s ownership rules still apply to your data, but the widget graph is shared. - Keep UI objects on the main thread. - Use `glib::clone!` with weak references for signal handlers to avoid cycles. - Store application state in your own struct, not in a web of `Rc>`. - Use `Rc>` only when you genuinely need shared mutable state on the main thread. - Prefer messages/channels over shared mutable state across threads. ```rust let label = gtk::Label::new(Some("0")); button.connect_clicked(glib::clone!( #[weak] label, move |_| { let count = label.text().parse::().unwrap_or(0) + 1; label.set_text(&count.to_string()); } )); ``` If the label is dropped, the handler does nothing instead of keeping the whole window alive. ## Relm4 Relm4 gives you a component model: `init`, `update`, and a `view!` macro. It is a good fit for apps with clear messages and state transitions. ```rust use relm4::gtk; use relm4::prelude::*; struct AppModel { count: u32, } #[derive(Debug)] enum AppMsg { Increment, } #[relm4::component] impl SimpleComponent for AppModel { type Init = (); type Input = AppMsg; type Output = (); view! { gtk::ApplicationWindow { set_title: Some("Notes"), set_default_width: 980, set_default_height: 680, gtk::Button { set_label: "Increment", connect_clicked => AppMsg::Increment, }, gtk::Label { set_label: &model.count.to_string(), }, } } fn init( _init: Self::Init, root: Self::Root, sender: ComponentSender, ) -> ComponentParts { let model = AppModel { count: 0 }; let widgets = view_output!(); ComponentParts { model, widgets } } fn update(&mut self, msg: AppMsg, _sender: ComponentSender) { match msg { AppMsg::Increment => self.count += 1, } } } ``` Relm4’s macros and component lifecycle are stable, but the API evolves. Pin the version and read the book for your release. ## Async work Rust async runtimes and the GLib main loop are separate worlds. Bridge them deliberately. - Use `glib::MainContext::spawn_local` for futures that touch GTK objects. - Use `async-channel` or `tokio::sync::mpsc` to send results from a Tokio runtime to the main thread. - Do not block the main loop with `block_on`. - Use `gio` async APIs for file and network I/O where possible. - Cancel work when the view closes; use `gio::Cancellable`, a `CancellationToken`, or drop the future. - Send results back with `async-channel` or a similar channel; do not share GTK objects across threads. ```rust glib::MainContext::default().spawn_local(async move { let contents = gio::File::for_path(path) .load_contents_future() .await .map(|(bytes, _etag)| bytes); match contents { Ok(bytes) => label.set_text(&String::from_utf8_lossy(&bytes)), Err(error) => eprintln!("load failed: {error}"), } }); ``` ## Packaging with Flatpak Flathub builds have no network access. Vendor Rust dependencies and provide a source list. ```bash # Generate a Flatpak source list from Cargo.lock python3 flatpak-cargo-generator.py Cargo.lock -o cargo-sources.json ``` ```yaml build-options: append-path: /usr/lib/sdk/rust-stable/bin env: CARGO_HOME: /run/build/notes/cargo CARGO_NET_OFFLINE: "true" sdk-extensions: - org.freedesktop.Sdk.Extension.rust-stable modules: - name: notes buildsystem: simple build-commands: - cargo build --release --offline - install -Dm755 target/release/notes /app/bin/notes - install -Dm644 data/io.github.yourname.Notes.desktop /app/share/applications/io.github.yourname.Notes.desktop - install -Dm644 data/io.github.yourname.Notes.metainfo.xml /app/share/metainfo/io.github.yourname.Notes.metainfo.xml - install -Dm644 data/io.github.yourname.Notes.svg /app/share/icons/hicolor/scalable/apps/io.github.yourname.Notes.svg sources: - type: dir path: . - cargo-sources.json ``` [`type: dir`](https://docs.flatpak.org/en/latest/module-sources.html#dir) is for local development. A Flathub submission uses a `git` source pinned to a tag/commit or an `archive` source with a checksum. The runtime does not include the Rust toolchain; the SDK extension and `CARGO_HOME` above are required for an offline build. Use the `org.gnome.Platform` runtime for gtk-rs/libadwaita apps. The runtime includes GTK and libadwaita; your binary links against them. ## Profiling and release checks - `perf`, `heaptrack`, and `cargo flamegraph` for CPU, allocation, and startup profiles. - `cargo llvm-cov` for coverage; `insta`, `proptest`, and `trybuild` for snapshot, property, and compile-fail tests. - `cargo audit`/`cargo deny` for advisories and license policy; generate an SBOM for releases. - Pin the Rust toolchain (`rust-toolchain.toml`) and the Flatpak SDK extension version. - Measure cold start with `hyperfine`; keep the first window under your budget before optimizing details. ## Common pitfalls | Pitfall | Fix | | --- | --- | | GTK object sent across threads | Keep it on the main thread; send data, not widgets | | `Rc` cycle keeps the window alive | Use weak references in signal handlers | | Blocking the main loop with `block_on` | Use `spawn_local` or a worker + channel | | Enabling GTK features not in the runtime | Match feature flags to the target runtime | | Missing Cargo sources in Flatpak | Generate `cargo-sources.json` | | Long compile times in CI | Cache `target/`, use `sccache` | | Assuming `Send` for UI state | Keep UI state local; use messages across threads | | Ignoring accessibility | gtk-rs exposes GTK semantics; set labels and roles | ## Sources - [gtk-rs book](https://gtk-rs.org/gtk4-rs/stable/latest/book/) - [gtk4 crate](https://docs.rs/gtk4/) - [libadwaita crate](https://docs.rs/libadwaita/) - [Relm4 book](https://relm4.org/book/stable/) - [Flatpak Rust sources](https://github.com/flatpak/flatpak-builder-tools/blob/master/cargo/README.md) - [cargo-audit](https://rustsec.org/) - [cargo-deny](https://embarkstudios.github.io/cargo-deny/) Next: [Performance](/linux-desktop-guide/engineering/performance/) — how to measure and defend the numbers users feel. --- # Testing URL: https://digitalesich.github.io/linux-desktop-guide/engineering/testing/ > A practical test strategy for Linux desktop apps: unit, integration, UI, accessibility, performance, packaging, headless Wayland, and CI release gates. Desktop apps are harder to test than libraries: they have a main loop, a display server, a session bus, a filesystem, and a user. The answer is not “test everything through the UI”. It is a pyramid with fast tests at the bottom and a small number of high-value end-to-end tests at the top. ## The test pyramid | Layer | What it proves | Speed | Tools | | --- | --- | --- | --- | | Unit | Pure logic, parsers, state machines | Milliseconds | pytest, GLib testing, QtTest, cargo test | | Integration | Storage, D-Bus, portals, async flows | Seconds | `GTestDBus`, `dbus-run-session`, temp XDG dirs | | UI | Keyboard flows, layout, accessibility | Seconds–minutes | dogtail, AT-SPI, Qt Quick Test | | Packaging | The artifact users install | Minutes | flatpak-builder, lintian, snapcraft | | Manual | Real desktop, real hardware, Orca | Hours | A human | Most of your tests should be unit and integration tests. UI tests are expensive and flaky; use them for the critical paths, not every button. ## Unit tests ### Python ```python from notes.domain.note import Note from notes.application.notes import NotesService class MemoryRepository: def __init__(self): self.notes = {} def list(self): return list(self.notes.values()) def save(self, note): self.notes[note.id] = note def delete(self, note_id): self.notes.pop(note_id, None) def test_create_trims_title(): service = NotesService(MemoryRepository()) note = service.create(" Hello ", "Body") assert note.title == "Hello" def test_empty_title_falls_back(): service = NotesService(MemoryRepository()) note = service.create(" ", "Body") assert note.title == "Untitled" ``` Run with `pytest -q`. Use fixtures for temp directories and fake clocks. Do not touch the real home directory. ### C / GLib Use `GTest`: ```c #include #include "note.h" static void test_note_title(void) { Note *note = note_new(" Hello ", "Body"); g_assert_cmpstr(note_get_title(note), ==, "Hello"); note_free(note); } int main(int argc, char **argv) { g_test_init(&argc, &argv, NULL); g_test_add_func("/note/title", test_note_title); return g_test_run(); } ``` ### Qt Use `QtTest` for C++ and `TestCase` for QML: ```cpp #include #include "note.h" class TestNote : public QObject { Q_OBJECT private slots: void trimsTitle() { Note note(" Hello ", "Body"); QCOMPARE(note.title(), QString("Hello")); } }; QTEST_MAIN(TestNote) #include "tst_note.moc" ``` ### Rust ```rust #[test] fn trims_title() { let note = notes::Note::new(" Hello ", "Body"); assert_eq!(note.title, "Hello"); } ``` Use `insta` for snapshots, `proptest` for parsers, and `trybuild` for macro/API contracts. ## Integration tests Integration tests exercise the seams: storage, D-Bus, portals, and async work. Keep them isolated from the user’s real session. ### Isolate XDG directories ```python import os from pathlib import Path import pytest @pytest.fixture def xdg(tmp_path, monkeypatch): config = tmp_path / "config" data = tmp_path / "data" state = tmp_path / "state" cache = tmp_path / "cache" runtime = tmp_path / "runtime" for path in (config, data, state, cache, runtime): path.mkdir() runtime.chmod(0o700) monkeypatch.setenv("XDG_CONFIG_HOME", str(config)) monkeypatch.setenv("XDG_DATA_HOME", str(data)) monkeypatch.setenv("XDG_STATE_HOME", str(state)) monkeypatch.setenv("XDG_CACHE_HOME", str(cache)) monkeypatch.setenv("XDG_RUNTIME_DIR", str(runtime)) return tmp_path ``` ### Isolate D-Bus ```bash dbus-run-session -- pytest -q ``` For GLib apps, `GTestDBus` starts a private session bus. For portal tests, run a mock portal or use the real one in a nested session. ### Test async work - Use a fake clock or `GLib.MainLoop` with a timeout. - Assert cancellation: cancel the task and verify no stale state is applied. - Assert error paths: permission denied, disk full, network timeout. - Do not sleep arbitrarily; wait for a condition with a deadline. ## UI tests UI tests should verify the user-visible contract, not implementation details. ### dogtail (AT-SPI) `dogtail` drives the accessibility tree, which makes it closer to a real user than pixel clicking. ```python from dogtail.tree import root def test_create_note(): app = root.application("io.github.yourname.Notes") app.button("New note").click() assert app.child("Untitled note").shows() ``` Advantages: works with keyboard and accessibility semantics; catches missing labels. Limitations: slower, depends on AT-SPI, can be flaky if the tree changes. ### Qt Quick Test ```qml import QtQuick import QtTest TestCase { name: "Notes" function test_createNote() { let model = createNotesModel() model.addNote("Hello") compare(model.count, 1) } } ``` Run with `qmltestrunner -input tests`. ### Screenshot tests Screenshot tests catch layout regressions that unit tests miss. - Run the app in a deterministic environment (fixed theme, font, scale, window size). - Capture with the compositor’s screenshot tool, not by reading the framebuffer. - Compare with a perceptual diff, not pixel equality. - Store baselines per theme and scale. - Review diffs manually; do not auto-accept. On CI, a headless Wayland compositor (`weston --backend=headless`) or Xvfb is usually easier than a full desktop session. Match the display backend to the behavior you are testing. ## Accessibility tests - Run the critical flows with Orca enabled. - Use Accerciser to inspect names, roles, states, and relations. - Use dogtail to assert accessible names and roles. - Test keyboard-only navigation, focus order, and dialog focus traps. - Test at 200% text scaling and in high-contrast mode. - Add accessibility checks to the release checklist, not just the test suite. ## Headless Wayland A headless compositor gives you a real Wayland session without a physical display: ```bash export XDG_RUNTIME_DIR=$(mktemp -d) chmod 700 "$XDG_RUNTIME_DIR" weston --backend=headless --socket=wayland-test & export WAYLAND_DISPLAY=wayland-test export GDK_BACKEND=wayland dbus-run-session -- ./build/notes ``` `weston` is the simplest starting point. For GNOME-specific behavior, use Mutter Devkit or a nested GNOME session. `grim` is wlroots-only; on GNOME use the Shell or portal screenshot path instead. ## Sanitizers, fuzzing, and reproducibility - Build C/C++ with ASan/UBSan in CI; run the parser and protocol tests under `cargo-fuzz` or AFL++. - Use `SOURCE_DATE_EPOCH` and a reproducible build environment; compare artifacts with `diffoscope` or `reprotest`. - Generate an SBOM and run `cargo audit`/`cargo deny`, `pip-audit`, or the equivalent for your ecosystem. - Test the upgrade path and the uninstall path, not just a fresh install. ## Packaging tests The artifact is part of the product. Test it. ```bash # Flatpak flatpak-builder --user --install --force-clean build-dir io.github.yourname.Notes.yml flatpak run io.github.yourname.Notes flatpak info --show-permissions io.github.yourname.Notes # Metadata desktop-file-validate data/io.github.yourname.Notes.desktop appstreamcli validate --no-net data/io.github.yourname.Notes.metainfo.xml flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak install --user -y flathub org.flatpak.Builder flatpak run --command=flatpak-builder-lint org.flatpak.Builder manifest io.github.yourname.Notes.yml # deb dpkg-buildpackage -b -us -uc lintian ../*.changes # Snap snapcraft pack sudo snap install --dangerous notes_0.1.0_amd64.snap snap run notes ``` Run the packaged app on a clean VM or container. A build that only works on your machine is not a release. ## CI A minimal GitHub Actions workflow: ```yaml name: ci on: [push, pull_request] jobs: test: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v4 - run: sudo apt-get update - run: sudo apt-get install -y python3 python3-gi gir1.2-gtk-4.0 gir1.2-adw-1 \ meson ninja-build flatpak flatpak-builder desktop-file-utils appstream \ python3-pytest - run: /usr/bin/python3 -m pytest -q - run: desktop-file-validate data/io.github.yourname.Notes.desktop - run: appstreamcli validate --no-net data/io.github.yourname.Notes.metainfo.xml flatpak: runs-on: ubuntu-26.04 steps: - uses: actions/checkout@v4 - uses: flatpak/flatpak-github-actions/flatpak-builder@v6 with: bundle: notes.flatpak manifest-path: io.github.yourname.Notes.yml cache-key: flatpak-builder-${{ hashFiles('**/*.yml', '**/*.json') }} ``` Cache dependencies, pin the runner image, and upload the built artifact. A CI job that builds a different app than the one you ship is worse than no CI. ## Flaky tests UI tests fail for reasons unrelated to the app: focus, timing, animations, D-Bus races, and screen size. Reduce flakiness: - Disable animations in tests. - Fix the window size and theme. - Wait for conditions, not fixed sleeps. - Isolate XDG, D-Bus, and network. - Use a fresh app instance per test. - Retry only known-flaky tests, and track the flake rate. - Fix the race instead of adding a longer timeout. ## Coverage Coverage is a smell detector, not a goal. - Aim for high coverage of domain logic and parsers. - Do not chase 100% on UI glue. - Use branch coverage where available. - Review uncovered error paths; those are where bugs live. - `coverage.py`, `gcovr`, `cargo llvm-cov`, and `lcov` all work. ## Release gates Before a release, require: - ☐ Unit and integration tests pass. - ☐ Critical UI flows pass under AT-SPI. - ☐ Accessibility checks pass (keyboard, Orca, contrast, scaling). - ☐ Metadata validates. - ☐ Flatpak/deb/Snap artifacts build and launch. - ☐ The app runs with denied permissions and explains the failure. - ☐ The upgrade path from the previous release is tested. - ☐ Performance budgets are checked. - ☐ No known critical security issue is open. ## Sources - [pytest](https://docs.pytest.org/) - [GLib testing](https://docs.gtk.org/glib/testing.html) - [Qt Test](https://doc.qt.io/qt-6/qtest-overview.html) - [dogtail](https://gitlab.com/dogtail/dogtail) - [Flathub linter](https://docs.flathub.org/docs/for-app-authors/linter) - [Flatpak Builder](https://docs.flatpak.org/en/latest/flatpak-builder.html) - [Weston](https://wayland.freedesktop.org/) Next: [Reliability & Logging](/linux-desktop-guide/engineering/reliability/) — what happens when the disk is full, the network is down, and the compositor restarts. --- # AppStream & Metadata URL: https://digitalesich.github.io/linux-desktop-guide/ship/appstream/ > How to write AppStream metainfo, screenshots, release notes, and desktop entries that make a Linux app discoverable, trustworthy, and installable from a software center. A software center does not see your app the way you do. It sees a name, a summary, an icon, screenshots, categories, a license, release notes, and a permission set. If any of those are missing or inconsistent, the app looks unfinished — or never appears at all. **[AppStream](https://www.freedesktop.org/software/appstream/docs/)** is the metadata standard behind GNOME Software, KDE Discover, Flathub, and most distribution catalogs. The file is small, but it is part of the product. ## Where the file goes ```text /usr/share/metainfo/io.github.yourname.Notes.metainfo.xml ``` For Flatpak, install it to `$FLATPAK_DEST/share/metainfo/`. The filename must match the component ID: `io.github.yourname.Notes.metainfo.xml`. Legacy `.appdata.xml` files are still accepted, but new apps should use `.metainfo.xml`. ## A complete example ```xml io.github.yourname.Notes Notes Write and organize notes CC0-1.0 GPL-3.0-or-later

Notes is a small, fast note-taking app for the Linux desktop.

It stores your notes locally, works offline, and integrates with the desktop through standard XDG interfaces.

io.github.yourname.Notes.desktop https://example.org/notes https://example.org/notes/issues https://example.org/notes Your Name Office Utility notes writing markdown Write notes with a calm, keyboard-first interface https://example.org/notes/screenshot-light.png The same notes in dark mode https://example.org/notes/screenshot-dark.png

First public release.

  • Local-first notes with an adaptive sidebar.
  • Keyboard-first navigation and search.
  • Flatpak build with minimal permissions.
notes
``` ## The fields that matter | Field | Why it matters | | --- | --- | | `` | Must match the desktop file, icon, Flatpak ID, and D-Bus name | | `` | The display name, not the binary name; keep it short (Flathub quality guidance prefers under 20 characters) | | `` | One line, no trailing period; Flathub quality guidance targets 10–25 characters, max ~35 | | `` | 1–3 short paragraphs; use `
    ` for features | | `` | The installed desktop-file ID (`.desktop`), not the executable command | | `` | The human or organization behind the app | | `` | Controls menu placement and discovery | | `` | Improves search; use real user terms | | `` | The single biggest factor in install conversion; tag non-English captures with `xml:lang` | | `` | Version history and user-facing notes | | `` | Required for many catalogs; [OARS 1.1](https://hughsie.github.io/oars/generate.html) | | `` | Declares binaries, MIME types, or libraries | | `` | Homepage, bug tracker, donation, translate | | `` | Declares the gettext/Qt translation domain so software centers can show available languages | ## Screenshots Screenshots are not decoration. They are how users decide whether the app is for them. - Use the real app, not a mockup. - Show the primary workflow, not a settings page. - Provide light and dark variants when the app supports both. - For Flathub, capture the app window at 1000×700 or smaller (2000×1400 for HiDPI) so text stays legible in the store UI. - Keep the real native window shadow, title bar, and rounded corners; do not crop them out, and do not use transparent backgrounds or fake window frames. - Tag screenshots with the language they show (`xml:lang`), and keep captions to one sentence without a trailing period. - Include a caption that explains what the user is looking at. - Localize captions and images where practical. - Keep the screenshots current; an outdated UI is a trust signal in the wrong direction. Flathub mirrors screenshots from the URLs in the metainfo. Use stable HTTPS URLs, not a temporary CDN link. ## Release notes Release notes are user-facing. Write them for the person deciding whether to update. ```xml

    Search is now instant, even with thousands of notes.

    • New command palette (Ctrl+K).
    • Fixed a crash when the keyring is locked.
    • Improved contrast in high-contrast mode.
    ``` - Put the newest release first. - Use ISO dates (`YYYY-MM-DD`). - Mention breaking changes and migration steps. - Do not paste the git log. - Keep the version consistent with the package and the app’s `--version`. - Treat this as a summary. The task pages and troubleshooting live in the [user documentation](/linux-desktop-guide/ship/documentation/), not in the metainfo file. ## Icons and branding - Install a scalable SVG and a 256×256 PNG under `hicolor`. - Make the icon legible at 16 px; test it in the app grid and the window switcher. - Do not use another project’s logo or a trademark you do not own. - Optional `` colors can tint the software-center page: ```xml #c6ff3d #3f7d20 ``` ## Desktop entry consistency The metainfo and desktop file must agree: | Metainfo | Desktop file | | --- | --- | | `` | Filename; `StartupWMClass` must match the actual `WM_CLASS` (test it) | | `` | Installed desktop-file ID (`.desktop`); `Exec` is the command, not the launchable | | `` | `Name` | | `` | `Comment` | | `` | `Categories` | | `` | `Keywords` | | MIME types in `` | `MimeType` | A mismatch is a common review failure. Validate both files in CI. ## Validation ```bash appstreamcli validate --no-net data/io.github.yourname.Notes.metainfo.xml desktop-file-validate data/io.github.yourname.Notes.desktop ``` Fix every error. Warnings are worth reviewing; some are stylistic, some become errors in the next AppStream release. For Flathub, also run: ```bash flatpak-builder-lint manifest io.github.yourname.Notes.yml ``` ## Common validation errors | Error | Cause | Fix | | --- | --- | --- | | `metainfo-filename-cid-mismatch` | ID differs from filename/Flatpak | Use one ID everywhere | | `missing-launchable` | No `` | Add the desktop-file ID | | `description-markup` | Invalid HTML/XML in description | Use `

    `, `

      `, `
    • `, ``, `` only | | `screenshot-no-media` | No image URL | Add a stable HTTPS screenshot | | `releases-info-missing` | No `` | Add at least one release | | `developer-info-missing` | No developer | Add `` | | `metadata-license-missing` | No metadata license | Add a valid metadata license | | `url-homepage-missing` | No homepage | Add `` | | `category-invalid` | Unknown category | Use the AppStream category list | | `summary-too-long` | Summary is a paragraph | Keep it short; Flathub quality guidance targets 10–25 characters | Icon and desktop-file consistency checks happen in `appstreamcli compose` and Flathub’s linter, not in the raw `validate` output. Run both. ## Metadata checklist - ☐ ID matches everywhere: metainfo, desktop, icon, Flatpak, D-Bus. - ☐ Name and summary are user-facing and concise. - ☐ Description explains the value, not the implementation. - ☐ At least one screenshot, light/dark variants where useful, with captions and `xml:lang` tags. - ☐ Release notes for the current version, newest first. - ☐ License is valid SPDX and matches the source. - ☐ Developer and contact URL are present. - ☐ Categories and keywords are accurate. - ☐ Content rating is present and accurate. - ☐ The desktop file passes validation. - ☐ If the app is translated, `` or `` is present and the `.mo`/`.qm` files are installed. - ☐ The metainfo passes `appstreamcli validate --no-net` and `flatpak-builder-lint appstream`. - ☐ The icon is installed and legible at 16 px. ## Sources - [AppStream documentation](https://www.freedesktop.org/software/appstream/docs/) - [Flathub metainfo guidelines](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines) - [Flathub quality guidelines](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines) - [Desktop Entry specification](https://specifications.freedesktop.org/desktop-entry-spec/latest/) - [Icon Theme specification](https://specifications.freedesktop.org/icon-theme-spec/latest/) - [OARS](https://hughsie.github.io/oars/generate.html) Next: [Flathub](/linux-desktop-guide/ship/flathub/) — the submission and maintenance process. --- # Flathub URL: https://digitalesich.github.io/linux-desktop-guide/ship/flathub/ > The complete Flathub submission and maintenance workflow: app IDs, manifests, linting, pull requests, review, runtime updates, verification, and the mistakes that get submissions… Flathub is a major vendor-neutral app store for Linux. It gives users a trusted place to install Flatpak apps, gives developers delta updates and a software-center listing, and gives the ecosystem a shared runtime model. It is the common delivery channel for the guide’s [Platform Targets](/linux-desktop-guide/targets/); the target pages record the runtime and the quality gates. It is also curated. A Flathub listing is a signal that the app builds from source, respects the sandbox, ships valid metadata, and has a maintainer who will keep it working. Treat the submission as a product launch, not a pull request. ## Before you submit You need: - A **working app** with a meaningful feature set and a real development history. - A **reverse-DNS application ID** you control, such as `io.github.yourname.Notes`. - A **source license** that allows redistribution, and a valid `metadata_license`. - A **desktop entry**, **icon**, and **AppStream metainfo** that pass validation. - **English localization** for the UI and metadata. - A **Flatpak manifest** that builds from source with no network access. - **Minimal static permissions**, with portals used where possible. - **Screenshots** and **release notes**. - A **maintainer** who will respond to reviews and runtime updates. Flathub reviewers are volunteers. A submission that ignores the requirements, uses a manifest generated without understanding, or asks reviewers to do the packaging work is likely to be closed. Generative AI policy Flathub requires disclosure of AI-generated code, documentation, and packaging. Flatpak manifests must not contain AI-generated or AI-assisted content. Do not let an agent open or automate the submission PR. Read the current [requirements](https://docs.flathub.org/docs/for-app-authors/requirements) before submitting. ## The application ID The ID is permanent. Choose it carefully. - Format: `{tld}.{vendor}.{product}`, at least three components, at most five. - Must match the metainfo ``, the desktop file name, the icon name, the Flatpak ID, and the D-Bus name. - For code-hosting IDs, use `io.github.username.App`, `io.gitlab.username.App`, `page.codeberg.username.App`, or `io.frama.username.App`. - The domain must be controlled by the project, or the code-hosting repository must be reachable. - Do not use a domain you do not control. Do not use another project’s namespace. Renaming later is possible but reviewed conservatively. Get it right once. ## The manifest Flathub requires the manifest at the top level, named after the app ID, with `.json`, `.yml`, or `.yaml`. ```yaml id: io.github.yourname.Notes runtime: org.gnome.Platform runtime-version: '50' # Ubuntu 26.04 baseline; rebase when the runtime moves sdk: org.gnome.Sdk command: notes finish-args: - --socket=wayland - --socket=fallback-x11 - --share=ipc - --device=dri modules: - name: notes buildsystem: meson sources: - type: git url: https://github.com/yourname/notes.git tag: v0.1.0 commit: 0123456789abcdef0123456789abcdef01234567 ``` `type: dir` is for local tutorials only. A Flathub submission must point at a public source (or include local sources in the PR when that is explicitly allowed); pin a tag and commit, and use an `archive` source with a checksum if you distribute a tarball. Rules: - Use the latest runtime available on Flathub at submission time. - Build everything from source; no prebuilt binaries, no network during build. - Keep `finish-args` minimal and explain every permission in the PR. - Do not include generated AI manifests. - Include license files for every module under `$FLATPAK_DEST/share/licenses/$FLATPAK_ID`. - Use `cleanup` and `cleanup-commands` to remove build artifacts. - Keep the manifest readable: logical key order, consistent indentation, no comments in JSON. ## Build and test locally Use the official builder: ```bash flatpak remote-add --if-not-exists --user flathub https://dl.flathub.org/repo/flathub.flatpakrepo flatpak install --user -y flathub org.flatpak.Builder flatpak run --command=flathub-build org.flatpak.Builder --install io.github.yourname.Notes.yml flatpak run io.github.yourname.Notes ``` Test the things reviewers will test: - Launch from the app grid and from the command line. - Open a file through the file manager. - Use the file chooser, notifications, and secrets. - Run with the network disabled if the app does not need it. - Check the app in light and dark mode, at 100% and 200% scaling. - Uninstall and reinstall; verify data survives or is removed as documented. - Check the desktop file and metainfo in a software center. ## Lint ```bash flatpak run --command=flatpak-builder-lint org.flatpak.Builder manifest io.github.yourname.Notes.yml flatpak run --command=flatpak-builder-lint org.flatpak.Builder repo repo appstreamcli validate --no-net data/io.github.yourname.Notes.metainfo.xml desktop-file-validate data/io.github.yourname.Notes.desktop ``` Fix errors; review warnings. Some linter errors require an exception with a justification. Do not disable a check to hide a real problem. ## Submit 1. Fork `flathub/flathub` (keep all branches). 2. Clone the `new-pr` branch. 3. Create a branch for your app. 4. Add the manifest and any required files. 5. Commit and push. 6. Open a pull request **against `new-pr`**, titled `Add io.github.yourname.Notes`. 7. Answer review comments; do not close the PR to make changes. 8. When asked, comment `bot, build` to start a test build. ```bash git clone --branch=new-pr git@github.com:yourname/flathub.git cd flathub git checkout -b notes-submission new-pr # add io.github.yourname.Notes.yml git add io.github.yourname.Notes.yml git commit -m "Add io.github.yourname.Notes" git push origin notes-submission ``` The PR template asks for permission justifications, build instructions, and confirmation that the app is functional. Fill it in. Reviewers can reject a PR that ignores the template. ## Review and approval - Reviewers may ask for changes to permissions, metadata, the manifest, or the app itself. - Keep the conversation technical and responsive. - Do not merge `master` into the submission branch. - Once approved, the app gets its own repository under the Flathub organization. - Accept the write-access invitation within a week and enable 2FA. After the first build is published, the listing appears on flathub.org. Screenshots from test builds do not appear; only official builds do. ## Maintenance - **Runtime updates:** move to a new runtime before the old one is EOL. Check the runtime support window, rebase the manifest, run `flatpak update`, and test the app on the new runtime. Do not assume an Ubuntu LTS window applies to a GNOME or KDE runtime. - **App updates:** open a PR against your app’s Flathub repository, not the main `flathub/flathub` repo. - **Beta channel:** create a `beta` branch after the stable app is accepted. - **Architectures:** Flathub builds `x86_64` and `aarch64` by default. Use `flathub.json` only if you must restrict. - **Permissions:** changes are reviewed. Explain new permissions and prefer portals. - **Metadata:** keep screenshots and release notes current. - **Security:** respond to reports; do not abandon the app silently. ## Verification Verified apps show a badge and can use the verified developer portal. Verification usually requires control of the domain in the app ID or a code-hosting repository. Follow the current [verification guide](https://docs.flathub.org/docs/for-app-authors/verification); do not guess the token path. ## Common rejection reasons | Reason | Fix | | --- | --- | | App is non-functional or trivially small | Ship a real, usable app | | Missing or invalid metainfo | Validate and complete it | | Permissions too broad | Use portals; justify each permission | | Network during build | Vendor all sources | | Prebuilt binaries | Build from source | | Wrong app ID or domain | Use a controlled reverse-DNS ID | | No English localization | Translate the UI and metadata | | Missing license files | Install them under `share/licenses` | | EOL runtime | Use the current runtime | | AI-generated manifest | Write and understand the manifest yourself | | No response to review | Maintain the submission | ## Flathub checklist - ☐ App ID is controlled and consistent everywhere. - ☐ Manifest builds from source with no network. - ☐ Runtime is current and supported. - ☐ `finish-args` are minimal and justified. - ☐ Desktop file, icon, and metainfo validate. - ☐ English localization is complete. - ☐ Screenshots and release notes are current. - ☐ License files are installed. - ☐ `flatpak-builder-lint` passes. - ☐ The app launches and works on a clean install. - ☐ The PR targets `new-pr` and follows the template. - ☐ A maintainer is committed to updates. ## Sources - [Flathub requirements](https://docs.flathub.org/docs/for-app-authors/requirements) - [Flathub submission](https://docs.flathub.org/docs/for-app-authors/submission) - [Flathub maintenance](https://docs.flathub.org/docs/for-app-authors/maintenance) - [Flathub runtimes](https://docs.flathub.org/docs/for-app-authors/runtimes) - [Flathub verification](https://docs.flathub.org/docs/for-app-authors/verification) - [Flathub linter](https://docs.flathub.org/docs/for-app-authors/linter) - [Flatpak documentation](https://docs.flatpak.org/) Next: [Release Checklist](/linux-desktop-guide/ship/release-checklist/) — the final pass before users install. --- # Release Checklist URL: https://digitalesich.github.io/linux-desktop-guide/ship/release-checklist/ > A practical pre-release, release-day, and post-release checklist for Linux desktop apps — quality gates, packaging, metadata, UX matrix, and rollback. A release is not “the tests pass and the tag is pushed”. It is the moment the app becomes someone else’s daily tool. This checklist is designed to catch the failures that unit tests do not: the dark-mode layout that clips, the Flatpak permission that blocks the file picker, the upgrade that loses settings. Use it as a living document. Delete items that do not apply; add the ones your users keep hitting. This checklist implements the mandatory [release criteria](/linux-desktop-guide/ship/quality-bar/#release-criteria). A failure in data preservation, accessible core tasks, justified permissions, or a supported installation and update path blocks release; the quality score does not override it. ## 1\. Freeze and version - ☐ The version number is final and follows semantic versioning. - ☐ The version matches the AppStream release, the package version, and `--version`. - ☐ The changelog/release notes are written for users. - ☐ Translations are complete for the release; the string freeze is in effect until release. - ☐ The git tag is signed and points to the release commit. - ☐ The build is reproducible from the tag in a clean environment. ## 2\. Quality gates ### Tests - ☐ Unit tests pass. - ☐ Integration tests pass. - ☐ Critical UI flows pass (create, open, save, delete, undo). - ☐ Packaging smoke test passes on a clean VM/container. - ☐ Upgrade from the previous release is tested. - ☐ The app starts with a fresh config and with an existing config. ### Accessibility - ☐ Keyboard-only flow works end to end. - ☐ Focus is visible and logical; dialogs trap and restore focus. - ☐ Orca reads the main flows without missing labels. - ☐ Contrast passes in light, dark, and high-contrast modes. - ☐ 200% text scaling and 200% display scaling work. - ☐ Reduced motion is respected. - ☐ Icon-only buttons have accessible labels. ### Performance - ☐ Cold start is within budget on the oldest supported hardware. - ☐ The first window appears before data loading completes. - ☐ Scrolling holds 60 fps in a realistic dataset. - ☐ Idle CPU is near zero; no unnecessary timers. - ☐ Memory stays bounded during a 30-minute session. - ☐ Large files and large lists do not freeze the UI. ### Security and privacy - ☐ Flatpak/Snap permissions are minimal and justified. - ☐ Secrets use the Secret Service, not config files. - ☐ Logs and crash reports contain no secrets or personal data. - ☐ Telemetry is opt-in and documented. - ☐ Dependencies are pinned and scanned for known CVEs. - ☐ `SECURITY.md` has a working contact. ### Documentation See [User Documentation](/linux-desktop-guide/ship/documentation/) for the full gate. - ☐ Install, first run, and every core task have a task-based page. - ☐ Changed steps and screenshots were tested on the reference target. - ☐ Release notes list user-visible changes, migration steps, and known issues. - ☐ Error messages and empty states match the new behavior. - ☐ Documentation is HTML, keyboard accessible, and passes WCAG 2.2 AA. - ☐ Internal links and anchors pass; no page describes a removed feature. - ☐ The review date, version, and reviewer are recorded in the release change. ## 3\. UX matrix Test the app in each of these states, not just your development setup. | Dimension | States to test | | --- | --- | | Color scheme | Light, dark, high contrast | | Accent | Default, custom system accent | | Scale | 100%, 125%, 150%, 200% | | Text size | Default, 150%, 200% | | Window size | 360 px, 720 px, 1280 px, maximized | | Monitors | Single, dual, different scales, unplug/replug | | Input | Mouse, keyboard only, touchpad, touch, IME, RTL | | Network | Online, offline, slow, captive portal | | Power | Battery, suspend/resume, low-power mode | | Files | Empty, large, read-only, permission denied, corrupt | | Locale | English, German (long words), Arabic (RTL), Japanese | | Session | Wayland-only Ubuntu Desktop; XWayland only for legacy X11 apps | Also test: - ☐ First run with no data. - ☐ Upgrade with existing data. - ☐ Uninstall/reinstall; data survives or is removed as documented. - ☐ File open from the file manager and from the command line. - ☐ Notification actions. - ☐ Screen lock/unlock while a task runs. - ☐ Compositor restart or session logout during a save. - ☐ Disk full during a save (simulate with a small tmpfs). ## 4\. Metadata and assets - ☐ AppStream metainfo validates (`appstreamcli validate --no-net`). - ☐ Desktop entry validates (`desktop-file-validate`). - ☐ Icon is installed in hicolor, legible at 16 px and 256 px. - ☐ Screenshots are current, light and dark, and follow the store’s dimensions (for Flathub, capture at 1000×700 logical pixels; export 2000×1400 for HiDPI). - ☐ Release notes are in the metainfo and on the website. - ☐ The app name, summary, and description match the product. - ☐ License and copyright are installed. - ☐ The about dialog shows the correct version, developers, and license. - ☐ The website/download page links to the current release. ## 5\. Packaging ### Flatpak - ☐ The manifest builds from a clean checkout with no network. - ☐ `flatpak-builder-lint` passes. - ☐ Static permissions are minimal; portals work. - ☐ The app launches from the app grid and the command line. - ☐ The runtime is current and supported. - ☐ The app works on a clean install with no host overrides. - ☐ Data is stored in the sandbox’s XDG directories. - ☐ Uninstall leaves no stray host files. ### deb - ☐ `dpkg-buildpackage` succeeds in a clean chroot. - ☐ `lintian` passes (or every warning is understood). - ☐ Dependencies are correct for the target release. - ☐ The package installs, upgrades, and removes cleanly. - ☐ The desktop file and icon appear after install. - ☐ The app works without the build dependencies installed. ### Snap - ☐ `snapcraft pack` succeeds with `core26` and Snapcraft 9.2+ for the GNOME extension. - ☐ Python apps stage their own runtime; `gsettings` is declared only when needed. - ☐ `snapcraft lint` passes. - ☐ `confinement: strict` is used. - ☐ The app installs and launches on a clean VM. - ☐ Interfaces are minimal; portals work. - ☐ Refresh does not lose data or interrupt work. ### AppImage (if shipped) - ☐ The AppImage runs on a clean distro. - ☐ The desktop file and icon are embedded. - ☐ A checksum and signature are published. - ☐ The update path is documented. ## 6\. Distribution - ☐ The release is tagged and the artifacts are uploaded. - ☐ Checksums and signatures are published. - ☐ Flathub/deb/Snap release notes are live. - ☐ The website download links point to the new version. - ☐ The announcement is accurate and includes migration notes. - ☐ The issue tracker has a release milestone and a known-issues list. - ☐ Support has the diagnostic instructions and the new version number. ## 7\. Release day - ☐ Smoke-test the published artifact, not the local build. - ☐ Install from the store/package manager as a user would. - ☐ Check the App Center and Flathub listings (name, icon, screenshots, version, permissions). - ☐ Verify the update path from the previous version. - ☐ Monitor `coredumpctl`/crash reports and the issue tracker for the first 24 hours. - ☐ Publish an SBOM, signed checksums, and provenance; verify a reproducible build where practical. - ☐ Have a rollback plan: - ☐ previous artifact still available; - ☐ store/package rollback procedure documented; - ☐ data migration is backward-compatible or reversible. ## 8\. Post-release - ☐ Triage new issues within the agreed window. - ☐ Respond to reviews and crash reports. - ☐ Measure adoption and error rates (privacy-preserving). - ☐ Schedule the next patch release. - ☐ Update the roadmap and changelog. - ☐ Thank translators and contributors. ## Release audit Run the canonical [one-hour audit](/linux-desktop-guide/ship/quality-bar/#the-one-hour-audit). It covers the clean install, first launch, file opening, dark mode, 200% scaling, keyboard navigation, error handling, store listing, and upgrade. For a release, add these checks: - Install the exact artifact that was published, not a local build. - Verify the upgrade from the previous version, including its data. - Confirm the release notes and store listing show the new version. If any check fails, fix it before you announce. ## Sources - [AppStream](https://www.freedesktop.org/software/appstream/docs/) - [Flathub requirements](https://docs.flathub.org/docs/for-app-authors/requirements) - [Debian Policy](https://www.debian.org/doc/debian-policy/) - [Snapcraft](https://snapcraft.io/docs/) - [Reproducible Builds](https://reproducible-builds.org/) - [Quality Signals](/linux-desktop-guide/reference/quality-signals/) Next: [The Quality Bar](/linux-desktop-guide/ship/quality-bar/) — a scorecard for the whole product. --- # The Quality Bar URL: https://digitalesich.github.io/linux-desktop-guide/ship/quality-bar/ > A seven-dimension scorecard for Linux desktop apps — from prototype to exemplary — with concrete criteria, an audit method, and a 90-day improvement plan. “Quality” is too vague to argue about and too important to leave implicit. This page turns it into a scorecard you can use in a design review, a release gate, or a conversation with your team. It is not a certification. It is a shared vocabulary for asking: **how native is this app, and what is the next most valuable thing to fix?** ## The seven dimensions | Dimension | The question it answers | | --- | --- | | **Design & UX** | Does the app feel calm, clear, and consistent with the host? | | **Platform integration** | Does it use the desktop’s files, notifications, secrets, and settings? | | **Performance** | Does it start fast and stay responsive? | | **Accessibility** | Can everyone use it, including keyboard and screen-reader users? | | **Reliability & security** | Does it fail safely and protect user data? | | **Distribution & metadata** | Can users find, install, and update it with confidence? | | **Maintainability & community** | Can it be sustained for the next five years? | ## Release criteria Before the score, check the gates that no amount of polish can replace: | Criterion | Evidence | Blocks release when | | --- | --- | --- | | **Data preservation** | Save/crash tests, atomic-write review, migration and recovery runbook | A known path loses user data, or an update cannot preserve it | | **Accessible core tasks** | Keyboard-only pass, Orca/Accerciser review, 200% zoom, reduced motion, high contrast | A core task is unreachable or unusable with assistive technology | | **Justified permissions** | Permission review and a denial test | The app requests broad host access it cannot justify, or fails without a useful denial path | | **Supported installation and updates** | Clean install, launch, update, uninstall, and metadata validation on the declared channel | The declared channel cannot install, update, or remove the app while preserving user data | A missing check is **unresolved**, not passed. A failure on any criterion blocks a release-ready assessment regardless of the aggregate score. ## The scorecard Score each dimension from 0 to 3. | Score | Label | Meaning | | --- | --- | --- | | 0 | Prototype | Works on the developer’s machine; not ready for users | | 1 | Usable | The core task works; platform fit is incomplete | | 2 | Native | Behaves like a first-class citizen of its primary desktop | | 3 | Exemplary | A reference for other apps; strong on secondary desktops too | ### 1\. Design & UX | Level | Criteria | | --- | --- | | 0 | Custom chrome, inconsistent spacing, no empty/error states | | 1 | Recognizable structure; some platform widgets; light/dark works | | 2 | Follows the primary HIG; adaptive layout; keyboard-first; polished states | | 3 | Feels inevitable; brand is distinctive without fighting the host; excellent across sizes and locales | ### 2\. Platform integration | Level | Criteria | | --- | --- | | 0 | Hardcoded paths; no desktop file; custom file picker | | 1 | Desktop file and icon; basic open/save; some notifications | | 2 | Portals, single instance, file activation, secrets, recent files, MPRIS/search where relevant | | 3 | Integration is invisible and reliable; works sandboxed with minimal permissions; degrades gracefully outside the primary desktop | ### 3\. Performance | Level | Criteria | | --- | --- | | 0 | Multi-second startup; UI freezes on common actions | | 1 | Acceptable startup; occasional jank; memory grows | | 2 | Within budget on target hardware; async I/O; 60 fps scrolling; bounded memory | | 3 | Measures and guards budgets in CI; excellent on old hardware; battery-friendly | ### 4\. Accessibility | Level | Criteria | | --- | --- | | 0 | Mouse-only; no labels; poor contrast | | 1 | Keyboard reachable; native widgets; some labels | | 2 | WCAG 2.2 AA; Orca-tested; 200% scaling; reduced motion; high contrast | | 3 | Accessibility is designed in; tested with real AT users; documented statement; regressions gated | ### 5\. Reliability & security | Level | Criteria | | --- | --- | | 0 | Crashes on bad input; secrets in plaintext; no error handling | | 1 | Basic error handling; atomic writes; no obvious secrets | | 2 | Sandboxed, least privilege, keyring, redacted logs, crash reporting opt-in, recovery paths | | 3 | Threat-modeled; fuzzed parsers; signed releases; incident response; security contact | ### 6\. Distribution & metadata | Level | Criteria | | --- | --- | | 0 | No metadata; manual copy of files; no updates | | 1 | Desktop file, icon, basic package; manual updates | | 2 | Valid AppStream, screenshots, Flathub/deb/Snap, signed updates, clean uninstall | | 3 | The declared channel or channels maintained from one tag; reproducible builds; verification; excellent store listing | A single well-maintained channel can reach level 3 when it is the declared support scope. Additional formats are optional and add maintenance cost; they are not a substitute for a working update path. ### 7\. Maintainability & community | Level | Criteria | | --- | --- | | 0 | One person, no tests, no docs, no license | | 1 | Some tests; README; a release process | | 2 | Clear architecture; CI; task-based docs; issue templates; translation workflow; bus factor > 1 | | 3 | Healthy contributors; regular releases; roadmap; docs tested with users and accessible; responsive security process; upstream contributions | User documentation is evidence for this dimension: task pages, tested steps, current screenshots, and an accessible HTML format. See [User Documentation](/linux-desktop-guide/ship/documentation/). ## How to score 1. **Score honestly.** A 3 is rare. Most good apps are 2s with one or two 3s. 2. **Score the shipped artifact**, not the repository. A beautiful demo with a broken Flatpak is not a 2. 3. **Score the primary desktop first.** A GNOME app on KDE can still be a 2 if it degrades gracefully. 4. **Use evidence.** “Accessibility: 2” needs an Orca test, not an intention. 5. **Pick the lowest dimension.** That is the next investment, not the most fun one. A simple rule: - **0–7:** prototype; do not ask users to rely on it. - **8–13:** usable; focus on the weakest dimension. - **14–18:** native; polish and distribution. - **19–21:** exemplary; maintain and share. The bands summarize the seven dimensions; they do not override the release criteria. No dimension should be 0, and no release criterion may fail, at a public release. ## The definition of native Use this as a quick binary check: - ☐ It launches on Wayland and respects the compositor. - ☐ It uses the host’s widgets, icons, and color scheme. - ☐ It follows the primary desktop’s HIG. - ☐ It has a desktop file, icon, and valid AppStream metadata. - ☐ It stores data in XDG directories. - ☐ It uses portals for files, notifications, and secrets. - ☐ It starts fast and never blocks the main loop. - ☐ It is fully keyboard accessible. - ☐ It works at 200% scaling and with reduced motion. - ☐ It ships a reproducible, sandboxed build with minimal permissions. - ☐ It has a maintainer and an update path. If all eleven are true, the app is native by this guide’s definition. That is a platform-fit label, not a release-readiness label; the release criteria above still apply. The scorecard tells you how _good_ it is beyond that. ## The one-hour audit You do not need a week to find the biggest gap. Block one hour: 1. **Install the published artifact** on a clean VM. 2. **Start it cold** and time the first window. 3. **Use it with the keyboard only** for five minutes. 4. **Switch to dark mode and 200% scaling**; look for clipping. 5. **Open a file through the file manager** (tests portals and MIME). 6. **Trigger a failure** (offline, read-only file, denied permission) and read the message. 7. **Inspect the accessibility tree** with Accerciser for the main screen. 8. **Check the software-center listing** and the permissions. 9. **Uninstall and reinstall**; check data behavior. Write down one thing per dimension. The lowest score is your next sprint. ## Common failure modes | Symptom | Likely dimension | First fix | | --- | --- | --- | | Looks great in screenshots, feels wrong in use | Design & UX | Use platform widgets and HIG patterns | | File picker is broken in Flatpak | Platform integration | Use `Gtk.FileDialog` / portal | | Startup feels slow | Performance | Defer work; profile with Sysprof | | Screen reader says “panel” for everything | Accessibility | Use native widgets; add labels | | Users lose data on crash | Reliability | Atomic writes; autosave | | App is invisible in the software center | Distribution | Fix AppStream metadata | | Only one person can release | Maintainability | Document and automate the release | | Reviews complain about permissions | Security/distribution | Remove static permissions; use portals | ## A 90-day plan For a new app that wants to reach “native”: **Days 1–30: foundation** - Choose the primary desktop and toolkit. - Build the core flow with native widgets. - Add XDG storage, desktop file, icon, and metainfo. - Set up CI, tests, and a Flatpak build. - Establish the performance budget. **Days 31–60: integration** - Add portals, single instance, notifications, and settings. - Run the accessibility audit; fix labels, focus, and scaling. - Test on Wayland at 100–200% scaling. - Add error states, autosave, and recovery. **Days 61–90: release** - Run the UX matrix and the release checklist. - Submit to Flathub; fix linter and review feedback. - Publish screenshots and release notes. - Set up crash reporting (opt-in) and a support path. - Schedule the first patch release. ## Sources - [Quality Signals](/linux-desktop-guide/reference/quality-signals/) - [WCAG 2.2](https://www.w3.org/TR/WCAG22/) - [AppStream](https://www.freedesktop.org/software/appstream/docs/) - [Flathub requirements](https://docs.flathub.org/docs/for-app-authors/requirements) - [XDG specifications](https://specifications.freedesktop.org/) Next: [Checklists](/linux-desktop-guide/reference/checklists/) — the condensed, copy-paste version for daily use. --- # User Documentation URL: https://digitalesich.github.io/linux-desktop-guide/ship/documentation/ > How to structure, write, test, and ship user documentation for a Linux desktop app — task-based pages, plain language, accessible formats, and review against ISO/IEC/IEEE 26514,… A user who cannot find out how to finish a task does not have that feature. Documentation is part of the product, not a wiki someone updates when there is time. Most app documentation fails for a predictable reason: it is organized around features instead of tasks. A page called “Sync” explains settings. A page called “Keep two machines in sync” gets someone to a result. The standards below are useful because they push you toward the second shape and give you evidence that the result works. ## What users need | Need | Where it belongs | Example | | --- | --- | --- | | Install and first run | README, software-center listing, in-app welcome | “Install from Flathub, then choose a folder” | | Core tasks | How-to pages, in-app help | “Create, rename, and delete a note” | | Reference | Settings, shortcuts, file formats | “Keyboard shortcuts” | | Troubleshooting | FAQ or recovery page | “The app cannot open my file” | | Changes | Release notes | “What changed in 2.4” | | Immediate feedback | In-app labels, errors, empty states | “This folder is read-only” | The README is not the manual. Keep it short enough to read in two minutes, and link to the task pages from the app and the store listing. ## Four kinds of documentation Diátaxis is a useful structure model, not a standard. It separates documentation by what the reader is trying to do: | Kind | Reader’s question | Example | Test | | --- | --- | --- | --- | | Tutorial | “Teach me the basics” | “Your first note in five minutes” | A new user can finish it without help | | How-to | “How do I …?” | “Import Markdown files” | A user with a goal can complete it | | Reference | “What does this setting do?” | “Command-line options” | Every option is accurate and complete | | Explanation | “Why does it work this way?” | “How sync resolves conflicts” | It answers the question without becoming a procedure | A small app does not need four sections. Start with a README, one tutorial, and one page per core task. Add reference and explanation when users ask for them, not because the model has four boxes. ## Standards worth using | Standard | What it covers | How to use it | What it does not give you | | --- | --- | --- | --- | | [ISO/IEC/IEEE 26514:2022](https://www.iso.org/standard/77451.html) | Design and development of information for users | The backbone of the documentation process: audience, structure, content, review, maintenance | A writing style, a toolchain, or a page template | | [ISO 24495-1:2023](https://www.iso.org/standard/78907.html) | Plain-language principles | Make text relevant, findable, understandable, and usable | Accessibility guidance or software-specific examples | | [ISO/IEC/IEEE 26513:2017](https://www.iso.org/standard/67417.html) | Testing and reviewing information for users | Plan editorial, technical, usability, and accessibility reviews | A test script you can run unchanged | | [IEC/IEEE 82079-1:2019](https://webstore.iec.ch/en/publication/29075) | Information for use of products | A broader product-information baseline when hardware, contracts, or regulation require it | Software-specific process detail | | [ISO 9241-11:2018](https://www.iso.org/standard/63500.html) | Usability: definitions and concepts | Frame task-based evaluation around effectiveness, efficiency, and satisfaction | Methods or documentation requirements | | [WCAG 2.2 AA](https://www.w3.org/TR/WCAG22/) | Accessible web content | The target for HTML documentation and in-app help | Product-documentation obligations | | [EN 301 549 V4.1.1, clause 12](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/04.01.01_60/en_301549v040101p.pdf) | Documentation and support services | Accessible product documentation and accessibility-feature disclosure in the EU | A statement that your product is in scope | A second edition of ISO/IEC/IEEE 26513 was in publication at the time of writing; check [ISO](https://www.iso.org/standard/89070.html) for the current edition. These standards overlap. You do not need to apply all of them to a small app. Use 26514 as the process backbone, 24495-1 for the language, 26513 for the review, and WCAG 2.2 AA for the published format. Add 82079-1 when a product-specific standard, a customer, or a regulated market requires it. **Aligning with a standard is not certification.** Say “structured after ISO/IEC/IEEE 26514” or “reviewed against ISO 24495-1” unless an auditor has checked you against the purchased standard text. Do not put a standard logo on a page that has not been assessed. ## Write the task, not the feature A procedure has five parts, in this order: 1. **Title** that starts with a verb: “Restore a deleted note”. 2. **Prerequisites**: what the user needs before starting. 3. **Steps**: one action per numbered item. 4. **Result**: what the user should see when it worked. 5. **Recovery**: what to do when the likely failure happens. ```text Restore a deleted note You need a note that was deleted in the last 30 days. 1. Open Settings > Trash. 2. Select the note. 3. Press Restore. The note returns to its notebook. If Trash is empty, the note was deleted more than 30 days ago or removed permanently. ``` Rules that prevent most support requests: - Keep one procedure per page. Split a page when the title needs “and”. - Put prerequisites before the first step, not in a note at the bottom. - Use the exact UI label the user sees. If the label changes, update the page in the same change as the code. - Show a command and its expected output together, not a command without context. - State the result. “The note returns to its notebook” tells the user the action worked; “You have restored the note” does not. - Cover the failure the user is most likely to hit. A recovery sentence is worth more than a troubleshooting chapter. - Link to reference instead of pasting an option table into every procedure. - Do not write “simply”, “just”, or “obviously”. They tell the user that their difficulty is their fault. - Test the steps on the target in [Platform Targets](/linux-desktop-guide/targets/), not on the machine where you wrote them. Plain language is not childish language. ISO 24495-1 asks four things of a document: readers get what they need (relevant), can find it (findable), can understand it (understandable), and can use it (usable). In practice: | Before | After | | --- | --- | | “Utilize the synchronization facility to reconcile local state” | “Sync your notes” | | “An error occurred while saving” | “The file is read-only. Save a copy in another folder.” | | “Notes may be optionally configured to be stored remotely” | “You can store notes on a server” | Use second person, active voice, and present tense. Define a term once, then use the same term everywhere. Prefer a concrete example over an abstract description. The full house rules are in the [Style Guide](/linux-desktop-guide/reference/style-guide/). ## In-app text In-app text is documentation at the moment of failure. An error message should answer four questions: 1. What happened? 2. Why? 3. What can the user do now? 4. Is their data safe? ```text Could not save the note. The folder is read-only. Choose another folder, or make this one writable. Your changes are still open in this window. ``` - Name the object and the action. “Could not save the note” is better than “Operation failed”. - Give the next action. If the user can fix it, say how. Do not send them to support for a permission problem. - Include a stable error code for support, but never show the code alone. - Do not blame the user or the system. Describe the state and the way out. - Empty states should say what will appear and offer the first action: “No notes yet. Create your first note.” - Release notes are user-facing. List visible changes, migration steps, and known issues; do not paste the git log. Documentation is not a fix for confusing UI. If every user needs a paragraph to find a button, change the button. ## Accessible documentation Documentation that only exists as a scanned PDF is not documentation for everyone. Publish HTML and make it work with the same standards as the app: - Use one page title, semantic headings, lists, and tables with header cells. - Make every link and control keyboard reachable; keep a visible focus indicator. - Meet WCAG 2.2 AA contrast and support 200% zoom without horizontal scrolling. - Write alt text for screenshots. Caption or transcribe video. - Never put the only copy of an instruction in an image. - Document the app’s accessibility features and how to use them. EN 301 549 clause 12 requires this for in-scope products. - Offer a support channel that works for people who cannot use a phone. - State known limitations instead of implying full conformance. If you sell to the EU public sector, or your product falls under the European Accessibility Act, clause 12 is the relevant baseline. Scope and exemptions depend on the product and the market; confirm applicability with counsel. The technical baseline and testing practice are in [Accessibility](/linux-desktop-guide/design/accessibility/). ## Keep documentation in the repository Treat documentation as source code. Keep it in the same repository, review it in the same change as the behavior it describes, and build it with the same pipeline. ```text docs/ ├── index.md # what the app is, install, first run ├── create-a-note.md # one core task ├── import-markdown.md # another core task ├── shortcuts.md # reference └── troubleshooting.md # recovery README.md # two-minute overview and links CHANGELOG.md # developer-facing history ``` - Keep one source of truth. Do not maintain a wiki and a repository copy that disagree. - Review docs in the same merge request as the code. A follow-up documentation ticket is a promise to ship stale instructions. - Run a link checker, a spelling checker, and a Markdown structure check in CI. Automate the mechanical checks; keep human review for meaning. - Generate screenshots from the shipped build, and retake them when the UI changes. A screenshot from an old release is worse than no screenshot. - Version the docs with the release. If users stay on an older major version, keep its docs available. - Translate task pages before reference pages, and freeze strings before a release. The guide’s own build follows this model: Markdown in `src/content/docs/`, internal link and anchor checks in `npm run check:docs`, and generated Markdown for agents. See [Documentation for agents](/linux-desktop-guide/reference/resources/#agent-access) for that side of the pipeline. ## Review and test ISO/IEC/IEEE 26513 treats documentation as something to review and test, not just proofread. Four passes catch different defects: | Pass | Who | Question | Evidence | | --- | --- | --- | --- | | Editorial | Writer or reviewer | Is it clear, consistent, and complete? | Comments resolved | | Technical | Engineer or QA | Are commands, labels, and versions correct? | Steps run on the reference target | | Usability | Three to five users | Can they finish the task without help? | Notes and pass/fail per task | | Accessibility | Accessibility tester | Does it work with keyboard, screen reader, zoom, and contrast? | Issues filed and fixed | Record the result. A review without a date and a version is a rumor. | Field | Example | | --- | --- | | Page | `create-a-note` | | Version | 2.4.0 | | Target | Ubuntu 26.04 LTS + GNOME | | Reviewed | 2026-11-10 | | Reviewer | Documentation reviewer | | Result | Pass; one screenshot updated | - Watch users attempt the task. Do not explain the page to them; that tests your explanation, not the documentation. - File documentation bugs like code bugs, with the page, the version, and the observed result. - Re-review when the UI, a command, or a supported target changes. - Automate link checks, spelling, heading structure, and screenshot diffs. Keep meaning, accuracy, and task success human. ## Feedback and ownership Documentation decays without an owner and a trigger. Name four roles, even if one person holds several: - **Page owner** — accountable for accuracy and the review record. - **Section owner** — keeps structure, cross-links, and terminology consistent across pages. - **Docs maintainer** — owns the build, link checks, screenshots, and agent output. - **Release manager** — confirms the documentation gate before a release. A named person beats a team name. Write the owner down where the team can see it, not only in someone’s head. ### Review triggers | Trigger | What gets reviewed | Owner | | --- | --- | --- | | UI label or flow change | Task pages, screenshots, in-app text | Page owner | | Release | Release notes, install and upgrade, known issues | Release manager | | Target or version change | Commands, versions, baseline links | Section owner | | Support cluster | Troubleshooting, error messages, prerequisites | Page owner | | Security or privacy change | Permissions, data handling, telemetry | Security reviewer | | Link or style check failure | Affected pages and anchors | Docs maintainer | | Agent miss or wrong answer | Page description, headings, related links | Page owner and docs maintainer | | Translation update | Source strings, screenshots, RTL layout | Localization owner | ### The loop 1. **Collect** — support tickets, search logs, helpful votes, docs issues, usability tests, link and style checks, MCP search misses, and agent reports. 2. **Triage** — tag the page and the cause: missing, wrong, unclear, stale, or unfindable. 3. **Fix** — update the source of truth. If the defect is repeatable, add a check. 4. **Verify** — run the task on the target, check the links, and re-read the page. 5. **Record** — date, version, reviewer, and result. 6. **Close the loop** — tell the reporter, and mention user-visible corrections in the release notes. ### Agent feedback Agents are readers and reviewers, not publishers. - An agent report includes the page slug, section, quote, expected and actual result, version, target, and the source that contradicts the page. - A human owner decides whether to change the page. Agents do not silently rewrite content or publish generated pages. - Treat a failed MCP search, an unknown slug, or a dead-end related link as a findability bug, not just a model failure. - Keep the docs MCP server read-only. Do not add write tools just to collect feedback. - Review agent-generated patches like any other patch, including the [style guide](/linux-desktop-guide/reference/style-guide/) checklist. Report a defect through [Feedback](/linux-desktop-guide/reference/feedback/) or the [structured issue form](https://github.com/digitalesIch/linux-desktop-guide/issues/new?template=docs-feedback.yml). The repository is public; readers need a GitHub account to open an issue. ## Documentation gate Use this as the release gate. The condensed version is in [Checklists](/linux-desktop-guide/reference/checklists/). - ☐ Every core task has a task-based page. - ☐ Install and first run are covered. - ☐ Each procedure has prerequisites, steps, a result, and a recovery path. - ☐ Commands and UI labels were run on the reference target. - ☐ Error messages say what happened, why, and what to do next. - ☐ Release notes describe user-visible changes, migration steps, and known issues. - ☐ Documentation is HTML, keyboard accessible, and passes WCAG 2.2 AA. - ☐ Screenshots are current and have alt text. - ☐ Internal links and anchors pass; no page describes a removed feature. - ☐ The page passes the [style guide](/linux-desktop-guide/reference/style-guide/) checklist. - ☐ The review date, version, and reviewer are recorded. - ☐ Documentation ships in the same release as the app. ## Sources Standards editions and links checked 2026-11-10. - [ISO/IEC/IEEE 26514:2022 — Design and development of information for users](https://www.iso.org/standard/77451.html) - [ISO 24495-1:2023 — Plain language: governing principles and guidelines](https://www.iso.org/standard/78907.html) - [ISO/IEC/IEEE 26513:2017 — Requirements for testers and reviewers of information for users](https://www.iso.org/standard/67417.html) - [ISO/IEC/IEEE 26513 — second edition status](https://www.iso.org/standard/89070.html) - [IEC/IEEE 82079-1:2019 — Preparation of information for use of products](https://webstore.iec.ch/en/publication/29075) - [ISO 9241-11:2018 — Usability: definitions and concepts](https://www.iso.org/standard/63500.html) - [WCAG 2.2](https://www.w3.org/TR/WCAG22/) - [EN 301 549 V4.1.1](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/04.01.01_60/en_301549v040101p.pdf) - [Diátaxis](https://diataxis.fr/) Next: [Release Checklist](/linux-desktop-guide/ship/release-checklist/) — put documentation into the release gate. --- # Agent Access URL: https://digitalesich.github.io/linux-desktop-guide/reference/agent-access/ > How an agent reads the published guide through MCP, the generated Markdown routes, or the local docs server. The guide publishes the same content for people and agents: HTML for readers, Markdown for retrieval, and an MCP server for clients that can start a local process. This page covers the published server and the fallback routes. It does not describe a hosted endpoint; there is none. ## The two MCP servers | Server | Reads | Use it when | | --- | --- | --- | | `docs` | `src/content/docs` in this repository | You edit the guide or work offline. | | `guide` | The published site over HTTPS | Another agent consumes the guide without this checkout. | Both expose the same five read-only tools, so an agent can move between them without relearning the interface. The `docs` server answers from the working tree, including unpublished edits. The `guide` server answers from the last deployment, including the CDN window of up to 10 minutes. Do not point the `guide` server at a source checkout and do not use the `docs` server to check what readers see. They answer different questions. ## Run the published server The server is distributed as `linux-desktop-guide-mcp` and needs Node.js 22 or newer: ```bash npx -y linux-desktop-guide-mcp ``` It needs no API key, account, or repository clone. The package is built from this repository. If your client cannot find the package, use the `llms.txt` routes below; they expose the same content without a local process. ## Client configuration ### OpenCode Add a second local server to `opencode.jsonc`: ```jsonc { "mcp": { "servers": { "guide": { "type": "local", "command": ["npx", "-y", "linux-desktop-guide-mcp"] } } } } ``` ### Claude Desktop Edit `claude_desktop_config.json` in the Claude application data directory: ```json { "mcpServers": { "linux-desktop-guide": { "command": "npx", "args": ["-y", "linux-desktop-guide-mcp"] } } } ``` ### Cursor Add the same block to `.cursor/mcp.json` in the project or `~/.cursor/mcp.json` for every project. ## Tools | Tool | Returns | | --- | --- | | `search_docs` | Ranked pages with a section filter, excerpt, and matching headings. | | `get_page` | One page as Markdown with its title, description, headings, word count, and public URL. | | `list_pages` | The published table of contents grouped by section. | | `get_related` | Related pages with a reason for each recommendation. | | `get_baseline` | The validated Ubuntu, GNOME, GTK, Qt, toolchain, and packaging versions. | Every result includes `fetchedAt` and `stale`. When `stale` is `true`, the network request failed and the server returned the cached copy. The published index is alphabetical within a section, not the sidebar order of the site. `list_pages` says so in its result. ## Cache and offline use The server caches responses outside the project: | Resource | Default lifetime | | --- | --- | | `llms.txt` manifest | 15 minutes | | A page’s `.md` route | 30 minutes | | `llms-full.txt` search corpus | 60 minutes | | `reference/baseline.json` | 60 minutes | Cached entries store `ETag` and `Last-Modified`. An expired entry is usually refreshed with a `304` response and no body transfer. If the network is unavailable, the server returns the cached copy with `stale: true`; a cold cache without a network returns an error that points to `llms.txt`. The cache lives in `~/.cache/linux-desktop-guide-mcp` on Linux, `~/Library/Caches/linux-desktop-guide-mcp` on macOS, and `%LOCALAPPDATA%\linux-desktop-guide-mcp\Cache` on Windows. ## Environment | Variable | Purpose | | --- | --- | | `LDG_SITE_URL` | Read another deployment or a local preview. The default is `https://digitalesich.github.io/linux-desktop-guide/`. | | `LDG_CACHE_DIR` | Move the cache. | | `LDG_NO_CACHE` | Set to `1` to disable cache reads and writes. | | `LDG_CACHE_TTL_MS` | Override every lifetime. Intended for tests and debugging. | The server accepts only the configured origin and base path. Redirects are followed inside that boundary, responses are capped at 2 MB, and requests time out after 10 seconds. It sends one retry for a network error, `429`, or `5xx`, respects `Retry-After` up to five seconds, and sends a user agent with the package version. It writes only to the cache directory and collects no telemetry. ## Without MCP An agent that cannot start a local process can read the same published content directly: ```bash curl https://digitalesich.github.io/linux-desktop-guide/llms.txt curl https://digitalesich.github.io/linux-desktop-guide/start/why-linux.md ``` `llms.txt` is the curated index, `llms-full.txt` is the complete guide in one file, and every page has a `.md` route. Content negotiation with `Accept: text/markdown` is not implemented; request the `.md` route explicitly. ## See also - [Style Guide](/linux-desktop-guide/reference/style-guide/) for the agent-facing Markdown rules. - [Feedback](/linux-desktop-guide/reference/feedback/) to report a broken route or a wrong result. - [Resources](/linux-desktop-guide/reference/resources/) for the upstream documentation behind the guide. --- # Baseline URL: https://digitalesich.github.io/linux-desktop-guide/reference/baseline/ > The validated Ubuntu 26.04 LTS + GNOME reference baseline: GNOME 50, GTK 4.22, libadwaita 1.9, Qt 6.10, and the packaging toolchain the examples are tested against. The full… This page is the validated **Ubuntu 26.04 LTS + GNOME** reference baseline. It is one target in a larger matrix, not the whole guide. The other targets and their roles are documented in [Platform Targets](/linux-desktop-guide/targets/). The values are stored in `src/data/baseline.json`. The page imports that file, and the local documentation MCP server (`docs`) reads the same data. ## Validated baseline | Area | Validated value | | --- | --- | | Ubuntu | 26.04 LTS (Resolute Raccoon) | | Display session | Wayland | | GNOME | 50 | | GTK | 4.22 | | libadwaita | 1.9 | | Qt | 6.10.2 | | KDE Frameworks | 6.24 | | KDE Plasma | 6.6 | | GNOME Flatpak runtime | `org.gnome.Platform//50` | | KDE Flatpak runtime | `org.kde.Platform//6.10` | | Snap base | `core26` | | Flatpak | 1.16.6 | | snapd | 2.78 | | Python | 3.14 | | GCC | 15.2 | | LLVM/Clang | 21 | | Rust | 1.93.1 | | Go | 1.26 | | .NET | 10 | | Java | 25 | | Zig | 0.15.2 | | Astro | 7.4.8 | | Starlight | 0.42.10 | | Node.js | 24 | ## Ubuntu support window | Support | Ends | | --- | --- | | Standard support | May 2031 | | Ubuntu Pro | May 2036 | | Legacy add-on | May 2041 | Ubuntu Pro is optional for app development. The baseline records the support window so readers can judge whether a release is a reasonable target; it does not make Pro a requirement. ## Baseline is not minimum support The baseline is the **tested reference environment**. It is not a claim that older releases are unsupported, and it is not a promise that every combination of components works together. - Use the baseline when you need the current, validated target for examples, screenshots, and recommendations. - Define a separate minimum-version policy when you ship a library or a package for an older LTS. - Use compile-time version checks (`GTK_CHECK_VERSION`, `QT_VERSION_CHECK`) or Rust feature flags, and guard newer APIs only when you genuinely support older releases. - A component can be newer than the baseline in a Flatpak runtime; record that difference in the manifest, not by rewriting this page. ## Review - Validated on: November 10, 2026 - Next review due: May 10, 2027 The review date is a trigger, not a guarantee. When it passes, open an issue for the review, re-check the primary sources below, update `src/data/baseline.json`, and run the verification steps. The review is manual until a scheduled freshness check is in place. ## Local validation On Ubuntu 26.04.1, the following were checked directly while preparing this baseline: - `gnome-shell --version` → GNOME Shell 50.1 - `apt-cache policy libgtk-4-1 libadwaita-1-0` → GTK 4.22.4, libadwaita 1.9.1 - `apt-cache policy qt6-base-dev` → Qt 6.10.2 - `apt-cache policy dotnet10 openjdk-25-jdk zig` → dotnet10, openjdk-25-jdk, and zig are available - `snap info core26` → core26 is available in the Snap Store - `flatpak --version` → Flatpak 1.16.6 - `snap --version` → snapd 2.78 - `desktop-file-validate` and `appstreamcli validate --no-net` on the first-app examples → pass These checks supplement the upstream sources; they do not replace them. Re-run the commands when the baseline is reviewed. ## Primary sources - [Ubuntu 26.04 LTS release notes](https://documentation.ubuntu.com/release-notes/26.04/) - [Ubuntu Desktop documentation](https://documentation.ubuntu.com/desktop/en/latest/) - [Ubuntu for Developers](https://ubuntu.com/developers/docs/) - [GNOME 50 release notes](https://release.gnome.org/50/) - [GTK 4 documentation](https://docs.gtk.org/gtk4/) - [libadwaita documentation](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/) - [Qt 6.10 release](https://www.qt.io/blog/qt-6.10-released) - [Qt 6 documentation](https://doc.qt.io/qt-6/) - [KDE Frameworks 6.24](https://kde.org/announcements/frameworks/6/6.24.0/) - [KDE Plasma 6.6](https://kde.org/announcements/plasma/6/6.6.0/) - [Flatpak documentation](https://docs.flatpak.org/) - [Flathub requirements](https://docs.flathub.org/docs/for-app-authors/requirements) - [Snap documentation](https://snapcraft.io/docs/) - [Snapcraft bases](https://ubuntu.com/docs/snapcraft/stable/reference/bases/) - [Snapcraft core24 to core26](https://ubuntu.com/docs/snapcraft/stable/how-to/change-bases/change-from-core24-to-core26/) ## Updating the baseline 1. Update `src/data/baseline.json`. 2. Move `validated_on` to the review date and set `review_due` six months later. 3. Run `npm run check:baseline`. 4. Run `npm run build` and check the pages that link here. 5. Update the primary-source links if a source moved. 6. Commit the change and note the upstream release in the commit message. The update is a normal OpenSpec change. It should explain which upstream release triggered it and which pages were re-checked. ## See also - [Platform Targets](/linux-desktop-guide/targets/) - [How Targets Are Chosen](/linux-desktop-guide/targets/selection/) - [Ubuntu Platform](/linux-desktop-guide/platform/ubuntu/) - [Choose Your Stack](/linux-desktop-guide/start/choose-stack/) - [GTK & libadwaita](/linux-desktop-guide/engineering/gtk/) - [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) --- # Checklists URL: https://digitalesich.github.io/linux-desktop-guide/reference/checklists/ > Condensed, copy-paste checklists for new Linux desktop projects, releases, accessibility, performance, security, metadata, and Flatpak. Copy these into an issue template, a `RELEASE.md`, or a pull request checklist. ## New project - ☐ Pick a primary desktop and toolkit (GTK/libadwaita or Qt/Kirigami). - ☐ Choose a reverse-DNS application ID and use it everywhere. - ☐ Set up version control, a license, and a README. - ☐ Create the app skeleton with `Gtk.Application` / `QApplication`. - ☐ Add a desktop entry, icon, and AppStream metainfo from day one. - ☐ Store data in XDG directories from the first commit. - ☐ Add a build system (Meson or CMake) and a Flatpak manifest. - ☐ Set up CI: build, tests, metadata validation. - ☐ Define a startup and memory budget. - ☐ Add a `SECURITY.md` and a code of conduct. - ☐ Decide the translation workflow (gettext + Weblate/Damned Lies). ## Before first release - ☐ Core flows work end to end. - ☐ Empty, loading, and error states exist. - ☐ Keyboard-only flow works. - ☐ Light, dark, and high-contrast modes work. - ☐ 100%, 125%, 150%, and 200% scaling work. - ☐ Data persists across restarts; writes are atomic. - ☐ File open/save uses portals or native dialogs. - ☐ Notifications are actionable and rate-limited. - ☐ Secrets use the Secret Service. - ☐ Desktop file, metainfo, and icon validate. - ☐ Flatpak builds and runs with minimal permissions. - ☐ Screenshots and release notes are ready. - ☐ Upgrade from the previous version is tested. - ☐ A rollback path exists. ## Accessibility - ☐ Every action is keyboard reachable. - ☐ Focus is visible, logical, and restored after dialogs. - ☐ Every control has an accessible name and role. - ☐ Icon-only buttons have labels. - ☐ Contrast meets WCAG 2.2 AA. - ☐ Text scales to 200% without clipping. - ☐ Layout works in RTL. - ☐ Reduced motion is respected. - ☐ Orca reads the main flows. - ☐ High-contrast mode works. - ☐ Media has captions/transcripts. - ☐ Accessibility statement and contact exist. ## Documentation Full gate: [User Documentation](/linux-desktop-guide/ship/documentation/). - ☐ Every core task has a task-based page. - ☐ Install and first run are covered. - ☐ Prerequisites, steps, result, and recovery are present. - ☐ Commands and UI labels were run on the reference target. - ☐ Error messages say what happened, why, and what to do next. - ☐ Release notes describe user-visible changes and migration steps. - ☐ Documentation is HTML, keyboard accessible, and passes WCAG 2.2 AA. - ☐ Screenshots are current and have alt text. - ☐ Links and anchors pass; no page describes a removed feature. - ☐ The page passes the [style guide](/linux-desktop-guide/reference/style-guide/) checklist. - ☐ Review date, version, and reviewer are recorded. - ☐ Documentation ships in the same release as the app. ## Performance - ☐ Cold start is within budget on the oldest supported hardware. - ☐ First window appears before data loading finishes. - ☐ Main loop never blocks; I/O is async and cancellable. - ☐ Scrolling holds 60 fps with realistic data. - ☐ Idle CPU is near zero. - ☐ Memory stays bounded over 30 minutes. - ☐ Large files/lists do not freeze the UI. - ☐ Timers stop when the window is hidden. - ☐ Startup benchmark runs in CI. - ☐ A profile has been taken with Sysprof on real hardware. ## Security and privacy - ☐ The app runs without root. - ☐ Flatpak/Snap permissions are minimal. - ☐ Portals are used where available. - ☐ Secrets are in the keyring, never in files or logs. - ☐ File writes are atomic; symlinks/traversal handled. - ☐ Untrusted images use Glycin or an equivalent. - ☐ Network uses TLS, timeouts, and cancellation. - ☐ No shell interpolation of untrusted data. - ☐ Telemetry/crash reporting is opt-in and redacted. - ☐ Dependencies are pinned and monitored. - ☐ Updates come from a signed channel. - ☐ `SECURITY.md` is current. ## Metadata - ☐ App ID matches metainfo, desktop file, icon, Flatpak, D-Bus. - ☐ Name and summary are concise and user-facing. - ☐ Description explains the value. - ☐ At least two screenshots, light and dark. - ☐ Release notes are current, newest first. - ☐ License is valid SPDX. - ☐ Developer and contact URL are present. - ☐ Categories and keywords are accurate. - ☐ Content rating is present. - ☐ `appstreamcli validate --no-net` passes. - ☐ `desktop-file-validate` passes. - ☐ Icon is legible at 16 px. ## Flatpak - ☐ Manifest at the top level, named after the app ID. - ☐ Current runtime and SDK. - ☐ Builds from source with no network. - ☐ No prebuilt binaries. - ☐ Minimal `finish-args`. - ☐ Portal-based file dialogs, notifications, secrets. - ☐ License files installed. - ☐ `flatpak-builder-lint` passes. - ☐ App launches from the app grid. - ☐ App works on a clean install with no overrides. - ☐ Data stays inside the sandbox’s XDG dirs. - ☐ Uninstall leaves no stray host files. ## Ubuntu platform - ☐ Target an LTS release and state its support window. - ☐ Pin the toolchain version used by CI and packaging. - ☐ Choose deb, snap, Flatpak, or OCI deliberately; state which path Ubuntu supports and which alternatives require extra setup. - ☐ Use portals or toolkit APIs for files, printing, notifications, secrets, and screen capture. - ☐ Use D-Bus APIs or the snapd REST API instead of shelling out to `nmcli`, `udisksctl`, `snap`, or `fwupdmgr`. - ☐ Never require `sudo`; use a named Polkit action for privileged work. - ☐ Ship and test an AppArmor profile for any system service. - ☐ Do not flash firmware directly; use fwupd/LVFS. - ☐ Do not require Ubuntu Pro; detect optional services and degrade gracefully. - ☐ Test with Orca, Accerciser, and the Ubuntu accessibility stack, including permission-denied paths. - ☐ Test the snap and deb install/upgrade paths on a clean Ubuntu installation. - ☐ If you ship a Snap, use `core26` and stage Python explicitly for Python apps. - ☐ If you ship Flatpak, document that Ubuntu does not install Flatpak or Flathub by default. - ☐ Validate the desktop file, AppStream metadata, and package with Ubuntu tooling. ## Release day - ☐ The channel choice passes the [quality signals](/linux-desktop-guide/reference/quality-signals/) check, with the date and sources recorded. - ☐ Install the published artifact on a clean VM. - ☐ Create, open, save, and delete data. - ☐ Open a file through the file manager. - ☐ Switch to dark mode and 200% scaling. - ☐ Tab through the main flow without a mouse. - ☐ Trigger an error and read the message. - ☐ Check the software-center listing. - ☐ Verify the upgrade from the previous version. - ☐ Publish checksums/signatures. - ☐ Announce with migration notes. - ☐ Watch crashes and issues for 24 hours. - ☐ Keep the previous artifact available. ## The ten-minute audit If you have ten minutes: 1. Launch the published build. 2. Press `Tab` ten times; can you reach everything? 3. Switch to dark mode; does anything disappear? 4. Set 200% scaling; does anything clip? 5. Open a file through the file manager. 6. Turn off the network; does the app explain itself? 7. Trigger a save error; is the message human? 8. Check the app in the software center. 9. Uninstall; did it leave junk behind? 10. Read the release notes; would you install it? Back to [The Quality Bar](/linux-desktop-guide/ship/quality-bar/) or [Resources](/linux-desktop-guide/reference/resources/). ## Sources - [XDG specifications](https://specifications.freedesktop.org/) - [AppStream](https://www.freedesktop.org/software/appstream/docs/) - [Flathub requirements](https://docs.flathub.org/docs/for-app-authors/requirements) - [Ubuntu 26.04 release notes](https://documentation.ubuntu.com/release-notes/26.04/) - [Quality Signals](/linux-desktop-guide/reference/quality-signals/) - [WCAG 2.2](https://www.w3.org/TR/WCAG22/) --- # Feedback URL: https://digitalesich.github.io/linux-desktop-guide/reference/feedback/ > How to report a wrong, missing, or unclear page in the Linux Desktop Guide — for human readers and agents, with a static GitHub Issues workflow that works on GitHub Pages. The guide is only as good as the corrections it receives. Report a command that fails, a prerequisite that is missing, a screenshot from an old release, a link that no longer resolves, or a page you could not find. This page is also the feedback contract for agents. The site is static and runs on GitHub Pages, so there is no form POST and no server log. GitHub Issues is the transport. ## Open an issue [Open a documentation issue](https://github.com/digitalesIch/linux-desktop-guide/issues/new?template=docs-feedback.yml) The repository is **public**. You need a GitHub account to open an issue; if you are signed out, GitHub will ask you to sign in first. ## What to report | Report | Example | | --- | --- | | Wrong command or output | `flatpak install` uses a runtime branch that is end-of-life | | Missing prerequisite | The page assumes Flatpak is installed | | Stale version or screenshot | The page shows GTK 4.18 but the baseline is 4.22 | | Broken link or anchor | A chapter link lands on a missing heading | | Unclear or incomplete step | A procedure works only with a setting the page never mentions | | Missing page | A core task has no task-based page | | Inaccessible documentation | A table is unreadable at 200% zoom, or an image has no alt text | Do not use this channel for app support, feature requests, or security vulnerabilities. For a vulnerability, use a [private security advisory](https://github.com/digitalesIch/linux-desktop-guide/security/advisories/new), not a public issue. ## What to include The issue form asks for these fields. A report without them usually needs a round trip. | Field | What to write | | --- | --- | | **Page** | The URL or slug, for example `/ship/documentation/` | | **Section** | The heading the problem appears under | | **Quote** | The exact sentence or code block | | **Expected** | What the page should say or do | | **Actual** | What it says or what happened | | **Version / commit** | The guide version, release, or commit you read | | **Target** | Ubuntu 26.04 LTS + GNOME, Fedora 45 KDE, or another supported target | | **Reporter** | Human or agent | | **Source** | The specification, upstream documentation, or test that supports the correction | Do not include secrets, tokens, personal data, or private file contents. A screenshot should be cropped to the relevant UI and stripped of personal information. ## For agents Agents are readers and reviewers, not publishers. They report; a human owner decides and publishes. Use the same fields from the command line: ```bash gh issue create \ --repo digitalesIch/linux-desktop-guide \ --title "[Docs] Wrong runtime branch in the Flatpak example" \ --body-file /tmp/docs-feedback.md ``` ```text Page: /platform/flatpak-portals/ Section: Build and install Quote: runtime-version 46 Expected: A supported runtime branch for the current baseline Actual: The example uses an end-of-life branch Version/commit: 2026-11-10 Target: Ubuntu 26.04 LTS + GNOME Reporter: agent Source: https://docs.flatpak.org/en/latest/available-runtimes.html ``` Rules for agents: - Do not edit the page or open a pull request that rewrites it without human review. - Do not include secrets, personal data, or unpublished user content. - Do not use the docs MCP server as a write path. It is read-only by design. - Treat a failed search, an unknown slug, or a dead-end related link as a findability bug; include the query and the page you expected. - Report the version, target, and source. “The docs are wrong” is not reproducible. ## How reports are handled 1. **Triage** — the maintainer tags the page and the cause: missing, wrong, unclear, stale, or unfindable. 2. **Fix** — the page owner updates the source of truth. A repeatable defect gets a check. 3. **Verify** — the step is run on the stated target, the links are checked, and the page is re-read. 4. **Record** — the review date, version, and reviewer are added to the page or the change. Maintainers add the `agent-report` label to reports from automated agents. 5. **Close** — the reporter is told what changed. User-visible corrections go into the release notes. The full ownership model and review triggers are in [User Documentation](/linux-desktop-guide/ship/documentation/#feedback-and-ownership). The writing rules are in the [Style Guide](/linux-desktop-guide/reference/style-guide/). ## See also - [User Documentation](/linux-desktop-guide/ship/documentation/) — process, standards, review, and the documentation gate - [Style Guide](/linux-desktop-guide/reference/style-guide/) — voice, terminology, links, code, and agent-facing writing - [Checklists](/linux-desktop-guide/reference/checklists/) — the condensed documentation gate --- # Glossary URL: https://digitalesich.github.io/linux-desktop-guide/reference/glossary/ > The Linux desktop terms used in this guide — Wayland, XDG, portals, GTK, libadwaita, Flatpak, AppStream, and more — defined in plain language. Terms are grouped by area. Links point to the canonical specification or documentation where one exists. For how the guide uses and formats these terms, see the [Style Guide](/linux-desktop-guide/reference/style-guide/). ## Platform | Term | Meaning | | --- | --- | | **AT-SPI** | Accessibility Technology Service Provider Interface. The D-Bus protocol that exposes the UI tree to screen readers such as Orca. | | **Compositor** | The Wayland process that owns the display, input, window placement, and security boundary. Examples: Mutter (GNOME), KWin (KDE). | | **D-Bus** | The standard inter-process communication bus on Linux desktops. Used for activation, notifications, settings, hardware services, and portals. | | **PipeWire** | The modern Linux multimedia server for audio, video, and screen sharing. | | **Portal** | An XDG Desktop Portal: a D-Bus interface that lets a sandboxed app ask the desktop to perform a privileged operation (open a file, capture the screen, send a notification). | | **Wayland** | The display protocol where the compositor owns the screen and clients render their own buffers. The Ubuntu Desktop session on 26.04 is Wayland-only; XWayland runs X11 apps. | | **X11** | The legacy display protocol. Still used via XWayland for compatibility, but not the native target on Ubuntu 26.04. | | **XWayland** | A compatibility layer that runs X11 apps inside a Wayland session. | | **XDG Base Directory** | The specification for `XDG_CONFIG_HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, `XDG_CACHE_HOME`, and `XDG_RUNTIME_DIR`. | | **XDG user dirs** | The localized folders (Documents, Downloads, etc.) defined in `~/.config/user-dirs.dirs`. | | **systemd user session** | A per-user systemd instance for background services, timers, and session management. | ## Toolkits | Term | Meaning | | --- | --- | | **Adwaita** | GNOME’s design system and default theme. | | **Blueprint** | A declarative UI language for GTK that compiles to `.ui` files. | | **GApplication** | GLib’s application lifecycle, single-instance, and D-Bus activation class. | | **GIO** | GLib’s I/O, file, network, D-Bus, and settings library. | | **GLib** | The C utility library underneath GTK: data structures, main loop, threads, I/O. | | **GObject** | The object system used by GTK and GNOME libraries: properties, signals, reference counting. | | **GSK** | GTK’s scene graph and rendering library. | | **GSettings** | The schema-based settings API backed by dconf. | | **GTK** | The widget toolkit behind GNOME. GTK 4 is the current stable line. | | **Kirigami** | KDE’s adaptive UI framework built on Qt Quick. | | **KDE Frameworks** | The libraries behind KDE apps: KConfig, KIO, KNotification, KWallet, and more. | | **libadwaita** | The GNOME design-system library that extends GTK with headerbars, split views, preferences, dialogs, and toasts. | | **QML** | Qt’s declarative UI language, used with Qt Quick. | | **Qt** | The cross-platform C++/QML application framework; Qt 6 is current. | ## Packaging and distribution | Term | Meaning | | --- | --- | | **AppImage** | A single-file portable Linux application format. No sandbox or standard update mechanism. | | **AppStream** | The metadata standard used by software centers for names, summaries, screenshots, releases, and licenses. | | **Bubblewrap** | The sandboxing tool used by Flatpak to create namespaces and restrict access. | | **deb** | The Debian/Ubuntu binary package format, installed with `dpkg`/`apt`. | | **Desktop Entry** | The `.desktop` file that tells the desktop how to launch an app and where it appears. | | **Flathub** | A major vendor-neutral Flatpak app store and build service. | | **Flatpak** | A cross-distribution packaging and sandboxing system built on runtimes, OSTree, and portals. It is not part of a stock Ubuntu installation; users install it deliberately. | | **hicolor** | The fallback icon theme hierarchy where apps install their icons. | | **MetaInfo** | The AppStream XML file, usually `.metainfo.xml`, that describes an app to software centers. | | **OSTree** | The content-addressed filesystem used by Flatpak for runtimes and delta updates. | | **PPA** | Personal Package Archive on Launchpad for Ubuntu packages. | | **Runtime** | A shared Flatpak base (GNOME, KDE, Freedesktop) containing the toolkit and common libraries. | | **Sandbox** | An isolated execution environment that restricts filesystem, network, device, and D-Bus access. | | **Snap** | Canonical’s packaging and confinement system, recommended by the Ubuntu App Center for third-party apps. The App Center also manages debs. | | **Snapcraft** | The build tool and recipe format for Snap packages. | | **snapd** | The background daemon that installs, confines, refreshes, and manages snaps. | | **Rockcraft** | Canonical’s tool for building minimal Ubuntu-based OCI images (rocks). | | **Verification** | Flathub’s process for proving that a publisher controls the app’s domain or repository. | ## Design and accessibility | Term | Meaning | | --- | --- | | **Accent color** | The user-selected highlight color used for selection, focus, and primary actions. | | **Adaptive layout** | A layout that changes structure (not just size) at different window widths. | | **HIG** | Human Interface Guidelines. GNOME, KDE, and elementary each publish one. | | **OARS** | Open Age Rating Service, the content rating embedded in AppStream metadata. | | **Reduced motion** | A system preference that asks apps to minimize or disable animations. | | **Symbolic icon** | A monochrome icon that inherits the text color and adapts to themes. | | **WCAG 2.2 AA** | The widely used web/content accessibility standard; desktop apps often use it as a baseline. | ## Security | Term | Meaning | | --- | --- | | **AppArmor** | Ubuntu’s default Linux Security Module; confines programs with per-application profiles. | | **CVE** | Common Vulnerabilities and Exposures: a public identifier for a security flaw. | | **Glycin** | A sandboxed image-loading library for untrusted images, used by GNOME apps. | | **ESM** | Expanded Security Maintenance: Ubuntu Pro updates for packages beyond the standard support window. | | **fwupd** | The D-Bus service and daemon for firmware updates via the Linux Vendor Firmware Service (LVFS). | | **Least privilege** | The principle that an app should request only the access it needs for a feature. | | **Livepatch** | Ubuntu Pro kernel patching that avoids reboots for supported livepatch fixes. | | **Polkit** | The D-Bus authority for privileged desktop actions; apps define named actions instead of using sudo. | | **Seccomp** | Linux syscall filtering, used by sandboxes to reduce the kernel attack surface. | | **Ubuntu Pro** | Canonical’s subscription for ESM, Livepatch, FIPS, CIS, and extended support; optional for apps. | | **Secret Service** | The D-Bus API for the user’s keyring; libsecret is the common client library. | ## Performance | Term | Meaning | | --- | --- | | **Cold start** | Launching the app when it is not already running or cached. | | **Frame time** | The time to produce one frame; 16.7 ms for 60 fps. | | **Sysprof** | The system-wide profiler shipped with GNOME and Ubuntu 26.04. | | **Wakeup** | A CPU wake event from a timer or interrupt; excessive wakeups drain battery. | ## Media and desktop services | Term | Meaning | | --- | --- | | **MPRIS** | The D-Bus interface for media players, used by desktop media controls and `playerctl`. | | **StatusNotifierItem** | The D-Bus-based system tray protocol used by some desktops. Support varies; do not rely on it. | | **PackageKit** | The D-Bus abstraction for package management used by software centers. | | **UPower** | The D-Bus service for battery and power information. | | **WirePlumber** | The modular session and policy manager for PipeWire. | | **BlueZ** | The Linux Bluetooth stack and its D-Bus API. | | **NetworkManager** | The Linux network management service and D-Bus API. | | **UDisks2** | The D-Bus API for disks, partitions, and mounts. | ## Documentation and agents | Term | Meaning | | --- | --- | | **agent docs** | The generated Markdown, `llms.txt`, and `llms-full.txt` that let an agent read the published guide without HTML. | | **docs MCP** | The local `docs` server that reads `src/content/docs` in a checkout and makes no network requests. | | **guide MCP** | The local `guide` server, published as `linux-desktop-guide-mcp`, that reads the deployed site and caches it. | | **MCP** | Model Context Protocol, the interface an agent uses to call the `docs` and `guide` tools. | ## See also - [Style Guide](/linux-desktop-guide/reference/style-guide/) - [The XDG Contract](/linux-desktop-guide/platform/xdg/) - [Flatpak & Portals](/linux-desktop-guide/platform/flatpak-portals/) - [GTK & libadwaita](/linux-desktop-guide/engineering/gtk/) - [Qt & Kirigami](/linux-desktop-guide/engineering/qt/) - [Resources](/linux-desktop-guide/reference/resources/) ## Sources - [XDG specifications](https://specifications.freedesktop.org/) - [Wayland](https://wayland.freedesktop.org/) - [D-Bus](https://www.freedesktop.org/wiki/Software/dbus/) - [XDG Desktop Portals](https://flatpak.github.io/xdg-desktop-portal/docs/) - [AppStream](https://www.freedesktop.org/software/appstream/docs/) - [Flatpak](https://docs.flatpak.org/) - [Snapcraft](https://snapcraft.io/docs/) - [AT-SPI](https://www.freedesktop.org/wiki/Accessibility/AT-SPI2/) --- # Quality Signals URL: https://digitalesich.github.io/linux-desktop-guide/reference/quality-signals/ > How to judge whether a toolkit, runtime, or packaging channel is actively maintained, healthy, and safe to depend on — using observable signals instead of popularity. A stack or packaging channel is a long-term dependency. The right question is not “which project is most popular?” but “who will fix the security bug in three years, and how quickly?” This page gives you a repeatable check. It is not a scorecard with a single winner. It is a set of signals that help you separate a healthy project from an abandoned one, a real standard from a vendor promotion, and a maintainable choice from a demo. ## The short version Before adopting a toolkit, runtime, or channel, check: 1. **Release recency and support policy** — is there a maintained release, and when does it end? 2. **Activity** — are people committing, reviewing, and releasing? 3. **Bus factor** — can the project survive one maintainer leaving? 4. **Responsiveness** — do issues and security reports get answered? 5. **Release engineering** — CI, signed releases, reproducible builds, metadata validation. 6. **Governance and funding** — is there a foundation, company, or community structure? 7. **Standards and adoption** — does it implement the open contracts the desktop relies on? 8. **Adoption** — is it used by real apps and distributions, not just starred on a forge? If several of these are weak, the project is a risk, even if it is popular. If most are strong, it is a reasonable dependency even if it is smaller. ## Signal table | Signal | What to measure | Where to verify | Healthy sign | Red flag | | --- | --- | --- | --- | --- | | **Release recency** | Date of the last stable release; planned next release | Repository releases/tags, project roadmap | A stable release within the supported window; a stated cadence | No release for a year with no explanation | | **Support policy** | Supported branches, EOL dates, backport policy | Project docs, distro package pages | Explicit support window and security-backport policy | “Use latest” with no maintenance statement | | **Commit activity** | Commits and distinct authors in the last 90 and 365 days | Git log, repository hosting API | Regular activity across multiple people | Bursts from one person, then months of silence | | **Bus factor** | Share of commits by the top contributor; number of active maintainers/reviewers | Repository insights, `MAINTAINERS`, governance docs | At least two active maintainers; no single point of failure | One person does everything; no review path | | **Issue responsiveness** | Median time to first response; share of issues/PRs closed in 90 days | repository hosting API, issue search | A stated response window and recent closures | Hundreds of unanswered issues; stale PRs | | **Security process** | `SECURITY.md`, disclosure policy, CVE handling | Repository, OSV, forge security advisories | Private reporting path and a patch cadence | No security contact; vulnerabilities ignored | | **Dependency health** | Outdated or vulnerable dependencies; update automation | OSV, Dependabot/Renovate, `cargo audit`, `pip-audit` | Automated updates and a clean advisory history | Known critical CVEs with no fix | | **Release engineering** | CI status, signed tags, checksums, SBOM, reproducible builds | CI badges, release assets, Reproducible Builds | Tested builds, signed releases, verifiable artifacts | Untagged builds, no checksums, “works on my machine” | | **Governance and funding** | Foundation/company backing, maintainer count, roadmap | Project site, governance docs, foundation pages | Transparent decision-making and funded maintenance | Single vendor with no succession plan | | **Standards compliance** | XDG, Wayland, portals, AppStream, MIME, accessibility | Specifications, `appstreamcli`, `desktop-file-validate` | Implements the contracts instead of private replacements | Proprietary APIs that lock users in | | **Adoption** | Real apps, distro packages, store listings | Distro package pages, Flathub/Snap, app repositories | Used by maintained apps and shipped by distros | Only demo apps; no packager interest | Download counts and stars on a code forge are **secondary**. They can indicate adoption, but they do not prove maintenance, security, or quality. Never use them as the only signal. ## How to check quickly Use primary sources and reproducible queries: - **Releases and tags:** repository releases page, `git tag --sort=-creatordate`, or the hosting API. - **Activity:** `git log --since="90 days ago" --format='%an' | sort | uniq -c | sort -rn`. - **Bus factor:** compare the top author’s commit share with the total and count maintainers in `MAINTAINERS`/`CODEOWNERS`. - **Issues:** use the forge’s search filters for open issues, stale issues, and median response time. - **Security:** check `SECURITY.md`, the forge’s security advisories, and [OSV](https://osv.dev/). - **Release engineering:** inspect CI configuration, release assets, checksums, and the [Reproducible Builds](https://reproducible-builds.org/) status where available. - **OpenSSF Scorecard:** use [OpenSSF Scorecard](https://scorecard.dev/) as one input for repository security practices; a missing score is not proof of a problem. - **Standards:** run `appstreamcli validate`, `desktop-file-validate`, `flatpak-builder-lint`, and the relevant `xdg-*` tools. - **Distro/store:** check the package in the Ubuntu archive, the Snap Store, or Flathub; confirm the publisher and the supported runtime. Record the date of the check. A project can change quickly, and a health check without a date is a rumor. ## Applying it to stacks For a toolkit or language binding, the signals map to practical questions: - **GTK/libadwaita:** release notes and NEWS, GNOME’s release cadence, the number of active maintainers, the accessibility and portal story, and whether the binding tracks the C API. - **Qt/Kirigami:** Qt’s release and LTS policy (LTS is commercially maintained; open-source users should track their distro or the latest stable), KDE Frameworks’ monthly cadence, and the health of the QML/Kirigami ecosystem. - **Rust bindings:** whether `gtk-rs`/`libadwaita-rs` support the target GTK/libadwaita release, the feature-flag matrix, and the maintenance of the higher-level frameworks you choose. - **Web/custom-render runtimes:** whether the runtime is open source, actively maintained, accessible, sandboxable, and packaged in a way that respects the desktop. A named runtime is an example, not an endorsement. ## Applying it to packaging channels Channels need the same scrutiny: - **deb/PPA:** who maintains the package, whether it tracks security updates, whether it builds in a clean chroot, and whether the archive or PPA is the source of truth. - **Snap:** base support (`core26` for 26.04), confinement, refresh behavior, Snapcraft version, review status, and whether the publisher is the upstream project. - **Flatpak/Flathub:** runtime branch and EOL, `finish-args`, portal usage, linter status, metainfo quality, verification, and whether the Flathub repository is maintained by upstream. - **AppImage:** no sandbox, no standard update channel; only use it when the portability trade-off is explicit. - **Source/self-hosted:** build reproducibility, release signing, dependency pinning, and a documented update path. A channel that is convenient but unmaintained is not high quality. A smaller channel with a clear maintainer, a security process, and reproducible builds can be the better choice. ## A practical gate Use this as a release gate, not a beauty contest: - [ ] The project has a stable release and a stated support window. - [ ] At least two people have commit or review rights, or there is a documented succession plan. - [ ] There is a security contact and a disclosed vulnerability process. - [ ] CI runs on the default branch and releases are built from tags. - [ ] The project implements the relevant open standards and validates its metadata. - [ ] The channel/runtime is supported and not end-of-life. - [ ] The choice is documented with the date and the sources you checked. If a dependency fails several gates, write down the risk and the migration path. Do not hide it behind a popularity claim. ## Sources - [CHAOSS metrics](https://chaoss.community/metrics/) — community-health metrics and definitions - [OpenSSF Scorecard](https://scorecard.dev/) — repository security practices - [OSV](https://osv.dev/) — open-source vulnerability database - [Reproducible Builds](https://reproducible-builds.org/) — build verification - [Flathub requirements](https://docs.flathub.org/docs/for-app-authors/requirements) and [runtimes](https://docs.flathub.org/docs/for-app-authors/runtimes) - [Snapcraft bases](https://ubuntu.com/docs/snapcraft/stable/reference/bases/) and [support schedule](https://ubuntu.com/docs/snapcraft/stable/reference/support-schedule/) - [Debian Policy](https://www.debian.org/doc/debian-policy/) and [Ubuntu packaging guide](https://ubuntu.com/project/docs/) - [AppStream documentation](https://www.freedesktop.org/software/appstream/docs/) - [XDG specifications](https://specifications.freedesktop.org/) --- # Resources URL: https://digitalesich.github.io/linux-desktop-guide/reference/resources/ > A curated, link-checked set of specifications, documentation, tooling, design assets, and communities for building native Linux desktop apps. The Linux desktop has excellent documentation. It is also spread across a dozen projects. This page collects the sources worth bookmarking, with a bias toward primary documentation and maintained projects. ## Specifications | Resource | Why it matters | | --- | --- | | [freedesktop.org specifications](https://specifications.freedesktop.org/) | The index of XDG, desktop entry, MIME, icon, trash, notification, and MPRIS specs | | [XDG Base Directory](https://specifications.freedesktop.org/basedir-spec/latest/) | Where config, data, state, cache, and runtime files belong | | [Desktop Entry](https://specifications.freedesktop.org/desktop-entry-spec/latest/) | The `.desktop` file format | | [Shared MIME-info](https://specifications.freedesktop.org/shared-mime-info-spec/latest/) | File type detection and `MimeType=` | | [Icon Theme](https://specifications.freedesktop.org/icon-theme-spec/latest/) | hicolor and icon naming | | [Trash](https://specifications.freedesktop.org/trash/latest/) | Safe deletion | | [Autostart](https://specifications.freedesktop.org/autostart-spec/latest/) | Login startup entries | | [Notifications](https://specifications.freedesktop.org/notification-spec/latest/) | Desktop notification protocol | | [MPRIS](https://specifications.freedesktop.org/mpris-spec/latest/) | Media player controls | | [Secret Service](https://specifications.freedesktop.org/secret-service-spec/latest/) | Keyring access | | [Wayland](https://wayland.freedesktop.org/) | Protocol documentation and compositor architecture | | [XDG Desktop Portal](https://flatpak.github.io/xdg-desktop-portal/docs/) | Portal interfaces and usage | | [AppStream](https://www.freedesktop.org/software/appstream/docs/) | Metadata for software centers | | [systemd](https://systemd.io/) | User services, timers, journald, coredump | | [systemd user units](https://www.freedesktop.org/software/systemd/man/latest/systemd.special.html) | Running background services per user | ## Project health | Resource | Why it matters | | --- | --- | | [Quality Signals](/linux-desktop-guide/reference/quality-signals/) | The guide’s repeatable maintenance and community-health check | | [CHAOSS metrics](https://chaoss.community/metrics/) | Established community-health metrics | | [OpenSSF Scorecard](https://scorecard.dev/) | Repository security practices | | [OSV](https://osv.dev/) | Open-source vulnerability database | | [Reproducible Builds](https://reproducible-builds.org/) | Build verification and reproducible-build guidance | ## Documentation quality The guide’s practical workflow is in [User Documentation](/linux-desktop-guide/ship/documentation/). These are the primary sources behind it. The guide’s own house rules are in the [Style Guide](/linux-desktop-guide/reference/style-guide/). | Resource | Why it matters | | --- | --- | | [ISO/IEC/IEEE 26514:2022](https://www.iso.org/standard/77451.html) | Design and development of information for users; the software-documentation backbone | | [ISO 24495-1:2023](https://www.iso.org/standard/78907.html) | Plain-language principles: relevant, findable, understandable, usable | | [ISO/IEC/IEEE 26513:2017](https://www.iso.org/standard/67417.html) | Reviewing and testing information for users | | [IEC/IEEE 82079-1:2019](https://webstore.iec.ch/en/publication/29075) | Horizontal principles for product information, when a contract or regulated market requires them | | [ISO 9241-11:2018](https://www.iso.org/standard/63500.html) | Usability vocabulary for task-based evaluation | | [WCAG 2.2](https://www.w3.org/TR/WCAG22/) | Accessible HTML documentation and in-app help | | [EN 301 549 V4.1.1](https://www.etsi.org/deliver/etsi_en/301500_301599/301549/04.01.01_60/en_301549v040101p.pdf) | EU accessibility requirements, including documentation and support services | | [Diátaxis](https://diataxis.fr/) | Structure model for tutorials, how-to guides, reference, and explanation | ## Ubuntu | Resource | Why it matters | | --- | --- | | [Ubuntu for Developers](https://ubuntu.com/developers/docs/) | Toolchains, IDEs, packaging, and support | | [Ubuntu Desktop documentation](https://documentation.ubuntu.com/desktop/en/latest/) | Desktop behavior, accessibility, and snap/deb policy | | [Ubuntu 26.04 release notes](https://documentation.ubuntu.com/release-notes/26.04/) | The baseline for this guide | | [Ubuntu Pro Client](https://documentation.ubuntu.com/pro-client/en/latest/) | `pro`, ESM, Livepatch, FIPS, CIS | | [Snap documentation](https://snapcraft.io/docs/) | snapd, confinement, interfaces, updates | | [Snapcraft](https://ubuntu.com/docs/snapcraft/stable/) | Building snaps | | [Rockcraft](https://github.com/canonical/rockcraft) | Ubuntu-based OCI images | | [Ubuntu packaging guide](https://ubuntu.com/project/docs/) | deb packaging | | [New package guide](https://ubuntu.com/project/docs/contributors/new-package/create-a-new-package/) | Ubuntu archive process | | [Build packages in a PPA](https://ubuntu.com/project/docs/contributors/bug-fix/build-packages-in-a-ppa/) | PPA upload and signing | | [Ubuntu accessibility stack](https://documentation.ubuntu.com/desktop/en/latest/explanation/accessibility-stack/) | AT-SPI, GTK, Orca, BRLTTY | | [AppArmor](https://apparmor.net/) | Confinement profiles and auditing | | [Polkit](https://www.freedesktop.org/software/polkit/docs/latest/) | Privileged actions | | [NetworkManager](https://networkmanager.dev/docs/developers/) | Connectivity and network state | | [PipeWire](https://docs.pipewire.org/) | Audio/video graph | | [WirePlumber](https://pipewire.pages.freedesktop.org/wireplumber/) | PipeWire session/policy manager | | [UPower](https://upower.freedesktop.org/docs/) | Battery and power state | | [BlueZ D-Bus API](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc) | Bluetooth | | [OpenPrinting CUPS](https://openprinting.github.io/cups/) | Printing | | [UDisks2 API](https://storaged.org/udisks/docs/) | Disks and mounts | | [fwupd / LVFS](https://fwupd.org/) | Firmware updates | | [PackageKit](https://www.freedesktop.org/software/PackageKit/) | Package management | | [snapd REST API](https://snapcraft.io/docs/snapd-api) | Snap automation | | [Ubuntu Desktop snap/deb packages](https://ubuntu.com/desktop/docs/en/26.04/explanation/snap-and-deb-packages) | App Center package formats | | [Permissions prompting](https://discourse.ubuntu.com/t/permissions-prompting-a-deep-dive/81785) | Security Center and Snap prompting | | [Ubuntu Insights](https://github.com/ubuntu/ubuntu-insights) | Opt-in system telemetry | ## Fedora | Resource | Why it matters | | --- | --- | | [Fedora Packaging Guidelines](https://docs.fedoraproject.org/en-US/packaging-guidelines/) | The normative rules for Fedora RPM packages | | [RPM Packaging Guide](https://rpm-packaging-guide.github.io/) | Upstream RPM tutorial and spec-file reference | | [Fedora Developer: RPM packaging](https://developer.fedoraproject.org/deployment/rpm/about.html) | Fedora’s packaging entry point | | [COPR user documentation](https://docs.copr.fedorainfracloud.org/user_documentation.html) | Build service for packages outside Fedora | | [Fedora SELinux Wiki](https://fedoraproject.org/wiki/SELinux) | SELinux modes, policy, and troubleshooting | | [Fedora SELinux troubleshooting](https://docs.fedoraproject.org/en-US/quick-docs/selinux-troubleshooting/) | Diagnosing AVC denials | | [audit2allow(1)](https://man7.org/linux/man-pages/man1/audit2allow.1.html) | Turning audit denials into candidate rules | | [firewalld documentation](https://firewalld.org/documentation/) | Zones, services, and ports for host services | | [xdg-desktop-portal-kde](https://invent.kde.org/plasma/xdg-desktop-portal-kde) | Plasma’s portal backend | | [Fedora Kinoite](https://docs.fedoraproject.org/en-US/fedora-kinoite/) | Plasma Atomic desktop and Flatpak-first model | | [Toolbx](https://containertoolbx.org/) | Containerized Fedora development environment | ## GNOME and GTK | Resource | Why it matters | | --- | --- | | [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/) | The design language behind libadwaita | | [GTK 4 documentation](https://docs.gtk.org/gtk4/) | Widgets, layout, actions, accessibility | | [libadwaita documentation](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/) | Adaptive widgets and GNOME patterns | | [Blueprint](https://gnome.pages.gitlab.gnome.org/blueprint-compiler/) | Declarative UI language for GTK | | [GNOME Builder](https://apps.gnome.org/Builder/) | IDE with Flatpak and GNOME integration | | [Workbench](https://apps.gnome.org/Workbench/) | Rapid GTK prototyping | | [GNOME Icon Library](https://gitlab.gnome.org/Teams/Design/icon-library) | Consistent symbolic and full-color icons | | [GNOME apps](https://apps.gnome.org/) | Reference implementations | | [GNOME developer documentation](https://developer.gnome.org/documentation/) | Tutorials, guides, and API docs | | [GTK Inspector](https://docs.gtk.org/gtk4/running.html) | Interactive debugging | | [Sysprof](https://apps.gnome.org/Sysprof/) | System-wide profiling | | [Accerciser](https://gitlab.gnome.org/GNOME/accerciser) | Accessibility tree inspector | | [Orca](https://gitlab.gnome.org/GNOME/orca) | Screen reader | | [Glycin](https://gitlab.gnome.org/GNOME/glycin) | Sandboxed image decoding for untrusted images | ## KDE and Qt | Resource | Why it matters | | --- | --- | | [KDE Human Interface Guidelines](https://develop.kde.org/hig/) | Plasma design conventions | | [Kirigami documentation](https://develop.kde.org/docs/getting-started/kirigami/) | Adaptive Qt Quick apps | | [Qt 6 documentation](https://doc.qt.io/qt-6/) | Qt Widgets, Qt Quick, Qt Test | | [Qt QML applications](https://doc.qt.io/qt-6/qmlapplications.html) | QML application structure | | [KDE Frameworks API](https://api.kde.org/) | KConfig, KIO, KNotification, KWallet | | [Qt Creator](https://doc.qt.io/qtcreator/) | IDE and profiler | | [Breeze icons](https://invent.kde.org/plasma/breeze) | KDE icon theme | | [GammaRay](https://github.com/KDAB/GammaRay) | Qt/QML runtime inspection | | [KDE Discuss](https://discuss.kde.org/) | Community support | ## Rust | Resource | Why it matters | | --- | --- | | [gtk-rs book](https://gtk-rs.org/gtk4-rs/stable/latest/book/) | GTK 4 bindings for Rust | | [gtk-rs documentation](https://gtk-rs.org/gtk4-rs/) | API docs and version support | | [Relm4 book](https://relm4.org/book/stable/) | Component architecture on gtk-rs | | [Iced](https://iced.rs/) | Elm-inspired cross-platform GUI | | [Slint](https://slint.dev/) | Declarative UI for embedded and desktop | | [libcosmic](https://github.com/pop-os/libcosmic) | COSMIC toolkit | | [COSMIC](https://system76.com/cosmic) | Rust-native desktop environment | ## Packaging and distribution | Resource | Why it matters | | --- | --- | | [Flatpak documentation](https://docs.flatpak.org/) | Sandbox, manifests, runtimes, portals | | [Flatpak runtimes](https://docs.flatpak.org/en/latest/available-runtimes.html) | Runtime branches and support windows | | [Flatpak Builder](https://docs.flatpak.org/en/latest/flatpak-builder.html) | Manifest format and build workflow | | [Flathub documentation](https://docs.flathub.org/) | Submission, maintenance, verification | | [Flathub requirements](https://docs.flathub.org/docs/for-app-authors/requirements) | What gets accepted | | [Flathub metainfo guidelines](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines) | Metadata, screenshots, branding, translations | | [Flathub quality guidelines](https://docs.flathub.org/docs/for-app-authors/metainfo-guidelines/quality-guidelines) | Store-listing quality targets | | [Flathub linter](https://docs.flathub.org/docs/for-app-authors/linter) | Manifest and metainfo validation | | [Flathub submission](https://docs.flathub.org/docs/for-app-authors/submission) | The PR process | | [AppStream documentation](https://www.freedesktop.org/software/appstream/docs/) | Metadata, validation, screenshots | | [AppStream validator issue tags](https://github.com/ximion/appstream/blob/main/src/as-validator-issue-tag.h) | Exact `appstreamcli` issue names | | [Debian Policy](https://www.debian.org/doc/debian-policy/) | The deb package contract | | [Debian packaging tutorial](https://www.debian.org/doc/manuals/debmake-doc/) | Practical deb building | | [Ubuntu packaging](https://ubuntu.com/project/docs/) | Ubuntu-specific packaging | | [Launchpad PPAs](https://ubuntu.com/docs/launchpad/user/Packaging/PPA) | Personal package archives | | [Snapcraft documentation](https://snapcraft.io/docs) | Snap packaging and confinement | | [AppImage documentation](https://docs.appimage.org/) | Portable single-file apps | ## Tools | Tool | Use | | --- | --- | | [Sysprof](https://apps.gnome.org/Sysprof/) | CPU, marks, and system profiling | | [heaptrack](https://github.com/KDE/heaptrack) | Heap allocation profiling | | [Valgrind](https://valgrind.org/) | Memory errors and callgrind | | [perf](https://perf.wiki.kernel.org/) | Linux perf events | | [dogtail](https://gitlab.com/dogtail/dogtail) | AT-SPI UI automation | | [Accerciser](https://gitlab.gnome.org/GNOME/accerciser) | Accessibility inspection | | [GammaRay](https://github.com/KDAB/GammaRay) | Qt/QML inspection | | [GTK Inspector](https://docs.gtk.org/gtk4/running.html) | GTK layout, CSS, and accessibility | | [Workbench](https://apps.gnome.org/Workbench/) | GTK prototyping | | [Blueprint](https://gnome.pages.gitlab.gnome.org/blueprint-compiler/) | GTK UI language | | [Meson i18n](https://mesonbuild.com/i18n-module.html) | gettext/POTFILES integration | | [CMake documentation](https://cmake.org/documentation/) | Build configuration and GNUInstallDirs | | [GStreamer documentation](https://gstreamer.freedesktop.org/documentation/) | Media pipelines and VA-API | ## Design assets | Resource | Use | | --- | --- | | [GNOME HIG](https://developer.gnome.org/hig/) | Patterns, spacing, typography | | [GNOME Icon Library](https://gitlab.gnome.org/Teams/Design/icon-library) | Symbolic and full-color icons | | [libadwaita CSS variables](https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/css-variables.html) | Semantic colors and spacing | | [KDE HIG](https://develop.kde.org/hig/) | Plasma patterns | | [Breeze icons](https://invent.kde.org/plasma/breeze) | KDE icon set | | [elementary HIG](https://docs.elementary.io/hig/) | Minimal, structured design | | [Inter](https://rsms.me/inter/) | UI font used by many Linux apps | | [Cantarell](https://gitlab.gnome.org/GNOME/cantarell-fonts) | GNOME UI font | | [JetBrains Mono](https://www.jetbrains.com/lp/mono/) | Monospace font for code | ## Community | Community | Where | | --- | --- | | GNOME | [discourse.gnome.org](https://discourse.gnome.org/) | | KDE | [discuss.kde.org](https://discuss.kde.org/) | | Flathub | [Matrix](https://matrix.to/#/#flathub:matrix.org) | | freedesktop | [Mailing lists](https://lists.freedesktop.org/) | | GNOME development | [gitlab.gnome.org](https://gitlab.gnome.org/) | | KDE development | [invent.kde.org](https://invent.kde.org/) | | Linux App Summit | [linuxappsummit.org](https://linuxappsummit.org/) | | GUADEC | [guadec.org](https://guadec.org/) | | Akademy | [akademy.kde.org](https://akademy.kde.org/) | ## Agent access The guide is published for human readers and agents from the same source. The agent-facing files are generated at build time, so they cannot drift from the HTML. - [llms.txt](/linux-desktop-guide/llms.txt) — curated index grouped by section, with links to every Markdown page. - [llms-full.txt](/linux-desktop-guide/llms-full.txt) — the complete guide in one Markdown file. - **Per-page Markdown** — append `.md` to a route: `/start/why-linux.md`, `/platform/ubuntu.md`, `/reference/baseline.md`. - **Discovery** — every HTML page advertises its Markdown sibling with `` and points to `/llms.txt` with `rel="describedby"`. - **Local MCP server** — `npm run mcp:docs` starts a read-only stdio server with `search_docs`, `get_page`, `list_pages`, `get_related`, and `get_baseline`. The MCP server runs locally over stdio; the project does not operate a remote endpoint. Content negotiation (`Accept: text/markdown`) depends on host or CDN support and is not enabled by the static site. The `.md` endpoints work on any static host. ## How to keep this current - Prefer primary documentation over blog posts. - Check the release notes for the toolkit version you target. - Re-test links and commands at least once per release cycle. - If a tool is unmaintained, say so and point to the replacement. - When in doubt, read the specification. It is shorter than the argument. ### Content review checklist 1. Check the Ubuntu release notes and release cycle; note any support-date discrepancy. 2. Check GNOME/GTK/libadwaita, Qt/KDE, Snapcraft, Flatpak, and Flathub release notes for the versions you target. 3. Update `src/data/baseline.json` and `reference/baseline.mdx` together. 4. Re-check the pages that cite the changed versions or channels. 5. Run `npm run check:baseline`, `npm run build`, `npm run check:agent-docs`, and `npm run test:docs-mcp`. 6. Re-run the external link check and the internal link/anchor check. 7. Review the diff for unsourced claims, vendor promotion, and Apple/macOS/Windows comparisons. 8. Record the review date and the sources you checked. Back to [The Native Standard](/linux-desktop-guide/start/native-standard/) or [Checklists](/linux-desktop-guide/reference/checklists/). --- # Style Guide URL: https://digitalesich.github.io/linux-desktop-guide/reference/style-guide/ > The house style for the Linux Desktop Guide and the apps it documents — voice, page shape, terminology, code, links, screenshots, accessibility, localization, and agent-facing… This is the house style for every page in `src/content/docs`, the generated agent Markdown, app user documentation, UI strings, error messages, and release notes. It exists so a reader can move between pages without relearning the voice, and so an agent can retrieve a section without guessing what “it” refers to. Accuracy wins over style. If a rule makes a page less correct, break the rule and fix the rule later. The process, standards, and review gates are in [User Documentation](/linux-desktop-guide/ship/documentation/); platform terms are in the [Glossary](/linux-desktop-guide/reference/glossary/). ## The short version 1. Write for a task or a decision, not a feature. 2. Put the answer first. 3. Use the reader’s terms and the UI’s exact labels. 4. Prefer a concrete example over an abstract description. 5. Keep one idea per paragraph and one procedure per page. 6. Be direct and honest about limits. 7. Link to the canonical source instead of restating it. 8. Make the page accessible and translatable. 9. Keep headings and slugs stable; they are link targets. 10. Test what you write on the target. ## Voice and audience Write for a competent developer who is new to this specific app or platform. Do not explain programming basics. Do explain a platform contract the reader may not know. - Use second person, active voice, and present tense: “Press Restore”, not “The note will be restored by pressing Restore”. - Be opinionated but fair. State the recommendation, then the trade-off. - Do not compare against another operating system or use it as the baseline. - Do not use marketing language: _powerful_, _seamless_, _revolutionary_, _best-in-class_. If a quality matters, measure it or name the mechanism. - Do not blame the reader. Avoid _simply_, _just_, _obviously_, and _of course_ as minimizers. - Admit uncertainty. “Not verified on Fedora 45” is better than a guess. - Use “you” for the reader. Use “the guide” rather than “we”; reserve “we” for maintainer notes where a person is actually speaking. - Contractions are fine in explanation. Use full forms in procedures and requirements. | Before | After | | --- | --- | | “The application utilizes a portal to facilitate file access” | “The app uses a portal to open files” | | “Simply click the button to restore your note” | “Press Restore” | | “This is the best solution for all users” | “This is the default for the Ubuntu + GNOME target” | ## Page shape - Page titles use Title Case. Section headings use sentence case. - Do not add an H1 in the body. Starlight renders the frontmatter title. - Open with one paragraph that states the problem or decision. - Each `##` section answers one question. Use `###` only for a real subcase. - Put the most important sentence first in each section. - End with a next step or a short `## See also`. Do not add a summary block that repeats the page. - Use tables for comparisons, checklists for gates, and code blocks for commands. - Keep the page navigable at a glance: a reader should understand the structure from the headings alone. A task page starts from this template: ```text --- title: Restore a Deleted Note description: One sentence for search results and social previews. --- State the user's goal and when they need this page. ## What you need ## Steps ## Result ## If it fails ## See also ``` ## Sentences and words - Keep sentences short enough to scan. One idea per sentence. - Prefer the concrete verb: _use_, not _utilize_; _start_, not _initiate_; _help_, not _facilitate_. - Prefer “for example” to “e.g.” and “that is” to “i.e.”. - Use the serial comma in lists of three or more. - Use a spaced em dash — like this — for an aside. Do not use a hyphen as a dash. - Use ISO dates (`2026-11-10`) in metadata, review records, changelogs, and sources. - Use numerals for versions, measurements, and counts in tables and procedures: `GTK 4.22`, `16.7 ms`, `3–5 users`. - Write `200%` without a space; write `16.7 ms` and `2 GB` with one. - Avoid “etc.” in a procedure. List the items or link to the reference. - Use “must” for a requirement, “should” for a recommendation, and “may” for an option. Do not use “shall” in guide prose; it belongs in specifications. - Use present tense for current behavior. Do not write “will” when the behavior already exists. ## Terminology Use one term for one concept. The full platform vocabulary is in the [Glossary](/linux-desktop-guide/reference/glossary/); do not create a second glossary. These are the terms this guide uses precisely: | Term | Use it to mean | Avoid | | --- | --- | --- | | **app** | The product the reader is building or using | “application” in prose | | **user** | The person using the app | “end user” unless contrasting with a developer or administrator | | **Linux desktop** | The ecosystem and the user’s session | “Linux Desktop” as a common noun | | **target** | A distribution + desktop + toolkit entry in `src/data/targets.json` | “platform” when the matrix entry is meant | | **reference target** | The primary supported target with one default stack and HIG | “main platform” | | **validation target** | A target used to check portability, not the primary recommendation | “secondary target” | | **compatibility target** | A target supported through a fallback, usually Flatpak | “unsupported target” | | **baseline** | The validated version set in `src/data/baseline.json` | “current versions” without a source | | **stack** | Toolkit, language or binding, and supporting libraries | “technology” | | **canonical** | The single source the guide treats as authoritative | “official” when no authority exists | | **deprecated** | Still available, but no longer recommended | “removed” | | **removed** | No longer available | “deprecated” | | **agent docs** | The generated Markdown, `llms.txt`, and `llms-full.txt` | “AI docs” | | **docs MCP** | The local, read-only `docs` server | “documentation API” | | **guide MCP** | The local, read-only server for the published guide, distributed as `linux-desktop-guide-mcp` | “remote docs API” | | **review trigger** | An event that requires a documentation review | “review event” | Capitalization and spelling that people get wrong: - **Flatpak** is the product; `flatpak` is the command. - **Flathub** is the store; never `flathub` in prose. - **Snap** is the product; `snap` is the command and the package. - **AppStream** is the metadata standard; `appstreamcli` is the tool. - **MetaInfo** is the AppStream file; the suffix is `.metainfo.xml`. - **Wayland**, **X11**, and **XWayland** are protocol names. - **D-Bus**, not DBus. - **GTK** is the toolkit; `Gtk` is the namespace in code. - **libadwaita** is lowercase; **Qt** and **Kirigami** keep their capitalization. - **XDG** is the prefix for the specifications; `xdg` appears in commands and paths. - **HIG** is the acronym for Human Interface Guidelines. - **MCP** is the protocol; `docs` is the name of this project’s server. Words to avoid unless the sentence gives them a measurement or a mechanism: | Avoid | Why | Use instead | | --- | --- | --- | | simply, just, obviously, of course | Makes difficulty the reader’s fault | State the action | | easy, user-friendly | Unmeasurable | Name the property: fewer steps, keyboard access, clear labels | | modern, powerful, seamless, robust | Marketing | Name the mechanism or the measured result | | utilize, leverage, facilitate | Nominalization | use, start, help | | click here, read more | Link text that fails out of context | The destination name | | secure | Unverifiable without a threat model | The specific control: sandboxed, least privilege, encrypted | | native | Vague without the platform contracts | The HIG, widgets, portals, and integration that apply | | works | Untestable without a target | “Runs on Ubuntu 26.04 LTS + GNOME” | | support | Ambiguous without a version and window | “Supported on GTK 4.22 until …” | ## Code and commands - Every command should be runnable on a stated target. Mark placeholders clearly: `io.github.yourname.Notes`, ``. - Put a `title=` on code blocks that represent a file or a terminal session. - Use the correct language tag: `bash`, `c`, `cpp`, `rust`, `python`, `xml`, `yaml`, `json`, `text`. - Do not add a `$` prompt unless you are showing output separately. Use `text` for output and directory trees. - One command per line. Put comments above the command and explain _why_, not _what_. - Show expected output when the reader needs it to confirm success. - Use the versions in the [Baseline](/linux-desktop-guide/reference/baseline/) and the [target matrix](/linux-desktop-guide/targets/). Do not invent a version. - Do not put secrets, tokens, personal paths, or real email addresses in an example. - Use `~` for the home directory in prose and paths. Use `$HOME` when the shell expands it. - Format keyboard shortcuts as `Ctrl+K`, `Alt+F2`, `Super`. Do not add spaces around `+`. - Format file paths and environment variables as code: `~/.config`, `XDG_CONFIG_HOME`. - Write versions without a `v` prefix in prose: `Ubuntu 26.04 LTS`, `Qt 6.11`. A git tag may keep the repository’s actual `v2.4.0` form. - Use `sudo` only when the step requires it, and say what it changes. ## Links - Internal links are root-relative with a trailing slash: `/platform/wayland/`. Add an anchor only when it is stable: `/reference/resources/#agent-access`. - Link text names the destination. “Read the [Wayland chapter](/linux-desktop-guide/platform/wayland/)” beats “Read more [here](/linux-desktop-guide/platform/wayland/)”. - Link the canonical source for a claim: the specification, the upstream documentation, or the project’s own release notes. - Do not use a bare URL as link text. Put the title in the text and the URL in the target. - Do not link the same concept repeatedly in one paragraph. Link once, then use the term. - Headings are anchors. Renaming a heading breaks incoming links; update the links in the same change. - A `## Sources` section lists primary sources with the date they were checked. - Do not link to `dist/` or generated files. Link to the source page or the live route. ## UI text and error messages - Use the exact label the user sees. If the label is **Restore**, do not write “recover”. - Write UI paths as `Settings > Trash`. - Buttons and menu items use sentence case and no trailing period: **Restore**, **Save a copy**. - An error message answers four questions: what happened, why, what to do now, and whether the data is safe. The full pattern is in [User Documentation](/linux-desktop-guide/ship/documentation/#in-app-text). - Do not show an error code alone. Add a stable code for support after the human-readable message. - Empty states say what will appear and offer the first action: “No notes yet. Create your first note.” - Release notes are user-facing: visible changes, migration steps, and known issues. Do not paste the git log. - Do not use documentation to excuse a confusing UI. If every user needs a paragraph to find a button, change the button. ## Tables, lists, and checklists - Use a table for a comparison or a mapping. Give the first column a clear label and keep cells short. - Do not put long prose in a table. If a cell needs three sentences, use a subsection. - Use numbered lists for sequence and bullets for options. - Keep nesting to two levels. If a list needs a third level, split it. - Checklists use `- ☐` and an imperative item: `- ☐ Verify the file opens through the portal.` - Use `**bold**` for a label you are naming and backticks for a literal value. - Do not use footnotes. Put sources at the end. ## Screenshots and media - Take screenshots from the shipped build, on the stated target, at the stated scale. - Do not fake window chrome or crop out the native shadow, title bar, and rounded corners. - Remove personal data, API keys, and private file names. - Write alt text that describes the information, not the file: “The Trash view with one note selected and the Restore button highlighted.” - Captions are one sentence and do not end with a period. - Provide light and dark variants where the UI differs. Do not use color alone to convey state. - Never put the only copy of an instruction in an image. - Compress images and keep them current. An outdated screenshot is a trust signal in the wrong direction. ## Accessibility and localization - Use one page title and a logical heading hierarchy. Do not skip from `##` to `####`. - Make link text meaningful out of context. “Download the manifest” is better than “Download”. - Provide alt text for images and captions or transcripts for video. - Meet WCAG 2.2 AA for contrast, focus, keyboard access, and 200% zoom. The technical baseline is in [Accessibility](/linux-desktop-guide/design/accessibility/). - Write plain language. Expand an abbreviation on first use. - Do not put instructions only in an image, a tooltip, or a hover state. - Write complete UI strings. Do not concatenate fragments; use placeholders and plural forms. - Give translators context: a comment for ambiguous strings, the screen where the string appears, and the maximum length if it matters. - Use locale APIs for dates, numbers, and currency in the app. Use ISO dates in metadata and review records. - Test the layout in RTL and with translated strings that are longer than English. - The localization workflow is in [Localization](/linux-desktop-guide/design/localization/). ## Writing for agents The same Markdown serves people and agents. There is no agent-only page and no hidden instruction. An agent that retrieves one section should be able to use it without reading the whole page. - Keep slugs and headings stable. They are the retrieval and link API. - Make the frontmatter title and description specific. They become search results, `llms.txt` entries, and MCP excerpts. - Make each section self-contained. Repeat the subject instead of writing “it” or “this” without an antecedent. - Avoid “above”, “below”, and “as mentioned earlier” as the only navigation. Link to the section by name. - Put facts in text, tables, or code blocks, not only in images, tabs, or JavaScript. - State versions, targets, and prerequisites explicitly. “Run the command” is incomplete without where and with which version. - Use the canonical terms from this page and the [Glossary](/linux-desktop-guide/reference/glossary/). Do not introduce a synonym in one section. - Do not edit `dist/`, `llms.txt`, `llms-full.txt`, or the generated per-page `.md` files. Edit the source in `src/content/docs/`. - The docs MCP server is read-only. Do not add write tools just to collect feedback. ### Agent feedback Agents are readers and reviewers, not publishers. When an agent finds a documentation defect, it should report: - the page slug and section heading; - the exact quote or code block; - what it expected and what it found; - the app or guide version and target; - the primary source that contradicts the page. A human owner decides whether to change the page. Agents must not silently rewrite content, publish generated pages, or submit an upstream change without review. The feedback loop and review triggers are in [User Documentation](/linux-desktop-guide/ship/documentation/#feedback-and-ownership). What agents can check well: - broken internal links and anchors; - code blocks that no longer match the example app or a manifest; - missing prerequisites and unstated versions; - duplicate pages and competing terms; - heading structure and missing descriptions. What a human must review: - technical accuracy and security claims; - tone and editorial judgment; - screenshots and visual claims; - translations and legal statements; - whether the page actually helps a user finish the task. ## Review checklist - ☐ The title and description are specific enough to identify the page in search. - ☐ There is no H1 in the body; section headings are sentence case. - ☐ The intro states the problem or decision in one paragraph. - ☐ Each `##` section answers one question. - ☐ Procedures have prerequisites, steps, a result, and a recovery path. - ☐ UI labels match the shipped app exactly. - ☐ Commands were run on the stated target; placeholders are marked. - ☐ Versions match the baseline and target matrix. - ☐ Internal links are root-relative with a trailing slash; external links are primary sources. - ☐ Sources have a checked date. - ☐ No minimizers, hype, blame, or competitor framing. - ☐ Terms match the glossary; one concept has one name. - ☐ Tables, lists, and checklists follow the formats above. - ☐ Screenshots are current, have alt text, and contain no personal data. - ☐ The page is keyboard accessible, readable at 200% zoom, and translatable. - ☐ Sections are self-contained and anchors are stable. - ☐ `npm run check:docs` passes. ## Sources Style rules checked 2026-11-10. The documentation process and its standards are in [User Documentation](/linux-desktop-guide/ship/documentation/). - [ISO 24495-1:2023 — Plain language: governing principles and guidelines](https://www.iso.org/standard/78907.html) - [WCAG 2.2](https://www.w3.org/TR/WCAG22/) - [Diátaxis](https://diataxis.fr/) - [GNOME Human Interface Guidelines](https://developer.gnome.org/hig/) - [KDE Human Interface Guidelines](https://develop.kde.org/hig/) Next: [Glossary](/linux-desktop-guide/reference/glossary/) — the platform terms this style guide tells you to use.