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
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:
titlebarMaximizeButtonandtitlebarDoubleClickare deleted. GNOME'sbutton-layoutpush becomes close-only, honoringtitlebarButtonSide(close:appmenu/appmenu:close); theaction-double-click-titlebarpush 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(grouptitlebar, defaulttrue): "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(defaultVideos/Wallpapers), scanned with the same newest-60 discipline (mp4/mkv/webm), rendered in the picker grid with a ▶ badge. - Selecting a video stops
hyprpaper.servicefor 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 existingpanama-gamingstart/end hooks, (b)solitarypolling fromhyprctl -j monitorson socket2 events, (c) session lock and idle, (d) battery whenvideoWallpaperPauseOnBattery(default on), and (e) the bar pill — aPillvisible while a video wallpaper is active, click to pause/resume (remote desktop, or just quiet). mpvpaper's-p -a FULLauto-pause rides along as belt-and-braces only. - Lock screen:
panama-lockuses a cached still frame (one-time ffmpeg frame grab on selection) when the wallpaper is a video. - A
panama-doctorcheck verifies VAAPI actually engaged (the known silent software-decode failure reads as "mpvpaper is heavy"). - Packages:
mpvpaper,mpvadded tosetup/packages/hyprland-packages. - Schema:
videoWallpaperDir(string),videoWallpaperPauseOnBattery(bool, default true); the active video path rideswallpaperPath(the picker treats stills and videos uniformly;Wallpaper.qmlroutes 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.