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
This commit is contained in:
Gabriel Brown
2026-08-23 23:39:04 -04:00
parent 7578348db1
commit cb7c09d208
68 changed files with 6115 additions and 1138 deletions
@@ -78,6 +78,73 @@ The final card is the ownership boundary. Network configuration and the exact
Users, Sharing, Color profiles, and Digital wellbeing handoffs open GNOME
Settings because Fedora's system services own those areas.
## Appearance
Six tabs, in the order the questions are actually asked: **Themes**, **Theme
editor**, **Background**, **Typography**, **Windows**, **Shell**. Themes leads
because light and dark is the control reached most often, and it used to be the
third section down, under a wallpaper grid and the whole lock screen. The old
`theme` section id still resolves to it, so deep links and IPC calls keep
working.
### Themes
`config/themes.json` is the catalog: ten shipped themes, each a full palette of
nineteen tokens plus sixteen ANSI colors. `services/ThemeCatalog.qml` reads it
with a `FileView` and carries Moon and Day as an embedded fallback, so the
shell renders correctly for the instant before the file loads and forever on a
machine where it is missing.
The tab is two galleries — dark and light — rather than a card each. Selection
is **per mode**: `themeDark` and `themeLight` remember what you chose on each
side, so flipping light and dark lands on your theme for that side rather than
resetting to the shipped defaults.
`services/ThemeProfiles.qml` resolves the active record; `Theme.qml` reads
every one of its nineteen color tokens from `ThemeProfiles.activePalette`.
There are no palette ternaries left in `Theme.qml`, which is what makes a
tenth theme a data change rather than a code change.
### Theme editor
Four wells — primary, secondary, background, foreground — a saturation slider,
an effects card, and the six HSV rows demoted to a fine-tune behind a
disclosure. Background and foreground are never stored alone: the five surfaces
and two text tints are mixed from them, so changing the ground moves the whole
family instead of leaving twelve tokens pointing at the old one.
Every route that writes color — typed hex, the colour wheel, the eyedropper,
the HSV sliders — ends in `ThemeProfiles.commitActive`, which also recomputes
`accentName` as the nearest curated accent. That is what keeps GNOME's accent
enum, kitty's border and the lock screen from going stale after a custom edit.
Editing a shipped theme forks it into a named custom carrying the whole
palette; saving snapshots the palette, the terminal colors, and all ten effect
values into the record.
The HSV rows commit on a **debounce**, not per move. Writing on every move
meant one drag across the hue row spent the whole gesture in apply-and-verify
round trips with the desktop repainting behind the pointer.
### Backgrounds, still and moving
Stills go through hyprpaper. Videos go through mpvpaper, supervised by
`services/VideoWallpaper.qml`, and Panama owns the pause policy rather than the
compositor: the video pauses whenever a game runs, on battery if the preference
says so, and whenever the bar pill is clicked — over mpv's JSON IPC socket, so
resuming does not restart the clip. hyprpaper's service is stopped while a
video plays, because both claim the background layer and stacking within a
layer is creation order. The lock screen gets a cached still frame; hyprlock
cannot play motion, and pretending otherwise would show a black screen.
### The honest titlebar
There is no minimize or maximize control, and no setting for one. Hyprland has
no minimize — it receives the request and does nothing with it — and maximize
is noise in a tiler, so `DesktopStyle` pushes a close-only GNOME button layout
and Panama's own Settings titlebar shows one button. `panamaTitlebar` turns
that bar off entirely, leaving the window pure Hyprland: Super+Q closes,
Super+drag moves, Escape still works.
## Adding a setting
One schema entry. That is the whole job.
@@ -141,13 +208,15 @@ or a copied default. Additions to this table require a concrete discoverability
reason and an update to `tests/quickshell/settings-ownership-contract`.
Window border color follows the same ownership rule. The inactive border is a
**scheme-relative role** owned by `ColorScheme.qml`: it changes only to retain
neutral contrast in light and dark modes. The focused Prism border is the
**accent role**, driven by the chosen `accentName` and also owned by
`ColorScheme.qml`: each accent carries a separate pair for light and dark, so
`ColorScheme.qml` restates the focused border alongside the inactive one on
every scheme change, rather than leaving a scheme flip to erase a
user-selected accent.
neutral contrast role — a **scheme-relative role** in the sense that it exists
to stay legible on either ground — owned by `ColorScheme.qml`, and it is drawn
from the active theme's `gutter` rather than from a hardcoded pair. The two
literals that used to live there were correct for two of the ten shipped themes
and for no custom one. The focused Prism border is the **accent role**, driven
by the chosen `accentName` and also owned by `ColorScheme.qml`: each accent
carries a separate pair for light and dark. Both roles are restated on a scheme
change, an accent change, and a theme change, rather than leaving a flip to
erase a user-selected accent or a theme switch to leave the border behind.
## The rows