diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 2295451..4061155 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -6,214 +6,233 @@ import Quickshell.Io import qs.Commons Singleton { - id: root + id: root - property list ddcMonitors: [] - readonly property list monitors: variants.instances - property bool appleDisplayPresent: false + property list ddcMonitors: [] + readonly property list monitors: variants.instances + property bool appleDisplayPresent: false + // Blacklist DDC buses that error or hang + property var ddcBlacklist: [] - function getMonitorForScreen(screen: ShellScreen): var { - return monitors.find(m => m.modelData === screen) - } - - 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 - } - - // Global helpers for IPC and shortcuts - function increaseBrightness(): void { - monitors.forEach(m => m.increaseBrightness()) - } - - function decreaseBrightness(): void { - monitors.forEach(m => m.decreaseBrightness()) - } - - function getDetectedDisplays(): list { - return detectedDisplays - } - - reloadableId: "brightness" - - Component.onCompleted: { - Logger.log("Brightness", "Service started") - } - - onMonitorsChanged: { - ddcMonitors = [] - ddcProc.running = true - } - - Variants { - id: variants - model: Quickshell.screens - Monitor {} - } - - // 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: { - // Do not filter out invalid displays. For some reason --brief returns some invalid which works fine - var displays = text.trim().split("\n\n") - 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 - property real lastBrightness: 0 - property real queuedBrightness: NaN - - // Signal for brightness changes - signal brightnessUpdated(real newBrightness) - - // Initialize brightness - readonly property Process initProc: Process { - stdout: StdioCollector { - onStreamFinished: { - var dataText = text.trim() - if (dataText === "") { - return - } - Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText) - - if (monitor.isAppleDisplay) { - var val = parseInt(dataText) - if (!isNaN(val)) { - monitor.brightness = val / 101 - Logger.log("Brightness", "Apple display brightness:", monitor.brightness) - } - } else if (monitor.isDdc) { - var parts = dataText.split(" ") - if (parts.length >= 4) { - var current = parseInt(parts[3]) - var max = parseInt(parts[4]) - if (!isNaN(current) && !isNaN(max) && max > 0) { - monitor.brightness = current / max - Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness) - } - } - } else { - // Internal backlight - var parts = dataText.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 - Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness) - } - } - } - - // Always update - monitor.brightnessUpdated(monitor.brightness) - } - } + function getMonitorForScreen(screen: ShellScreen): var { + return monitors.find(m => m.modelData === screen); } - // 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 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; } + // Global helpers for IPC and shortcuts function increaseBrightness(): void { - var stepSize = Settings.data.brightness.brightnessStep / 100.0 - setBrightnessDebounced(brightness + stepSize) + monitors.forEach(m => m.increaseBrightness()); } function decreaseBrightness(): void { - var stepSize = Settings.data.brightness.brightnessStep / 100.0 - setBrightnessDebounced(monitor.brightness - stepSize) + monitors.forEach(m => m.decreaseBrightness()); } - 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) - - 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 getDetectedDisplays(): list { + return detectedDisplays; } - function setBrightnessDebounced(value: real): void { - queuedBrightness = value - timer.restart() + reloadableId: "brightness" + + Component.onCompleted: { + Logger.log("Brightness", "Service started"); } - 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 + onMonitorsChanged: { + ddcMonitors = []; + ddcProc.running = true; } - onBusNumChanged: initBrightness() - Component.onCompleted: initBrightness() - } + Variants { + id: variants + model: Quickshell.screens + Monitor {} + } + + // 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 + // Add a timeout so detect can't hang the UI + command: ["sh", "-c", "timeout 3s ddcutil detect --brief || true"] + stdout: StdioCollector { + onStreamFinished: { + // Do not filter out invalid displays. For some reason --brief returns some invalid which works fine + var displays = text.trim().split("\n\n"); + 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 string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? "" + // Treat embedded panels as internal only + readonly property bool isInternalPanel: modelData.name.startsWith("eDP") || modelData.name.startsWith("LVDS") || modelData.name.startsWith("DSI") + // Only use DDC if not internal and not blacklisted + readonly property bool isDdc: busNum !== "" && !isInternalPanel && root.ddcBlacklist.indexOf(busNum) === -1 + readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") + readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") + + property real brightness + property real lastBrightness: 0 + property real queuedBrightness: NaN + + // Signal for brightness changes + signal brightnessUpdated(real newBrightness) + + // Initialize brightness + readonly property Process initProc: Process { + stdout: StdioCollector { + onStreamFinished: { + var dataText = text.trim(); + if (dataText === "") { + return; + } + + // If DDC responded with an error, blacklist this bus and fall back to internal + if (monitor.isDdc && dataText.indexOf("ERR") !== -1) { + if (root.ddcBlacklist.indexOf(monitor.busNum) === -1) { + Logger.warn("Brightness", "Blacklisting DDC bus", monitor.busNum); + root.ddcBlacklist = root.ddcBlacklist.concat([monitor.busNum]); + } + // Re-init using the new method (will now be 'internal') + monitor.initBrightness(); + return; + } + + Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText); + + if (monitor.isAppleDisplay) { + var val = parseInt(dataText); + if (!isNaN(val)) { + monitor.brightness = val / 101; + Logger.log("Brightness", "Apple display brightness:", monitor.brightness); + } + } else if (monitor.isDdc) { + var parts = dataText.split(" "); + if (parts.length >= 4) { + var current = parseInt(parts[3]); + var max = parseInt(parts[4]); + if (!isNaN(current) && !isNaN(max) && max > 0) { + monitor.brightness = current / max; + Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness); + } + } + } else { + // Internal backlight + var parts = dataText.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; + Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness); + } + } + } + + // Always update + 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 increaseBrightness(): void { + var stepSize = Settings.data.brightness.brightnessStep / 100.0; + setBrightnessDebounced(brightness + stepSize); + } + + function decreaseBrightness(): void { + var stepSize = Settings.data.brightness.brightnessStep / 100.0; + setBrightnessDebounced(monitor.brightness - stepSize); + } + + 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); + + if (isAppleDisplay) { + Quickshell.execDetached(["asdbctl", "set", rounded]); + } else if (isDdc) { + // Add timeout so ddcutil can't hang + Quickshell.execDetached(["sh", "-c", "timeout 1s ddcutil -b " + busNum + " setvcp 10 " + rounded + " >/dev/null 2>&1 || true"]); + } 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) { + // Add timeout and a fallback ERR marker to trigger blacklist + initProc.command = ["sh", "-c", "timeout 1s ddcutil -b " + busNum + " getvcp 10 --brief || echo 'VCP 10 ERR'"]; + } 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() + } }