fix freezing because of ddcutil

This commit is contained in:
Oleksiy Nedobiychuk 2025-08-31 03:16:42 +02:00
parent 5dedf5c1b5
commit 1c323675d1

View file

@ -11,46 +11,44 @@ Singleton {
property list<var> ddcMonitors: [] property list<var> ddcMonitors: []
readonly property list<Monitor> monitors: variants.instances readonly property list<Monitor> monitors: variants.instances
property bool appleDisplayPresent: false property bool appleDisplayPresent: false
// Blacklist DDC buses that error or hang
property var ddcBlacklist: []
function getMonitorForScreen(screen: ShellScreen): var { function getMonitorForScreen(screen: ShellScreen): var {
return monitors.find(m => m.modelData === screen); return monitors.find(m => m.modelData === screen)
} }
function getAvailableMethods(): list<string> { function getAvailableMethods(): list<string> {
var methods = []; var methods = []
if (monitors.some(m => m.isDdc)) if (monitors.some(m => m.isDdc))
methods.push("ddcutil"); methods.push("ddcutil")
if (monitors.some(m => !m.isDdc)) if (monitors.some(m => !m.isDdc))
methods.push("internal"); methods.push("internal")
if (appleDisplayPresent) if (appleDisplayPresent)
methods.push("apple"); methods.push("apple")
return methods; return methods
} }
// Global helpers for IPC and shortcuts // Global helpers for IPC and shortcuts
function increaseBrightness(): void { function increaseBrightness(): void {
monitors.forEach(m => m.increaseBrightness()); monitors.forEach(m => m.increaseBrightness())
} }
function decreaseBrightness(): void { function decreaseBrightness(): void {
monitors.forEach(m => m.decreaseBrightness()); monitors.forEach(m => m.decreaseBrightness())
} }
function getDetectedDisplays(): list<var> { function getDetectedDisplays(): list<var> {
return detectedDisplays; return detectedDisplays
} }
reloadableId: "brightness" reloadableId: "brightness"
Component.onCompleted: { Component.onCompleted: {
Logger.log("Brightness", "Service started"); Logger.log("Brightness", "Service started")
} }
onMonitorsChanged: { onMonitorsChanged: {
ddcMonitors = []; ddcMonitors = []
ddcProc.running = true; ddcProc.running = true
} }
Variants { Variants {
@ -71,20 +69,35 @@ Singleton {
// Detect DDC monitors // Detect DDC monitors
Process { Process {
id: ddcProc id: ddcProc
// Add a timeout so detect can't hang the UI property list<var> ddcMonitors: []
command: ["sh", "-c", "timeout 3s ddcutil detect --brief || true"] command: ["ddcutil", "detect", "--sleep-multiplier=0.5"]
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
// Do not filter out invalid displays. For some reason --brief returns some invalid which works fine // Do not filter out invalid displays. For some reason --brief returns some invalid which works fine
var displays = text.trim().split("\n\n"); 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]+)/); 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 { return {
"model": modelMatch ? modelMatch[1] : "", "model": model,
"busNum": busMatch ? busMatch[1] : "" "busNum": bus,
}; "isDdc": !ddcModel,
}); }
})
root.ddcMonitors = ddcProc.ddcMonitors.filter(m => m.isDdc)
} }
} }
} }
@ -93,11 +106,8 @@ Singleton {
id: monitor id: monitor
required property ShellScreen modelData 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 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 bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal") readonly property string method: isAppleDisplay ? "apple" : (isDdc ? "ddcutil" : "internal")
@ -112,55 +122,44 @@ Singleton {
readonly property Process initProc: Process { readonly property Process initProc: Process {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
var dataText = text.trim(); var dataText = text.trim()
if (dataText === "") { if (dataText === "") { Logger.log("HERE", "TEXT: ", info[0], info[1]);
return;
}
// If DDC responded with an error, blacklist this bus and fall back to internal return
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') Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText)
monitor.initBrightness();
return;
}
Logger.log("Brightness", "Raw brightness data for", monitor.modelData.name + ":", dataText);
if (monitor.isAppleDisplay) { if (monitor.isAppleDisplay) {
var val = parseInt(dataText); var val = parseInt(dataText)
if (!isNaN(val)) { if (!isNaN(val)) {
monitor.brightness = val / 101; monitor.brightness = val / 101
Logger.log("Brightness", "Apple display brightness:", monitor.brightness); Logger.log("Brightness", "Apple display brightness:", monitor.brightness)
} }
} else if (monitor.isDdc) { } else if (monitor.isDdc) {
var parts = dataText.split(" "); var parts = dataText.split(" ")
if (parts.length >= 4) { if (parts.length >= 4) {
var current = parseInt(parts[3]); var current = parseInt(parts[3])
var max = parseInt(parts[4]); var max = parseInt(parts[4])
if (!isNaN(current) && !isNaN(max) && max > 0) { if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.brightness = current / max; monitor.brightness = current / max
Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness); Logger.log("Brightness", "DDC brightness:", current + "/" + max + " =", monitor.brightness)
} }
} }
} else { } else {
// Internal backlight // Internal backlight
var parts = dataText.split(" "); var parts = dataText.split(" ")
if (parts.length >= 2) { if (parts.length >= 2) {
var current = parseInt(parts[0]); var current = parseInt(parts[0])
var max = parseInt(parts[1]); var max = parseInt(parts[1])
if (!isNaN(current) && !isNaN(max) && max > 0) { if (!isNaN(current) && !isNaN(max) && max > 0) {
monitor.brightness = current / max; monitor.brightness = current / max
Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness); Logger.log("Brightness", "Internal brightness:", current + "/" + max + " =", monitor.brightness)
} }
} }
} }
// Always update // Always update
monitor.brightnessUpdated(monitor.brightness); monitor.brightnessUpdated(monitor.brightness)
} }
} }
} }
@ -170,66 +169,65 @@ Singleton {
interval: 200 interval: 200
onTriggered: { onTriggered: {
if (!isNaN(monitor.queuedBrightness)) { if (!isNaN(monitor.queuedBrightness)) {
monitor.setBrightness(monitor.queuedBrightness); monitor.setBrightness(monitor.queuedBrightness)
monitor.queuedBrightness = NaN; monitor.queuedBrightness = NaN
} }
} }
} }
function increaseBrightness(): void { function increaseBrightness(): void {
var stepSize = Settings.data.brightness.brightnessStep / 100.0; var stepSize = Settings.data.brightness.brightnessStep / 100.0
setBrightnessDebounced(brightness + stepSize); setBrightnessDebounced(brightness + stepSize)
} }
function decreaseBrightness(): void { function decreaseBrightness(): void {
var stepSize = Settings.data.brightness.brightnessStep / 100.0; var stepSize = Settings.data.brightness.brightnessStep / 100.0
setBrightnessDebounced(monitor.brightness - stepSize); setBrightnessDebounced(monitor.brightness - stepSize)
} }
function setBrightness(value: real): void { function setBrightness(value: real): void {
value = Math.max(0, Math.min(1, value)); value = Math.max(0, Math.min(1, value))
var rounded = Math.round(value * 100); var rounded = Math.round(value * 100)
if (Math.round(brightness * 100) === rounded) if (Math.round(brightness * 100) === rounded)
return; return
if (isDdc && timer.running) { if (isDdc && timer.running) {
queuedBrightness = value; queuedBrightness = value
return; return
} }
brightness = value; brightness = value
brightnessUpdated(brightness); brightnessUpdated(brightness)
if (isAppleDisplay) { if (isAppleDisplay) {
Quickshell.execDetached(["asdbctl", "set", rounded]); Quickshell.execDetached(["asdbctl", "set", rounded])
} else if (isDdc) { } else if (isDdc) {
// Add timeout so ddcutil can't hang Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded])
Quickshell.execDetached(["sh", "-c", "timeout 1s ddcutil -b " + busNum + " setvcp 10 " + rounded + " >/dev/null 2>&1 || true"]);
} else { } else {
Quickshell.execDetached(["brightnessctl", "s", rounded + "%"]); Quickshell.execDetached(["brightnessctl", "s", rounded + "%"])
} }
if (isDdc) { if (isDdc) {
timer.restart(); timer.restart()
} }
} }
function setBrightnessDebounced(value: real): void { function setBrightnessDebounced(value: real): void {
queuedBrightness = value; queuedBrightness = value
timer.restart(); timer.restart()
} }
function initBrightness(): void { function initBrightness(): void {
if (isAppleDisplay) { if (isAppleDisplay) {
initProc.command = ["asdbctl", "get"]; initProc.command = ["asdbctl", "get"]
} else if (isDdc) { } else if (isDdc) {
// Add timeout and a fallback ERR marker to trigger blacklist initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]
initProc.command = ["sh", "-c", "timeout 1s ddcutil -b " + busNum + " getvcp 10 --brief || echo 'VCP 10 ERR'"];
} else { } else {
// Internal backlight - try to find the first available backlight device // 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.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; initProc.running = true
} }
onBusNumChanged: initBrightness() onBusNumChanged: initBrightness()