colour -> color, behaviour -> behavior, centre -> center, favourite -> favorite, and about twenty other pairs, applied consistently across comments, docs, error/UI copy, and a handful of QML identifiers that used the British spelling as their actual name: SystemSettings' serialiseValue/serialiseTable/normaliseGradient, Displays' normaliseModes, Wallpaper's normalisePolicy, SettingsBackup's serialiseHomeState, DateTime's ntpSynchronised property, Clipboard's _normalise helper, and ShortcutCapture's cancelled signal (with its onCancelled handler in ShortcutsPage.qml). Every call site and the two tests that assert on the literal source text (settings-ownership and settings-backup-live contracts) were updated in lockstep. Left untouched: config/dot/espanso/match/packages/misspell-en/ is a vendored third-party autocorrect dictionary -- its entries are typo corrections, not our prose, and rewriting them would fight the package's own purpose (and any future re-sync from upstream). The already-American `favorites` property (Home page pinned accessories) was never actually misspelled -- only nearby comments and error strings said "favourites" -- so no data migration was needed there. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
243 lines
10 KiB
Markdown
243 lines
10 KiB
Markdown
# 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 serialized 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 customization 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; colors are validated as hex before serialization.
|
||
- 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. Customization
|
||
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.
|