Files
Panama/docs/superpowers/specs/2026-08-23-appearance-redesign.md
Gabriel Brown cb7c09d208 Give the desktop real themes, video wallpapers, and honest titlebars
Appearance now opens on Themes: light and dark side by side, each
remembering its own choice, over galleries of ten shipped themes —
Tokyo Moon and Day joined by Moon Rose, Catppuccin, Nord, Gruvbox and
Everforest in both modes. A theme is a complete palette: the catalog
lives in themes.json, Theme.qml reads every color token from the
active record, and one render pipeline carries it to kitty, tmux,
btop, GTK, Vicinae, Firefox's chrome, and the lock screen. The Theme
editor builds new ones from four wells — wheel, hex, or eyedropper —
with derived surfaces, a saturation slider, debounced fine-tune, and
effects that save with the theme. Custom edits finally keep GNOME's
accent, kitty's border, and hyprlock in sync.

Wallpapers can be video: mpvpaper per output, hardware-decoded, muted
and looped, supervised and respawned. Panama owns the pausing — games,
battery, and a bar pill for right now — because the compositor
rebuilds full-screen blur for every frame a video wallpaper draws.
The lock screen gets a still frame.

Titlebars stop lying. GNOME apps get close-only on your chosen side,
the maximize and double-click settings are gone, the Settings window
obeys the same rules, and its titlebar can be turned off entirely.
Typography becomes five labeled dropdowns instead of a wall of
samples.

Contracts updated and written throughout (165 now); per the redesign
workflow none were executed — the full sweep runs once at the end.

Claude-Session: https://claude.ai/code/session_01Ms2FbjQy31TVf3CEvQhGM8
2026-08-23 23:39:04 -04:00

205 lines
12 KiB
Markdown

