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