From 2d31e04eaf5eed6f57ef1cd4c6c1a080652e9cc3 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 13:35:44 +0200 Subject: [PATCH 1/2] Possible brightness implementation --- Modules/Bar/Bar.qml | 4 + Modules/Bar/Brightness.qml | 63 +++++ Modules/Demo/DemoPanel.qml | 54 +++++ Services/BrightnessService.qml | 428 +++++++++++++++++++++++++++++++++ 4 files changed, 549 insertions(+) create mode 100644 Modules/Bar/Brightness.qml create mode 100644 Services/BrightnessService.qml diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 3cadc23..a7db3ab 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -119,6 +119,10 @@ Variants { anchors.verticalCenter: parent.verticalCenter } + Brightness { + anchors.verticalCenter: parent.verticalCenter + } + Clock { anchors.verticalCenter: parent.verticalCenter } diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml new file mode 100644 index 0000000..0d5d690 --- /dev/null +++ b/Modules/Bar/Brightness.qml @@ -0,0 +1,63 @@ +import QtQuick +import Quickshell +import qs.Modules.Settings +import qs.Services +import qs.Widgets + +Item { + id: root + + width: pill.width + height: pill.height + + // Used to avoid opening the pill on Quickshell startup + property bool firstBrightnessReceived: false + + function getIcon() { + if (!BrightnessService.available) { + return "brightness_auto" + } + var brightness = BrightnessService.brightness + return brightness <= 0 ? "brightness_1" : + brightness < 33 ? "brightness_low" : + brightness < 66 ? "brightness_medium" : "brightness_high" + } + + // Connection used to open the pill when brightness changes + Connections { + target: Brightness + function onBrightnessUpdated() { + // console.log("[Bar:Brightness] onBrightnessUpdated") + if (!firstBrightnessReceived) { + // Ignore the first brightness change + firstBrightnessReceived = true + } else { + pill.show() + } + } + } + + NPill { + id: pill + icon: getIcon() + iconCircleColor: Colors.mPrimary + collapsedIconColor: Colors.mOnSurface + autoHide: true + text: Math.round(BrightnessService.brightness) + "%" + tooltipText: "Brightness: " + Math.round(BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod + "\nLeft click for advanced settings.\nScroll up/down to change BrightnessService." + + onWheel: function (angle) { + if (!BrightnessService.available) return + + if (angle > 0) { + BrightnessService.increaseBrightness(1) + } else if (angle < 0) { + BrightnessService.decreaseBrightness(1) + } + } + onClicked: { + settingsPanel.requestedTab = SettingsPanel.Tab.Display + settingsPanel.isLoaded = true + } + } +} \ No newline at end of file diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 9d3eaaf..91e797b 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -273,6 +273,60 @@ NLoader { Layout.fillWidth: true } } + + // Brightness Control + ColumnLayout { + spacing: Style.marginMedium * scaling + NText { + text: "Brightness Control" + color: Colors.mSecondary + font.weight: Style.fontWeightBold + } + + NText { + text: `Brightness: ${Math.round(Brightness.brightness)}%` + Layout.alignment: Qt.AlignVCenter + } + + RowLayout { + spacing: Style.marginSmall * scaling + NIconButton { + icon: "brightness_low" + fontPointSize: Style.fontSizeLarge * scaling + onClicked: { + Brightness.decreaseBrightness(1) + } + } + NSlider { + from: 0 + to: 100 + stepSize: 1 + value: Brightness.brightness + implicitWidth: bgRect.width * 0.5 + onMoved: { + Brightness.setBrightnessDebounced(value) + } + } + NIconButton { + icon: "brightness_high" + fontPointSize: Style.fontSizeLarge * scaling + onClicked: { + Brightness.increaseBrightness(1) + } + } + } + + NText { + text: `Method: ${Brightness.currentMethod} | Available: ${Brightness.available}` + color: Colors.mOnSurfaceVariant + font.pointSize: Style.fontSizeSmall * scaling + Layout.alignment: Qt.AlignHCenter + } + + NDivider { + Layout.fillWidth: true + } + } } } } diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml new file mode 100644 index 0000000..31b0866 --- /dev/null +++ b/Services/BrightnessService.qml @@ -0,0 +1,428 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io + +Singleton { + id: root + + // Public properties + property real brightness: 0 + property real maxBrightness: 100 + property bool available: false + property string currentMethod: "" + property var detectedDisplays: [] + + // Private properties + property var _brightnessMethods: [] + property var _currentDisplay: null + property bool _initialized: false + property real _targetBrightness: 0 + property bool _isSettingBrightness: false + + // Signal when brightness changes + signal brightnessUpdated(real newBrightness) + signal methodChanged(string newMethod) + + // Initialize the service + Component.onCompleted: { + initializeBrightness() + } + + function initializeBrightness() { + if (_initialized) return + + console.log("[Brightness] Initializing brightness service...") + + // Start method detection + detectMethods() + + _initialized = true + } + + function detectMethods() { + _brightnessMethods = [] + + // Check for brightnessctl + brightnessctlProcess.running = true + + // Check for ddcutil + ddcutilProcess.running = true + + // Internal backlight is always available if we can access /sys/class/backlight + backlightCheck.running = true + + console.log("[Brightness] Starting method detection...") + } + + function checkMethodsComplete() { + // Check if all method detection processes have finished + if (!brightnessctlProcess.running && !ddcutilProcess.running && !backlightCheck.running) { + console.log("[Brightness] Available methods:", _brightnessMethods) + + // Now detect displays + detectDisplays() + } + } + + // Process objects for method detection + Process { + id: brightnessctlProcess + command: ["which", "brightnessctl"] + running: false + onExited: function(exitCode, exitStatus) { + if (exitCode === 0) { + _brightnessMethods.push("brightnessctl") + console.log("[Brightness] brightnessctl available") + } + checkMethodsComplete() + } + } + + Process { + id: ddcutilProcess + command: ["which", "ddcutil"] + running: false + onExited: function(exitCode, exitStatus) { + if (exitCode === 0) { + _brightnessMethods.push("ddcutil") + console.log("[Brightness] ddcutil available") + } + checkMethodsComplete() + } + } + + Process { + id: backlightCheck + command: ["test", "-d", "/sys/class/backlight"] + running: false + onExited: function(exitCode, exitStatus) { + if (exitCode === 0) { + _brightnessMethods.push("internal") + console.log("[Brightness] Internal backlight available") + } + checkMethodsComplete() + } + } + + function detectDisplays() { + detectedDisplays = [] + + // Get internal displays + backlightProcess.running = true + + // Get external displays via ddcutil + if (_brightnessMethods.indexOf("ddcutil") !== -1) { + ddcutilDetectProcess.running = true + } else { + // If no ddcutil, just check internal displays + checkDisplaysComplete() + } + + console.log("[Brightness] Starting display detection...") + } + + function checkDisplaysComplete() { + // Check if all display detection processes have finished + var internalFinished = !backlightProcess.running + var externalFinished = _brightnessMethods.indexOf("ddcutil") === -1 || !ddcutilDetectProcess.running + + if (internalFinished && externalFinished) { + console.log("[Brightness] Detected displays:", detectedDisplays) + + // Set current display to first available + if (detectedDisplays.length > 0) { + _currentDisplay = detectedDisplays[0] + currentMethod = _currentDisplay.method + available = true + console.log("[Brightness] Using display:", _currentDisplay.name, "method:", currentMethod) + + // Start initial brightness update + updateBrightness() + } else { + console.warn("[Brightness] No displays detected") + } + } + } + + // Process objects for display detection + Process { + id: backlightProcess + command: ["ls", "/sys/class/backlight"] + running: false + stdout: SplitParser { + onRead: function(line) { + var trimmedLine = line.replace(/^\s+|\s+$/g, "") + if (trimmedLine) { + detectedDisplays.push({ + name: trimmedLine, + type: "internal", + method: "internal" + }) + } + } + } + onExited: function(exitCode, exitStatus) { + checkDisplaysComplete() + } + } + + Process { + id: ddcutilDetectProcess + command: ["ddcutil", "detect"] + running: false + stdout: SplitParser { + onRead: function(line) { + console.log("[Brightness] ddcutil detect line:", line) + if (line.indexOf("Display") !== -1) { + // Simple parsing for Display number + var parts = line.split("Display") + if (parts.length > 1) { + var numberPart = parts[1].replace(/^\s+|\s+$/g, "") + var number = numberPart.split(" ")[0] + if (number && !isNaN(number)) { + detectedDisplays.push({ + name: "Display " + number, + type: "external", + method: "ddcutil", + index: number + }) + console.log("[Brightness] Added external display:", "Display " + number) + } + } + } + // Also look for connector information + if (line.indexOf("DRM connector:") !== -1) { + console.log("[Brightness] Found DRM connector:", line) + } + } + } + onExited: function(exitCode, exitStatus) { + checkDisplaysComplete() + } + } + + function updateBrightness() { + if (!_currentDisplay) return + + // Prevent multiple simultaneous brightness checks + if (brightnessGetProcess.running) { + console.log("[Brightness] Brightness check already in progress, skipping...") + return + } + + // Don't update if we're currently setting brightness + if (_isSettingBrightness) { + console.log("[Brightness] Skipping update while setting brightness...") + return + } + + console.log("[Brightness] Updating brightness for display:", _currentDisplay.name) + + // Try the brightness script first + if (_currentDisplay.method === "ddcutil" && _currentDisplay.index) { + // For ddcutil, try using the display index directly + brightnessGetProcess.command = ["ddcutil", "--display", _currentDisplay.index, "getvcp", "10"] + } else { + // Use the brightness script + brightnessGetProcess.command = ["sh", "-c", Quickshell.shellDir + "/Bin/brigthness.sh", "get", _currentDisplay.name] + } + brightnessGetProcess.running = true + } + + function updateBrightnessDebounced() { + // Use debouncing to prevent excessive updates + debounceTimer.restart() + } + + function setBrightness(newBrightness) { + if (!_currentDisplay || !available) { + console.warn("[Brightness] No display available for brightness control") + return false + } + + // Clamp brightness to valid range + newBrightness = Math.max(0, Math.min(100, newBrightness)) + + // Prevent setting if already setting + if (brightnessSetProcess.running) { + console.log("[Brightness] Brightness set already in progress, skipping...") + return false + } + + console.log("[Brightness] Setting brightness to:", newBrightness, "for display:", _currentDisplay.name) + + // Mark that we're setting brightness + _isSettingBrightness = true + + // Try ddcutil directly for external displays + if (_currentDisplay.method === "ddcutil" && _currentDisplay.index) { + brightnessSetProcess.command = ["ddcutil", "--display", _currentDisplay.index, "setvcp", "10", newBrightness.toString()] + } else { + // Use the brightness script for internal displays + brightnessSetProcess.command = ["sh", "-c", Quickshell.shellDir + "/Bin/brigthness.sh", "set", _currentDisplay.name, newBrightness.toString()] + } + brightnessSetProcess.running = true + + return true + } + + function setBrightnessDebounced(newBrightness) { + // Store the target brightness for debounced setting + _targetBrightness = newBrightness + + // Update UI immediately for responsiveness + if (brightness !== newBrightness) { + brightness = newBrightness + brightnessUpdated(brightness) + } + + setDebounceTimer.restart() + } + + // Process objects for brightness control + Process { + id: brightnessGetProcess + running: false + stdout: SplitParser { + onRead: function(line) { + var newBrightness = -1 + + // Handle ddcutil output format: "current value = X," + if (line.indexOf("current value =") !== -1) { + var match = line.match(/current value\s*=\s*(\d+)/) + if (match) { + newBrightness = parseFloat(match[1]) + } + } else { + // Handle direct numeric output + newBrightness = parseFloat(line.replace(/^\s+|\s+$/g, "")) + } + + if (!isNaN(newBrightness) && newBrightness >= 0) { + if (brightness !== newBrightness) { + brightness = newBrightness + brightnessUpdated(brightness) + console.log("[Brightness] Brightness updated to:", brightness) + } + } else { + console.warn("[Brightness] Invalid brightness value:", line) + } + } + } + onExited: function(exitCode, exitStatus) { + // Only log errors + if (exitCode !== 0) { + console.warn("[Brightness] Brightness get process failed with code:", exitCode) + } + } + } + + Process { + id: brightnessSetProcess + running: false + stdout: SplitParser { + onRead: function(line) { + var result = parseFloat(line.replace(/^\s+|\s+$/g, "")) + if (!isNaN(result) && result >= 0) { + brightness = result + brightnessUpdated(brightness) + console.log("[Brightness] Brightness set to:", brightness) + } else { + console.warn("[Brightness] Failed to set brightness - invalid output:", line) + } + } + } + onExited: function(exitCode, exitStatus) { + if (exitCode === 0) { + // If ddcutil succeeded but didn't output a number, refresh the brightness + if (_currentDisplay.method === "ddcutil") { + // Longer delay to let the display update and avoid conflicts + refreshTimer.interval = 1000 + refreshTimer.start() + } + } else { + console.warn("[Brightness] Set brightness process failed with exit code:", exitCode) + } + + // Clear the setting flag after a delay + settingCompleteTimer.start() + } + } + + // Timer to clear the setting flag + Timer { + id: settingCompleteTimer + interval: 800 + repeat: false + onTriggered: { + _isSettingBrightness = false + } + } + + // Timer to refresh brightness after setting + Timer { + id: refreshTimer + interval: 500 + repeat: false + onTriggered: updateBrightnessDebounced() + } + + + + function increaseBrightness(step = 5) { + return setBrightnessDebounced(brightness + step) + } + + function decreaseBrightness(step = 5) { + return setBrightnessDebounced(brightness - step) + } + + function setDisplay(displayIndex) { + if (displayIndex >= 0 && displayIndex < detectedDisplays.length) { + _currentDisplay = detectedDisplays[displayIndex] + currentMethod = _currentDisplay.method + methodChanged(currentMethod) + updateBrightness() + return true + } + return false + } + + function getDisplayInfo() { + return _currentDisplay || null + } + + function getAvailableMethods() { + return _brightnessMethods + } + + function getDetectedDisplays() { + return detectedDisplays + } + + // Refresh brightness periodically - but less frequently + Timer { + interval: 5000 // Update every 5 seconds instead of 2 + running: available && _initialized + repeat: true + onTriggered: updateBrightness() + } + + // Debounce timer for UI updates + Timer { + id: debounceTimer + interval: 300 + repeat: false + onTriggered: updateBrightness() + } + + // Debounce timer for setting brightness + Timer { + id: setDebounceTimer + interval: 100 + repeat: false + onTriggered: setBrightness(_targetBrightness) + } +} \ No newline at end of file From 22df558e144375baeae550b1b2926c17da4e5c24 Mon Sep 17 00:00:00 2001 From: Ly-sec Date: Fri, 15 Aug 2025 14:05:02 +0200 Subject: [PATCH 2/2] Brightness implementation with IPC --- Modules/Bar/Brightness.qml | 25 +- Modules/Demo/DemoPanel.qml | 4 +- Modules/Settings/Tabs/DisplayTab.qml | 49 +++ Services/BrightnessService.qml | 627 +++++++++++---------------- Services/IPCManager.qml | 12 + Services/Settings.qml | 10 + 6 files changed, 335 insertions(+), 392 deletions(-) diff --git a/Modules/Bar/Brightness.qml b/Modules/Bar/Brightness.qml index 0d5d690..aa754b7 100644 --- a/Modules/Bar/Brightness.qml +++ b/Modules/Bar/Brightness.qml @@ -12,6 +12,7 @@ Item { // Used to avoid opening the pill on Quickshell startup property bool firstBrightnessReceived: false + property real lastBrightness: -1 function getIcon() { if (!BrightnessService.available) { @@ -25,18 +26,28 @@ Item { // Connection used to open the pill when brightness changes Connections { - target: Brightness + target: BrightnessService.focusedMonitor function onBrightnessUpdated() { - // console.log("[Bar:Brightness] onBrightnessUpdated") + var currentBrightness = BrightnessService.brightness + + // Ignore if this is the first time or if brightness hasn't actually changed if (!firstBrightnessReceived) { - // Ignore the first brightness change firstBrightnessReceived = true - } else { + lastBrightness = currentBrightness + return + } + + // Only show pill if brightness actually changed (not just loaded from settings) + if (Math.abs(currentBrightness - lastBrightness) > 0.1) { pill.show() } + + lastBrightness = currentBrightness } } + + NPill { id: pill icon: getIcon() @@ -44,15 +55,15 @@ Item { collapsedIconColor: Colors.mOnSurface autoHide: true text: Math.round(BrightnessService.brightness) + "%" - tooltipText: "Brightness: " + Math.round(BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod + "\nLeft click for advanced settings.\nScroll up/down to change BrightnessService." + tooltipText: "Brightness: " + Math.round(BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod + "\nLeft click for advanced settings.\nScroll up/down to change brightness." onWheel: function (angle) { if (!BrightnessService.available) return if (angle > 0) { - BrightnessService.increaseBrightness(1) + BrightnessService.increaseBrightness() } else if (angle < 0) { - BrightnessService.decreaseBrightness(1) + BrightnessService.decreaseBrightness() } } onClicked: { diff --git a/Modules/Demo/DemoPanel.qml b/Modules/Demo/DemoPanel.qml index 91e797b..ed5e613 100644 --- a/Modules/Demo/DemoPanel.qml +++ b/Modules/Demo/DemoPanel.qml @@ -294,7 +294,7 @@ NLoader { icon: "brightness_low" fontPointSize: Style.fontSizeLarge * scaling onClicked: { - Brightness.decreaseBrightness(1) + Brightness.decreaseBrightness() } } NSlider { @@ -311,7 +311,7 @@ NLoader { icon: "brightness_high" fontPointSize: Style.fontSizeLarge * scaling onClicked: { - Brightness.increaseBrightness(1) + Brightness.increaseBrightness() } } } diff --git a/Modules/Settings/Tabs/DisplayTab.qml b/Modules/Settings/Tabs/DisplayTab.qml index 3234699..c4694bd 100644 --- a/Modules/Settings/Tabs/DisplayTab.qml +++ b/Modules/Settings/Tabs/DisplayTab.qml @@ -47,6 +47,55 @@ Item { color: Colors.mOnSurface } + // Brightness Section + ColumnLayout { + spacing: Style.marginSmall * scaling + Layout.fillWidth: true + Layout.topMargin: Style.marginSmall * scaling + + NText { + text: "Brightness Step Size" + font.weight: Style.fontWeightBold + color: Colors.mOnSurface + } + + NText { + text: "Adjust the step size for brightness changes (scroll wheel, ipc bind)" + font.pointSize: Style.fontSizeSmall * scaling + color: Colors.mOnSurface + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.fillWidth: true + spacing: Style.marginMedium * scaling + + NSlider { + Layout.fillWidth: true + from: 1 + to: 50 + value: Settings.data.brightness.brightnessStep + stepSize: 1 + onMoved: { + Settings.data.brightness.brightnessStep = value + } + } + + NText { + text: Settings.data.brightness.brightnessStep + "%" + Layout.alignment: Qt.AlignVCenter + color: Colors.mOnSurface + } + } + } + + NDivider { + Layout.fillWidth: true + Layout.topMargin: Style.marginLarge * 2 * scaling + Layout.bottomMargin: Style.marginLarge * scaling + } + Repeater { model: Quickshell.screens || [] delegate: Rectangle { diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 31b0866..ce9e130 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -1,4 +1,5 @@ pragma Singleton +pragma ComponentBehavior: Bound import QtQuick import Quickshell @@ -7,422 +8,282 @@ import Quickshell.Io Singleton { id: root - // Public properties - property real brightness: 0 - property real maxBrightness: 100 - property bool available: false - property string currentMethod: "" - property var detectedDisplays: [] + property list ddcMonitors: [] + readonly property list monitors: variants.instances + property bool appleDisplayPresent: false - // Private properties - property var _brightnessMethods: [] - property var _currentDisplay: null - property bool _initialized: false - property real _targetBrightness: 0 - property bool _isSettingBrightness: false + // Public properties for backward compatibility + readonly property real brightness: focusedMonitor ? focusedMonitor.brightness * 100 : Settings.data.brightness.lastBrightness + readonly property bool available: focusedMonitor !== null + readonly property string currentMethod: focusedMonitor ? focusedMonitor.method : Settings.data.brightness.lastMethod + readonly property var detectedDisplays: monitors.map(m => ({ + name: m.modelData.name, + type: m.isDdc ? "external" : "internal", + method: m.method, + index: m.busNum + })) - // Signal when brightness changes - signal brightnessUpdated(real newBrightness) - signal methodChanged(string newMethod) - - // Initialize the service - Component.onCompleted: { - initializeBrightness() + // Get the currently focused monitor + readonly property Monitor focusedMonitor: { + if (monitors.length === 0) return null + // For now, return the first monitor. Could be enhanced to detect focused monitor + return monitors[0] } - function initializeBrightness() { - if (_initialized) return - - console.log("[Brightness] Initializing brightness service...") - - // Start method detection - detectMethods() - - _initialized = true + function getMonitorForScreen(screen: ShellScreen): var { + return monitors.find(m => m.modelData === screen) } - function detectMethods() { - _brightnessMethods = [] - - // Check for brightnessctl - brightnessctlProcess.running = true - - // Check for ddcutil - ddcutilProcess.running = true - - // Internal backlight is always available if we can access /sys/class/backlight - backlightCheck.running = true - - console.log("[Brightness] Starting method detection...") - } - - function checkMethodsComplete() { - // Check if all method detection processes have finished - if (!brightnessctlProcess.running && !ddcutilProcess.running && !backlightCheck.running) { - console.log("[Brightness] Available methods:", _brightnessMethods) - - // Now detect displays - detectDisplays() + function increaseBrightness(step = null): void { + if (focusedMonitor) { + var stepSize = step !== null ? step : Settings.data.brightness.brightnessStep + focusedMonitor.setBrightness(focusedMonitor.brightness + (stepSize / 100)) } } - // Process objects for method detection - Process { - id: brightnessctlProcess - command: ["which", "brightnessctl"] - running: false - onExited: function(exitCode, exitStatus) { - if (exitCode === 0) { - _brightnessMethods.push("brightnessctl") - console.log("[Brightness] brightnessctl available") - } - checkMethodsComplete() + function decreaseBrightness(step = null): void { + if (focusedMonitor) { + var stepSize = step !== null ? step : Settings.data.brightness.brightnessStep + focusedMonitor.setBrightness(focusedMonitor.brightness - (stepSize / 100)) } } - Process { - id: ddcutilProcess - command: ["which", "ddcutil"] - running: false - onExited: function(exitCode, exitStatus) { - if (exitCode === 0) { - _brightnessMethods.push("ddcutil") - console.log("[Brightness] ddcutil available") - } - checkMethodsComplete() + function setBrightness(newBrightness: real): void { + if (focusedMonitor) { + focusedMonitor.setBrightness(newBrightness / 100) } } - Process { - id: backlightCheck - command: ["test", "-d", "/sys/class/backlight"] - running: false - onExited: function(exitCode, exitStatus) { - if (exitCode === 0) { - _brightnessMethods.push("internal") - console.log("[Brightness] Internal backlight available") - } - checkMethodsComplete() + function setBrightnessDebounced(newBrightness: real): void { + if (focusedMonitor) { + focusedMonitor.setBrightnessDebounced(newBrightness / 100) } } - function detectDisplays() { - detectedDisplays = [] - - // Get internal displays - backlightProcess.running = true - - // Get external displays via ddcutil - if (_brightnessMethods.indexOf("ddcutil") !== -1) { - ddcutilDetectProcess.running = true - } else { - // If no ddcutil, just check internal displays - checkDisplaysComplete() - } - - console.log("[Brightness] Starting display detection...") + // Backward compatibility functions + function updateBrightness(): void { + // No longer needed with the new architecture } - function checkDisplaysComplete() { - // Check if all display detection processes have finished - var internalFinished = !backlightProcess.running - var externalFinished = _brightnessMethods.indexOf("ddcutil") === -1 || !ddcutilDetectProcess.running - - if (internalFinished && externalFinished) { - console.log("[Brightness] Detected displays:", detectedDisplays) - - // Set current display to first available - if (detectedDisplays.length > 0) { - _currentDisplay = detectedDisplays[0] - currentMethod = _currentDisplay.method - available = true - console.log("[Brightness] Using display:", _currentDisplay.name, "method:", currentMethod) - - // Start initial brightness update - updateBrightness() - } else { - console.warn("[Brightness] No displays detected") - } - } - } - - // Process objects for display detection - Process { - id: backlightProcess - command: ["ls", "/sys/class/backlight"] - running: false - stdout: SplitParser { - onRead: function(line) { - var trimmedLine = line.replace(/^\s+|\s+$/g, "") - if (trimmedLine) { - detectedDisplays.push({ - name: trimmedLine, - type: "internal", - method: "internal" - }) - } - } - } - onExited: function(exitCode, exitStatus) { - checkDisplaysComplete() - } - } - - Process { - id: ddcutilDetectProcess - command: ["ddcutil", "detect"] - running: false - stdout: SplitParser { - onRead: function(line) { - console.log("[Brightness] ddcutil detect line:", line) - if (line.indexOf("Display") !== -1) { - // Simple parsing for Display number - var parts = line.split("Display") - if (parts.length > 1) { - var numberPart = parts[1].replace(/^\s+|\s+$/g, "") - var number = numberPart.split(" ")[0] - if (number && !isNaN(number)) { - detectedDisplays.push({ - name: "Display " + number, - type: "external", - method: "ddcutil", - index: number - }) - console.log("[Brightness] Added external display:", "Display " + number) - } - } - } - // Also look for connector information - if (line.indexOf("DRM connector:") !== -1) { - console.log("[Brightness] Found DRM connector:", line) - } - } - } - onExited: function(exitCode, exitStatus) { - checkDisplaysComplete() - } - } - - function updateBrightness() { - if (!_currentDisplay) return - - // Prevent multiple simultaneous brightness checks - if (brightnessGetProcess.running) { - console.log("[Brightness] Brightness check already in progress, skipping...") - return - } - - // Don't update if we're currently setting brightness - if (_isSettingBrightness) { - console.log("[Brightness] Skipping update while setting brightness...") - return - } - - console.log("[Brightness] Updating brightness for display:", _currentDisplay.name) - - // Try the brightness script first - if (_currentDisplay.method === "ddcutil" && _currentDisplay.index) { - // For ddcutil, try using the display index directly - brightnessGetProcess.command = ["ddcutil", "--display", _currentDisplay.index, "getvcp", "10"] - } else { - // Use the brightness script - brightnessGetProcess.command = ["sh", "-c", Quickshell.shellDir + "/Bin/brigthness.sh", "get", _currentDisplay.name] - } - brightnessGetProcess.running = true - } - - function updateBrightnessDebounced() { - // Use debouncing to prevent excessive updates - debounceTimer.restart() - } - - function setBrightness(newBrightness) { - if (!_currentDisplay || !available) { - console.warn("[Brightness] No display available for brightness control") - return false - } - - // Clamp brightness to valid range - newBrightness = Math.max(0, Math.min(100, newBrightness)) - - // Prevent setting if already setting - if (brightnessSetProcess.running) { - console.log("[Brightness] Brightness set already in progress, skipping...") - return false - } - - console.log("[Brightness] Setting brightness to:", newBrightness, "for display:", _currentDisplay.name) - - // Mark that we're setting brightness - _isSettingBrightness = true - - // Try ddcutil directly for external displays - if (_currentDisplay.method === "ddcutil" && _currentDisplay.index) { - brightnessSetProcess.command = ["ddcutil", "--display", _currentDisplay.index, "setvcp", "10", newBrightness.toString()] - } else { - // Use the brightness script for internal displays - brightnessSetProcess.command = ["sh", "-c", Quickshell.shellDir + "/Bin/brigthness.sh", "set", _currentDisplay.name, newBrightness.toString()] - } - brightnessSetProcess.running = true - + function setDisplay(displayIndex: int): bool { + // No longer needed with the new architecture return true } - function setBrightnessDebounced(newBrightness) { - // Store the target brightness for debounced setting - _targetBrightness = newBrightness - - // Update UI immediately for responsiveness - if (brightness !== newBrightness) { - brightness = newBrightness - brightnessUpdated(brightness) - } - - setDebounceTimer.restart() + function getDisplayInfo(): var { + return focusedMonitor ? { + name: focusedMonitor.modelData.name, + type: focusedMonitor.isDdc ? "external" : "internal", + method: focusedMonitor.method, + index: focusedMonitor.busNum + } : null } - // Process objects for brightness control - Process { - id: brightnessGetProcess - running: false - stdout: SplitParser { - onRead: function(line) { - var newBrightness = -1 - - // Handle ddcutil output format: "current value = X," - if (line.indexOf("current value =") !== -1) { - var match = line.match(/current value\s*=\s*(\d+)/) - if (match) { - newBrightness = parseFloat(match[1]) - } - } else { - // Handle direct numeric output - newBrightness = parseFloat(line.replace(/^\s+|\s+$/g, "")) - } - - if (!isNaN(newBrightness) && newBrightness >= 0) { - if (brightness !== newBrightness) { - brightness = newBrightness - brightnessUpdated(brightness) - console.log("[Brightness] Brightness updated to:", brightness) - } - } else { - console.warn("[Brightness] Invalid brightness value:", line) - } - } - } - onExited: function(exitCode, exitStatus) { - // Only log errors - if (exitCode !== 0) { - console.warn("[Brightness] Brightness get process failed with code:", exitCode) - } - } + function getAvailableMethods(): list { + var methods = [] + if (monitors.some(m => m.isDdc)) methods.push("ddcutil") + if (monitors.some(m => !m.isDdc)) methods.push("internal") + if (appleDisplayPresent) methods.push("apple") + return methods } - Process { - id: brightnessSetProcess - running: false - stdout: SplitParser { - onRead: function(line) { - var result = parseFloat(line.replace(/^\s+|\s+$/g, "")) - if (!isNaN(result) && result >= 0) { - brightness = result - brightnessUpdated(brightness) - console.log("[Brightness] Brightness set to:", brightness) - } else { - console.warn("[Brightness] Failed to set brightness - invalid output:", line) - } - } - } - onExited: function(exitCode, exitStatus) { - if (exitCode === 0) { - // If ddcutil succeeded but didn't output a number, refresh the brightness - if (_currentDisplay.method === "ddcutil") { - // Longer delay to let the display update and avoid conflicts - refreshTimer.interval = 1000 - refreshTimer.start() - } - } else { - console.warn("[Brightness] Set brightness process failed with exit code:", exitCode) - } - - // Clear the setting flag after a delay - settingCompleteTimer.start() - } - } - - // Timer to clear the setting flag - Timer { - id: settingCompleteTimer - interval: 800 - repeat: false - onTriggered: { - _isSettingBrightness = false - } - } - - // Timer to refresh brightness after setting - Timer { - id: refreshTimer - interval: 500 - repeat: false - onTriggered: updateBrightnessDebounced() - } - - - - function increaseBrightness(step = 5) { - return setBrightnessDebounced(brightness + step) - } - - function decreaseBrightness(step = 5) { - return setBrightnessDebounced(brightness - step) - } - - function setDisplay(displayIndex) { - if (displayIndex >= 0 && displayIndex < detectedDisplays.length) { - _currentDisplay = detectedDisplays[displayIndex] - currentMethod = _currentDisplay.method - methodChanged(currentMethod) - updateBrightness() - return true - } - return false - } - - function getDisplayInfo() { - return _currentDisplay || null - } - - function getAvailableMethods() { - return _brightnessMethods - } - - function getDetectedDisplays() { + function getDetectedDisplays(): list { return detectedDisplays } - // Refresh brightness periodically - but less frequently - Timer { - interval: 5000 // Update every 5 seconds instead of 2 - running: available && _initialized - repeat: true - onTriggered: updateBrightness() + reloadableId: "brightness" + + onMonitorsChanged: { + ddcMonitors = [] + ddcProc.running = true } - // Debounce timer for UI updates - Timer { - id: debounceTimer - interval: 300 - repeat: false - onTriggered: updateBrightness() + Variants { + id: variants + model: Quickshell.screens + Monitor {} } - // Debounce timer for setting brightness - Timer { - id: setDebounceTimer - interval: 100 - repeat: false - onTriggered: setBrightness(_targetBrightness) + // Check for Apple Display support + Process { + running: true + command: ["sh", "-c", "which asdbctl >/dev/null 2>&1 && asdbctl get || echo ''"] + stdout: StdioCollector { + onStreamFinished: root.appleDisplayPresent = text.trim().length > 0 + } + } + + // Detect DDC monitors + Process { + id: ddcProc + command: ["ddcutil", "detect", "--brief"] + stdout: StdioCollector { + onStreamFinished: { + var displays = text.trim().split("\n\n").filter(d => d.startsWith("Display ")) + root.ddcMonitors = displays.map(d => { + var modelMatch = d.match(/Monitor:.*:(.*):.*/) + var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/) + return { + model: modelMatch ? modelMatch[1] : "", + busNum: busMatch ? busMatch[1] : "" + } + }) + } + } + } + + + + component Monitor: QtObject { + id: monitor + + required property ShellScreen modelData + readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model) + readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? "" + readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") + readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") + + property real brightness: getStoredBrightness() + property real queuedBrightness: NaN + + // Signal for brightness changes + signal brightnessUpdated(real newBrightness) + + // Initialize brightness + readonly property Process initProc: Process { + stdout: StdioCollector { + onStreamFinished: { + console.log("[BrightnessService] Raw brightness data for", monitor.modelData.name + ":", text.trim()) + + if (monitor.isAppleDisplay) { + var val = parseInt(text.trim()) + if (!isNaN(val)) { + monitor.brightness = val / 101 + console.log("[BrightnessService] Apple display brightness:", monitor.brightness) + } + } else if (monitor.isDdc) { + var parts = text.trim().split(" ") + if (parts.length >= 2) { + var current = parseInt(parts[0]) + var max = parseInt(parts[1]) + if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.brightness = current / max + console.log("[BrightnessService] DDC brightness:", current + "/" + max + " =", monitor.brightness) + } + } + } else { + // Internal backlight + var parts = text.trim().split(" ") + if (parts.length >= 2) { + var current = parseInt(parts[0]) + var max = parseInt(parts[1]) + if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.brightness = current / max + console.log("[BrightnessService] Internal brightness:", current + "/" + max + " =", monitor.brightness) + } + } + } + + if (monitor.brightness > 0) { + // Save the detected brightness to settings + monitor.saveBrightness(monitor.brightness) + monitor.brightnessUpdated(monitor.brightness) + } + } + } + } + + // Timer for debouncing rapid changes + readonly property Timer timer: Timer { + interval: 200 + onTriggered: { + if (!isNaN(monitor.queuedBrightness)) { + monitor.setBrightness(monitor.queuedBrightness) + monitor.queuedBrightness = NaN + } + } + } + + function getStoredBrightness(): real { + // Try to get stored brightness for this specific monitor + var stored = Settings.data.brightness.monitorBrightness.find(m => m.name === modelData.name) + if (stored) { + return stored.brightness / 100 + } + // Fallback to general last brightness + return Settings.data.brightness.lastBrightness / 100 + } + + function saveBrightness(value: real): void { + var brightnessPercent = Math.round(value * 100) + + // Update general last brightness + Settings.data.brightness.lastBrightness = brightnessPercent + Settings.data.brightness.lastMethod = method + + // Update monitor-specific brightness + var monitorIndex = Settings.data.brightness.monitorBrightness.findIndex(m => m.name === modelData.name) + var monitorData = { + name: modelData.name, + brightness: brightnessPercent, + method: method + } + + if (monitorIndex >= 0) { + Settings.data.brightness.monitorBrightness[monitorIndex] = monitorData + } else { + Settings.data.brightness.monitorBrightness.push(monitorData) + } + } + + function setBrightness(value: real): void { + value = Math.max(0, Math.min(1, value)) + var rounded = Math.round(value * 100) + + if (Math.round(brightness * 100) === rounded) return + + if (isDdc && timer.running) { + queuedBrightness = value + return + } + + brightness = value + brightnessUpdated(brightness) + + // Save to settings + saveBrightness(value) + + if (isAppleDisplay) { + Quickshell.execDetached(["asdbctl", "set", rounded]) + } else if (isDdc) { + Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded]) + } else { + Quickshell.execDetached(["brightnessctl", "s", rounded + "%"]) + } + + if (isDdc) { + timer.restart() + } + } + + function setBrightnessDebounced(value: real): void { + queuedBrightness = value + timer.restart() + } + + function initBrightness(): void { + if (isAppleDisplay) { + initProc.command = ["asdbctl", "get"] + } else if (isDdc) { + initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] + } else { + // Internal backlight - try to find the first available backlight device + initProc.command = ["sh", "-c", "for dev in /sys/class/backlight/*; do if [ -f \"$dev/brightness\" ] && [ -f \"$dev/max_brightness\" ]; then echo \"$(cat $dev/brightness) $(cat $dev/max_brightness)\"; break; fi; done"] + } + initProc.running = true + } + + onBusNumChanged: initBrightness() + Component.onCompleted: initBrightness() } } \ No newline at end of file diff --git a/Services/IPCManager.qml b/Services/IPCManager.qml index 07f195b..745c529 100644 --- a/Services/IPCManager.qml +++ b/Services/IPCManager.qml @@ -48,4 +48,16 @@ Item { lockScreen.locked = !lockScreen.locked } } + + IpcHandler { + target: "brightness" + + function increase() { + BrightnessService.increaseBrightness() + } + + function decrease() { + BrightnessService.decreaseBrightness() + } + } } diff --git a/Services/Settings.qml b/Services/Settings.qml index 5e2d4f4..b5168bd 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -179,6 +179,16 @@ Singleton { property string fontFamily: "Roboto" // Family for all text property list monitorsScale: [] } + + // brightness + property JsonObject brightness + + brightness: JsonObject { + property real lastBrightness: 50.0 + property string lastMethod: "internal" + property list monitorBrightness: [] + property int brightnessStep: 5 + } } } }