# Appearance redesign — full themes, editor, video wallpapers, honest titlebars
Approved 2026-08-23 (consolidated interactive mock). Four workstreams; the theme system
is the core. No test is executed until the whole settings redesign is done (see the
test backlog spec); tests are written alongside.
## 1. The theme system
A **theme** is a complete palette plus an accent pair, bound to one scheme. Selecting a
theme restyles the shell, kitty, GTK, the lock screen, Vicinae, Firefox's chrome, tmux,
and btop. Ten ship; the editor makes more.
### The catalog — one source of truth
`config/dot/quickshell/config/themes.json` holds the shipped themes. Unlike
`palette.json` (which stays as-is for the curated accent pairs), the catalog is **not**
duplicated into a JS table: a new `services/ThemeCatalog.qml` singleton reads it with
`FileView` + `JSON.parse` (the ManualPage pattern), scripts read it with `jq`, and the
Node halves of contracts read the JSON directly with `fs`. `ThemeCatalog` embeds a
minimal Moon/Day fallback so the shell renders correctly for the instant before the
file loads (and forever if it is missing).
A theme record:
```json
{ "id": "mocha", "name": "Catppuccin Mocha", "scheme": "dark",
"accent": "#cba6f7", "secondary": "#f5c2e7",
"palette": { "bg": "…", "bgDark": "…", "bgHighlight": "…", "bgPanel": "…",
"bgPopover": "…", "fg": "…", "fgDim": "…", "fgMuted": "…", "gutter": "…",
"accentAlt": "…", "cyan": "…", "teal": "…", "green": "…", "yellow": "…",
"orange": "…", "red": "…", "redDeep": "…", "magenta": "…", "pink": "…" },
"ansi": { "black": "…", "red": "…", "green": "…", "yellow": "…", "blue": "…",
"magenta": "…", "cyan": "…", "white": "…", "brightBlack": "…", "…": "…",
"brightWhite": "…" } }
```
The nineteen palette keys are exactly `Theme.qml`'s color tokens. `ansi` (16 keys)
exists so terminal themes can be generated rather than hand-kept per theme.
Shipped lineup: **Tokyo Moon** (default dark — today's exact values), **Moon Rose**
(Moon's palette, rose accent), **Catppuccin Mocha**, **Nord**, **Gruvbox**,
**Everforest**; **Tokyo Day** (default light — today's values), **Catppuccin Latte**,
**Gruvbox Light**, **Everforest Light**.
### Records, selection, and scheme flips
`ThemeProfileModel.js` custom records extend to
`{ id, name, scheme, accent, secondary, shipped, palette?, ansi?, effects? }` — both
whitelists (`copyProfile`, `normalizeStoredProfile`) learn the new optional fields,
each palette/ansi value hex-validated, `effects` validated against the ten effect keys.
A stored custom without `palette` inherits its scheme's default theme palette
(moon/day), which also grandfathers every existing saved profile.
Selection becomes per-mode: new internal prefs `themeDark` (default `"moon"`) and
`themeLight` (default `"day"`). The Themes tab's light/dark hero writes `colorScheme`;
`ThemeProfiles` resolves `activeId = colorScheme == "light" ? themeLight : themeDark`.
`reconcileScheme`'s force-reset to moon/day dies — a scheme flip now lands on *your*
chosen theme for that scheme, custom or shipped. Picking a theme card writes its id
into the mode's key (and flips `colorScheme` when the card belongs to the other mode).
`accentName` desync bug dies with it: every commit path (theme select, curated swatch,
hex, wheel, eyedropper, HSV) ends in one function that also recomputes `accentName` as
the nearest curated accent (for GNOME's enum) — libadwaita, kitty borders, and the
lock screen stop trailing the truth.
### Theme.qml
The 19 color ternaries and 7 alpha ternaries become lookups:
`palette(token)` reads `ThemeProfiles.activeProfile.palette?.[token] ?? ThemeCatalog`
default for the active scheme. Semantic aliases (`ok/warn/danger/urgent`), geometry,
type, and motion tokens are untouched.
### Reach — the render pipeline
`panama-theme-apps` generalizes: it resolves the active theme itself (settings.json →
`themeProfileId`/`themeDark`/`themeLight` → catalog or stored customs, all via `jq`)
and renders from the palette instead of copying per-scheme files:
| target | today | becomes |
|---|---|---|
| kitty | copy tokyonight-{moon,day}.conf + append accent | render `current-theme.conf` from palette + ansi (template), live-apply via existing socket loop |
| hyprlock | scheme literals duplicated in 3 scripts | all eight placeholders filled from the palette (the triplication dies) |
| GTK settings.ini | template (scheme only) | unchanged |
| GTK gtk.css (3.0/4.0) | static, `#82aaff` baked in, "light" file secretly dark | marker-delimited `@define-color` block regenerated from palette; structural CSS untouched; light file actually light |
| libadwaita accent | gsettings enum | unchanged (nearest-curated mapping) |
| Vicinae | two hardcoded TOMLs | render `panama-dark.toml` / `panama-light.toml` from palette; `vicinae.json` points at them |
| Firefox | vendored Edge-Frfox palette, no Panama colors | render `chrome/custom.css` (the shipped override seam) mapping Edge-Frfox variables to the palette |
| tmux | copy tokyonight files | render `current-theme.conf` from palette |
| btop | sed the theme name | render a `panama.theme` from palette, point btop at it |
| nvim | reads colorScheme itself | **unchanged this phase** — stays tokyonight moon/day; a per-theme colorscheme map is future work |
`bin/panama-hook theme-set` keeps firing (and moves out of the kitty `if`-condition it
is accidentally inside, so it genuinely runs after all generators).
### Effects belong to themes
The Effects card moves from the theme tab to the **Theme editor**. Saving a custom
theme snapshots the ten effect values into the record; applying a theme that carries
`effects` commits them; shipped themes carry none and leave yours alone.
### The UI
`AppearancePage` tabs become **Themes | Theme editor | Background | Typography |
Windows | Shell** (tab values `themes`, `editor`, plus existing).
**Themes tab** (the category's landing): the light/dark hero (two preview cards,
writes `colorScheme`), a dark gallery and a light gallery of theme cards (shipped +
customs for that scheme; custom cards get Delete), each card a mini palette preview;
clicking applies live. A dashed "Build your own theme" card jumps to the editor tab.
**Theme editor tab**: start-from chips (any theme, or the active one), scheme segment,
four color wells — Primary, Secondary, **Background**, **Foreground** — each with a
hex field, a color wheel (Qt `ColorDialog`; if unavailable under Quickshell, a small
in-shell HSV wheel popover), and the hyprpicker eyedropper. Background edits derive
the surface tokens (bgDark/bgPanel/bgHighlight/bgPopover/gutter) by mixing;
Foreground derives fgDim/fgMuted; a global **Saturation** slider re-saturates the
derived palette. HSV fine-tune for the accent pair stays behind a disclosure (debounced
now — today it commits two preferences per pixel of drag). Then the Effects card, then
save-as / delete. Edits apply live immediately (the existing fork-to-custom model);
there is no staged revert in this phase.
## 2. Typography
`FontPicker` is rebuilt as a closed dropdown: a button showing the current family in
its own face, opening a popover with a search field and the filtered list (the current
12-match cap stays). All five rows use it — Interface, Icons, Application, Document,
Monospace — with details naming what each controls. The always-open lists and the
ActionRow disclosure pattern for fonts both retire. Sizes, hinting, antialiasing,
Icons & pointer unchanged.
## 3. Titlebars — close-only, everywhere
- Schema: `titlebarMaximizeButton` and `titlebarDoubleClick` are **deleted**. GNOME's
`button-layout` push becomes close-only, honoring `titlebarButtonSide`
(`close:appmenu` / `appmenu:close`); the `action-double-click-titlebar` push is
dropped (GNOME's default stands). A migration note: removed keys vanish from
settings.json on next write; DesktopStyle simply stops reading them.
- New bool `panamaTitlebar` (group `titlebar`, default `true`): "Titlebar on Panama
windows". Off = the Settings window renders no titlebar at all — pure Hyprland
(Super+Q closes, Super+drag moves, Escape works); the sidebar and tab strip anchor
to the window top.
- When on, the Settings titlebar obeys `titlebarButtonSide`, shows **close only** (the
minimize button was a lie — Hyprland has no minimize), and its close button gains
keyboard focus + Accessible metadata (the only unreachable control in the app today).
## 4. Video wallpapers
**Engine: mpvpaper** (Terra packages 1.9, which carries the libmpv fence-leak
workaround; Fedora/RPM Fusion don't ship it). One process per output,
`hwdec=vaapi profile=fast no-audio loop-file=inf panscan=1.0` plus a per-output
`input-ipc-server` socket under `$XDG_RUNTIME_DIR`. VAAPI verified on this machine:
~0.18 core for 1440p30 h264 vs ~0.70 software. Rejected: in-shell QtMultimedia — a
continuously animating in-process surface drives repaints across every shell window
(Quickshell's own documented failure mode), and a decoder crash would take the whole
shell down instead of a wallpaper.
Mechanics, owned by a new `services/VideoWallpaper.qml` + `scripts/panama-video-wallpaper`:
- Videos come from `videoWallpaperDir` (default `Videos/Wallpapers`), scanned with the
same newest-60 discipline (mp4/mkv/webm), rendered in the picker grid with a ▶ badge.
- Selecting a video **stops `hyprpaper.service`** for the session (both fight for the
background layer; stacking is a race) and starts supervised mpvpaper; returning to a
still kills mpvpaper and restarts hyprpaper. The processes are watched and respawned
(mpvpaper has an open hotplug-segfault issue), and restarted nightly as leak
insurance.
- **Pause is Panama's job, not the compositor's.** Research finding: Hyprland rebuilds
the full-screen blur chain on every wallpaper frame while blur-enabled layers exist,
and occluded wallpapers keep compositing except under solitary mode — which any
overlay surface (a toast mid-game) breaks. So: pause via mpv JSON IPC
(`Quickshell.Io.Socket`, no socat) on (a) the existing `panama-gaming` start/end
hooks, (b) `solitary` polling from `hyprctl -j monitors` on socket2 events,
(c) session lock and idle, (d) battery when `videoWallpaperPauseOnBattery` (default
on), and (e) the bar pill — a `Pill` visible while a video wallpaper is active,
click to pause/resume (remote desktop, or just quiet). mpvpaper's `-p -a FULL`
auto-pause rides along as belt-and-braces only.
- Lock screen: `panama-lock` uses a cached still frame (one-time ffmpeg frame grab on
selection) when the wallpaper is a video.
- A `panama-doctor` check verifies VAAPI actually engaged (the known silent
software-decode failure reads as "mpvpaper is heavy").
- Packages: `mpvpaper`, `mpv` added to `setup/packages/hyprland-packages`.
- Schema: `videoWallpaperDir` (string), `videoWallpaperPauseOnBattery` (bool, default
true); the active video path rides `wallpaperPath` (the picker treats stills and
videos uniformly; `Wallpaper.qml` routes by extension).
- The 120fps HEVC Canyons file gets flagged in UI copy (interval/fps advice), not
special-cased.
## 5. Blast radius (themes workstream)
Contracts rewritten alongside (not run): `theme-profiles-contract` (new record
fields), `accent-controls-contract` (editor rebuild), `palette-contract` (unchanged
accents, plus themes.json shape checks), `adwaita-accent-contract` (nearest-curated
mapping), `gtk-theme-contract` (generated css block), `lock-screen-theme-contract`
(palette-fed placeholders), `desktop-style-contract` (titlebar keys removed),
`settings-hardcoded-values-contract`, `wallpaper-*` (video), plus new
`theme-catalog-contract` (themes.json shape: 10 themes, 19 palette keys, 16 ansi keys,
valid hexes, unique ids, moon/day byte-matching Theme.qml's former literals).
Generators: settings docs/commands unaffected (no new pages); SettingsSearch entries
updated (Themes, Theme editor, video wallpaper terms; titlebar entries pruned).
`docs/settings.md`, settings README, manual chapter updates ride along.
## Non-goals this phase
nvim per-theme colorschemes; ohmyposh/wofi theming; staged preview-and-revert in the
editor; per-token palette overrides beyond the four wells + saturation; theme
import/export.