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
|
-- Look and feel -- Tokyo Night Moon
|
||||||
--
|
--
|
||||||
-- Colours here must stay in sync with quickshell/config/Theme.qml.
|
-- 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
|
-- bg #222436 base
|
||||||
--
|
--
|
||||||
-- Performance note: every animation below is event-driven. Nothing uses the
|
-- Performance note: every animation below is event-driven. Nothing uses the
|
||||||
@@ -14,6 +14,30 @@
|
|||||||
|
|
||||||
local prefs = require("prefs")
|
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({
|
hl.config({
|
||||||
general = {
|
general = {
|
||||||
gaps_in = prefs.get("gapsIn", 5),
|
gaps_in = prefs.get("gapsIn", 5),
|
||||||
@@ -22,11 +46,13 @@ hl.config({
|
|||||||
border_size = prefs.get("borderSize", 2),
|
border_size = prefs.get("borderSize", 2),
|
||||||
|
|
||||||
col = {
|
col = {
|
||||||
-- The focused accent role: blue leads, orchid follows, on a
|
-- The focused accent role, on a diagonal so the pair is visible
|
||||||
-- diagonal so the pair is visible on both a tall and a wide
|
-- on both a tall and a wide window. Driven by the chosen
|
||||||
-- window. ColorScheme never writes this role; a future accent
|
-- accentName (see the `accents` table above); services/
|
||||||
-- picker can own it without fighting light/dark mode.
|
-- ColorScheme.qml applies the same values live, and restates them
|
||||||
active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 },
|
-- 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
|
-- The neutral inactive role follows the colour scheme because a
|
||||||
-- dark neutral disappears against a light desktop.
|
-- dark neutral disappears against a light desktop.
|
||||||
-- services/ColorScheme.qml applies the same values live; this is
|
-- 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
|
-- New in 0.56. Kept deliberately faint: in this direction the gradient
|
||||||
-- border is the signature, and a strong halo would compete with it.
|
-- border is the signature, and a strong halo would compete with it.
|
||||||
-- This is just enough to lift the focused window off the wallpaper.
|
-- 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 = {
|
glow = {
|
||||||
enabled = prefs.get("glowEnabled", true),
|
enabled = prefs.get("glowEnabled", true),
|
||||||
range = prefs.get("glowRange", 8),
|
range = prefs.get("glowRange", 8),
|
||||||
render_power = 2,
|
render_power = 2,
|
||||||
color = "rgba(82aaff33)",
|
color = "rgba(" .. accentStart .. "33)",
|
||||||
color_inactive = "rgba(00000000)",
|
color_inactive = "rgba(00000000)",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -20,23 +20,45 @@ Flow {
|
|||||||
|
|
||||||
readonly property string current: DesktopPreferences.get("accentName") || "blue"
|
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 {
|
Repeater {
|
||||||
// Object key order is insertion order here, so the palette's own order
|
// The schema's option order is the palette's order, so blue is first
|
||||||
// is what the user sees; blue first because it is what Panama ships.
|
// because it is what Panama ships.
|
||||||
model: Object.keys(Theme.accents)
|
model: root.options
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
id: entry
|
id: entry
|
||||||
|
|
||||||
required property var modelData
|
required property var modelData
|
||||||
|
|
||||||
readonly property var pair: Theme.accents[entry.modelData]
|
readonly property string name: entry.modelData.value
|
||||||
readonly property bool selected: entry.modelData === root.current
|
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 start: Theme.dark ? entry.pair.dark : entry.pair.light
|
||||||
readonly property color end: Theme.dark ? entry.pair.darkSecondary : entry.pair.lightSecondary
|
readonly property color end: Theme.dark ? entry.pair.darkSecondary : entry.pair.lightSecondary
|
||||||
|
|
||||||
spacing: 5
|
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 {
|
Rectangle {
|
||||||
width: 46
|
width: 46
|
||||||
height: 46
|
height: 46
|
||||||
@@ -59,12 +81,6 @@ Flow {
|
|||||||
GradientStop { position: 1.0; color: entry.end }
|
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
|
// 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.
|
// hiding the names behind a hover would waste that.
|
||||||
Text {
|
Text {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: entry.pair.label
|
text: entry.modelData.label
|
||||||
color: entry.selected ? Theme.fg : Theme.fgMuted
|
color: entry.selected ? Theme.fg : Theme.fgMuted
|
||||||
font.family: Theme.fontFamily
|
font.family: Theme.fontFamily
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
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
|
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
|
**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
|
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).
|
**accent role**, driven by the chosen `accentName` and also owned by
|
||||||
`ColorScheme.qml` must never write the focused border, so changing schemes
|
`ColorScheme.qml`: each accent carries a separate pair for light and dark, so
|
||||||
cannot erase a user-selected accent.
|
`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
|
## The rows
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
# Generates Panama's hyprlock configuration into the state directory. The
|
# Generates Panama's hyprlock configuration into the state directory. The
|
||||||
# tracked config is never rewritten and remains the fallback if generation
|
# gitignored fallback config (rendered from the tracked .template at link
|
||||||
# fails, so a malformed preference can never leave the session without a
|
# time) is never rewritten by this script and remains the fallback if
|
||||||
# working locker.
|
# generation fails, so a malformed preference can never leave the session
|
||||||
|
# without a working locker.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -78,6 +79,31 @@ valid_path() {
|
|||||||
[[ "$1" == /* && "$1" != *","* && "$1" != *$'\n'* ]]
|
[[ "$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() {
|
load_preferences() {
|
||||||
if [[ -r "$settings" ]] && jq -e 'type == "object"' "$settings" >/dev/null 2>&1; then
|
if [[ -r "$settings" ]] && jq -e 'type == "object"' "$settings" >/dev/null 2>&1; then
|
||||||
settings_valid=true
|
settings_valid=true
|
||||||
@@ -99,6 +125,12 @@ load_preferences() {
|
|||||||
color_scheme="$(read_string colorScheme dark)"
|
color_scheme="$(read_string colorScheme dark)"
|
||||||
[[ "$color_scheme" == dark || "$color_scheme" == light ]] || color_scheme=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="$(read_string wallpaperMode single)"
|
||||||
[[ "$wallpaper_mode" == single || "$wallpaper_mode" == slideshow || "$wallpaper_mode" == per-monitor ]] \
|
[[ "$wallpaper_mode" == single || "$wallpaper_mode" == slideshow || "$wallpaper_mode" == per-monitor ]] \
|
||||||
|| wallpaper_mode=single
|
|| wallpaper_mode=single
|
||||||
@@ -121,8 +153,6 @@ load_preferences() {
|
|||||||
background_color='rgba(225, 226, 231, 1.0)'
|
background_color='rgba(225, 226, 231, 1.0)'
|
||||||
foreground_color='rgba(55, 96, 191, 1.0)'
|
foreground_color='rgba(55, 96, 191, 1.0)'
|
||||||
dim_color='rgba(97, 114, 176, 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)'
|
error_color='rgba(245, 42, 101, 1.0)'
|
||||||
field_color='rgba(208, 213, 227, 0.85)'
|
field_color='rgba(208, 213, 227, 0.85)'
|
||||||
dim_hex='6172b0'
|
dim_hex='6172b0'
|
||||||
@@ -131,13 +161,18 @@ load_preferences() {
|
|||||||
background_color='rgba(34, 36, 54, 1.0)'
|
background_color='rgba(34, 36, 54, 1.0)'
|
||||||
foreground_color='rgba(200, 211, 245, 1.0)'
|
foreground_color='rgba(200, 211, 245, 1.0)'
|
||||||
dim_color='rgba(130, 139, 184, 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)'
|
error_color='rgba(255, 117, 127, 1.0)'
|
||||||
field_color='rgba(46, 47, 61, 0.85)'
|
field_color='rgba(46, 47, 61, 0.85)'
|
||||||
dim_hex='828bb8'
|
dim_hex='828bb8'
|
||||||
error_hex='ff757f'
|
error_hex='ff757f'
|
||||||
fi
|
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() {
|
monitor_names() {
|
||||||
|
|||||||
@@ -16,21 +16,61 @@
|
|||||||
# is authoritative for GTK3 applications. Pinned to dark, it contradicted the
|
# is authoritative for GTK3 applications. Pinned to dark, it contradicted the
|
||||||
# scheme in light mode, so it is generated from a template here instead.
|
# 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
|
# 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
|
# 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
|
# already open change now. Without the second, a scheme change appears to do
|
||||||
# nothing until you open a new window.
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
scheme="${1:-dark}"
|
scheme="${1:-dark}"
|
||||||
case "$scheme" in
|
case "$scheme" in
|
||||||
dark|light) ;;
|
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
|
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 ─────────────────────────────────────────────────────────────────
|
# ── hyprlock ─────────────────────────────────────────────────────────────────
|
||||||
# The lock screen. hyprlock is launched fresh on every lock (`pidof 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.
|
# 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
|
if [[ "$scheme" == "light" ]]; then
|
||||||
lock_fg="55, 96, 191" # #3760bf
|
lock_fg="55, 96, 191" # #3760bf
|
||||||
lock_muted="97, 114, 176" # #6172b0
|
lock_muted="97, 114, 176" # #6172b0
|
||||||
lock_accent="46, 125, 233" # #2e7de9
|
|
||||||
lock_error="245, 42, 101" # #f52a65
|
lock_error="245, 42, 101" # #f52a65
|
||||||
lock_bg="225, 226, 231" # #e1e2e7
|
lock_bg="225, 226, 231" # #e1e2e7
|
||||||
lock_field="208, 213, 227" # #d0d5e3
|
lock_field="208, 213, 227" # #d0d5e3
|
||||||
@@ -54,7 +93,6 @@ if [[ "$scheme" == "light" ]]; then
|
|||||||
else
|
else
|
||||||
lock_fg="200, 211, 245" # #c8d3f5
|
lock_fg="200, 211, 245" # #c8d3f5
|
||||||
lock_muted="130, 139, 184" # #828bb8
|
lock_muted="130, 139, 184" # #828bb8
|
||||||
lock_accent="130, 170, 255" # #82aaff
|
|
||||||
lock_error="255, 117, 127" # #ff757f
|
lock_error="255, 117, 127" # #ff757f
|
||||||
lock_bg="34, 36, 54" # #222436
|
lock_bg="34, 36, 54" # #222436
|
||||||
lock_field="46, 47, 61" # #2e2f3d
|
lock_field="46, 47, 61" # #2e2f3d
|
||||||
@@ -62,6 +100,10 @@ else
|
|||||||
lock_error_hex="ff757f"
|
lock_error_hex="ff757f"
|
||||||
fi
|
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"
|
status_hyprlock="skipped"
|
||||||
if [[ -r "$lock_template" ]]; then
|
if [[ -r "$lock_template" ]]; then
|
||||||
# Written atomically: a lock triggered mid-write would otherwise read a
|
# 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"
|
status_kitty="skipped"
|
||||||
if [[ -r "$theme_file" ]]; then
|
if [[ -r "$theme_file" ]]; then
|
||||||
# Written atomically: kitty may read this while a new window is starting.
|
# 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 \
|
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
|
&& mv "$kitty_dir/current-theme.conf.tmp" "$kitty_dir/current-theme.conf" 2>/dev/null; then
|
||||||
status_kitty="written"
|
status_kitty="written"
|
||||||
|
else
|
||||||
|
rm -f "$kitty_dir/current-theme.conf.tmp"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Live-apply to running terminals. kitty appends its PID to the socket name
|
# Live-apply to running terminals. kitty appends its PID to the socket name
|
||||||
@@ -182,7 +231,8 @@ if [[ -r "$theme_file" ]]; then
|
|||||||
applied=0
|
applied=0
|
||||||
while read -r socket; do
|
while read -r socket; do
|
||||||
[[ -n "$socket" ]] || continue
|
[[ -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))
|
applied=$((applied + 1))
|
||||||
fi
|
fi
|
||||||
done < <(ss -xl 2>/dev/null | grep -oE '@mykitty[^[:space:]]*' | sort -u)
|
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
|
// for the Prism gradient; Theme owns which colours, this owns the form the
|
||||||
// compositor wants them in.
|
// compositor wants them in.
|
||||||
function hyprColor(value: var): string {
|
function hyprColor(value: var): string {
|
||||||
// Qt gives "#rrggbb"; Hyprland wants rgba(rrggbbaa).
|
// Qt gives "#rrggbb" -- or "#aarrggbb" if the colour ever carries an
|
||||||
return "rgba(" + String(value).replace("#", "").slice(0, 6) + "ee)";
|
// 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 accentBorderStart: root.hyprColor(Theme.accent)
|
||||||
readonly property string accentBorderEnd: root.hyprColor(Theme.accentSecondary)
|
readonly property string accentBorderEnd: root.hyprColor(Theme.accentSecondary)
|
||||||
property string lastError: ""
|
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
|
// Applied one command at a time: Process runs a single command, and several
|
||||||
// of these are separate programs.
|
// of these are separate programs.
|
||||||
property var pending: []
|
property var pending: []
|
||||||
@@ -75,7 +85,7 @@ Singleton {
|
|||||||
Timer {
|
Timer {
|
||||||
id: settle
|
id: settle
|
||||||
interval: 1200
|
interval: 1200
|
||||||
onTriggered: root.apply()
|
onTriggered: root.apply(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
@@ -86,12 +96,30 @@ Singleton {
|
|||||||
Timer {
|
Timer {
|
||||||
id: coalesce
|
id: coalesce
|
||||||
interval: 250
|
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 = "";
|
root.lastError = "";
|
||||||
|
|
||||||
|
const accentName = DesktopPreferences.get("accentName") || "blue";
|
||||||
|
const schemeChanged = force || root.dark !== root.appliedDark;
|
||||||
|
const accentChanged = force || accentName !== root.appliedAccentName;
|
||||||
|
|
||||||
|
if (!schemeChanged && !accentChanged)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const commands = [];
|
||||||
|
|
||||||
|
if (schemeChanged) {
|
||||||
const scheme = root.dark ? "prefer-dark" : "prefer-light";
|
const scheme = root.dark ? "prefer-dark" : "prefer-light";
|
||||||
|
|
||||||
// adw-gtk3, not Adwaita. This is the bug that made dark mode look
|
// adw-gtk3, not Adwaita. This is the bug that made dark mode look
|
||||||
@@ -109,34 +137,51 @@ Singleton {
|
|||||||
// because the failure mode is silent in exactly this way.
|
// because the failure mode is silent in exactly this way.
|
||||||
const gtkTheme = root.dark ? "adw-gtk3-dark" : "adw-gtk3";
|
const gtkTheme = root.dark ? "adw-gtk3-dark" : "adw-gtk3";
|
||||||
|
|
||||||
const commands = [
|
commands.push(["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme]);
|
||||||
["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme],
|
commands.push(["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme]);
|
||||||
["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme]
|
|
||||||
];
|
|
||||||
|
|
||||||
// Unfocused window borders need scheme-relative contrast, and stay this
|
// Unfocused window borders need scheme-relative contrast, and stay
|
||||||
// service's to own. The FOCUSED border is the accent role and belongs to
|
// this service's to own. The FOCUSED border below is the accent
|
||||||
// the theme -- see modules/settings/README.md -- so it is written from
|
// role and belongs to the theme -- see modules/settings/README.md.
|
||||||
// the chosen accent rather than from the scheme.
|
// Unlike that role, this one does not depend on the accent, so an
|
||||||
//
|
// accent-only change never needs to restate it.
|
||||||
// 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",
|
commands.push(["hyprctl", "eval",
|
||||||
`hl.config({ general = { col = { inactive_border = "${root.inactiveBorder}" } } })`]);
|
`hl.config({ general = { col = { inactive_border = "${root.inactiveBorder}" } } })`]);
|
||||||
|
}
|
||||||
|
|
||||||
// A two-stop gradient at the shipped angle. Written as a Lua TABLE: the
|
if (schemeChanged || accentChanged) {
|
||||||
// string form of a gradient carries only one stop, and passing
|
// Each accent carries a separate pair for light and dark, so this
|
||||||
// "rgba(a) rgba(b) 115deg" as a string is accepted and silently keeps
|
// runs on either kind of change: a scheme flip restates the same
|
||||||
// the previous value.
|
// 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",
|
commands.push(["hyprctl", "eval",
|
||||||
`hl.config({ general = { col = { active_border = { colors = { "${root.accentBorderStart}", "${root.accentBorderEnd}" }, angle = 115 } } } })`]);
|
`hl.config({ general = { col = { active_border = ${activeBorder} } } })`]);
|
||||||
|
|
||||||
// Applications that predate org.freedesktop.appearance and carry their
|
// Applications that predate org.freedesktop.appearance and carry
|
||||||
// own palettes -- terminals, chiefly. Everything that reads the portal
|
// their own palettes -- terminals, chiefly. Everything that reads
|
||||||
// (GTK4, Qt6, Chromium, Electron) is already handled by the gsettings
|
// the portal (GTK4, Qt6, Chromium, Electron) is already handled by
|
||||||
// write above and needs nothing here.
|
// the gsettings write above and needs nothing here. The accent is
|
||||||
commands.push([root.appThemePath, root.dark ? "dark" : "light"]);
|
// 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);
|
root.enqueue(commands);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ Singleton {
|
|||||||
// entry here still appears in results and routes to Home rather than being
|
// entry here still appears in results and routes to Home rather than being
|
||||||
// dropped, so adding a group can never make a setting unreachable.
|
// dropped, so adding a group can never make a setting unreachable.
|
||||||
readonly property var groupPages: ({
|
readonly property var groupPages: ({
|
||||||
|
"appearance": "appearance",
|
||||||
"clock": "appearance",
|
"clock": "appearance",
|
||||||
"vitals": "appearance",
|
"vitals": "appearance",
|
||||||
"typography": "appearance",
|
"typography": "appearance",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ assert_schema_entry() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
[[ -x "$helper" ]] || fail 'panama-lock helper is missing or not executable'
|
[[ -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"
|
assert_schema_entry "$key"
|
||||||
done
|
done
|
||||||
|
|
||||||
@@ -112,6 +112,25 @@ if rg -q '^label \{' "$generated"; then
|
|||||||
fail 'hidden clock, date, and user labels were still generated'
|
fail 'hidden clock, date, and user labels were still generated'
|
||||||
fi
|
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 '{
|
write_settings '{
|
||||||
"lockBackgroundMode":"wallpaper",
|
"lockBackgroundMode":"wallpaper",
|
||||||
"wallpaperMode":"per-monitor",
|
"wallpaperMode":"per-monitor",
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
# A setting has one schema-routed owner. A second page may mirror it only when
|
# 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
|
# 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
|
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")
|
raise SystemExit("Hyprland startup values disagree with the live scheme roles")
|
||||||
|
|
||||||
without_comments = re.sub(r"//.*", "", scheme)
|
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:
|
if 'inactive_border = "${root.inactiveBorder}"' not in scheme:
|
||||||
raise SystemExit("ColorScheme does not apply its effective inactive role")
|
raise SystemExit("ColorScheme does not apply its effective inactive role")
|
||||||
PY
|
PY
|
||||||
|
|||||||
Reference in New Issue
Block a user