From 5dedf5c1b5c293be318e310cbff24384d62cc22d Mon Sep 17 00:00:00 2001 From: Oleksiy Nedobiychuk Date: Fri, 29 Aug 2025 00:43:52 +0200 Subject: [PATCH 1/6] brightness: avoid DDC on internal panels, add timeouts, auto-blacklist bad DDC buses Signed-off-by: Oleksiy Nedobiychuk --- Services/BrightnessService.qml | 403 +++++++++++++++++---------------- 1 file changed, 211 insertions(+), 192 deletions(-) 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() + } } From 1c323675d121f633da478d22e4a2b97501846752 Mon Sep 17 00:00:00 2001 From: Oleksiy Nedobiychuk Date: Sun, 31 Aug 2025 03:16:42 +0200 Subject: [PATCH 2/6] fix freezing because of ddcutil --- Services/BrightnessService.qml | 418 ++++++++++++++++----------------- 1 file changed, 208 insertions(+), 210 deletions(-) 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() + } } From 821c262a93986407073816a11348e3f42dad816a Mon Sep 17 00:00:00 2001 From: Oleksiy Nedobiychuk Date: Sun, 31 Aug 2025 03:28:18 +0200 Subject: [PATCH 3/6] remove an extra logger --- Services/BrightnessService.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 7731481..c66c1f4 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -123,8 +123,7 @@ Singleton { stdout: StdioCollector { onStreamFinished: { var dataText = text.trim() - if (dataText === "") { Logger.log("HERE", "TEXT: ", info[0], info[1]); - + if (dataText === "") { return } Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText) From ef86570b246e2e8bf41dcaceea83417a67e1e61e Mon Sep 17 00:00:00 2001 From: Oleksiy Date: Sun, 31 Aug 2025 01:36:03 +0000 Subject: [PATCH 4/6] removed an extra logger call --- Services/BrightnessService.qml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 7731481..c66c1f4 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -123,8 +123,7 @@ Singleton { stdout: StdioCollector { onStreamFinished: { var dataText = text.trim() - if (dataText === "") { Logger.log("HERE", "TEXT: ", info[0], info[1]); - + if (dataText === "") { return } Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText) From 3151b1634ca311508a4b18cb8bb16a2dde8eb320 Mon Sep 17 00:00:00 2001 From: Oleksiy Nedobiychuk Date: Sun, 31 Aug 2025 20:51:04 +0200 Subject: [PATCH 5/6] fix to issue #165 --- .../BluetoothPanel/BluetoothDevicesList.qml | 72 +++++++++++++++---- Modules/BluetoothPanel/BluetoothPanel.qml | 38 ++++++---- Services/BluetoothService.qml | 19 ++++- 3 files changed, 99 insertions(+), 30 deletions(-) diff --git a/Modules/BluetoothPanel/BluetoothDevicesList.qml b/Modules/BluetoothPanel/BluetoothDevicesList.qml index 9d4f981..a3768fe 100644 --- a/Modules/BluetoothPanel/BluetoothDevicesList.qml +++ b/Modules/BluetoothPanel/BluetoothDevicesList.qml @@ -12,6 +12,7 @@ ColumnLayout { id: root property string label: "" + property string tooltipText: "" property var model: { } @@ -29,12 +30,15 @@ ColumnLayout { } Repeater { + id: deviceList Layout.fillWidth: true model: root.model visible: BluetoothService.adapter && BluetoothService.adapter.enabled Rectangle { + id: bluetoothDeviceRectangle property bool canConnect: BluetoothService.canConnect(modelData) + property bool canDisconnect: BluetoothService.canDisconnect(modelData) property bool isBusy: BluetoothService.isDeviceBusy(modelData) Layout.fillWidth: true @@ -42,13 +46,19 @@ ColumnLayout { radius: Style.radiusM * scaling color: { - if (availableDeviceArea.containsMouse && !isBusy) - return Color.mTertiary + if (availableDeviceArea.containsMouse){ + if (canDisconnect && !isBusy) + return Color.mError + + if(!isBusy) + return Color.mTertiary + return Color.mPrimary + } if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting) return Color.mPrimary - if (modelData.blocked) + if (modelData.blocked ) return Color.mError return Color.mSurfaceVariant @@ -56,6 +66,13 @@ ColumnLayout { border.color: Color.mOutline border.width: Math.max(1, Style.borderS * scaling) + NTooltip { + id: tooltip + target: bluetoothDeviceRectangle + positionAbove: Settings.data.bar.position === "bottom" + text: root.tooltipText + } + RowLayout { anchors.fill: parent anchors.margins: Style.marginM * scaling @@ -193,6 +210,7 @@ ColumnLayout { Layout.fillWidth: true } + // Call to action Rectangle { Layout.preferredWidth: 80 * scaling @@ -204,12 +222,14 @@ ColumnLayout { border.color: { if (availableDeviceArea.containsMouse) { return Color.mOnTertiary - } else { - return Color.mPrimary } + if (bluetoothDeviceRectangle.canDisconnect && !isBusy) { + return Color.mError + } + return Color.mPrimary } border.width: Math.max(1, Style.borderS * scaling) - opacity: canConnect || isBusy ? 1 : 0.5 + opacity: canConnect || isBusy || canDisconnect ? 1 : 0.5 NText { anchors.centerIn: parent @@ -220,7 +240,7 @@ ColumnLayout { if (modelData.blocked) { return "Blocked" } - if (modelData.paired || modelData.trusted) { + if(modelData.connected){ return "Disconnect" } return "Connect" @@ -228,8 +248,13 @@ ColumnLayout { font.pointSize: Style.fontSizeXS * scaling font.weight: Style.fontWeightMedium color: { + if (availableDeviceArea.containsMouse) { return Color.mOnTertiary + } + + if (bluetoothDeviceRectangle.canDisconnect && !isBusy) { + return Color.mError } else { return Color.mPrimary } @@ -240,20 +265,39 @@ ColumnLayout { MouseArea { id: availableDeviceArea - + acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent hoverEnabled: true - cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) - enabled: canConnect && !isBusy + cursorShape: (canConnect || canDisconnect) && !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor) + onEntered: { + if (root.tooltipText && !isBusy) { + tooltip.show() + } + } + onExited: { + if(root.tooltipText && !isBusy) { + tooltip.hide() + } + } onClicked: { + if (!modelData || modelData.pairing) { return } - if (modelData.paired || modelData.trusted) { - BluetoothService.disconnectDevice(modelData) - } else { - BluetoothService.connectDeviceWithTrust(modelData) + if (root.tooltipText && !isBusy) { + tooltip.hide() + } + + + if (mouse.button === Qt.LeftButton){ + if (modelData.connected) { + BluetoothService.disconnectDevice(modelData) + } else { + BluetoothService.connectDeviceWithTrust(modelData) + } + } else if (mouse.button === Qt.RightButton) { + BluetoothService.forgetDevice(modelData) } } } diff --git a/Modules/BluetoothPanel/BluetoothPanel.qml b/Modules/BluetoothPanel/BluetoothPanel.qml index 77ed79c..8dfcf47 100644 --- a/Modules/BluetoothPanel/BluetoothPanel.qml +++ b/Modules/BluetoothPanel/BluetoothPanel.qml @@ -83,30 +83,40 @@ NPanel { // Connected devices BluetoothDevicesList { label: "Connected devices" - model: { - if (!BluetoothService.adapter || !Bluetooth.devices) - return [] - - var filtered = Bluetooth.devices.values.filter(dev => { - return dev && !dev.blocked && (dev.paired || dev.trusted) - }) + property var items: { + if (!BluetoothService.adapter || !Bluetooth.devices) return [] + var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && dev.connected) return BluetoothService.sortDevices(filtered) } + model: items + visible: items.length > 0 + Layout.fillWidth: true + } + + // Known devices + BluetoothDevicesList { + label: "Known devices" + tooltipText: "Left click to connect, right click to forget" + property var items: { + if (!BluetoothService.adapter || !Bluetooth.devices) return [] + var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted)) + return BluetoothService.sortDevices(filtered) + } + model: items + visible: items.length > 0 Layout.fillWidth: true } // Available devices BluetoothDevicesList { label: "Available devices" - model: { - if (!BluetoothService.adapter || !Bluetooth.devices) - return [] - - var filtered = Bluetooth.devices.values.filter(dev => { - return dev && !dev.blocked && !dev.paired && !dev.trusted - }) + property var items: { + if (!BluetoothService.adapter || !Bluetooth.devices) return [] + var filtered = Bluetooth.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted) return BluetoothService.sortDevices(filtered) } + model: items + visible: items.length > 0 Layout.fillWidth: true } diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index 2e88f5b..98c8053 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -3,6 +3,7 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Bluetooth +import qs.Commons Singleton { id: root @@ -85,8 +86,23 @@ Singleton { function canConnect(device) { if (!device) return false + /* + Paired - return !device.paired && !device.pairing && !device.blocked + Means you’ve successfully exchanged keys with the device. + + The devices remember each other and can authenticate without repeating the pairing process. + + Example: once your headphones are paired, you don’t need to type a PIN every time. + Hence, instead of !device.paired, should be device.connected + */ + return !device.connected && !device.pairing && !device.blocked + } + + function canDisconnect(device) { + if (!device) + return false + return device.connected && !device.pairing && !device.blocked } function getSignalStrength(device) { @@ -162,7 +178,6 @@ Singleton { return } - device.trusted = false device.disconnect() } From 3891c7008a59a043129bbe880b50498dd4d2d5dd Mon Sep 17 00:00:00 2001 From: Oleksiy Nedobiychuk Date: Sun, 31 Aug 2025 21:00:15 +0200 Subject: [PATCH 6/6] fix(bluetooth): enable disconnect/remove for paired devices #165 --- Modules/BluetoothPanel/BluetoothDevicesList.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/BluetoothPanel/BluetoothDevicesList.qml b/Modules/BluetoothPanel/BluetoothDevicesList.qml index a3768fe..d234393 100644 --- a/Modules/BluetoothPanel/BluetoothDevicesList.qml +++ b/Modules/BluetoothPanel/BluetoothDevicesList.qml @@ -264,6 +264,7 @@ ColumnLayout { } MouseArea { + id: availableDeviceArea acceptedButtons: Qt.LeftButton | Qt.RightButton anchors.fill: parent @@ -279,7 +280,7 @@ ColumnLayout { tooltip.hide() } } - onClicked: { + onClicked: function(mouse) { if (!modelData || modelData.pairing) { return