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

12 KiB

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:

{ "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.