quickshell: pick the accent from the palette, don't trust base0D

Every icon in the bar was already bound to a Theme colour — the problem
was which one. base0D is the accent for 33 sites and every status colour
harmonises toward it, but the generator fills each slot independently:
on wallhaven-5y39p9 all six accent slots come back orange (H=0.08)
except base0D, which comes back blue-grey. Hence a blue NixOS logo and
a blue workspace pill in an amber bar.

Accent now auditions all six slots and takes the one nearest S=0.60.
Nearest, not max: max picks the neon fe5162 on wallhaven-k8dqr6, close
enough to err that errors stop reading as errors. Only 5y39p9 and
k8dqr6 change; the other four keep the colour they already had.

Renamed base0D -> accent since it no longer names the slot it comes from.
This commit is contained in:
rope 2026-08-30 14:57:37 +01:00
parent 559ccc55db
commit 27414dafbc

View file

@ -545,7 +545,12 @@ in
property string base03: "${c.base03}" property string base03: "${c.base03}"
property string base04: "${c.base04}" property string base04: "${c.base04}"
property string base05: "${c.base05}" property string base05: "${c.base05}"
property string base09: "${c.base09}"
property string base0A: "${c.base0A}"
property string base0B: "${c.base0B}"
property string base0C: "${c.base0C}"
property string base0D: "${c.base0D}" property string base0D: "${c.base0D}"
property string base0E: "${c.base0E}"
} }
} }
@ -563,23 +568,52 @@ in
readonly property color base03: "#" + pal.base03 readonly property color base03: "#" + pal.base03
readonly property color base04: "#" + pal.base04 readonly property color base04: "#" + pal.base04
readonly property color base05: "#" + pal.base05 readonly property color base05: "#" + pal.base05
// The one themed accent: active, focused, selected, filled. // The one themed accent: active, focused, selected, filled
readonly property color base0D: "#" + pal.base0D // and every status colour below is harmonised toward it, so a
// bad pick tints the whole bar rather than just the highlights.
//
// NOT pal.base0D on its own. The generator fills each slot
// independently and is free to hand that one a colour from
// outside the rest of the palette's family: on the amber
// wallhaven-5y39p9 every accent slot comes back orange
// (H=0.08) except base0D, which comes back blue-grey (8590ae,
// H=0.62) that was a blue NixOS logo in an otherwise amber
// bar. So audition all six and take whichever sits nearest a
// good accent saturation.
//
// Nearest 0.6, not "the most saturated": max picks the neon
// fe5162 (S=0.99) on wallhaven-k8dqr6, near enough to `err`
// that errors stop reading as errors. On a monochrome
// wallpaper every slot collapses to the same value, so the
// choice stops mattering rather than going wrong.
readonly property real accentSat: 0.60
readonly property color accent: {
const slots = [pal.base09, pal.base0A, pal.base0B,
pal.base0C, pal.base0D, pal.base0E];
let best = Qt.color("#" + pal.base0D);
let bestD = Math.abs(best.hslSaturation - accentSat);
for (let i = 0; i < slots.length; i++) {
const c = Qt.color("#" + slots[i]);
const d = Math.abs(c.hslSaturation - accentSat);
if (d < bestD) { best = c; bestD = d; }
}
return best;
}
// Status colours: fixed hues, pulled toward the theme. // Status colours: fixed hues, pulled toward the theme.
// Sourcing these from stylix doesn't work base08-base0F all // Sourcing these from stylix doesn't work base08-base0F all
// collapse to near-identical values on a monochrome wallpaper, // collapse to near-identical values on a monochrome wallpaper,
// which leaves "battery critical" and "battery low" the same // which leaves "battery critical" and "battery low" the same
// colour. So the HUE is fixed and only the hue is: each one is // colour. So the HUE is fixed and only the hue is: each one is
// then mixed toward base0D, which keeps red reading as red // then mixed toward the accent, which keeps red reading as red
// while landing it in the wallpaper's family instead of looking // while landing it in the wallpaper's family instead of looking
// pasted on. Blending toward base0D and not toward the card // pasted on. Blending toward the accent and not toward the card
// keeps luminance roughly intact, so these stay legible as text // keeps luminance roughly intact, so these stay legible as text
// on cardBg mixing into the background would sink them. // on cardBg mixing into the background would sink them.
readonly property real statusHarmony: 0.25 // 0 = pure hue, 1 = pure accent readonly property real statusHarmony: 0.25 // 0 = pure hue, 1 = pure accent
function harmonised(hex) { function harmonised(hex) {
let h = Qt.color(hex); let h = Qt.color(hex);
return Qt.tint(base0D, Qt.rgba(h.r, h.g, h.b, 1 - statusHarmony)); return Qt.tint(accent, Qt.rgba(h.r, h.g, h.b, 1 - statusHarmony));
} }
readonly property color err: harmonised("#e0525f") // critical: temp 85, batt 15, auth failed readonly property color err: harmonised("#e0525f") // critical: temp 85, batt 15, auth failed
readonly property color warn: harmonised("#e0a44b") // caution: temp 70, batt 30 readonly property color warn: harmonised("#e0a44b") // caution: temp 70, batt 30
@ -803,7 +837,7 @@ in
radius: Theme.radiusCard radius: Theme.radiusCard
color: Theme.cardBg color: Theme.cardBg
border.width: Theme.borderWidth border.width: Theme.borderWidth
border.color: pam.active ? Theme.base0D border.color: pam.active ? Theme.accent
: root.failed ? Theme.err : root.failed ? Theme.err
: Theme.base03 : Theme.base03
Behavior on border.color { ColorAnimation { duration: Theme.animFade } } Behavior on border.color { ColorAnimation { duration: Theme.animFade } }
@ -863,7 +897,7 @@ in
text: "arrow-right" text: "arrow-right"
font.family: Theme.iconFont font.family: Theme.iconFont
font.pixelSize: 18 font.pixelSize: 18
color: Theme.base0D color: Theme.accent
} }
MouseArea { MouseArea {
id: confirmMa id: confirmMa
@ -991,7 +1025,7 @@ in
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 15 font.pixelSize: 15
font.bold: true font.bold: true
color: Theme.base0D color: Theme.accent
bottomPadding: 4 bottomPadding: 4
} }
@ -1006,7 +1040,7 @@ in
text: modelData[0] text: modelData[0]
font.family: Theme.fontFamily font.family: Theme.fontFamily
font.pixelSize: 13 font.pixelSize: 13
color: Theme.base0D color: Theme.accent
} }
Text { Text {
text: modelData[1] text: modelData[1]
@ -2010,7 +2044,7 @@ in
// variants scale with just a trackH override. // variants scale with just a trackH override.
component PillSlider: Item { component PillSlider: Item {
property real value: 0 property real value: 0
property color fillColor: Theme.base0D property color fillColor: Theme.accent
property real trackH: 8 property real trackH: 8
readonly property real knobD: trackH + 8 readonly property real knobD: trackH + 8
signal moved(real v) signal moved(real v)
@ -2052,7 +2086,7 @@ in
// something you can grab. // something you can grab.
component MeterBar: Item { component MeterBar: Item {
property real value: 0 property real value: 0
property color fillColor: Theme.base0D property color fillColor: Theme.accent
property real trackH: 5 property real trackH: 5
height: trackH height: trackH
Rectangle { Rectangle {
@ -2074,7 +2108,7 @@ in
property bool on: false property bool on: false
signal toggled(bool v) signal toggled(bool v)
width: 34; height: 18; radius: 9 width: 34; height: 18; radius: 9
color: on ? Theme.base0D : Theme.base02 color: on ? Theme.accent : Theme.base02
Behavior on color { ColorAnimation { duration: Theme.animFade } } Behavior on color { ColorAnimation { duration: Theme.animFade } }
Rectangle { Rectangle {
width: 12; height: 12; radius: 6 width: 12; height: 12; radius: 6
@ -3264,14 +3298,14 @@ in
ColorOverlay { ColorOverlay {
anchors.fill: launchLogo anchors.fill: launchLogo
source: launchLogo source: launchLogo
color: Theme.base0D color: Theme.accent
} }
} }
SText { SText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "${config.home.username}" text: "${config.home.username}"
color: Theme.base0D color: Theme.accent
} }
SText { SText {
@ -3300,7 +3334,7 @@ in
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "equal" text: "equal"
font.pixelSize: 18 font.pixelSize: 18
color: Theme.base0D color: Theme.accent
} }
SText { SText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -3359,7 +3393,7 @@ in
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "sparkles" text: "sparkles"
font.pixelSize: 14 font.pixelSize: 14
color: Theme.base0D color: Theme.accent
} }
SText { SText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -3682,7 +3716,7 @@ in
visible: icon === "" visible: icon === ""
text: glyph text: glyph
font.pixelSize: 20 font.pixelSize: 20
color: Theme.base0D color: Theme.accent
} }
} }
@ -3779,7 +3813,7 @@ in
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: modelData.icon text: modelData.icon
font.pixelSize: 20 font.pixelSize: 20
color: Theme.base0D color: Theme.accent
} }
Column { Column {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -3890,7 +3924,7 @@ in
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: ctxMenu.pinned ? "pin-off" : "pin" text: ctxMenu.pinned ? "pin-off" : "pin"
font.pixelSize: 14 font.pixelSize: 14
color: Theme.base0D color: Theme.accent
} }
SText { SText {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@ -4092,7 +4126,7 @@ in
ColorOverlay { ColorOverlay {
anchors.fill: nixLogoImg anchors.fill: nixLogoImg
source: nixLogoImg source: nixLogoImg
color: Theme.base0D color: Theme.accent
} }
MouseArea { MouseArea {
@ -4126,7 +4160,7 @@ in
width: 6 width: 6
height: wsItem.modelData.focused ? 18 : 6 height: wsItem.modelData.focused ? 18 : 6
radius: 3 radius: 3
color: wsItem.modelData.focused ? Theme.base0D color: wsItem.modelData.focused ? Theme.accent
: wsMa.containsMouse ? Theme.base04 : Theme.base03 : wsMa.containsMouse ? Theme.base04 : Theme.base03
Behavior on height { Behavior on height {
NumberAnimation { duration: 200; easing.type: Easing.OutExpo } NumberAnimation { duration: 200; easing.type: Easing.OutExpo }
@ -4165,7 +4199,7 @@ in
anchors.centerIn: parent anchors.centerIn: parent
text: "image" text: "image"
font.pixelSize: 18 font.pixelSize: 18
color: bar.activeDropdown === wallDropdown ? Theme.base0D color: bar.activeDropdown === wallDropdown ? Theme.accent
: wallMa.containsMouse ? Theme.base05 : Theme.base04 : wallMa.containsMouse ? Theme.base05 : Theme.base04
Behavior on color { ColorAnimation { duration: Theme.animFade } } Behavior on color { ColorAnimation { duration: Theme.animFade } }
} }
@ -4338,7 +4372,7 @@ in
GlanceStat { GlanceStat {
value: srvWidget.connected ? Math.round(srvWidget.cpu) + "%" : "--" value: srvWidget.connected ? Math.round(srvWidget.cpu) + "%" : "--"
caption: "cpu" caption: "cpu"
valueColor: srvWidget.connected ? Theme.base0D : Theme.base03 valueColor: srvWidget.connected ? Theme.accent : Theme.base03
} }
GlanceStat { GlanceStat {
value: srvWidget.connected && srvWidget.memTotal > 0 value: srvWidget.connected && srvWidget.memTotal > 0
@ -5268,7 +5302,7 @@ in
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: modelData.buttonType !== QsMenuButtonType.None visible: modelData.buttonType !== QsMenuButtonType.None
text: modelData.checkState === Qt.Checked ? "\u2713" : "" text: modelData.checkState === Qt.Checked ? "\u2713" : ""
color: Theme.base0D color: Theme.accent
font.pixelSize: 12 font.pixelSize: 12
} }
@ -5440,7 +5474,7 @@ in
// hover same selection language as the // hover same selection language as the
// workspace pill above it. // workspace pill above it.
border.width: Theme.borderWidth border.width: Theme.borderWidth
border.color: active ? Theme.base0D border.color: active ? Theme.accent
: wallTileMa.containsMouse ? Theme.base04 : wallTileMa.containsMouse ? Theme.base04
: "transparent" : "transparent"
Behavior on border.color { ColorAnimation { duration: Theme.animFade } } Behavior on border.color { ColorAnimation { duration: Theme.animFade } }
@ -5517,7 +5551,7 @@ in
width: parent.width - 42 - 76 - 20 width: parent.width - 42 - 76 - 20
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
value: srvWidget.cpu / 100 value: srvWidget.cpu / 100
fillColor: srvWidget.cpu >= 90 ? Theme.err : Theme.base0D fillColor: srvWidget.cpu >= 90 ? Theme.err : Theme.accent
} }
SText { SText {
width: 76 width: 76
@ -5697,7 +5731,7 @@ in
width: parent.width - masterVolLabel.width - 8 width: parent.width - masterVolLabel.width - 8
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
value: volWidget.sink && volWidget.sink.audio ? Math.min(1, volWidget.sink.audio.volume) : 0 value: volWidget.sink && volWidget.sink.audio ? Math.min(1, volWidget.sink.audio.volume) : 0
fillColor: volWidget.muted ? Theme.base03 : Theme.base0D fillColor: volWidget.muted ? Theme.base03 : Theme.accent
onMoved: (v) => { if (volWidget.sink && volWidget.sink.audio) volWidget.sink.audio.volume = v; } onMoved: (v) => { if (volWidget.sink && volWidget.sink.audio) volWidget.sink.audio.volume = v; }
} }
@ -5810,8 +5844,8 @@ in
width: (parent.width - 12) / 3 width: (parent.width - 12) / 3
height: 26 height: 26
radius: 6 radius: 6
color: active ? Theme.base0D : "transparent" color: active ? Theme.accent : "transparent"
border.color: active ? Theme.base0D : Theme.base03 border.color: active ? Theme.accent : Theme.base03
border.width: 1 border.width: 1
SText { SText {
@ -5892,7 +5926,7 @@ in
height: 16 height: 16
trackH: 4 trackH: 4
value: modelData.audio ? Math.min(1, modelData.audio.volume) : 0 value: modelData.audio ? Math.min(1, modelData.audio.volume) : 0
fillColor: modelData.audio && modelData.audio.muted ? Theme.base03 : Theme.base0D fillColor: modelData.audio && modelData.audio.muted ? Theme.base03 : Theme.accent
onMoved: (v) => { if (modelData.audio) modelData.audio.volume = v; } onMoved: (v) => { if (modelData.audio) modelData.audio.volume = v; }
} }
@ -6355,7 +6389,7 @@ in
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "video" text: "video"
color: recCard.recording ? Theme.err color: recCard.recording ? Theme.err
: recCard.selecting ? Theme.base0D : Theme.base05 : recCard.selecting ? Theme.accent : Theme.base05
font.pixelSize: 15 font.pixelSize: 15
} }
SText { SText {
@ -6495,7 +6529,7 @@ in
SIcon { SIcon {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
text: "coffee" text: "coffee"
color: caffCard.on ? Theme.base0D : Theme.base05 color: caffCard.on ? Theme.accent : Theme.base05
font.pixelSize: 15 font.pixelSize: 15
} }
SText { SText {
@ -6862,7 +6896,7 @@ in
SIcon { SIcon {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
text: calPopup.weatherGlyph(modelData.code) text: calPopup.weatherGlyph(modelData.code)
color: Theme.base0D color: Theme.accent
font.pixelSize: 16 font.pixelSize: 16
} }
SText { SText {
@ -6984,7 +7018,7 @@ in
width: 3 width: 3
radius: 1.5 radius: 1.5
height: 3 + lvl * 13 height: 3 + lvl * 13
color: Theme.base0D color: Theme.accent
opacity: 0.35 + lvl * 0.65 opacity: 0.35 + lvl * 0.65
x: vinyl.width / 2 - width / 2 x: vinyl.width / 2 - width / 2
y: vinyl.height / 2 - vinyl.ringR - height y: vinyl.height / 2 - vinyl.ringR - height
@ -7302,7 +7336,7 @@ in
height: 16 height: 16
trackH: 4 trackH: 4
value: mediaCard.pwNode && mediaCard.pwNode.audio ? Math.min(1, mediaCard.pwNode.audio.volume) : 0 value: mediaCard.pwNode && mediaCard.pwNode.audio ? Math.min(1, mediaCard.pwNode.audio.volume) : 0
fillColor: mediaCard.pwNode && mediaCard.pwNode.audio && mediaCard.pwNode.audio.muted ? Theme.base03 : Theme.base0D fillColor: mediaCard.pwNode && mediaCard.pwNode.audio && mediaCard.pwNode.audio.muted ? Theme.base03 : Theme.accent
onMoved: (v) => { if (mediaCard.pwNode && mediaCard.pwNode.audio) mediaCard.pwNode.audio.volume = v; } onMoved: (v) => { if (mediaCard.pwNode && mediaCard.pwNode.audio) mediaCard.pwNode.audio.volume = v; }
} }
@ -7505,7 +7539,7 @@ in
property bool shown: false property bool shown: false
property string icon: "volume-2" property string icon: "volume-2"
property real value: 0 property real value: 0
property color fill: Theme.base0D property color fill: Theme.accent
// Takes over the server glance's slot: fixed position, no // Takes over the server glance's slot: fixed position, no
// dependence on how tall the workspace column has grown. // dependence on how tall the workspace column has grown.
@ -7547,7 +7581,7 @@ in
show(a.muted ? "volume-x" show(a.muted ? "volume-x"
: v >= 0.5 ? "volume-2" : v >= 0.5 ? "volume-2"
: v > 0 ? "volume-1" : "volume", : v > 0 ? "volume-1" : "volume",
v, a.muted ? Theme.base03 : Theme.base0D); v, a.muted ? Theme.base03 : Theme.accent);
} }
Connections { Connections {
@ -7562,7 +7596,7 @@ in
Connections { Connections {
target: bar.shellRoot target: bar.shellRoot
function onOsdBrightness(icon, value) { function onOsdBrightness(icon, value) {
osdItem.show(icon, value, Theme.base0D); osdItem.show(icon, value, Theme.accent);
} }
} }