diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 4061155..7731481 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -6,233 +6,231 @@ import Quickshell.Io import qs.Commons Singleton { - id: root + id: root + property list ddcMonitors: [] + readonly property list monitors: variants.instances + property bool appleDisplayPresent: false + + 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 property list ddcMonitors: [] - readonly property list monitors: variants.instances - property bool appleDisplayPresent: false - // Blacklist DDC buses that error or hang - property var ddcBlacklist: [] + command: ["ddcutil", "detect", "--sleep-multiplier=0.5"] + 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") + - function getMonitorForScreen(screen: ShellScreen): var { - return monitors.find(m => m.modelData === screen); + ddcProc.ddcMonitors = displays.map(d => { + + var ddcModelMatc = d.match(/This monitor does not support DDC\/CI/) + var modelMatch = d.match(/Model:\s*(.*)/) + var busMatch = d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/) + var ddcModel = ddcModelMatc ? ddcModelMatc.length > 0 : false + var model = modelMatch ? modelMatch[1] : "Unknown" + var bus = busMatch ? busMatch[1] : "Unknown" + Logger.log( + "Detected DDC Monitor:", model, + "on bus", bus, "is DDC:", !ddcModel + ) + return { + "model": model, + "busNum": bus, + "isDdc": !ddcModel, + } + }) + root.ddcMonitors = ddcProc.ddcMonitors.filter(m => m.isDdc) + + + } + } + } + + 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 === "") { Logger.log("HERE", "TEXT: ", info[0], info[1]); + + 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 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; + // Timer for debouncing rapid changes + readonly property Timer timer: Timer { + interval: 200 + onTriggered: { + if (!isNaN(monitor.queuedBrightness)) { + monitor.setBrightness(monitor.queuedBrightness) + monitor.queuedBrightness = NaN + } + } } - // Global helpers for IPC and shortcuts function increaseBrightness(): void { - monitors.forEach(m => m.increaseBrightness()); + var stepSize = Settings.data.brightness.brightnessStep / 100.0 + setBrightnessDebounced(brightness + stepSize) } function decreaseBrightness(): void { - monitors.forEach(m => m.decreaseBrightness()); + var stepSize = Settings.data.brightness.brightnessStep / 100.0 + setBrightnessDebounced(monitor.brightness - stepSize) } - function getDetectedDisplays(): list { - return detectedDisplays; + 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() + } } - reloadableId: "brightness" - - Component.onCompleted: { - Logger.log("Brightness", "Service started"); + function setBrightnessDebounced(value: real): void { + queuedBrightness = value + timer.restart() } - onMonitorsChanged: { - ddcMonitors = []; - ddcProc.running = true; + 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 } - 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() - } + onBusNumChanged: initBrightness() + Component.onCompleted: initBrightness() + } }