// The four colors a theme is actually made of. // // Primary and Secondary are the two ends of the Prism accent. Background and // Foreground are the ground and the text, and neither is stored alone: the // surfaces (bgDark, bgPanel, bgHighlight, bgPopover, gutter) and the two text // tints (fgDim, fgMuted) are mixed from them, so changing the ground moves the // whole family with it rather than leaving twelve tokens pointing at the old // one. // // One eyedropper process and one colour wheel are shared by all four wells -- // the target is remembered rather than duplicated four times. import QtQuick import QtQuick.Dialogs import Quickshell import Quickshell.Io import qs.config import qs.services Grid { id: root width: parent ? parent.width : 620 columns: root.width < 520 ? 1 : 2 spacing: 12 // Which well the wheel or the eyedropper is currently answering. property string target: "primary" property string lastError: "" readonly property real cellWidth: root.columns > 1 ? (root.width - root.spacing) / 2 : root.width function currentColor(which: string): string { if (which === "primary") return String(ThemeProfiles.activeProfile.accent); if (which === "secondary") return String(ThemeProfiles.activeProfile.secondary); if (which === "background") return String(ThemeProfiles.activePalette.bg); return String(ThemeProfiles.activePalette.fg); } // Every route through this component ends here, so the wheel, the // eyedropper and the typed hex cannot drift apart. function apply(which: string, hex: string): void { const value = String(hex).trim().toLowerCase(); if (!/^#[0-9a-f]{6}$/.test(value)) { root.lastError = "That is not a colour this theme can use."; return; } root.lastError = ""; if (which === "primary") ThemeProfiles.setAccentPair(value, ThemeProfiles.activeProfile.secondary); else if (which === "secondary") ThemeProfiles.setAccentPair(ThemeProfiles.activeProfile.accent, value); else if (which === "background") ThemeProfiles.setGroundColors(value, ThemeProfiles.activePalette.fg); else ThemeProfiles.setGroundColors(ThemeProfiles.activePalette.bg, value); } function pick(which: string): void { if (screenPicker.running) return; root.target = which; root.lastError = ""; screenPicker.exec(["hyprpicker", "--format=hex", "--lowercase-hex", "--quiet", "--no-fancy"]); } function openWheel(which: string): void { root.target = which; wheel.selectedColor = root.currentColor(which); wheel.open(); } Repeater { model: [ { key: "primary", role: "Primary" }, { key: "secondary", role: "Secondary" }, { key: "background", role: "Background" }, { key: "foreground", role: "Foreground" }, ] ColorWell { required property var modelData width: root.cellWidth role: modelData.role swatchColor: root.currentColor(modelData.key) picking: screenPicker.running onCommitted: value => root.apply(modelData.key, value) onWheelRequested: root.openWheel(modelData.key) onPickRequested: root.pick(modelData.key) } } ColorDialog { id: wheel title: "Choose a colour" // Qt hands back a color value, not a string; toString() on it is // "#aarrggbb" when there is alpha, so the channels are read directly. onAccepted: root.apply(root.target, Qt.rgba( wheel.selectedColor.r, wheel.selectedColor.g, wheel.selectedColor.b, 1).toString()) } Process { id: screenPicker stdout: StdioCollector { onStreamFinished: root.apply(root.target, this.text) } onExited: (exitCode, exitStatus) => { if (exitCode !== 0) root.lastError = "Screen colour picking was cancelled or unavailable."; } } }