# Desktop integration: the parts an OS owns that we still hand to someone else

Written 2026-08-19, after the default-applications work landed.

Settings is now a superpowered control panel, and files open in the right
applications. What remains are the jobs a desktop environment is expected to own
but that this one currently delegates, ignores, or leaves to whatever GNOME
component happens to be installed.

Six items, ordered so each one is useful the day it lands rather than only at the
end. Effort labels are relative to each other, not to the clock.

## What the probe found

Two of these are far cheaper than assumed:

- **`sushi` 50 is already installed** and owns `org.gnome.NautilusPreviewer`, so
  Quick Look is a wiring-and-declaring job, not a build.
- **Vicinae script commands already exist and are repo-managed**
  (`config/local/share/vicinae/scripts/panama/`, linked by
  `setup/scripts/link-vicinae-scripts`), so launcher search is a generator on top
  of a mechanism that works.

And two need software this machine does not have: `restic` (backups) is not
installed, and nothing enumerates the keyring beyond lock state.

---

## Phase 1 — Finish the file-opening story (small)

Curating defaults only helps if the applications are actually present. A fresh
install seeds `org.gnome.Loupe`, `org.gnome.Papers` and `org.gnome.Decibels`
only when they exist; otherwise it correctly leaves the role alone and the
machine falls back to installation order — the exact problem we just fixed.

- Declare Loupe, Papers, Decibels, Nautilus, mpv and sushi so a fresh machine
  actually has what seeding prefers.
- Wire preview.
- Extend the family contract to assert every preferred handler is a declared
  package.

**Done when** a fresh machine gets the same handlers this one has, and the
contract fails loudly if a curated application is not a declared dependency.

**Landed 2026-08-19.** The previewer answers on `org.gnome.NautilusPreviewer2`,
not the `NautilusPreviewer` interface its bus name suggests, and it opened
*tiled* -- shoving the file manager aside for something meant to be an overlay
-- so it now has a float-and-center window rule. No compositor keybind: the
selection to preview lives in the file manager, and nothing outside it knows
what is selected. `xdg-utils` and `desktop-file-utils` were also undeclared,
which the dependency contract caught the moment seeding started calling them.

## Phase 2 — Make GTK applications wear our accent (small)

The named accents theme our own surfaces; Loupe, Papers, Nautilus and every other
libadwaita application still render in GNOME blue. GNOME exposes
`org.gnome.desktop.interface accent-color` as an enum (blue, teal, green,
yellow, orange, red, pink, purple, slate).

- Give each named accent a nearest-GNOME-enum member in `Theme.qml`, beside the
  dark/light pair it already carries.
- Apply it alongside the existing GTK theme write, so one accent change moves
  both worlds.
- Contract: every named accent maps to a member the enum actually publishes,
  read from `gsettings range` rather than a hardcoded list.

**Done when** switching accent visibly recolors Papers and Nautilus.

## Phase 3 — Disks and storage (medium)

There is no way to see what is using the disk, and removable media is handled by
`udiskie` in the tray with no surface in Settings. `udisks2` is installed.

- Helper `panama-disks`: JSON snapshot of filesystems, sizes, usage, mount
  points, removable devices, and SMART health where the device exposes it.
- Page: usage bars per filesystem, removable devices with mount / unmount /
  eject, and "open in Files".
- **Deliberately out of scope: partitioning and formatting.** A settings pane is
  the wrong place to hand someone a way to erase a disk in two clicks; that stays
  with GNOME Disks, which we can launch.

**Done when** the page answers "what is filling my drive" and "is it safe to pull
this USB stick out".

**Landed 2026-08-19.** Built as mock A over mock B on one scroll rather than
tabs: every other settings page is a scrolling card stack, and a tab would not be
deep-linkable from the launcher command or from search.

Three things the page had to get right, each now pinned by a contract, because
each is a way it could quietly lie:

- `/` and `/home` are one btrfs filesystem sharing one pool of free space. `df`
  reports them separately, and a page that copies `df` shows double the free
  space that exists.
