Files
Panama/config/dot/quickshell/modules/settings
Gabriel Brown 7e1c85b094 Group the long pages by what you are trying to do
Notifications repeated both lock-screen switch labels for every application, so
twenty apps meant sixty rows of the same two sentences and the page could not be
scanned at all. Each app is one row now, carrying what its switches add up to --
"On, lock screen shows the sender only", "On, hidden on the lock screen",
"Notifications off" -- with the switches behind it, one app open at a time. The
identifier only appears while an app is open, which is the only time it
disambiguates anything, and the content switch dims when the app cannot reach
the lock screen at all, because there it means nothing.

Shortcuts were already grouped; the problem was that "Windows" caught focus,
movement, splitting, resizing and window state alike and held 43 of the 93
binds. A section that long is a list, not a grouping. They are separated by
intent now -- Focus, Move & split, Size, Window state -- and the split was
checked against the binds this machine actually has rather than trusted from the
keywords. Order matters in two places worth naming: "Next window splits down" is
about splitting rather than focus, and "Focus session" is quiet mode bound to a
workspace rather than window focus, so both are settled before the general
checks.

Refresh rate gets its own row. That need was created by collapsing the
resolution list: the rates for a resolution were only ever reachable by opening
it, so changing nothing but the rate meant going through the mode you already
had. It appears only when the current resolution offers more than one.

Default-application rows carry a chevron, having previously opened a chooser
while looking completely inert.

The notification contract asserted the literal Notifs.appRule(app.id).enabled,
which moved when the rows collapsed. The rule is still read through a binding on
Notifs.appRule, so a rule changed elsewhere still reaches the row -- the
assertion now requires that, rather than requiring one particular spelling of
it.

The power profile rows were left alone. A three-way choice in three rows looks
wasteful until you notice each row explains what the profile does, and that page
has empty space to spare; a segmented control would trade information for space
that is not scarce.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-19 22:54:51 -04:00
..
2026-08-18 15:00:18 -04:00
2026-08-19 13:05:13 -04:00
2026-08-19 13:05:13 -04:00
2026-08-19 15:07:17 -04:00
2026-08-19 13:05:13 -04:00
2026-08-18 15:00:18 -04:00
2026-08-19 10:52:10 -04:00
2026-08-19 13:05:13 -04:00
2026-08-19 13:05:13 -04:00

Settings

The control center for everything Panama owns. Anything the system owns — hardware, accounts, printers — is delegated to GNOME Settings and labeled as such rather than half-reimplemented.

System Health

The stable internal services route renders System Health. It is reachable from the Settings sidebar and its live 54px footer, the degraded-only bar indicator, and Vicinae's Check System Health command. Healthy scans reserve no bar space and produce no notification.

services/Health.qml owns the last accepted redacted snapshot. It invokes scripts/panama-doctor for scans and bounded repairs, wl-copy only for an explicit Copy Report, and bounded notify-send only when an external repair fails. For a concise terminal view, run:

~/.config/quickshell/scripts/panama-doctor --summary

The helper diagnoses Panama-owned desktop services, dependencies, links, and configured integrations. It does not read secret values, clipboard or notification contents, calendar events, SSIDs, addresses, or arbitrary command output. Its repair interface is an authored allow-list: it never installs a package, runs sudo, deletes user data, or repairs a service Panama does not own. A repair remains degraded until a fresh scan observes recovery.

The final card is the ownership boundary. Network configuration and the exact Users, Sharing, Color profiles, and Digital wellbeing handoffs open GNOME Settings because Fedora's system services own those areas.

Adding a setting

One schema entry. That is the whole job.

// config/PreferenceSchema.qml
{
    key: "blurSize", type: "int", def: 8, min: 1, max: 20, step: 1,
    unit: "px", group: "effects",
    label: "Blur radius",
    detail: "Larger is softer and costs more frame time",
    hypr: { path: ["decoration", "blur", "size"], option: "decoration:blur:size", readAs: "int" }
}
// the page
SliderRow { setting: "blurSize" }

Persistence, validation, clamping, reset, search indexing, and — with a hypr block — live application to the compositor and the startup replay all derive from that entry. There is nothing else to register.

If it is compositor-backed, add the matching prefs.get("blurSize", 8) in hypr/looks.lua so the Hyprland config still stands alone with no settings file.

Setting ownership

Every preference has one primary page, derived from its schema group and the route in services/SettingsSearch.qml. Search results always open that owner. A control may appear on a second page only when the same adaptation is part of another established mental model; otherwise use a labeled handoff to the owner instead of duplicating it.

Intentional mirrors

