import QtQuick import Quickshell import Quickshell.Hyprland import qs.Common import qs.Modals.Common import qs.Services import qs.Widgets DankModal { id: root layerNamespace: "dms:color-picker" HyprlandFocusGrab { windows: [root.contentWindow] active: CompositorService.isHyprland && root.shouldHaveFocus } property string pickerTitle: I18n.tr("Choose Color") property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary property var onColorSelectedCallback: null signal colorSelected(color selectedColor) property color currentColor: Theme.primary property real hue: 0 property real saturation: 1 property real value: 1 property real alpha: 1 property real gradientX: 0 property real gradientY: 0 readonly property var standardColors: ["#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4", "#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722", "#d32f2f", "#c2185b", "#7b1fa2", "#512da8", "#303f9f", "#1976d2", "#0288d1", "#0097a7", "#00796b", "#388e3c", "#689f38", "#afb42b", "#fbc02d", "#ffa000", "#f57c00", "#e64a19", "#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#283593", "#1565c0", "#0277bd", "#00838f", "#00695c", "#2e7d32", "#558b2f", "#9e9d24", "#f9a825", "#ff8f00", "#ef6c00", "#d84315", "#ffffff", "#9e9e9e", "#212121"] function show() { currentColor = selectedColor; updateFromColor(currentColor); open(); } function hide() { onColorSelectedCallback = null; close(); } function hideInstant() { instantClose(); } onColorSelected: color => { if (onColorSelectedCallback) { onColorSelectedCallback(color); } } function copyColorToClipboard(colorValue) { Quickshell.execDetached(["sh", "-c", `echo -n "${colorValue}" | wl-copy`]); ToastService.showInfo(`Color ${colorValue} copied`); SessionData.addRecentColor(currentColor); } function updateFromColor(color) { hue = color.hsvHue; saturation = color.hsvSaturation; value = color.hsvValue; alpha = color.a; gradientX = saturation; gradientY = 1 - value; } function updateColor() { currentColor = Qt.hsva(hue, saturation, value, alpha); } function updateColorFromGradient(x, y) { saturation = Math.max(0, Math.min(1, x)); value = Math.max(0, Math.min(1, 1 - y)); updateColor(); selectedColor = currentColor; } function applyPickedColor(colorStr) { if (colorStr.length < 7 || !colorStr.startsWith('#')) return; const pickedColor = Qt.color(colorStr); root.selectedColor = pickedColor; root.currentColor = pickedColor; root.updateFromColor(pickedColor); copyColorToClipboard(colorStr); root.show(); } function pickColorFromScreen() { hideInstant(); Proc.runCommand("dms-color-pick", ["dms", "color", "pick", "--json"], (output, exitCode) => { if (exitCode !== 0) { console.warn("dms color pick exited with code:", exitCode); root.show(); return; } try { const result = JSON.parse(output); if (result.hex) { applyPickedColor(result.hex); } else { console.warn("Failed to parse dms color pick output: missing hex"); root.show(); } } catch (e) { console.warn("Failed to parse dms color pick JSON:", e); root.show(); } }); } modalWidth: 680 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680 backgroundColor: Theme.surfaceContainer cornerRadius: Theme.cornerRadius borderColor: Theme.outlineMedium borderWidth: 1 keepContentLoaded: true allowStacking: true onBackgroundClicked: hide() content: Component { FocusScope { id: colorContent property alias hexInput: hexInput anchors.fill: parent implicitHeight: mainColumn.implicitHeight focus: true Keys.onEscapePressed: event => { root.hide(); event.accepted = true; } Column { id: mainColumn anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top anchors.margins: Theme.spacingM spacing: Theme.spacingM Row { width: parent.width spacing: Theme.spacingS Column { width: parent.width - 90 spacing: Theme.spacingXS StyledText { text: root.pickerTitle font.pixelSize: Theme.fontSizeLarge color: Theme.surfaceText font.weight: Font.Medium } StyledText { text: I18n.tr("Select a color from the palette or use custom sliders") font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceTextMedium } } DankActionButton { iconName: "colorize" iconSize: Theme.iconSize - 4 iconColor: Theme.surfaceText onClicked: () => { root.pickColorFromScreen(); } } DankActionButton { iconName: "close" iconSize: Theme.iconSize - 4 iconColor: Theme.surfaceText onClicked: () => { root.hide(); } } } Row { width: parent.width spacing: Theme.spacingM Rectangle { id: gradientPicker width: parent.width - 70 height: 280 radius: Theme.cornerRadius border.color: Theme.outlineStrong border.width: 1 clip: true Rectangle { anchors.fill: parent color: Qt.hsva(root.hue, 1, 1, 1) Rectangle { anchors.fill: parent gradient: Gradient { orientation: Gradient.Horizontal GradientStop { position: 0.0 color: "#ffffff" } GradientStop { position: 1.0 color: "transparent" } } } Rectangle { anchors.fill: parent gradient: Gradient { orientation: Gradient.Vertical GradientStop { position: 0.0 color: "transparent" } GradientStop { position: 1.0 color: "#000000" } } } } Rectangle { id: pickerCircle width: 16 height: 16 radius: 8 border.color: "white" border.width: 2 color: "transparent" x: root.gradientX * parent.width - width / 2 y: root.gradientY * parent.height - height / 2 Rectangle { anchors.centerIn: parent width: parent.width - 4 height: parent.height - 4 radius: width / 2 border.color: "black" border.width: 1 color: "transparent" } } MouseArea { anchors.fill: parent cursorShape: Qt.CrossCursor onPressed: mouse => { const x = Math.max(0, Math.min(1, mouse.x / width)); const y = Math.max(0, Math.min(1, mouse.y / height)); root.gradientX = x; root.gradientY = y; root.updateColorFromGradient(x, y); } onPositionChanged: mouse => { if (pressed) { const x = Math.max(0, Math.min(1, mouse.x / width)); const y = Math.max(0, Math.min(1, mouse.y / height)); root.gradientX = x; root.gradientY = y; root.updateColorFromGradient(x, y); } } } } Rectangle { id: hueSlider width: 50 height: 280 radius: Theme.cornerRadius border.color: Theme.outlineStrong border.width: 1 gradient: Gradient { orientation: Gradient.Vertical GradientStop { position: 0.00 color: "#ff0000" } GradientStop { position: 0.17 color: "#ffff00" } GradientStop { position: 0.33 color: "#00ff00" } GradientStop { position: 0.50 color: "#00ffff" } GradientStop { position: 0.67 color: "#0000ff" } GradientStop { position: 0.83 color: "#ff00ff" } GradientStop { position: 1.00 color: "#ff0000" } } Rectangle { id: hueIndicator width: parent.width height: 4 color: "white" border.color: "black" border.width: 1 y: root.hue * parent.height - height / 2 } MouseArea { anchors.fill: parent cursorShape: Qt.SizeVerCursor onPressed: mouse => { const h = Math.max(0, Math.min(1, mouse.y / height)); root.hue = h; root.updateColor(); root.selectedColor = root.currentColor; } onPositionChanged: mouse => { if (pressed) { const h = Math.max(0, Math.min(1, mouse.y / height)); root.hue = h; root.updateColor(); root.selectedColor = root.currentColor; } } } } } Column { width: parent.width spacing: Theme.spacingS StyledText { text: I18n.tr("Material Colors") font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText font.weight: Font.Medium } GridView { width: parent.width height: 140 cellWidth: 38 cellHeight: 38 clip: true interactive: false model: root.standardColors delegate: Rectangle { width: 36 height: 36 color: modelData radius: 4 border.color: Theme.outlineStrong border.width: 1 MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: () => { const pickedColor = Qt.color(modelData); root.selectedColor = pickedColor; root.currentColor = pickedColor; root.updateFromColor(pickedColor); } } } } } Column { width: parent.width spacing: Theme.spacingS Row { width: parent.width spacing: Theme.spacingS Column { width: 210 spacing: Theme.spacingXS StyledText { text: I18n.tr("Recent Colors") font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText font.weight: Font.Medium } Row { width: parent.width spacing: Theme.spacingXS Repeater { model: 5 Rectangle { width: 36 height: 36 radius: 4 border.color: Theme.outlineStrong border.width: 1 color: { if (index < SessionData.recentColors.length) { return SessionData.recentColors[index]; } return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency); } opacity: index < SessionData.recentColors.length ? 1.0 : 0.3 MouseArea { anchors.fill: parent cursorShape: index < SessionData.recentColors.length ? Qt.PointingHandCursor : Qt.ArrowCursor enabled: index < SessionData.recentColors.length onClicked: () => { if (index < SessionData.recentColors.length) { const pickedColor = SessionData.recentColors[index]; root.selectedColor = pickedColor; root.currentColor = pickedColor; root.updateFromColor(pickedColor); } } } } } } } Column { width: parent.width - 330 spacing: Theme.spacingXS StyledText { text: I18n.tr("Opacity") font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText font.weight: Font.Medium } DankSlider { width: parent.width value: Math.round(root.alpha * 100) minimum: 0 maximum: 100 showValue: false onSliderValueChanged: newValue => { root.alpha = newValue / 100; root.updateColor(); root.selectedColor = root.currentColor; } } } Rectangle { width: 100 height: 50 radius: Theme.cornerRadius color: root.currentColor border.color: Theme.outlineStrong border.width: 2 anchors.verticalCenter: parent.verticalCenter } } } Column { width: parent.width spacing: Theme.spacingS Row { width: parent.width spacing: Theme.spacingM Column { width: (parent.width - Theme.spacingM * 2) / 3 spacing: Theme.spacingXS StyledText { text: I18n.tr("Hex") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceTextMedium font.weight: Font.Medium } Row { width: parent.width spacing: Theme.spacingXS DankTextField { id: hexInput width: parent.width - 36 height: 36 text: root.currentColor.toString() font.pixelSize: Theme.fontSizeMedium textColor: { if (text.length === 0) return Theme.surfaceText; const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/; return hexPattern.test(text) ? Theme.surfaceText : Theme.error; } placeholderText: "#000000" backgroundColor: Theme.surfaceHover borderWidth: 1 focusedBorderWidth: 2 topPadding: Theme.spacingS bottomPadding: Theme.spacingS onAccepted: () => { const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/; if (!hexPattern.test(text)) return; const color = Qt.color(text); if (color) { root.selectedColor = color; root.currentColor = color; root.updateFromColor(color); } } } DankActionButton { iconName: "content_copy" iconSize: Theme.iconSize - 6 iconColor: Theme.surfaceText buttonSize: 36 anchors.verticalCenter: parent.verticalCenter onClicked: () => { root.copyColorToClipboard(hexInput.text); } } } } Column { width: (parent.width - Theme.spacingM * 2) / 3 spacing: Theme.spacingXS StyledText { text: I18n.tr("RGB") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceTextMedium font.weight: Font.Medium } Row { width: parent.width spacing: Theme.spacingXS Rectangle { width: parent.width - 36 height: 36 radius: Theme.cornerRadius color: Theme.surfaceHover border.color: Theme.outline border.width: 1 StyledText { anchors.centerIn: parent text: { const r = Math.round(root.currentColor.r * 255); const g = Math.round(root.currentColor.g * 255); const b = Math.round(root.currentColor.b * 255); if (root.alpha < 1) { const a = Math.round(root.alpha * 255); return `${r}, ${g}, ${b}, ${a}`; } return `${r}, ${g}, ${b}`; } font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText } } DankActionButton { iconName: "content_copy" iconSize: Theme.iconSize - 6 iconColor: Theme.surfaceText buttonSize: 36 anchors.verticalCenter: parent.verticalCenter onClicked: () => { const r = Math.round(root.currentColor.r * 255); const g = Math.round(root.currentColor.g * 255); const b = Math.round(root.currentColor.b * 255); let rgbString; if (root.alpha < 1) { const a = Math.round(root.alpha * 255); rgbString = `rgba(${r}, ${g}, ${b}, ${a})`; } else { rgbString = `rgb(${r}, ${g}, ${b})`; } Quickshell.execDetached(["sh", "-c", `echo -n "${rgbString}" | wl-copy`]); ToastService.showInfo(`${rgbString} copied`); } } } } Column { width: (parent.width - Theme.spacingM * 2) / 3 spacing: Theme.spacingXS StyledText { text: I18n.tr("HSV") font.pixelSize: Theme.fontSizeSmall color: Theme.surfaceTextMedium font.weight: Font.Medium } Row { width: parent.width spacing: Theme.spacingXS Rectangle { width: parent.width - 36 height: 36 radius: Theme.cornerRadius color: Theme.surfaceHover border.color: Theme.outline border.width: 1 StyledText { anchors.centerIn: parent text: { const h = Math.round(root.hue * 360); const s = Math.round(root.saturation * 100); const v = Math.round(root.value * 100); if (root.alpha < 1) { const a = Math.round(root.alpha * 100); return `${h}°, ${s}%, ${v}%, ${a}%`; } return `${h}°, ${s}%, ${v}%`; } font.pixelSize: Theme.fontSizeMedium color: Theme.surfaceText } } DankActionButton { iconName: "content_copy" iconSize: Theme.iconSize - 6 iconColor: Theme.surfaceText buttonSize: 36 anchors.verticalCenter: parent.verticalCenter onClicked: () => { const h = Math.round(root.hue * 360); const s = Math.round(root.saturation * 100); const v = Math.round(root.value * 100); let hsvString; if (root.alpha < 1) { const a = Math.round(root.alpha * 100); hsvString = `${h}, ${s}, ${v}, ${a}`; } else { hsvString = `${h}, ${s}, ${v}`; } Quickshell.execDetached(["sh", "-c", `echo -n "${hsvString}" | wl-copy`]); ToastService.showInfo(`HSV ${hsvString} copied`); } } } } } DankButton { visible: root.onColorSelectedCallback !== null && root.onColorSelectedCallback !== undefined width: 70 buttonHeight: 36 text: I18n.tr("Save") backgroundColor: Theme.primary textColor: Theme.background anchors.right: parent.right onClicked: { SessionData.addRecentColor(root.currentColor); root.colorSelected(root.currentColor); root.hide(); } } } } } } }