diff --git a/config/dot/hypr/looks.lua b/config/dot/hypr/looks.lua index b8b5646..e668e26 100644 --- a/config/dot/hypr/looks.lua +++ b/config/dot/hypr/looks.lua @@ -29,7 +29,11 @@ hl.config({ active_border = { colors = { "rgba(82aaffee)", "rgba(b172b0ee)" }, angle = 115 }, -- Unfocused windows get no colour at all. The gradient only means -- something if exactly one window on screen is wearing it. - inactive_border = "rgba(3b426199)", + -- Follows the colour scheme: a dark neutral is invisible against a + -- light desktop. services/ColorScheme.qml applies changes live; + -- this is the value a fresh session starts from. + inactive_border = prefs.get("colorScheme", "dark") == "light" + and "rgba(a8aecb99)" or "rgba(3b426199)", }, resize_on_border = true, diff --git a/config/dot/quickshell/config/PreferenceSchema.qml b/config/dot/quickshell/config/PreferenceSchema.qml index 95eaba7..c5d89ed 100644 --- a/config/dot/quickshell/config/PreferenceSchema.qml +++ b/config/dot/quickshell/config/PreferenceSchema.qml @@ -386,6 +386,26 @@ Singleton { detail: "A time earlier than the start simply means the next morning" }, + // ── Colour scheme ─────────────────────────────────────────────────── + // Light is Tokyo Night Day, the official light variant, rather than a + // palette invented to merely not be dark. Both share the same hues at + // different lightness, which is what keeps the Prism identity intact + // across the switch. + // + // services/ColorScheme.qml pushes the choice to GTK and to the + // compositor's border colours, because an application toolbar or a + // window border still wearing the other scheme is more jarring than + // either scheme on its own. + { + key: "colorScheme", type: "enum", def: "dark", group: "appearance", + label: "Appearance", + detail: "Light and dark share one identity, not two themes", + options: [ + { value: "dark", label: "Dark" }, + { value: "light", label: "Light" } + ] + }, + // ── Typography ────────────────────────────────────────────────────── // The single largest thing in this desktop that used to be changeable // only by editing Theme.qml. diff --git a/config/dot/quickshell/config/Theme.qml b/config/dot/quickshell/config/Theme.qml index 7538fdf..a490e56 100644 --- a/config/dot/quickshell/config/Theme.qml +++ b/config/dot/quickshell/config/Theme.qml @@ -18,36 +18,47 @@ import QtQuick Singleton { id: root + // ── Colour scheme ─────────────────────────────────────────────────────── + // Tokyo Night ships an official light variant (Day), so light mode is that + // rather than a palette invented to merely not be dark. The two share the + // same hues at different lightness, which is what lets the Prism identity + // survive the switch: blue still leads into orchid, it is simply a darker + // blue on a lighter ground. + // + // Every token below is a binding on this, so flipping it repaints the whole + // shell without anything needing to know it happened. + readonly property bool dark: DesktopPreferences.get("colorScheme") !== "light" + // ── Palette ───────────────────────────────────────────────────────────── // Canonical Tokyo Night Moon. `accent` matches the GNOME accent exactly. - readonly property color bg: "#222436" - readonly property color bgDark: "#1e2030" - readonly property color bgHighlight: "#2f334d" - readonly property color bgPanel: "#2e2f3d" // dock glass surface - readonly property color bgPopover: "#21212f" // Openbar submenu background + readonly property color bg: root.dark ? "#222436" : "#e1e2e7" + readonly property color bgDark: root.dark ? "#1e2030" : "#d3d5de" + readonly property color bgHighlight: root.dark ? "#2f334d" : "#c4c8da" + readonly property color bgPanel: root.dark ? "#2e2f3d" : "#d9dae3" // dock glass surface + readonly property color bgPopover: root.dark ? "#21212f" : "#eaeaee" // Openbar submenu background - readonly property color fg: "#c8d3f5" - readonly property color fgDim: "#828bb8" - readonly property color fgMuted: "#636da6" - readonly property color gutter: "#3b4261" + readonly property color fg: root.dark ? "#c8d3f5" : "#3760bf" + readonly property color fgDim: root.dark ? "#828bb8" : "#6172b0" + readonly property color fgMuted: root.dark ? "#636da6" : "#848cb5" + readonly property color gutter: root.dark ? "#3b4261" : "#a8aecb" // The pair. `accent` is the primary and carries every state meaning // (focused, active, on). `accentSecondary` is the orchid from the tmux // theme — it never appears alone, only as the far end of a gradient. That // restraint is the whole point: the two colours meeting is the signature, // so the pink stops being special the moment it's used as a flat fill. - readonly property color accent: "#82aaff" // blue - readonly property color accentSecondary: "#b172b0" // orchid, from tmux - readonly property color accentAlt: "#65bcff" // blue1, a lighter blue - readonly property color cyan: "#86e1fc" - readonly property color teal: "#4fd6be" - readonly property color green: "#c3e88d" - readonly property color yellow: "#ffc777" - readonly property color orange: "#ff966c" - readonly property color red: "#ff757f" - readonly property color redDeep: "#c53b53" - readonly property color magenta: "#c099ff" - readonly property color pink: "#fca7ea" + readonly property color accent: root.dark ? "#82aaff" : "#2e7de9" // blue + readonly property color accentSecondary: root.dark ? "#b172b0" : "#9854f1" // orchid, from tmux + readonly property color accentAlt: root.dark ? "#65bcff" : "#007197" // blue1, a lighter blue + readonly property color cyan: root.dark ? "#86e1fc" : "#007197" + readonly property color teal: root.dark ? "#4fd6be" : "#118c74" + readonly property color green: root.dark ? "#c3e88d" : "#587539" + readonly property color yellow: root.dark ? "#ffc777" : "#8c6c3e" + readonly property color orange: root.dark ? "#ff966c" : "#b15c00" + readonly property color red: root.dark ? "#ff757f" : "#f52a65" + readonly property color redDeep: root.dark ? "#c53b53" : "#c64343" + readonly property color magenta: root.dark ? "#c099ff" : "#9854f1" + readonly property color pink: root.dark ? "#fca7ea" : "#d20065" // Semantic aliases — prefer these in widgets so intent survives a repaint. readonly property color ok: green @@ -59,11 +70,14 @@ Singleton { // The dock sits on compositor blur, so its fill is deliberately very // translucent. Popovers are near-opaque because they carry text that must // stay legible. The bar itself has no surface or alpha token. - readonly property real dockAlpha: 0.34 - readonly property real popoverAlpha: 0.92 - readonly property real overlayAlpha: 0.55 - readonly property real hoverAlpha: 0.14 - readonly property real activeAlpha: 0.24 + // Light surfaces need more opacity for the same sense of a solid panel: the + // same 0.34 that reads as glass over a dark desktop reads as haze over a + // light one, and text on it stops being legible. + readonly property real dockAlpha: root.dark ? 0.34 : 0.62 + readonly property real popoverAlpha: root.dark ? 0.92 : 0.97 + readonly property real overlayAlpha: root.dark ? 0.55 : 0.40 + readonly property real hoverAlpha: root.dark ? 0.14 : 0.10 + readonly property real activeAlpha: root.dark ? 0.24 : 0.18 // ── Geometry ──────────────────────────────────────────────────────────── readonly property int barHeight: 36 diff --git a/config/dot/quickshell/modules/settings/AppearancePage.qml b/config/dot/quickshell/modules/settings/AppearancePage.qml index 2e1e456..879caef 100644 --- a/config/dot/quickshell/modules/settings/AppearancePage.qml +++ b/config/dot/quickshell/modules/settings/AppearancePage.qml @@ -73,6 +73,15 @@ SettingsPage { } } + SettingsCard { + title: "Colour scheme" + subtitle: ColorScheme.lastError !== "" + ? ColorScheme.lastError + : "Light is Tokyo Night Day, the official light variant — the same hues at a different lightness, so the blue-into-orchid signature survives the switch. Applications and window borders follow." + + ChoiceRow { setting: "colorScheme"; divider: false } + } + SettingsCard { title: "Typography" subtitle: Fonts.lastError !== "" diff --git a/config/dot/quickshell/modules/settings/DesktopPreview.qml b/config/dot/quickshell/modules/settings/DesktopPreview.qml index 0f7a30c..67acc0a 100644 --- a/config/dot/quickshell/modules/settings/DesktopPreview.qml +++ b/config/dot/quickshell/modules/settings/DesktopPreview.qml @@ -50,10 +50,13 @@ Rectangle { // Stands in for the wallpaper. Static: an animated gradient here would // repaint forever behind a settings page. + // Follows the colour scheme. A preview that stays dark while the shell + // around it is light does not read as "your desktop" -- it reads as a + // screenshot of someone else's. gradient: Gradient { orientation: Gradient.Vertical - GradientStop { position: 0.0; color: "#2b3050" } - GradientStop { position: 1.0; color: "#241f33" } + GradientStop { position: 0.0; color: Theme.dark ? "#2b3050" : "#b9c0dd" } + GradientStop { position: 1.0; color: Theme.dark ? "#241f33" : "#cbbcd4" } } // The bar, so the preview reads as this desktop and not a generic one. @@ -203,7 +206,7 @@ Rectangle { shadowEnabled: true shadowColor: win.focused && root.glowOn ? Theme.alpha(Theme.accent, 0.5) - : Theme.alpha("#15161e", 0.85) + : Theme.alpha(Theme.dark ? "#15161e" : "#6172b0", Theme.dark ? 0.85 : 0.45) shadowBlur: win.focused && root.glowOn ? Math.min(1.0, root.px(root.glowRange) / 12) : Math.min(1.0, root.px(root.shadowRange) / 24) diff --git a/config/dot/quickshell/services/ColorScheme.qml b/config/dot/quickshell/services/ColorScheme.qml new file mode 100644 index 0000000..4353142 --- /dev/null +++ b/config/dot/quickshell/services/ColorScheme.qml @@ -0,0 +1,97 @@ +pragma Singleton + +// Keeps everything that draws a window agreeing about light or dark. +// +// The shell repaints itself from Theme.qml the moment the preference changes, +// because every token there is a binding. Two other things draw on this desktop +// and do not read Panama's store: +// +// GTK applications read gsettings colour-scheme and gtk-theme +// the compositor draws window borders and shadows +// +// A toolbar or a window border still wearing the other scheme is more jarring +// than either scheme on its own, which is why this exists rather than the +// setting simply being a Theme property. + +import Quickshell +import Quickshell.Io +import QtQuick +import qs.config + +Singleton { + id: root + + readonly property bool dark: DesktopPreferences.get("colorScheme") !== "light" + property string lastError: "" + + // Applied one command at a time: Process runs a single command, and several + // of these are separate programs. + property var pending: [] + + Process { + id: runner + onExited: (exitCode, exitStatus) => { + if (exitCode !== 0) + root.lastError = "The colour scheme could not be applied everywhere."; + root.drain(); + } + } + + function drain(): void { + if (runner.running || root.pending.length === 0) + return; + const next = root.pending[0]; + root.pending = root.pending.slice(1); + runner.exec(next); + } + + function enqueue(commands: var): void { + root.pending = root.pending.concat(commands); + root.drain(); + } + + // Pushed once at startup as well as on change: gsettings and the compositor + // do not read Panama's store, so a scheme chosen in a previous session is + // only in effect for the shell until this runs. + Component.onCompleted: settle.restart() + + Timer { + id: settle + interval: 1200 + onTriggered: root.apply() + } + + Connections { + target: DesktopPreferences + function onRevisionChanged(): void { coalesce.restart(); } + } + + Timer { + id: coalesce + interval: 250 + onTriggered: root.apply() + } + + function apply(): void { + root.lastError = ""; + + const scheme = root.dark ? "prefer-dark" : "prefer-light"; + // Adwaita's light and dark are the same theme; only the preference and + // the -dark suffix differ, so applications that honour either agree. + const gtkTheme = root.dark ? "Adwaita-dark" : "Adwaita"; + + const commands = [ + ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", scheme], + ["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", gtkTheme] + ]; + + // Unfocused window borders. The focused border is the prism gradient and + // is already scheme-independent; the inactive one is a flat neutral that + // would be invisible against the opposite background. + const inactive = root.dark ? "rgba(3b426199)" : "rgba(a8aecb99)"; + commands.push(["hyprctl", "eval", + `hl.config({ general = { col = { inactive_border = "${inactive}" } } })`]); + + root.enqueue(commands); + } +}