diff --git a/config/dot/hypr/README.md b/config/dot/hypr/README.md index cd5e179..1c259e0 100644 --- a/config/dot/hypr/README.md +++ b/config/dot/hypr/README.md @@ -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 diff --git a/config/dot/quickshell/modules/settings/README.md b/config/dot/quickshell/modules/settings/README.md new file mode 100644 index 0000000..0b63665 --- /dev/null +++ b/config/dot/quickshell/modules/settings/README.md @@ -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. diff --git a/docs/superpowers/plans/2026-08-17-panama-cohesion.md b/docs/superpowers/plans/2026-08-17-panama-cohesion.md index 41d0ec8..c0d1822 100644 --- a/docs/superpowers/plans/2026-08-17-panama-cohesion.md +++ b/docs/superpowers/plans/2026-08-17-panama-cohesion.md @@ -285,3 +285,64 @@ can run in parallel with it. Nothing here is committed yet; `main` has 20+ uncommitted paths from prior work that should get a restore point before Stage 1 begins. + +--- + +## Stage 5 — Replace GNOME Settings for what Panama owns + +Not in the original plan. Added after the settings vocabulary made new pages +cheap enough that the limiting factor stopped being effort and started being +scope. Built jointly with the codex agent, which took default applications, the +page migrations, and the remaining hardcoded values. + +- [x] **Wallpaper** — thumbnail grid, applied over hyprpaper IPC. +- [x] **Power & Lock** — screen blank, lock, suspend, lock-before-sleep. +- [x] **Date & Time** — timezone and network time via `timedatectl`. +- [x] **Accessibility** — pointer size, text scale, motion, contrast. +- [x] **Search that indexes settings**, not page names. +- [x] **Editable Dock** — reorder, unpin, add. +- [x] **Settings snapshots** — save, list, restore. +- [x] **Rebindable shortcuts.** +- [ ] Displays: resolution, refresh rate, scale, rotation. +- [ ] Per-application notification rules. +- [ ] Window rules (float/tile/workspace) as a page. + +**Landed.** Each of these turned up something the compositor or its tools do +differently than documented, and in every case the failure mode was silence +rather than an error: + +* **hyprpaper 0.8 ignores the "all outputs" form.** `,` is accepted + and does nothing, so a wallpaper set that way appears to succeed and never + changes. Its IPC is also much smaller than older versions suggest — `preload`, + `listloaded`, `unload`, and `reload` all answer "invalid hyprpaper request". +* **`cursor:inactive_timeout` is answered as `float`, not `int`.** A wrong + `readAs` does not fail loudly; it makes every write to that key look rejected, + and the user sees an error for a change that worked. +* **Snapshot filenames collided at one-second resolution.** A save followed + promptly by a restore produced the same name twice, and the restore's own + safety snapshot overwrote the file it was about to read. Found by the + contract, which restores immediately after saving. +* **Keying bind overrides by description moved every bind sharing one.** + Rebinding `SUPER+C` also dragged `XF86Calculator` onto the same chord. + Descriptions are not unique; chords are. +* **The GNOME delegation allow-list named a panel that does not exist.** + `users` is not in `gnome-control-center --list`, so that button opened + nothing. +* **A copy of the Quickshell config shares the live shell's ID.** Quickshell + derives it from content rather than path, so an "isolated" harness built by + copying the config directory can kill or drive the running desktop. This took + the live shell down twice during development. Harnesses pointing at a single + distinct `.qml` file are unaffected. + +Two configurations are now generated rather than edited, because `~/.config/hypr` +is a symlink into this repository and writing there at runtime would put machine +state into a tracked file: hypridle's config, into `XDG_STATE_HOME` with a +systemd drop-in pointing at it, and the wallpaper, which is applied over IPC and +re-applied at shell start instead of being written into `hyprpaper.conf`. + +The schema gained a `json` type for structured values — the dock's pinned list +and the keybind overrides — so they live in the one settings file and are +covered by the one reset, rather than each growing a preference store of its +own. `restoreDefaults()` spans every store Panama owns, including the Home +accessory arrangement, which it reaches through that service's existing public +aliases rather than an API added for the purpose.