Make Panama settings one shared source of truth
Panama had grown into three configuration surfaces that only agreed because they had been typed to agree: looks.lua hardcoded values, DesktopPreferences independently defaulted the same values, and SystemSettings replayed them at startup. Nothing kept them in sync, and the Lua side read no shared state at all. This lands the first three stages of docs/superpowers/plans/2026-08-17-panama-cohesion.md. Fix silently failing Hyprland writes. On a Lua-configured Hyprland, hyprctl keyword refuses the write, prints the refusal to stdout, and still exits 0, so the HDR, VRR, and direct-scanout toggles persisted their value and reported success while the compositor never changed. Writes now go through hyprctl eval, which has the same hazard on syntax and runtime errors, so success is defined as reading the value back and finding it equal. The existing contract passed throughout the outage because it re-applied the values already in place; the new one flips each value to something it does not hold. Derive preferences from a schema. Every setting used to be restated four times -- a property alias, a JSON adapter property, a change handler, and a line in reset -- where omitting any one failed silently. PreferenceSchema.qml is now the single source, and persistence, validation, reset, and the Hyprland mapping all derive from it. Unknown keys on disk survive a write so a rollback does not discard a newer build's settings, and a corrupt file falls back to shipped defaults. The store moved to ~/.config/panama/settings.json, migrating from the old state directory without deleting it. Share that file with Hyprland. prefs.lua reads it at config time with every shipped literal kept as the fallback, so the config still stands alone. The Lua is the default, the JSON is the truth, and Settings is the editor. The compositor-adjustable surface goes from 3 keys to 23. Also fixes two test-hygiene bugs found by running the suite end to end for the first time: settings-pages-contract could see the window settings-window-contract leaves behind, and the new write contract was persisting its deliberately-wrong values into the user's real store. Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
# Panama Cohesion Implementation Plan
|
||||
|
||||
**Goal:** Make Panama one product instead of several good parts. A user changes
|
||||
how their desktop looks and behaves entirely from Panama Settings; the Lua config
|
||||
holds the shipped defaults; one JSON file is the truth both sides read.
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-08-17-panama-cohesion-design.md`
|
||||
|
||||
**Tech Stack:** Quickshell 0.3.0, Qt 6 QML, Hyprland 0.56.2 (Lua config),
|
||||
`hyprctl eval`, Bash contract tests.
|
||||
|
||||
## Global constraints
|
||||
|
||||
- `hyprctl keyword` is banned. It exits 0 without acting on this build.
|
||||
- Verify every write by reading the value back (`hyprctl getoption`), never by
|
||||
trusting an exit code.
|
||||
- No UI-supplied string is interpolated into `eval`, a shell command, or a config
|
||||
value. Numbers range-checked, choices allow-listed, colours hex-validated.
|
||||
- A missing or malformed `settings.json` degrades to shipped defaults. The Lua
|
||||
read is `pcall`-wrapped so it can never take down the compositor config.
|
||||
- Tokyo Night Moon and Prism stay the only identity; customisation adjusts its
|
||||
parameters, it does not replace it.
|
||||
- Motion stays event-driven at every setting. No idle repaint.
|
||||
- Do not restart the running Quickshell process during development; it hot-reloads.
|
||||
- Keep unrelated in-flight Panama work untouched (see the
|
||||
`feat/home-accessories-customization` worktree).
|
||||
|
||||
---
|
||||
|
||||
## Stage 0 — Stop the lying (ship first, standalone)
|
||||
|
||||
The display-policy toggles report success while doing nothing. This is a
|
||||
correctness bug in shipped behaviour and does not depend on any of the
|
||||
architecture below.
|
||||
|
||||
**Files:** Modify `config/dot/quickshell/services/SystemSettings.qml`;
|
||||
Test `tests/quickshell/settings-hyprland-write-contract.sh`
|
||||
|
||||
- [x] Write a contract that sets a display policy through `SystemSettings`, then
|
||||
asserts via `hyprctl getoption` that the compositor value actually changed —
|
||||
and that a rejected write leaves `lastError` non-empty.
|
||||
- [x] Run it; confirm it fails against the current `hyprctl keyword` implementation.
|
||||
- [x] Add a single `applyOptions(values)` boundary that serialises validated values
|
||||
into `hl.config{}` and runs them through `hyprctl eval`.
|
||||
- [x] Route `setAutoHdr`, `setVrrPolicy`, and `setDirectScanoutPolicy` through it.
|
||||
- [x] Treat "wrote it back and read it back equal" as the only success condition;
|
||||
persist to preferences only on verified success.
|
||||
- [x] Run the contract to green, and confirm live that HDR/VRR/scanout change.
|
||||
|
||||
**Exit criteria:** the three toggles do what they claim, and a failed write says so.
|
||||
|
||||
**Landed.** `hyprctl eval` also exits 0 on syntax and runtime errors — it reports
|
||||
them as an `error:` line on stdout — so exit status is useless for both commands.
|
||||
`applyOptions` therefore parses stdout for the error line *and* reads every
|
||||
written option back with `hyprctl -j --batch getoption`, committing to
|
||||
preferences only for options that read back equal. A `writableOptions` registry
|
||||
holds the group/key/option path and allow-list per option, so the UI never names
|
||||
an option or supplies an unchecked value. Policy is applied as one batch, so the
|
||||
shell cannot come up half-configured.
|
||||
|
||||
New: `tests/quickshell/settings-hyprland-write-contract.sh` — flips each policy
|
||||
to a value it does not hold and reads it back, so a no-op write cannot pass.
|
||||
The pre-existing `settings-system-contract.sh` re-applied the values already in
|
||||
place, which is why it passed throughout the outage.
|
||||
|
||||
---
|
||||
|
||||
## Stage 1 — One schema, one store
|
||||
|
||||
**Files:** Create `config/dot/quickshell/config/PreferenceSchema.qml`;
|
||||
Modify `config/dot/quickshell/config/DesktopPreferences.qml`,
|
||||
`config/dot/quickshell/config/Settings.qml`;
|
||||
Test `tests/quickshell/preference-schema-contract.sh`
|
||||
|
||||
Schema entry shape:
|
||||
|
||||
```qml
|
||||
{ key: "dockHideDelayMs", type: "int", def: 250, min: 0, max: 2000, step: 25,
|
||||
group: "dock", label: "Hide delay",
|
||||
detail: "Prevents flicker when crossing icons" }
|
||||
```
|
||||
|
||||
- [x] Write a contract asserting: every schema key round-trips through disk;
|
||||
an out-of-range value is clamped rather than stored; an unknown key in the
|
||||
file is preserved rather than dropped; and `reset()` returns *every* key to
|
||||
its schema default with no hand-maintained list.
|
||||
- [x] Run it; confirm it fails.
|
||||
- [x] Author `PreferenceSchema.qml` covering the existing 16 user-facing keys.
|
||||
- [x] Rewrite `DesktopPreferences` to derive persistence, change notification,
|
||||
validation, and reset from the schema — deleting the four-places-per-key
|
||||
boilerplate and `resetDesktopDefaults()`'s hand-written body.
|
||||
- [x] Move the store to `~/.config/panama/settings.json`, migrating the existing
|
||||
file from `Quickshell.stateDir` on first run if present.
|
||||
- [x] Keep `Settings.qml` as the stable public read surface; existing consumers
|
||||
must not change.
|
||||
- [x] Run the new contract plus `settings-preferences-contract.sh` and
|
||||
`settings-pages-contract.sh` to green.
|
||||
|
||||
**Exit criteria:** adding a setting is one schema line; reset is complete by
|
||||
construction; the store lives at a stable, user-visible path.
|
||||
|
||||
**Landed.** Reads go through `DesktopPreferences.get(key)` and writes through
|
||||
`set(key, value)`; a `revision` counter gives function-call bindings something to
|
||||
invalidate, which a bare call would not have. `Settings.qml` stays the typed
|
||||
public surface — every consumer outside it was already reading through it.
|
||||
`set()` returns false on an unknown key or an unrepresentable value, so a
|
||||
rejection is observable instead of inferred.
|
||||
|
||||
Unknown keys on disk are carried through writes untouched, so rolling back to an
|
||||
older Panama does not discard a newer version's settings. A corrupt file falls
|
||||
back to shipped defaults rather than costing the user a working desktop. Both are
|
||||
pinned by contract.
|
||||
|
||||
Migration reads the old `Quickshell.stateDir` file only when the new one is
|
||||
absent, and never deletes the original. Verified live: the running shell adopted
|
||||
all 17 values into `~/.config/panama/settings.json` with the legacy files intact.
|
||||
|
||||
New: `config/PreferenceSchema.qml`, `preference-schema-harness.qml`,
|
||||
`tests/quickshell/preference-schema-contract.sh`. Full suite green — 6 settings
|
||||
contracts, 22 other Quickshell contracts, 3 Python bridge tests.
|
||||
|
||||
---
|
||||
|
||||
## Stage 2 — Hyprland reads the same file
|
||||
|
||||
**Files:** Create `config/dot/hypr/prefs.lua`;
|
||||
Modify `config/dot/hypr/hyprland.lua`, `looks.lua`, `input.lua`, `monitors.lua`;
|
||||
Test `tests/hypr/prefs-fallback-contract.sh`
|
||||
|
||||
- [x] Write a contract that verifies `Hyprland --verify-config` passes with the
|
||||
file absent, empty, truncated mid-object, and containing wrong-typed values —
|
||||
and that each case yields the shipped default.
|
||||
- [x] Run it; confirm it fails (no `prefs.lua` yet).
|
||||
- [x] Implement a dependency-free JSON reader exposing `prefs.get(key, fallback)`,
|
||||
`pcall`-wrapped, reading `$XDG_CONFIG_HOME/panama/settings.json`.
|
||||
- [x] Require it first in `hyprland.lua`, before `looks`.
|
||||
- [x] Convert the appearance and behaviour literals in `looks.lua` and `input.lua`
|
||||
to `prefs.get("<key>", <current literal>)`, keeping every current value as
|
||||
the fallback so shipped behaviour is byte-identical.
|
||||
- [x] Extend `PreferenceSchema.qml` with the Hyprland-owned keys, each carrying
|
||||
the `hl.config` path it maps to.
|
||||
- [x] Have `SystemSettings` derive its `eval` payload from that mapping, so a new
|
||||
Hyprland setting needs no new writer code.
|
||||
- [x] Run the contract, `Hyprland --verify-config`, and a live reload to green.
|
||||
|
||||
**Exit criteria:** one file is the truth; the Lua is the default; Settings is the
|
||||
editor; changes apply live *and* survive a reboot.
|
||||
|
||||
**Landed.** The compositor-adjustable surface went from 3 keys to 22.
|
||||
`SystemSettings` no longer names any option: it walks `PreferenceSchema.hyprEntries()`,
|
||||
builds one nested `hl.config{}` payload from each entry's table path, and verifies
|
||||
against the entry's `option` path. Adding a live-adjustable Hyprland setting is now
|
||||
a schema entry plus a `prefs.get` call, with no new writer code.
|
||||
|
||||
Verification had to learn the compositor's answer shapes: `getoption` returns the
|
||||
value in a different field per type — `int`, `bool`, `float`, `str`, and `css` for
|
||||
gaps, which read back as a four-value box (`"10 10 10 10"`). A verifier that only
|
||||
understood `int` would have reported every other type as rejected. All five are
|
||||
covered by contract.
|
||||
|
||||
`keyboardLayout` is the first setting whose value reaches an `hl.config` string,
|
||||
so the schema gained a `pattern` field enforced in `coerce()`. The contract
|
||||
includes a Lua-injection attempt through it; the value is rejected, nothing
|
||||
executes, and the layout is unchanged.
|
||||
|
||||
Two test-hygiene bugs found and fixed along the way, both pre-existing in shape:
|
||||
`settings-window-contract` leaves a Settings window that the compositor destroys
|
||||
asynchronously, which made `settings-pages-contract` see a duplicate when run
|
||||
straight after it — the pages contract now waits for a clean slate. And the new
|
||||
write contract was persisting its deliberately-wrong values into the *real*
|
||||
`~/.config/panama/settings.json`, where the next `hyprctl reload` would faithfully
|
||||
apply them; it now runs against an isolated `XDG_CONFIG_HOME` while still driving
|
||||
the live compositor.
|
||||
|
||||
Verified live: writing `gapsOut`/`windowRounding` into the shared file and running
|
||||
`hyprctl reload` — the path a fresh login takes — applied both, and the compositor
|
||||
and store agree on every key. Full suite green: 29 shell contracts, 3 Python
|
||||
bridge tests, run sequentially.
|
||||
|
||||
---
|
||||
|
||||
## Stage 3 — Generic rows, then fill the pages
|
||||
|
||||
Per the visual-work rule, this stage stops for a decision before any page is
|
||||
rewritten.
|
||||
|
||||
**Files:** Create `modules/settings/SettingsPage.qml`, `ToggleRow.qml`,
|
||||
`SliderRow.qml`, `ChoiceRow.qml`, `ActionRow.qml`, `TextRow.qml`;
|
||||
Modify all eleven `*Page.qml`; Test `tests/quickshell/settings-rows-contract.sh`
|
||||
|
||||
- [ ] **Build static mocks** of the new Appearance page and one rebuilt existing
|
||||
page, serve them over HTTP, report the URL, and **stop for a decision.**
|
||||
- [ ] Write a contract asserting each row type binds a schema key by name, reflects
|
||||
external changes, and clamps out-of-range input.
|
||||
- [ ] Implement the row components and `SettingsPage` (the scaffold currently
|
||||
copy-pasted eleven times).
|
||||
- [ ] Rewrite the eleven pages on top of them; delete the dead read-only rows that
|
||||
only existed because a real control was expensive.
|
||||
- [ ] Promote the hardcoded `Settings.qml` values into real controls: weather
|
||||
location/unit/interval, vitals interval, night-light schedule, the four
|
||||
notification timing and history limits, capture directories, and recorder
|
||||
arguments.
|
||||
- [ ] Give Appearance real content: accent pair, window rounding, gaps, border
|
||||
size, blur, animation speed, bar height, font scale, wallpaper.
|
||||
- [ ] Make the dock pin list editable (reorder, add, remove) instead of a
|
||||
16-entry literal.
|
||||
- [ ] Run the rows contract and the existing settings contracts to green.
|
||||
|
||||
**Exit criteria:** no shipped behaviour value is reachable only by editing a file.
|
||||
|
||||
---
|
||||
|
||||
## Stage 4 — Shortcuts from the compositor
|
||||
|
||||
**Files:** Create `services/Keybinds.qml`; Modify `modules/settings/ShortcutsPage.qml`,
|
||||
`config/dot/hypr/keybinds.lua`; Test `tests/quickshell/keybinds-contract.sh`
|
||||
|
||||
- [ ] Write a contract asserting the page's bind count matches `hyprctl binds -j`
|
||||
exactly, so it can never drift again.
|
||||
- [ ] Run it; confirm it fails at 19 of 113.
|
||||
- [ ] Implement `Keybinds.qml` reading `hyprctl binds -j`, grouped and searchable.
|
||||
- [ ] Backfill `description` in `keybinds.lua` for the 29 binds that lack one.
|
||||
- [ ] Rebuild `ShortcutsPage` on the live data; delete the hardcoded array.
|
||||
- [ ] Add rebinding: overrides in the same JSON, applied by `keybinds.lua` after
|
||||
the defaults and live via `eval`, with conflict detection against existing binds.
|
||||
- [ ] Run the contract to green.
|
||||
|
||||
**Exit criteria:** the page shows every real bind, always current, and can change them.
|
||||
|
||||
---
|
||||
|
||||
## Sequencing note
|
||||
|
||||
Stage 0 is independent — ship it alone. Stages 1 and 2 are the architecture and
|
||||
should land together, since Stage 2 is what makes Stage 1 worth doing. Stage 3
|
||||
is the largest and is gated on a visual decision. Stage 4 is independent of 3 and
|
||||
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.
|
||||
@@ -0,0 +1,242 @@
|
||||
# Panama Cohesion Design
|
||||
|
||||
## Purpose
|
||||
|
||||
Panama has grown from a Hyprland config into a desktop: 15 Lua/conf files, 130 QML
|
||||
files, 18 services, a 12-page settings application, and 29 contract tests. Each
|
||||
feature was built well on its own. What is missing is the seam between them.
|
||||
|
||||
This document audits the current state and defines the architecture that turns
|
||||
the pieces into one product, with a single goal:
|
||||
|
||||
> **A user should never need a text editor to change how their desktop behaves.**
|
||||
|
||||
That goal is not currently met, and the reason is structural rather than a matter
|
||||
of missing pages.
|
||||
|
||||
---
|
||||
|
||||
## Part 1 — Audit
|
||||
|
||||
### Finding 1 (critical, live bug): every Hyprland write from Settings is a no-op
|
||||
|
||||
`services/SystemSettings.qml` applies display policy with `hyprctl keyword`:
|
||||
|
||||
```qml
|
||||
autoHdrWrite.exec(["hyprctl", "keyword", "render:cm_auto_hdr", enabled ? "1" : "0"]);
|
||||
```
|
||||
|
||||
On a Lua-configured Hyprland, `hyprctl keyword` does not work:
|
||||
|
||||
```
|
||||
$ hyprctl getoption decoration:rounding -j → "int": 18
|
||||
$ hyprctl keyword decoration:rounding 4
|
||||
keyword can't work with non-legacy parsers. Use eval.
|
||||
$ echo $? → 0
|
||||
$ hyprctl getoption decoration:rounding -j → "int": 18
|
||||
```
|
||||
|
||||
It prints the refusal to **stdout** and exits **0**. `SystemSettings` branches on
|
||||
`exitCode === 0`, so all three writers take the success path: they persist the
|
||||
requested value to `panama-settings.json`, clear `lastError`, and the UI redraws
|
||||
as if the change took effect. Nothing reached the compositor.
|
||||
|
||||
Game-aware HDR, VRR policy, and direct scanout have therefore never worked from
|
||||
Settings, and the app confidently reports that they did. `applyPersistedDisplayPolicy()`
|
||||
replays the same three no-ops one second after every shell start.
|
||||
|
||||
The correct mechanism on this build is `hyprctl eval`, which is verified working:
|
||||
|
||||
```
|
||||
$ hyprctl eval 'hl.config({ decoration = { rounding = 4 } })' → ok
|
||||
$ hyprctl getoption decoration:rounding -j → "int": 4
|
||||
```
|
||||
|
||||
`eval` is also strictly more capable than `keyword` — it can set *any* config
|
||||
value, including gradients, animation curves, and nested tables. It executes
|
||||
arbitrary Lua, so the existing "never interpolate UI text into a command" rule
|
||||
must extend to it: values are validated and serialised numerically, never
|
||||
concatenated from user input.
|
||||
|
||||
### Finding 2: three disconnected sources of truth for the same three values
|
||||
|
||||
| Value | `looks.lua` | `DesktopPreferences.qml` | Applied by |
|
||||
| --- | --- | --- | --- |
|
||||
| `render:cm_auto_hdr` | `1` | `autoHdr: true` | `hyprctl keyword` (no-op) |
|
||||
| `misc:vrr` | `3` | `vrrPolicy: 3` | `hyprctl keyword` (no-op) |
|
||||
| `render:direct_scanout` | `2` | `directScanoutPolicy: 2` | `hyprctl keyword` (no-op) |
|
||||
|
||||
They agree today only because they were typed to agree. Editing the Lua does not
|
||||
change what Settings displays; changing Settings does not touch the Lua. The Lua
|
||||
side reads no shared state whatsoever — there is no bridge in either direction.
|
||||
|
||||
### Finding 3: the preference store costs four hand-edits per knob
|
||||
|
||||
Every key in `config/DesktopPreferences.qml` is written out four times: a
|
||||
`property alias`, a `JsonAdapter` property, a `Connections` handler, and a line
|
||||
in `resetDesktopDefaults()`. Seventeen keys produce 68 lines of pure bookkeeping.
|
||||
|
||||
The failure modes are silent. Omit the `Connections` handler and the setting
|
||||
stops persisting with no error. Omit the reset line and "Restore defaults"
|
||||
quietly skips it. This cost is the direct reason the settings app stalled at 16
|
||||
user-facing knobs.
|
||||
|
||||
### Finding 4: ~40 comparable values are hardcoded one file away
|
||||
|
||||
`config/Settings.qml` still hardcodes, as `readonly`: weather latitude/longitude,
|
||||
location label, temperature unit and refresh interval; the vitals poll interval
|
||||
and the GPU sysfs path; the night-light schedule (`17.0`–`10.0`); all four
|
||||
notification timing and history limits; the 16-entry dock pin list; the
|
||||
screenshot and recording directories; and the `wf-recorder` argument string.
|
||||
|
||||
Every one of these is exactly the kind of thing the settings app exists for. None
|
||||
is reachable from it.
|
||||
|
||||
### Finding 5: appearance is not adjustable at all
|
||||
|
||||
`Theme.qml` defines ~50 tokens, all `readonly`, none mutable. The Appearance
|
||||
page's "Theme" card is two dead text rows:
|
||||
|
||||
```qml
|
||||
SettingRow { label: "Color palette"; detail: "Tokyo Night Moon"; value: "Prism" }
|
||||
SettingRow { label: "Interface type"; detail: "Adwaita Sans"; value: "System" }
|
||||
```
|
||||
|
||||
Meanwhile `looks.lua` hardcodes gaps, border size, rounding, blur, shadow, glow,
|
||||
and fourteen animation curves. The page named "Appearance" adjusts a clock format
|
||||
and three vitals toggles.
|
||||
|
||||
### Finding 6: Shortcuts is a hand-typed copy of 19 of 113 real binds
|
||||
|
||||
`keybinds.lua` makes 95 `hl.bind` calls producing **113** live binds. The page
|
||||
hardcodes an array of **19**. It cannot show the other 94, cannot change any, and
|
||||
drifts the moment a bind is edited.
|
||||
|
||||
The compositor already exposes the real list, and it is 74% self-describing:
|
||||
|
||||
```
|
||||
$ hyprctl binds -j → 113 binds, 84 carrying a human description
|
||||
modmask 64 key T description "Terminal"
|
||||
```
|
||||
|
||||
### Finding 7: "Restore defaults" is incomplete by construction
|
||||
|
||||
`resetDesktopDefaults()` resets `DesktopPreferences` only. Anything persisted
|
||||
elsewhere survives an action that claims to restore Panama's defaults.
|
||||
|
||||
### Finding 8: page scaffolding is copy-pasted eleven times
|
||||
|
||||
Every page repeats the same `Flickable` → `Column` → `x: 34` → `y: 30` →
|
||||
title/subtitle block. Every toggle repeats a four-property inline anchor
|
||||
incantation:
|
||||
|
||||
```qml
|
||||
SettingsToggle { anchors.right: parent.right; anchors.verticalCenter: parent.verticalCenter;
|
||||
checked: DesktopPreferences.showCpu; onToggled: value => DesktopPreferences.showCpu = value }
|
||||
```
|
||||
|
||||
Across eleven pages there are 56 rows but only ~15 toggles, 14 buttons, and 3
|
||||
sliders — over half the rows are static text. Pages settled for read-only text
|
||||
because a real control was expensive to add. That is a tooling problem wearing a
|
||||
product problem's clothes.
|
||||
|
||||
### What is genuinely good and must be preserved
|
||||
|
||||
- The `SystemSettings` allow-list discipline — UI never builds a command string.
|
||||
- The Prism design language and its restraint, documented in `Theme.qml`.
|
||||
- Event-driven motion; nothing repaints while idle.
|
||||
- The 29 contract tests and the spec → plan → implement workflow.
|
||||
- Delegation of hardware, accounts, and printers to GNOME rather than
|
||||
half-reimplementing them.
|
||||
|
||||
---
|
||||
|
||||
## Part 2 — Target architecture
|
||||
|
||||
Four changes, in dependency order. Each is independently useful and independently
|
||||
shippable.
|
||||
|
||||
### A. One schema, one store
|
||||
|
||||
Replace the hand-maintained preference object with a declarative schema —
|
||||
one entry per setting carrying key, type, default, bounds or options, group,
|
||||
label, and detail:
|
||||
|
||||
```qml
|
||||
{ key: "dockHideDelayMs", type: "int", def: 250, min: 0, max: 2000, step: 25,
|
||||
group: "dock", label: "Hide delay",
|
||||
detail: "Prevents flicker when crossing icons" }
|
||||
```
|
||||
|
||||
Persistence, change notification, validation, reset, and the settings UI all
|
||||
derive from that one entry. Adding a knob becomes one line instead of four edits
|
||||
plus a hand-built row, and reset becomes complete by construction rather than by
|
||||
diligence.
|
||||
|
||||
The store moves from Quickshell's opaque per-shell state directory to
|
||||
`~/.config/panama/settings.json`, so it is a stable path that the compositor can
|
||||
also read, and one a user can back up, diff, or put in a dotfiles repo.
|
||||
|
||||
### B. Hyprland reads the same file
|
||||
|
||||
`config/dot/hypr/prefs.lua` gains a small dependency-free JSON reader and exposes
|
||||
`prefs.get(key, fallback)`. `looks.lua`, `input.lua`, and `monitors.lua` read
|
||||
through it, keeping their current literals as the fallback:
|
||||
|
||||
```lua
|
||||
rounding = prefs.get("windowRounding", 18),
|
||||
gaps_out = prefs.get("gapsOut", 10),
|
||||
```
|
||||
|
||||
A missing, empty, or malformed file yields the shipped defaults. The read is
|
||||
wrapped in `pcall` so a corrupt file can never take down the config.
|
||||
|
||||
This closes the loop:
|
||||
|
||||
- **Lua is the default.** It ships the curated values and works standalone.
|
||||
- **The JSON is the truth.** Both sides read it.
|
||||
- **Settings is the editor.** It writes the JSON *and* applies live via
|
||||
`hyprctl eval`, so changes take effect immediately and survive a reboot.
|
||||
|
||||
### C. Generic rows, then fill the pages
|
||||
|
||||
Add `SettingsPage` (the repeated scaffold), plus `ToggleRow`, `SliderRow`,
|
||||
`ChoiceRow`, `ActionRow`, and `TextRow`. Rewrite the eleven pages on top of them,
|
||||
promote the ~40 hardcoded `Settings.qml` values into real controls, and give
|
||||
Appearance genuine content: accent pair, window rounding, gaps, border size,
|
||||
blur, animation speed, bar height, font scale, and wallpaper.
|
||||
|
||||
Appearance customisation stays inside the design language. The user picks how much
|
||||
of it there is — spacing, softness, motion — not a free-form palette editor that
|
||||
would let the Prism identity be dismantled by accident.
|
||||
|
||||
### D. Shortcuts from the compositor, then editable
|
||||
|
||||
Generate the Shortcuts page from `hyprctl binds -j` so it shows all 113 binds and
|
||||
can never drift. Backfill descriptions for the 29 binds that lack one. Then allow
|
||||
rebinding: overrides live in the same JSON, `keybinds.lua` applies them after the
|
||||
defaults, and Settings applies them live with `hyprctl eval`.
|
||||
|
||||
---
|
||||
|
||||
## Constraints
|
||||
|
||||
- No UI-supplied string is ever interpolated into an `eval`, a shell command, or
|
||||
a config value. Numbers are range-checked; choices are matched against an
|
||||
allow-list; colours are validated as hex before serialisation.
|
||||
- A malformed or absent `settings.json` must degrade to shipped defaults, never
|
||||
to a broken compositor.
|
||||
- `hyprctl keyword` is banned in this codebase. It exits 0 without acting.
|
||||
- Every write path must be observably verified — read the value back rather than
|
||||
trusting an exit code. Finding 1 exists because an exit code was trusted.
|
||||
- Tokyo Night Moon and Prism remain the only visual identity. Customisation
|
||||
adjusts its parameters, it does not replace it.
|
||||
- Motion stays event-driven. No idle repaint, at any setting.
|
||||
- GNOME keeps ownership of hardware, accounts, printers, and users.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Arbitrary theme/palette import.
|
||||
- A global menu (previously investigated; GTK apps expose `org.gtk.Actions` but
|
||||
not `org.gtk.Menus`, so coverage would be too inconsistent to ship).
|
||||
- Replacing any GNOME-delegated panel.
|
||||
Reference in New Issue
Block a user