- zram is a block device and is not storage. Counting it as a drive would
  overstate this machine by 8 GB.
- unmount and eject refuse anything not on a removable drive. The UI is what
  asks, and a UI can be wrong.

Adding the page also found a fourth hard-coded page list in `ShellState.qml`.
Missing from it, a page does not error -- `openSettings()` falls back to "home",
so the launcher command opens the wrong page and logs nothing. There is now a
registry contract holding the sidebar, the shell switch, and the allow-list
together.

## Phase 4 — Launcher-native settings search (small)

Settings has a search index. The launcher has script commands. They do not know
about each other, so finding a setting means opening Settings first.

- Generate one script command per settings page from the same schema that feeds
  in-app search, with the page's keywords, so typing "night light" in the
  launcher opens that page directly.
- Generator gets a `--check` staleness mode, matching `panama-settings-docs`.
- Contract: every page has a command, every command resolves to a real page.

**Done when** the launcher is the fastest way to reach any setting.

**Landed 2026-08-19.** The product prefix is gone from all eighteen hand-written
commands. The nineteen generated ones are titled "Settings: <page>" -- not
branding, which was the thing being dropped, but a qualifier: a bare page label
collides with the feature of the same name ("Screen Intelligence" is both a thing
you open and a page about it), and two commands sharing a title are
indistinguishable in a launcher.

Six pages turned out to have no search vocabulary at all -- sound, privacy,
region, accounts, home-phone, about -- because their contents come from the
system rather than our schema. That was an in-app search gap too: "volume" found
nothing in Settings either. They now carry entries, so both surfaces improved.

## Phase 5 — Keychain and secrets (medium, security-sensitive)

`panama-keyring` knows whether the keyring is locked and can unlock it. Nothing
can see what is *in* it, so managing a stored credential means installing
Seahorse.

- Extend the helper to enumerate collections and items through libsecret,
  returning **attributes and labels only**.
- Page: unlock state, lock/unlock, items grouped by collection, delete, change
  password.
- Rules that are non-negotiable and get their own contracts: a secret value never
  appears in a snapshot, never reaches a command line (argv is world-readable via
  `/proc`), never lands in a log or an error message, and is revealed only on an
  explicit per-item request.

**Done when** a stored password can be found, inspected and removed without
leaving Settings, and the contracts prove no value leaks on the way.

**Landed 2026-08-19.** Built into Privacy & Security rather than as its own
page: that page already claimed the topic, already had the keyring rows, and
already answered the "saved passwords" search. A new page would have been a
fourth page-registry edit for a card.

The list is collapsed until asked for -- opening Privacy should not enumerate
someone's passwords as a side effect -- and copying puts the value on the
clipboard without it ever entering Quickshell: the helper reads it, writes it to
`wl-copy` on stdin, and a detached guard clears it about a minute later only if
the clipboard still holds it, comparing a SHA-256 rather than the password.

Seahorse is not installed, so changing a keyring's password is not offered.
Claiming a button that hands off to a missing application would be worse than
not having it.

## Phase 6 — Backups (large, staged)

The one genuinely missing safety net. `restic` is not installed.

- **6a** — destination and repository setup, plus a manual snapshot. Repository
  password lives in the keyring (which is why this follows phase 5), never in the
  repo and never on argv.
- **6b** — a systemd user timer, with the page showing schedule, last run, and
  failures.
- **6c** — restore: browse a snapshot and copy files out.

Blast radius: writes to a destination the user chooses. Nothing in this phase
modifies or deletes existing data; restore copies out rather than over.

**Done when** losing the home directory is an inconvenience rather than a
catastrophe.

**Landed 2026-08-19, as snapshots rather than backups.** The machine already had
snapper running hourly and btrfs underneath, so the tool was never the gap. The
gap was that snapper's only config covered `/`, and `/home` is a separate
subvolume with no config at all -- six hundred and forty-three snapshots existed
and not one of them held a document. A `home` config now exists, the hourly
timeline covers it, and retention is deliberately conservative (5 hourly, 7
daily, 4 weekly) because Steam's 1.2 TB lives on that subvolume and churns on
every game update.

