Records how to add a setting -- one schema entry -- and the failure modes found building the pages. Every one of them fails silently rather than loudly, which is why they are worth writing down: a wrong readAs makes successful writes look rejected, hyprpaper accepts an all-outputs wallpaper request and ignores it, and a copy of the Quickshell config shares the live shell's ID so an "isolated" harness can kill the running desktop. Also documents the two generated configurations and why they are generated: ~/.config/hypr is a symlink into this repository, so writing there at runtime would commit machine state to a tracked file. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
110 lines
4.6 KiB
Markdown
110 lines
4.6 KiB
Markdown
# Panama Settings
|
|
|
|
The control centre for everything Panama owns. Anything the system owns —
|
|
hardware, accounts, printers — is delegated to GNOME Settings and labelled as
|
|
such rather than half-reimplemented.
|
|
|
|
## 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.
|
|
|
|
## 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 copy of the Quickshell config shares the live shell's ID.** Quickshell
|
|
derives the Shell ID from config *content*, not path, so
|
|
`cp -a config/dot/quickshell $tmp && qs -p $tmp kill` kills the running
|
|
desktop, and `qs -p $tmp ipc call …` can drive it. Harnesses that point at a
|
|
single distinct `.qml` file are safe; copying the whole directory is not.
|
|
|
|
## 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 favourites and aliases |
|
|
| `$XDG_STATE_HOME/panama/backups/` | Settings snapshots |
|
|
| `$XDG_STATE_HOME/panama/hypridle.conf` | Generated idle config |
|
|
|
|
`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.
|