Document the settings vocabulary and its traps
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
This commit is contained in:
@@ -44,6 +44,30 @@ Validate any change without leaving your session:
|
||||
Hyprland --verify-config
|
||||
```
|
||||
|
||||
## Generated configuration
|
||||
|
||||
Two ecosystem tools cannot read the shared settings file, and their configs live
|
||||
in this directory — which is a symlink into the Panama repository, so writing to
|
||||
them at runtime would put machine state into a tracked file. Both are therefore
|
||||
generated elsewhere:
|
||||
|
||||
| Tool | Generated to | Pointed there by |
|
||||
|---|---|---|
|
||||
| `hypridle` | `$XDG_STATE_HOME/panama/hypridle.conf` | a systemd user drop-in installed by `panama-idle install` |
|
||||
| `hyprpaper` | not generated — the wallpaper is applied over IPC and re-applied at shell start | — |
|
||||
|
||||
`quickshell/scripts/panama-idle` regenerates the hypridle config from the
|
||||
settings store and restarts the daemon. `hypridle.conf` in this directory
|
||||
remains the shipped default and is what runs when the drop-in is not installed;
|
||||
Panama Settings shows which of the two states you are in rather than presenting
|
||||
controls that quietly do nothing.
|
||||
|
||||
Remove the drop-in and go back to the shipped config with:
|
||||
|
||||
```sh
|
||||
~/.config/quickshell/scripts/panama-idle remove
|
||||
```
|
||||
|
||||
## Settings: one file, both sides
|
||||
|
||||
`~/.config/panama/settings.json` is shared with the Quickshell side. The
|
||||
@@ -67,6 +91,29 @@ wrong-typed settings file costs you your customisations and nothing else;
|
||||
`tests/hypr/prefs-fallback-contract.sh` pins that, including that Hyprland still
|
||||
accepts the config in each of those states.
|
||||
|
||||
### Adding a keybind: use `bind`, not `hl.bind`
|
||||
|
||||
Every bind in `keybinds.lua` goes through a local `bind()` wrapper that
|
||||
substitutes the chord from a stored override, so shortcuts can be moved from
|
||||
Panama Settings without editing this file.
|
||||
|
||||
```lua
|
||||
bind(mod .. " + Q", hl.dsp.window.close(), { description = "Close window" })
|
||||
```
|
||||
|
||||
Only the **chord** is ever taken from settings — the action is always the Lua
|
||||
value written here. A stored override can therefore move a shortcut but can
|
||||
never make one do something else, which is what makes reading overrides from a
|
||||
file the user can edit safe.
|
||||
|
||||
Overrides are keyed by the **shipped chord**, not the description. Descriptions
|
||||
are not unique: "Calculator" is both `SUPER + C` and the `XF86Calculator`
|
||||
hardware key, and keying by description moved both onto the same new chord,
|
||||
silently costing the hardware key.
|
||||
|
||||
An override whose value is not a plausible chord is ignored in favour of the
|
||||
shipped one, so a hand-edited `settings.json` cannot cost you a keymap.
|
||||
|
||||
### Keybind descriptions are required
|
||||
|
||||
Every `hl.bind` must pass a `description`. Hyprland reports Lua-defined binds
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user