# 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.