Make the accent colour choosable

Phase 4, first slice. Theme.qml hardcoded the Prism pair, so the one
thing that carries every state meaning in the desktop -- focused, active,
on -- was the one thing nobody could change. 74 files read Theme.accent,
so making it a setting moves all of them at once.

Named accents rather than a colour picker, which is the design decision
worth defending. One hex cannot serve both schemes: a colour legible on
the Moon background is usually illegible on the Day one, and a picker
that lets someone build an unreadable desktop is not a feature. So each
name carries a curated pair per scheme, and every one of the sixteen
resulting colours measures at least 3:1 against the ground it sits on --
checked, not assumed. It is also GNOME's model, which is the parity
being chased.

The focused window border comes with it, and only because the ownership
rule made that safe. ColorScheme owns the inactive border as a
scheme-relative contrast role; the focused Prism border is the accent
role owned by the theme. Writing it from the accent would have been
reckless before that boundary existed, since a scheme change would have
erased the user's choice. Both borders are now pushed together, because
each accent carries separate light and dark pairs, so switching schemes
must restate the focused border too.

The gradient is written as a Lua table, not a string. The string form
carries only one stop, and passing two as a string is accepted and
silently keeps the previous value.

Swatches are drawn as the gradient they produce rather than as flat
dots, because the gradient is what is being chosen. Each carries its
name permanently rather than in a tooltip: telling swatches apart by
colour is precisely what someone with a colour vision deficiency cannot
do, which is also why the palette is named in the first place.

Verified end to end by switching to rose and watching the compositor
report eeff757f/eec099ff, then reverting.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-18 17:02:18 -04:00
parent bd5d030d91
commit c37ca3baee
6 changed files with 172 additions and 10 deletions
+25 -2
View File
@@ -29,6 +29,16 @@ Singleton {
readonly property string inactiveBorderDark: "rgba(3b426199)"
readonly property string inactiveBorderLight: "rgba(a8aecb99)"
readonly property string inactiveBorder: root.dark ? root.inactiveBorderDark : root.inactiveBorderLight
// The focused border follows the chosen accent. `ee` is the shipped alpha
// for the Prism gradient; Theme owns which colours, this owns the form the
// compositor wants them in.
function hyprColor(value: var): string {
// Qt gives "#rrggbb"; Hyprland wants rgba(rrggbbaa).
return "rgba(" + String(value).replace("#", "").slice(0, 6) + "ee)";
}
readonly property string accentBorderStart: root.hyprColor(Theme.accent)
readonly property string accentBorderEnd: root.hyprColor(Theme.accentSecondary)
property string lastError: ""
// Applied one command at a time: Process runs a single command, and several
@@ -104,11 +114,24 @@ Singleton {
["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme]
];
// Unfocused window borders need scheme-relative contrast. The focused
// Prism border is deliberately owned by the accent/theme layer.
// Unfocused window borders need scheme-relative contrast, and stay this
// service's to own. The FOCUSED border is the accent role and belongs to
// the theme -- see modules/settings/README.md -- so it is written from
// the chosen accent rather than from the scheme.
//
// Both are pushed together because both change when the scheme flips:
// each accent carries a separate pair for light and dark, so switching
// schemes must restate the focused border too, not only the neutral one.
commands.push(["hyprctl", "eval",
`hl.config({ general = { col = { inactive_border = "${root.inactiveBorder}" } } })`]);
// A two-stop gradient at the shipped angle. Written as a Lua TABLE: the
// string form of a gradient carries only one stop, and passing
// "rgba(a) rgba(b) 115deg" as a string is accepted and silently keeps
// the previous value.
commands.push(["hyprctl", "eval",
`hl.config({ general = { col = { active_border = { colors = { "${root.accentBorderStart}", "${root.accentBorderEnd}" }, angle = 115 } } } })`]);
// Applications that predate org.freedesktop.appearance and carry their
// own palettes -- terminals, chiefly. Everything that reads the portal
// (GTK4, Qt6, Chromium, Electron) is already handled by the gsettings