mirror of
https://github.com/zoriya/noctalia-shell.git
synced 2025-12-06 06:36:15 +00:00
fix freezing because of ddcutil
This commit is contained in:
@@ -6,233 +6,231 @@ import Quickshell.Io
|
||||
import qs.Commons
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
id: root
|
||||
|
||||
property list<var> ddcMonitors: []
|
||||
readonly property list<Monitor> monitors: variants.instances
|
||||
property bool appleDisplayPresent: false
|
||||
|
||||
function getMonitorForScreen(screen: ShellScreen): var {
|
||||
return monitors.find(m => m.modelData === screen)
|
||||
}
|
||||
|
||||
function getAvailableMethods(): list<string> {
|
||||
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<var> {
|
||||
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<var> ddcMonitors: []
|
||||
readonly property list<Monitor> 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<string> {
|
||||
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<var> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user