Setting Primary page Mirror Why the mirror earns its place
animationsEnabled Appearance Accessibility Reduced motion belongs both to visual polish and motion accessibility.
cursorInactiveTimeout Mouse Accessibility Pointer visibility is configured with pointer behavior but affects motor and visual access.
cursorSize Accessibility Mouse Large cursors are an accessibility adaptation that users also look for beside pointer controls.
inactiveOpacity Appearance Accessibility Window translucency is an appearance choice with a direct readability impact.
lockMinutes Power Privacy Idle timing owns the mechanism; privacy owns the expectation that the unattended desktop locks.
lockOnSleep Power Privacy Suspend owns the transition; privacy owns whether waking requires authentication.

Lock-screen visuals belong only to Appearance: background source, blur, clock, date, user name, and password-field presentation. Power owns when the session locks, while Privacy keeps only the established timing mirrors above. Visual controls must not be copied onto either page.

Displays is the sole owner of mode, scale, rotation, arrangement, and primary role. Those values form one safety transaction: every connected output is applied, verified, confirmed, or restored together. Other pages may link to Displays, but must never expose a second geometry control or persist a partial layout.

Mirrors must remain the same schema-backed control, never a second preference or a copied default. Additions to this table require a concrete discoverability reason and an update to tests/quickshell/settings-ownership-contract.sh.

Window border color follows the same ownership rule. The inactive border is a scheme-relative role owned by ColorScheme.qml: it changes only to retain neutral contrast in light and dark modes. The focused Prism border is the accent role, driven by the chosen accentName and also owned by ColorScheme.qml: each accent carries a separate pair for light and dark, so ColorScheme.qml restates the focused border alongside the inactive one on every scheme change, rather than leaving a scheme flip to erase a user-selected accent.

The rows

Component For
SettingsPage The page scaffold: title, lede, optional pinned header
ToggleRow { setting } A boolean
SliderRow { setting } A number; zeroLabel renders 0 as "Never"/"Instant"/"None"
ChoiceRow { setting } An enum, as a segmented control
ActionRow A button: opens a GNOME panel, runs a one-shot
TextRow A genuinely read-only fact

TextRow is for facts, not for settings that were merely expensive to wire. Before Stage 3 more than half of all rows were static text standing in for controls; that is the failure this vocabulary exists to prevent.

Rows write through SystemSettings.commitPreference(key, value), which routes compositor-backed keys through apply-and-verify and local keys straight to the store. A row never needs to know which kind it holds.

Things that will bite you

readAs describes the answer, not the setting. hyprctl getoption returns the value in a different JSON field per type — int, bool, float, str, and css for gaps (a four-value box). Declaring the wrong one does not fail loudly: it makes every write to that key look rejected, and the user sees an error for a change that worked. tests/quickshell/schema-hypr-shape-contract.sh asks the compositor for the real shape of every mapped option.

Never trust an exit code from hyprctl. keyword refuses to work on a Lua-configured Hyprland, prints the refusal to stdout, and exits 0. eval exits 0 on syntax and runtime errors too. The only trustworthy signal that a write landed is reading the value back.

The Settings window is tiled. implicitWidth is a hint; the layout decides, and it ranges from a half-screen split to the full display. SliderRow stacks its control under the label below 520px. Test narrow.

Binding an anchor to undefined does not reliably release it. Switching layouts that way left a slider anchored to both edges with the label squeezed into what was left. Position explicitly instead.

Inside a SettingsCard, parent is the card's internal Column. So parent.modelData in a nested Repeater is undefined and the rows silently never appear — you get a card with a heading and nothing under it. Address the outer model through an explicit id.

A TapHandler declared as a child of SettingRow lands in the trailing slot, because that is the row's default property, so only the right-hand edge becomes clickable. Use activatable: true with onActivated for a whole-row target.

A content-identical Quickshell entry can share the live shell's ID. Quickshell derives the Shell ID from config content, not path. Runtime harnesses therefore create a distinct semantic entry file, address that exact file with qs -p, and discover its PID from the exact Config path in qs list --all. They terminate only that recorded PID with kill; never use qs kill from a copied configuration.

Where state lives

File Holds
~/.config/panama/settings.json Everything in the schema. Read by the shell and by hypr/prefs.lua
$XDG_STATE_HOME/panama/panama-home.json Home accessory favorites and aliases
$XDG_STATE_HOME/panama/backups/ Settings snapshots
$XDG_STATE_HOME/panama/hypridle.conf Generated idle config
$XDG_STATE_HOME/panama/hyprlock.conf Generated lock-screen appearance

SystemSettings.restoreDefaults() spans all of them. A reset that silently skipped one would be worse than having no reset, because nothing would say so.

Not stored by Panama

Timezone and network time are read from and written to timedatectl directly. They belong to the machine and are shared with sessions that never see Panama's file; storing a copy would create a second answer to a question the system already answers.