Files
Panama/docs/superpowers/specs/2026-08-17-panama-cohesion-design.md
T
Gabriel Brown d96863b687 Convert British spellings to American across the repo
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
2026-08-19 08:07:55 -04:00

10 KiB
Raw Blame History

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:

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.010.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:

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 FlickableColumnx: 34y: 30 → title/subtitle block. Every toggle repeats a four-property inline anchor incantation:

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:

{ 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:

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.