Restore is file and folder level, and sets the current version aside as
`.before-restore-N` rather than overwriting it.

**Rollback is deliberately absent, and this is the interesting part.** snapper's
rollback works by changing the btrfs default subvolume. This system's fstab pins
`subvol=root` and `subvol=home` explicitly, which overrides the default -- so a
rollback would report success and change nothing after a reboot. A recovery
feature that silently does nothing is worse than not having one. Making it work
means editing fstab and the bootloader, which is the one class of change whose
failure cannot be fixed from inside the desktop; it deserves its own tested plan.

**Still open.** Whether to carve Steam onto its own subvolume so snapshots skip
it. Deferred on purpose: the page reports free space, so the cost can be watched
for a week and decided on evidence.

---

## Order and reasoning

Phases 1 and 2 finish work already in flight and are visible immediately. Phase 3
is self-contained and answers a question people actually ask. Phase 4 is small
but changes daily habit. Phase 5 must precede 6 because 6 needs somewhere safe to
keep a repository password. Phase 6 is the largest and is staged so each part
stands alone.


---

## Deprecating GNOME Settings

Not removing it -- making it unnecessary. Tracked here because "which panels do
we own" is a question that otherwise gets answered one panel at a time.

**Owned (2026-08-19).** Users and Sharing. Users drives accountsservice, the same
daemon GNOME's panel drives, so a name or picture set here is what the login and
lock screens read. Sharing covers remote login and remote desktop, and reports
file and media sharing as absent rather than offering a switch for software that
is not installed.

**Owned (2026-08-19, later).** Printers, driverless only. Adding a printer that
describes its own capabilities over IPP is supported; choosing a PPD or fetching
a vendor driver is not, and the page says so. That restraint is the point: a
wrong driver produces a printer that accepts jobs, reports success, and prints
nothing, which is the worst failure this page could ship because it looks like it
worked. A printer old enough to need a PPD stays a system-config-printer job.

**Universal Access is ours, and mostly already was.** The magnifier
(`cursor:zoom_factor`), pointer size, text scale, motion and dimming were all
present; high contrast was the real gap and is now covered. It reaches GTK4
applications through the desktop portal, which republishes GNOME's a11y setting
as `org.freedesktop.appearance contrast` -- no high-contrast theme involved, and
none is installed. Sticky, slow and bounce keys stay absent: there is no Wayland
or Hyprland implementation, and the compositor would happily store the XKB option
while nothing acted on it.

**Remote desktop is configurable now.** Port, view-only, and clearing stored
credentials go through grdctl. SETTING credentials opens a terminal running
grdctl itself, because it prompts for the password on a terminal and core-dumps
without one -- and the alternative, passing it as an argument, would publish it
through /proc to every process on the machine. Typed into grdctl directly, it
never passes through Panama.

**Still handed to GNOME: Color and Wacom.** Color is not a matter of effort. colord
runs here with seven profiles and ZERO devices registered, because the daemons
that register displays (gsd-color, colord-kde) do not run under this session, and
Hyprland exposes no ICC, gamma, or color-management option at all. A Color page
could import a profile and attach it to nothing, and nothing would load a curve
into the display: it would look like it worked and change nothing. That is the
same failure refused for snapshot rollback and printer drivers. Each
wraps hardware-specific tooling with no D-Bus surface worth reimplementing, and
each launches with XDG_CURRENT_DESKTOP=GNOME because gnome-control-center refuses
to run otherwise. Digital Wellbeing is deliberately absent: it configures
gnome-shell's own screen-time reporting, which does not run here.

**Note.** This machine has no printer, so the page was built against a temporary
CUPS queue that was created, exercised, and removed. The driverless add path
itself is verified by construction and by its refusals rather than against
hardware; the first real printer added here is worth watching.
