Managing a stored credential meant installing Seahorse. The keyring rows on Privacy could say whether it was locked and nothing about what was in it. Four rules, each pinned by a contract, because each is a way this could leak the thing it exists to protect: Listing never reads values. Enumerating reports labels and attributes; it does not ask the keyring to hand over what it is protecting. A secret never reaches a command line. /proc makes argv readable by every process on this machine, so a password passed as an argument is published to all of them. The helper reads the value in process and writes it to wl-copy on stdin. A secret never reaches an error message, a log, or a QML property. An exception raised while holding a password does not get to choose what text is printed, so the clipboard tool's stderr is discarded rather than echoed. Forgetting one is irreversible, so the first press asks and the second does it, and the confirming button is the only one wearing danger. The list is collapsed until asked for: opening Privacy should not enumerate someone's passwords as a side effect. A copied value clears itself about a minute later, but only if the clipboard still holds it -- the guard compares a SHA-256, so the waiting process never has the password. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
8.9 KiB
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:
sushi50 is already installed and ownsorg.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 bysetup/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 rangerather 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/homeare one btrfs filesystem sharing one pool of free space.dfreports them separately, and a page that copiesdfshows 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
--checkstaleness mode, matchingpanama-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: " -- 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.
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.