The sidebar was a flat scan of thirty-one rows; now it reads like a settings app. Multi-subject categories (Input, Network & Sharing, Applications, Users & Accounts, Privacy & Security, System) carry an Appearance-style tab strip above the page, drawn by the shell so the leaf pages themselves are untouched. The taxonomy lives in one new file, services/SettingsRoutes.qml; the sidebar, the strip, route validation, search breadcrumbs, and both generators derive from it. ShellState.settingsPage still holds leaf ids, so every deep link, IPC call, and search result keeps working — and now lands on the exact tab. Dictation moves out of Sound onto its own page under Input, with a handoff back to Sound for the microphone. The strip scrolls when System's nine tabs outgrow a tiled window. All 161 contracts pass. Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
212 lines
10 KiB
Markdown
212 lines
10 KiB
Markdown
# 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.
|
||
|
||
## Navigation
|
||
|
||
The sidebar lists fifteen **categories**, not one row per page. A category
|
||
covering several subjects — Input, Network & Sharing, Applications, Users &
|
||
Accounts, Privacy & Security, System — draws a tab strip above the page, and
|
||
each of its **leaf** pages is one tab; a category with a single subject is a
|
||
leaf itself and shows no strip. The column used to be a flat list of thirty-one
|
||
pages, which made finding Printers a scan of the whole thing. Grouped, it sits
|
||
under Network & Sharing, where somebody looking for it already expects it.
|
||
|
||
Leaf pages are ordinary `SettingsPage` files and know nothing about this.
|
||
`SettingsShell` draws the strip and hosts the page inside it, so moving a page
|
||
between categories never touches the page.
|
||
|
||
`services/SettingsRoutes.qml` is the one place the taxonomy is written down.
|
||
The sidebar, the tab strip, `ShellState`'s route validation, the breadcrumb
|
||
subtitles on search results ("System › Storage"), and
|
||
`scripts/panama-settings-commands` all derive from it — so a page moves in one
|
||
edit rather than five, and none of them can disagree about where it lives.
|
||
|
||
`ShellState.settingsPage` still holds a **leaf** id, the same ids callers have
|
||
always used, so every existing deep link, IPC call, and search hit keeps
|
||
working and now lands on the exact tab. A category id is accepted too, and
|
||
resolves to that category's first available tab.
|
||
|
||
Availability gating belongs to the strip rather than the sidebar:
|
||
`SettingsRoutes.pageAvailable()` drops the Containers tab on a machine without
|
||
podman and Snapshots without a snapper configuration, but only once a scan has
|
||
proven the stack absent, so a machine that has it never sees the tab blink.
|
||
|
||
## System Health
|
||
|
||
The stable internal `services` route renders **System Health**, a tab of
|
||
**System**. It is reachable from there, from its live 54px footer, from the
|
||
degraded-only bar indicator, and from 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:
|
||
|
||
```bash
|
||
~/.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.
|
||
|
||
```qml
|
||
// 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" }
|
||
}
|
||
```
|
||
|
||
```qml
|
||
// 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`.
|
||
|
||
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`
|
||
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.
|