Finish threading the accent colour through the whole desktop
The recent accent-colour setting only reached part of the desktop. looks.lua still hardcoded the focused-window border and glow to blue, so any hyprctl reload -- or every fresh session, for about a second -- reverted a chosen accent; it now reads accentName the same way it already read colorScheme. The lock screen and terminal stayed blue regardless of the chosen accent despite the setting's own description claiming otherwise; panama-lock and panama-theme-apps now resolve and apply the real accent. AccentPicker built its swatch model from Theme.accents directly instead of the schema's own options list, so the two could drift silently; switched it to read the schema. Its hit target only covered the swatch, not the name label added specifically for colour-vision accessibility -- extended to the whole row. Settings search had no route for the "appearance" group, so searching for the accent or colour scheme landed on Home. ColorScheme's hex-to-Hyprland helper assumed 6-digit colours and would silently corrupt a future translucent one; fixed it to read from the end of the string instead of the start. An accent-only change no longer reruns the full colour-scheme pipeline. The gradient it builds for the focused border now goes through SystemSettings' existing serialiser instead of a second, under-escaped copy of the same logic. The settings-ownership contract test enforced the old rule that ColorScheme must never touch the focused border; updated it to verify the real, intended rule instead of contradicting the code. Claude-Session: https://claude.ai/code/session_01E6TJUAh41HaP25MVHWkhRZ
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
-- Look and feel -- Tokyo Night Moon
|
||||
--
|
||||
-- Colours here must stay in sync with quickshell/config/Theme.qml.
|
||||
-- accent #82aaff borders / focus
|
||||
-- accent user-selectable, see `accents` below -- blue (#82aaff) ships
|
||||
-- bg #222436 base
|
||||
--
|
||||
-- Performance note: every animation below is event-driven. Nothing uses the
|
||||
@@ -14,6 +14,30 @@
|
||||
|
||||
local prefs = require("prefs")
|
||||
|
||||
-- Mirrors config/Theme.qml's `accents` map. Lua has no import path into a QML
|
||||
-- singleton, so the hex pairs are restated here -- just the four hex strings
|
||||
-- each named accent needs, not the labels, which stay UI-only.
|
||||
local accents = {
|
||||
blue = { dark = "82aaff", darkSecondary = "b172b0", light = "2e7de9", lightSecondary = "9854f1" },
|
||||
orchid = { dark = "c099ff", darkSecondary = "fca7ea", light = "7847bd", lightSecondary = "9854f1" },
|
||||
teal = { dark = "86e1fc", darkSecondary = "82aaff", light = "007197", lightSecondary = "2e7de9" },
|
||||
green = { dark = "c3e88d", darkSecondary = "86e1fc", light = "587539", lightSecondary = "007197" },
|
||||
amber = { dark = "ffc777", darkSecondary = "ff966c", light = "8c6c3e", lightSecondary = "b15c00" },
|
||||
orange = { dark = "ff966c", darkSecondary = "ff757f", light = "b15c00", lightSecondary = "c64343" },
|
||||
rose = { dark = "ff757f", darkSecondary = "c099ff", light = "f52a65", lightSecondary = "9854f1" },
|
||||
slate = { dark = "828bb8", darkSecondary = "82aaff", light = "6172b0", lightSecondary = "2e7de9" },
|
||||
}
|
||||
|
||||
-- The accent pair a fresh session or `hyprctl reload` starts from.
|
||||
-- services/ColorScheme.qml overwrites this live, from the same Theme.accents
|
||||
-- data, once the shell settles (~1.2s after startup -- see its `settle`
|
||||
-- Timer). This table exists only so the compositor is never observably blue
|
||||
-- for a non-blue accent during the gap before that first live apply.
|
||||
local accentScheme = prefs.get("colorScheme", "dark")
|
||||
local accentPair = accents[prefs.get("accentName", "blue")] or accents.blue
|
||||
local accentStart = accentScheme == "light" and accentPair.light or accentPair.dark
|
||||
local accentEnd = accentScheme == "light" and accentPair.lightSecondary or accentPair.darkSecondary
|
||||
|
||||
hl.config({
|
||||
general = {
|
||||
gaps_in = prefs.get("gapsIn", 5),
|
||||
@@ -22,11 +46,13 @@ hl.config({
|
||||
border_size = prefs.get("borderSize", 2),
|
||||
|
||||
col = {
|
||||
-- The focused accent role: blue leads, orchid follows, on a
|
||||
-- diagonal so the pair is visible on both a tall and a wide
|
||||
-- window. ColorScheme never writes this role; a future accent
|
||||
-- picker can own it without fighting light/dark mode.
|
||||
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
|
||||
-- The focused accent role, on a diagonal so the pair is visible
|
||||
-- on both a tall and a wide window. Driven by the chosen
|
||||
-- accentName (see the `accents` table above); services/
|
||||
-- ColorScheme.qml applies the same values live, and restates them
|
||||
-- from Theme.accent/Theme.accentSecondary on every scheme change
|
||||
-- too, since each accent carries a separate pair per scheme.
|
||||
active_border = { colors = { "rgba(" .. accentStart .. "ee)", "rgba(" .. accentEnd .. "ee)" }, angle = 115 },
|
||||
-- The neutral inactive role follows the colour scheme because a
|
||||
-- dark neutral disappears against a light desktop.
|
||||
-- services/ColorScheme.qml applies the same values live; this is
|
||||
@@ -104,11 +130,13 @@ hl.config({
|
||||
-- New in 0.56. Kept deliberately faint: in this direction the gradient
|
||||
-- border is the signature, and a strong halo would compete with it.
|
||||
-- This is just enough to lift the focused window off the wallpaper.
|
||||
-- Derived from the same accent as active_border above, not hardcoded,
|
||||
-- so the halo never disagrees with the border it surrounds.
|
||||
glow = {
|
||||
enabled = prefs.get("glowEnabled", true),
|
||||
range = prefs.get("glowRange", 8),
|
||||
render_power = 2,
|
||||
color = "rgba(82aaff33)",
|
||||
color = "rgba(" .. accentStart .. "33)",
|
||||
color_inactive = "rgba(00000000)",
|
||||
},
|
||||
|
||||
|
||||
@@ -20,23 +20,45 @@ Flow {
|
||||
|
||||
readonly property string current: DesktopPreferences.get("accentName") || "blue"
|
||||
|
||||
// The schema's own option list, not Theme.accents directly -- two lists
|
||||
// hand-kept in sync is how they drift. This is the same pattern
|
||||
// ChoiceRow.qml uses for every other enum row.
|
||||
readonly property var spec: PreferenceSchema.spec("accentName")
|
||||
readonly property var options: root.spec && root.spec.options ? root.spec.options : []
|
||||
|
||||
Repeater {
|
||||
// Object key order is insertion order here, so the palette's own order
|
||||
// is what the user sees; blue first because it is what Panama ships.
|
||||
model: Object.keys(Theme.accents)
|
||||
// The schema's option order is the palette's order, so blue is first
|
||||
// because it is what Panama ships.
|
||||
model: root.options
|
||||
|
||||
Column {
|
||||
id: entry
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property var pair: Theme.accents[entry.modelData]
|
||||
readonly property bool selected: entry.modelData === root.current
|
||||
readonly property string name: entry.modelData.value
|
||||
readonly property var pair: Theme.accents[entry.name]
|
||||
readonly property bool selected: entry.name === root.current
|
||||
readonly property color start: Theme.dark ? entry.pair.dark : entry.pair.light
|
||||
readonly property color end: Theme.dark ? entry.pair.darkSecondary : entry.pair.lightSecondary
|
||||
|
||||
spacing: 5
|
||||
|
||||
// The hit target is the whole swatch+label unit, not just the
|
||||
// 46px circle: the label exists specifically so someone with a
|
||||
// colour vision deficiency can identify an accent without it, and
|
||||
// a label that cannot itself be tapped defeats that.
|
||||
HoverHandler {
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
if (!SystemSettings.commitPreference("accentName", entry.name))
|
||||
console.warn("AccentPicker: commitPreference rejected accent", entry.name);
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 46
|
||||
height: 46
|
||||
@@ -59,12 +81,6 @@ Flow {
|
||||
GradientStop { position: 1.0; color: entry.end }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: SystemSettings.commitPreference("accentName", entry.modelData)
|
||||
}
|
||||
}
|
||||
|
||||
// Always shown, not a tooltip. Telling swatches apart by colour is
|
||||
@@ -73,7 +89,7 @@ Flow {
|
||||
// hiding the names behind a hover would waste that.
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: entry.pair.label
|
||||
text: entry.modelData.label
|
||||
color: entry.selected ? Theme.fg : Theme.fgMuted
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
|
||||
@@ -96,9 +96,11 @@ reason and an update to `tests/quickshell/settings-ownership-contract.sh`.
|
||||
Window border colour 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 owned by the visual theme (and, eventually, an accent picker).
|
||||
`ColorScheme.qml` must never write the focused border, so changing schemes
|
||||
cannot erase a user-selected accent.
|
||||
**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.
|
||||
|
||||
## The rows
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Generates Panama's hyprlock configuration into the state directory. The
|
||||
# tracked config is never rewritten and remains the fallback if generation
|
||||
# fails, so a malformed preference can never leave the session without a
|
||||
# working locker.
|
||||
# gitignored fallback config (rendered from the tracked .template at link
|
||||
# time) is never rewritten by this script and remains the fallback if
|
||||
# generation fails, so a malformed preference can never leave the session
|
||||
# without a working locker.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -78,6 +79,31 @@ valid_path() {
|
||||
[[ "$1" == /* && "$1" != *","* && "$1" != *$'\n'* ]]
|
||||
}
|
||||
|
||||
# Same 8 named accents as config/Theme.qml's `accents` map (kept in sync by
|
||||
# hand -- there is no shared source between QML and a shell script), primary
|
||||
# hue per scheme only: the lock screen shows a flat focus ring, not the
|
||||
# two-stop gradient the window border does. Falls back to blue for an unknown
|
||||
# name, matching Theme.accentPair's own fallback.
|
||||
accent_hex() {
|
||||
local name="$1" scheme="$2"
|
||||
case "$name" in
|
||||
orchid) [[ "$scheme" == light ]] && printf '7847bd' || printf 'c099ff' ;;
|
||||
teal) [[ "$scheme" == light ]] && printf '007197' || printf '86e1fc' ;;
|
||||
green) [[ "$scheme" == light ]] && printf '587539' || printf 'c3e88d' ;;
|
||||
amber) [[ "$scheme" == light ]] && printf '8c6c3e' || printf 'ffc777' ;;
|
||||
orange) [[ "$scheme" == light ]] && printf 'b15c00' || printf 'ff966c' ;;
|
||||
rose) [[ "$scheme" == light ]] && printf 'f52a65' || printf 'ff757f' ;;
|
||||
slate) [[ "$scheme" == light ]] && printf '6172b0' || printf '828bb8' ;;
|
||||
*) [[ "$scheme" == light ]] && printf '2e7de9' || printf '82aaff' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# hyprlock wants "R, G, B" decimal, not hex.
|
||||
hex_to_rgb() {
|
||||
local hex="$1"
|
||||
printf '%d, %d, %d' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
|
||||
}
|
||||
|
||||
load_preferences() {
|
||||
if [[ -r "$settings" ]] && jq -e 'type == "object"' "$settings" >/dev/null 2>&1; then
|
||||
settings_valid=true
|
||||
@@ -99,6 +125,12 @@ load_preferences() {
|
||||
color_scheme="$(read_string colorScheme dark)"
|
||||
[[ "$color_scheme" == dark || "$color_scheme" == light ]] || color_scheme=dark
|
||||
|
||||
accent_name="$(read_string accentName blue)"
|
||||
case "$accent_name" in
|
||||
blue|orchid|teal|green|amber|orange|rose|slate) ;;
|
||||
*) accent_name=blue ;;
|
||||
esac
|
||||
|
||||
wallpaper_mode="$(read_string wallpaperMode single)"
|
||||
[[ "$wallpaper_mode" == single || "$wallpaper_mode" == slideshow || "$wallpaper_mode" == per-monitor ]] \
|
||||
|| wallpaper_mode=single
|
||||
@@ -121,8 +153,6 @@ load_preferences() {
|
||||
background_color='rgba(225, 226, 231, 1.0)'
|
||||
foreground_color='rgba(55, 96, 191, 1.0)'
|
||||
dim_color='rgba(97, 114, 176, 1.0)'
|
||||
accent_color='rgba(46, 125, 233, 1.0)'
|
||||
accent_ring_color='rgba(46, 125, 233, 0.9)'
|
||||
error_color='rgba(245, 42, 101, 1.0)'
|
||||
field_color='rgba(208, 213, 227, 0.85)'
|
||||
dim_hex='6172b0'
|
||||
@@ -131,13 +161,18 @@ load_preferences() {
|
||||
background_color='rgba(34, 36, 54, 1.0)'
|
||||
foreground_color='rgba(200, 211, 245, 1.0)'
|
||||
dim_color='rgba(130, 139, 184, 1.0)'
|
||||
accent_color='rgba(130, 170, 255, 1.0)'
|
||||
accent_ring_color='rgba(130, 170, 255, 0.9)'
|
||||
error_color='rgba(255, 117, 127, 1.0)'
|
||||
field_color='rgba(46, 47, 61, 0.85)'
|
||||
dim_hex='828bb8'
|
||||
error_hex='ff757f'
|
||||
fi
|
||||
|
||||
# The focus ring is the accent role, not a scheme-relative one: it follows
|
||||
# accentName the same way ColorScheme.qml's active_border does, and needs
|
||||
# both the accent and the scheme since each accent carries a separate pair.
|
||||
accent_rgb="$(hex_to_rgb "$(accent_hex "$accent_name" "$color_scheme")")"
|
||||
accent_color="rgba($accent_rgb, 1.0)"
|
||||
accent_ring_color="rgba($accent_rgb, 0.9)"
|
||||
}
|
||||
|
||||
monitor_names() {
|
||||
|
||||
@@ -16,21 +16,61 @@
|
||||
# is authoritative for GTK3 applications. Pinned to dark, it contradicted the
|
||||
# scheme in light mode, so it is generated from a template here instead.
|
||||
#
|
||||
# panama-theme-apps dark|light
|
||||
# panama-theme-apps dark|light [accent]
|
||||
#
|
||||
# kitty gets it twice: the generated include file so terminals opened later
|
||||
# start correct, and a live `set-colors` over its control socket so terminals
|
||||
# already open change now. Without the second, a scheme change appears to do
|
||||
# nothing until you open a new window.
|
||||
#
|
||||
# kitty's active_border_color and hyprlock's focus ring are also the accent
|
||||
# role, not just the scheme -- see accent_hex() below, which mirrors the same
|
||||
# lookup hypr/looks.lua and scripts/panama-lock carry for the same reason.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
scheme="${1:-dark}"
|
||||
case "$scheme" in
|
||||
dark|light) ;;
|
||||
*) printf 'usage: panama-theme-apps [dark|light]\n' >&2; exit 2 ;;
|
||||
*) printf 'usage: panama-theme-apps [dark|light] [accent]\n' >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
# Falls back to blue for an unknown or omitted name, matching Theme.qml's own
|
||||
# accentPair fallback -- so a caller that only ever knew about scheme (a stale
|
||||
# ColorScheme.qml, a manual invocation) still gets a real accent instead of an
|
||||
# empty string reaching sed.
|
||||
accent="${2:-blue}"
|
||||
case "$accent" in
|
||||
blue|orchid|teal|green|amber|orange|rose|slate) ;;
|
||||
*) accent=blue ;;
|
||||
esac
|
||||
|
||||
# Same 8 named accents as config/Theme.qml's `accents` map (kept in sync by
|
||||
# hand -- there is no shared source between QML and a shell script), primary
|
||||
# hue per scheme only: both consumers below draw a flat colour, not the
|
||||
# two-stop gradient the window border does.
|
||||
accent_hex() {
|
||||
local name="$1" scheme="$2"
|
||||
case "$name" in
|
||||
orchid) [[ "$scheme" == light ]] && printf '7847bd' || printf 'c099ff' ;;
|
||||
teal) [[ "$scheme" == light ]] && printf '007197' || printf '86e1fc' ;;
|
||||
green) [[ "$scheme" == light ]] && printf '587539' || printf 'c3e88d' ;;
|
||||
amber) [[ "$scheme" == light ]] && printf '8c6c3e' || printf 'ffc777' ;;
|
||||
orange) [[ "$scheme" == light ]] && printf 'b15c00' || printf 'ff966c' ;;
|
||||
rose) [[ "$scheme" == light ]] && printf 'f52a65' || printf 'ff757f' ;;
|
||||
slate) [[ "$scheme" == light ]] && printf '6172b0' || printf '828bb8' ;;
|
||||
*) [[ "$scheme" == light ]] && printf '2e7de9' || printf '82aaff' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# hyprlock wants "R, G, B" decimal, not hex.
|
||||
hex_to_rgb() {
|
||||
local hex="$1"
|
||||
printf '%d, %d, %d' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
|
||||
}
|
||||
|
||||
accent_border_hex="$(accent_hex "$accent" "$scheme")"
|
||||
|
||||
# ── hyprlock ─────────────────────────────────────────────────────────────────
|
||||
# The lock screen. hyprlock is launched fresh on every lock (`pidof hyprlock ||
|
||||
# hyprlock`), so it reads this file each time and needs no restart.
|
||||
@@ -45,7 +85,6 @@ lock_template="$lock_dir/hyprlock.conf.template"
|
||||
if [[ "$scheme" == "light" ]]; then
|
||||
lock_fg="55, 96, 191" # #3760bf
|
||||
lock_muted="97, 114, 176" # #6172b0
|
||||
lock_accent="46, 125, 233" # #2e7de9
|
||||
lock_error="245, 42, 101" # #f52a65
|
||||
lock_bg="225, 226, 231" # #e1e2e7
|
||||
lock_field="208, 213, 227" # #d0d5e3
|
||||
@@ -54,7 +93,6 @@ if [[ "$scheme" == "light" ]]; then
|
||||
else
|
||||
lock_fg="200, 211, 245" # #c8d3f5
|
||||
lock_muted="130, 139, 184" # #828bb8
|
||||
lock_accent="130, 170, 255" # #82aaff
|
||||
lock_error="255, 117, 127" # #ff757f
|
||||
lock_bg="34, 36, 54" # #222436
|
||||
lock_field="46, 47, 61" # #2e2f3d
|
||||
@@ -62,6 +100,10 @@ else
|
||||
lock_error_hex="ff757f"
|
||||
fi
|
||||
|
||||
# The focus ring is the accent role, not a scheme-relative one -- follows
|
||||
# accentName the same way scripts/panama-lock's own generator does.
|
||||
lock_accent="$(hex_to_rgb "$accent_border_hex")"
|
||||
|
||||
status_hyprlock="skipped"
|
||||
if [[ -r "$lock_template" ]]; then
|
||||
# Written atomically: a lock triggered mid-write would otherwise read a
|
||||
@@ -169,9 +211,16 @@ theme_file="$kitty_dir/themes/tokyonight-moon.conf"
|
||||
status_kitty="skipped"
|
||||
if [[ -r "$theme_file" ]]; then
|
||||
# Written atomically: kitty may read this while a new window is starting.
|
||||
# The accent override is appended after the copied theme rather than
|
||||
# edited into the tracked theme file: kitty applies settings top to
|
||||
# bottom, so a later active_border_color line wins, and this file is
|
||||
# itself generated -- the tracked per-scheme theme stays scheme-only.
|
||||
if cp "$theme_file" "$kitty_dir/current-theme.conf.tmp" 2>/dev/null \
|
||||
&& printf 'active_border_color #%s\n' "$accent_border_hex" >>"$kitty_dir/current-theme.conf.tmp" \
|
||||
&& mv "$kitty_dir/current-theme.conf.tmp" "$kitty_dir/current-theme.conf" 2>/dev/null; then
|
||||
status_kitty="written"
|
||||
else
|
||||
rm -f "$kitty_dir/current-theme.conf.tmp"
|
||||
fi
|
||||
|
||||
# Live-apply to running terminals. kitty appends its PID to the socket name
|
||||
@@ -182,7 +231,8 @@ if [[ -r "$theme_file" ]]; then
|
||||
applied=0
|
||||
while read -r socket; do
|
||||
[[ -n "$socket" ]] || continue
|
||||
if kitty @ --to "unix:@${socket#@}" set-colors --all --configured "$theme_file" >/dev/null 2>&1; then
|
||||
if kitty @ --to "unix:@${socket#@}" set-colors --all --configured "$theme_file" >/dev/null 2>&1 \
|
||||
&& kitty @ --to "unix:@${socket#@}" set-colors --override "active_border_color=#$accent_border_hex" >/dev/null 2>&1; then
|
||||
applied=$((applied + 1))
|
||||
fi
|
||||
done < <(ss -xl 2>/dev/null | grep -oE '@mykitty[^[:space:]]*' | sort -u)
|
||||
|
||||
@@ -34,13 +34,23 @@ Singleton {
|
||||
// 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)";
|
||||
// Qt gives "#rrggbb" -- or "#aarrggbb" if the colour ever carries an
|
||||
// alpha channel. slice(-6) keeps the trailing rrggbb either way;
|
||||
// slice(0, 6) would instead grab "aarrgg" out of an 8-digit string and
|
||||
// call it RGB. Hyprland wants rgba(rrggbbaa).
|
||||
return "rgba(" + String(value).replace("#", "").slice(-6) + "ee)";
|
||||
}
|
||||
readonly property string accentBorderStart: root.hyprColor(Theme.accent)
|
||||
readonly property string accentBorderEnd: root.hyprColor(Theme.accentSecondary)
|
||||
property string lastError: ""
|
||||
|
||||
// What the last push actually sent, so apply() can tell an accent-only
|
||||
// change apart from a scheme change and skip the steps that do not depend
|
||||
// on whichever did not move. Their starting values do not matter: the
|
||||
// first apply() always runs with force set, which ignores both.
|
||||
property bool appliedDark: false
|
||||
property string appliedAccentName: ""
|
||||
|
||||
// Applied one command at a time: Process runs a single command, and several
|
||||
// of these are separate programs.
|
||||
property var pending: []
|
||||
@@ -75,7 +85,7 @@ Singleton {
|
||||
Timer {
|
||||
id: settle
|
||||
interval: 1200
|
||||
onTriggered: root.apply()
|
||||
onTriggered: root.apply(true)
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -86,57 +96,92 @@ Singleton {
|
||||
Timer {
|
||||
id: coalesce
|
||||
interval: 250
|
||||
onTriggered: root.apply()
|
||||
onTriggered: root.apply(false)
|
||||
}
|
||||
|
||||
function apply(): void {
|
||||
// `force` pushes every step regardless of what moved -- startup needs
|
||||
// that, because gsettings and the compositor keep their own state and
|
||||
// have no way to know a previous Quickshell session already told them the
|
||||
// answer. Everywhere else this reacts to ANY preference changing (see
|
||||
// onRevisionChanged above), so most calls have nothing to do with either
|
||||
// scheme or accent; only the steps whose inputs actually moved since the
|
||||
// last push run, which keeps sampling accent swatches from also rewriting
|
||||
// gsettings and re-running the whole app-theming script on every sample.
|
||||
function apply(force: bool): void {
|
||||
root.lastError = "";
|
||||
|
||||
const scheme = root.dark ? "prefer-dark" : "prefer-light";
|
||||
const accentName = DesktopPreferences.get("accentName") || "blue";
|
||||
const schemeChanged = force || root.dark !== root.appliedDark;
|
||||
const accentChanged = force || accentName !== root.appliedAccentName;
|
||||
|
||||
// adw-gtk3, not Adwaita. This is the bug that made dark mode look
|
||||
// broken while light mode looked fine:
|
||||
//
|
||||
// Neither "Adwaita" nor "Adwaita-dark" is an installed theme on Fedora
|
||||
// 44 -- only adw-gtk3 and adw-gtk3-dark are. Naming a theme that does
|
||||
// not exist makes GTK fall back to its built-in default, which is
|
||||
// LIGHT. So asking for light accidentally worked, asking for dark
|
||||
// silently produced light, and applications that take their cue from
|
||||
// the GTK theme rather than the portal -- Chromium and Electron, when
|
||||
// built against GTK -- stayed light no matter what the portal said.
|
||||
//
|
||||
// gtk-theme-contract asserts these names are actually installed,
|
||||
// because the failure mode is silent in exactly this way.
|
||||
const gtkTheme = root.dark ? "adw-gtk3-dark" : "adw-gtk3";
|
||||
if (!schemeChanged && !accentChanged)
|
||||
return;
|
||||
|
||||
const commands = [
|
||||
["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme],
|
||||
["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme]
|
||||
];
|
||||
const commands = [];
|
||||
|
||||
// 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}" } } })`]);
|
||||
if (schemeChanged) {
|
||||
const scheme = root.dark ? "prefer-dark" : "prefer-light";
|
||||
|
||||
// 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 } } } })`]);
|
||||
// adw-gtk3, not Adwaita. This is the bug that made dark mode look
|
||||
// broken while light mode looked fine:
|
||||
//
|
||||
// Neither "Adwaita" nor "Adwaita-dark" is an installed theme on Fedora
|
||||
// 44 -- only adw-gtk3 and adw-gtk3-dark are. Naming a theme that does
|
||||
// not exist makes GTK fall back to its built-in default, which is
|
||||
// LIGHT. So asking for light accidentally worked, asking for dark
|
||||
// silently produced light, and applications that take their cue from
|
||||
// the GTK theme rather than the portal -- Chromium and Electron, when
|
||||
// built against GTK -- stayed light no matter what the portal said.
|
||||
//
|
||||
// gtk-theme-contract asserts these names are actually installed,
|
||||
// because the failure mode is silent in exactly this way.
|
||||
const gtkTheme = root.dark ? "adw-gtk3-dark" : "adw-gtk3";
|
||||
|
||||
// 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
|
||||
// write above and needs nothing here.
|
||||
commands.push([root.appThemePath, root.dark ? "dark" : "light"]);
|
||||
commands.push(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme]);
|
||||
commands.push(["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme]);
|
||||
|
||||
// Unfocused window borders need scheme-relative contrast, and stay
|
||||
// this service's to own. The FOCUSED border below is the accent
|
||||
// role and belongs to the theme -- see modules/settings/README.md.
|
||||
// Unlike that role, this one does not depend on the accent, so an
|
||||
// accent-only change never needs to restate it.
|
||||
commands.push(["hyprctl", "eval",
|
||||
`hl.config({ general = { col = { inactive_border = "${root.inactiveBorder}" } } })`]);
|
||||
}
|
||||
|
||||
if (schemeChanged || accentChanged) {
|
||||
// Each accent carries a separate pair for light and dark, so this
|
||||
// runs on either kind of change: a scheme flip restates the same
|
||||
// accent's other pair, and an accent change restates the same
|
||||
// scheme's other colours.
|
||||
//
|
||||
// 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. Multi-stop must be the table form --
|
||||
// built by the same serialiseValue() SystemSettings uses for every
|
||||
// other gradient, rather than a second hand-rolled copy of that
|
||||
// escaping here.
|
||||
const activeBorder = SystemSettings.serialiseValue({
|
||||
colors: [root.accentBorderStart, root.accentBorderEnd],
|
||||
angle: 115
|
||||
});
|
||||
commands.push(["hyprctl", "eval",
|
||||
`hl.config({ general = { col = { active_border = ${activeBorder} } } })`]);
|
||||
|
||||
// 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 write above and needs nothing here. The accent is
|
||||
// passed through too: kitty's border and the hyprlock fallback
|
||||
// template are also the accent role, not just the scheme -- so
|
||||
// this still has to run on an accent-only change, even though the
|
||||
// scheme-relative work it also does is then redundant.
|
||||
commands.push([root.appThemePath, root.dark ? "dark" : "light", accentName]);
|
||||
}
|
||||
|
||||
root.appliedDark = root.dark;
|
||||
root.appliedAccentName = accentName;
|
||||
|
||||
root.enqueue(commands);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ Singleton {
|
||||
// entry here still appears in results and routes to Home rather than being
|
||||
// dropped, so adding a group can never make a setting unreachable.
|
||||
readonly property var groupPages: ({
|
||||
"appearance": "appearance",
|
||||
"clock": "appearance",
|
||||
"vitals": "appearance",
|
||||
"typography": "appearance",
|
||||
|
||||
@@ -69,7 +69,7 @@ assert_schema_entry() {
|
||||
}
|
||||
|
||||
[[ -x "$helper" ]] || fail 'panama-lock helper is missing or not executable'
|
||||
for key in lockBackgroundMode lockBlurLevel lockShowClock lockShowDate lockShowUser lockFadeOnEmpty; do
|
||||
for key in lockBackgroundMode lockBlurLevel lockShowClock lockShowDate lockShowUser lockFadeOnEmpty accentName; do
|
||||
assert_schema_entry "$key"
|
||||
done
|
||||
|
||||
@@ -112,6 +112,25 @@ if rg -q '^label \{' "$generated"; then
|
||||
fail 'hidden clock, date, and user labels were still generated'
|
||||
fi
|
||||
|
||||
# ── The focus ring follows the chosen accent, not just the scheme ───────────
|
||||
# A pinned blue literal only ever proves the DEFAULT accent renders correctly,
|
||||
# not that the helper actually reads accentName. Orchid is picked because its
|
||||
# hex is nowhere close to blue's in either scheme, so a helper that quietly
|
||||
# ignored accentName and kept emitting blue would be caught here.
|
||||
write_settings '{"accentName":"orchid","colorScheme":"dark"}'
|
||||
run_helper generate
|
||||
rg -Fq 'outer_color = rgba(192, 153, 255, 0.9)' "$generated" || fail 'dark orchid accent did not drive the focus ring'
|
||||
rg -Fq 'check_color = rgba(192, 153, 255, 1.0)' "$generated" || fail 'dark orchid accent did not drive the success colour'
|
||||
|
||||
write_settings '{"accentName":"orchid","colorScheme":"light"}'
|
||||
run_helper generate
|
||||
rg -Fq 'outer_color = rgba(120, 71, 189, 0.9)' "$generated" || fail 'light orchid accent did not drive the focus ring'
|
||||
rg -Fq 'check_color = rgba(120, 71, 189, 1.0)' "$generated" || fail 'light orchid accent did not drive the success colour'
|
||||
|
||||
write_settings '{"accentName":"not-a-real-accent","colorScheme":"dark"}'
|
||||
run_helper generate
|
||||
rg -Fq 'outer_color = rgba(130, 170, 255, 0.9)' "$generated" || fail 'an unknown accent name did not fall back to blue'
|
||||
|
||||
write_settings '{
|
||||
"lockBackgroundMode":"wallpaper",
|
||||
"wallpaperMode":"per-monitor",
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
# A setting has one schema-routed owner. A second page may mirror it only when
|
||||
# this contract names the exact owner and mirror set. The same ownership rule
|
||||
# keeps colour scheme propagation away from the focused Prism border.
|
||||
# says who writes each half of the window border: ColorScheme.qml owns the
|
||||
# neutral inactive role AND the accent-derived focused role, restating both
|
||||
# together on every scheme or accent change.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -124,8 +126,28 @@ if dark.group(1) not in looks or light.group(1) not in looks:
|
||||
raise SystemExit("Hyprland startup values disagree with the live scheme roles")
|
||||
|
||||
without_comments = re.sub(r"//.*", "", scheme)
|
||||
if re.search(r"(?<![A-Za-z_])active_border\b", without_comments):
|
||||
raise SystemExit("ColorScheme writes the focused border")
|
||||
|
||||
# The focused border is the accent role, and ColorScheme.qml owns it too:
|
||||
# each named accent carries a separate pair per scheme, so a scheme change
|
||||
# must restate the focused border, not just the neutral one, or a chosen
|
||||
# accent goes stale the moment light/dark flips.
|
||||
start = re.search(r'property string accentBorderStart:\s*root\.hyprColor\(Theme\.accent\)', scheme)
|
||||
end = re.search(r'property string accentBorderEnd:\s*root\.hyprColor\(Theme\.accentSecondary\)', scheme)
|
||||
if not start or not end:
|
||||
raise SystemExit("ColorScheme does not derive the focused border from the chosen accent")
|
||||
|
||||
# Written as a Lua TABLE, not a string: the string form of a Hyprland gradient
|
||||
# carries only one stop, so writing it that way is accepted and silently
|
||||
# keeps whatever the previous accent left behind. Built through the same
|
||||
# serialiseValue() SystemSettings.qml uses for every other gradient, rather
|
||||
# than a second hand-rolled (and unescaped) copy of that table syntax here.
|
||||
if 'SystemSettings.serialiseValue({' not in without_comments:
|
||||
raise SystemExit("ColorScheme does not build the focused border through the shared gradient serialiser")
|
||||
if 'colors: [root.accentBorderStart, root.accentBorderEnd]' not in without_comments:
|
||||
raise SystemExit("ColorScheme does not derive the focused-border gradient from the chosen accent")
|
||||
if 'active_border = ${activeBorder}' not in without_comments:
|
||||
raise SystemExit("ColorScheme does not write the focused border as the serialised gradient table")
|
||||
|
||||
if 'inactive_border = "${root.inactiveBorder}"' not in scheme:
|
||||
raise SystemExit("ColorScheme does not apply its effective inactive role")
|
||||
PY
|
||||
|
||||
Reference in New Issue
Block a user