From 13ef60775806a79c210054b3a9d4268a72364355 Mon Sep 17 00:00:00 2001 From: bbedward Date: Tue, 15 Jul 2025 16:22:06 -0400 Subject: [PATCH] display: use ddcutil for brightness changing --- README.md | 3 +- Services/BrightnessService.qml | 227 +++++++++++++++++---------- Services/ProcessMonitorService.qml | 15 +- Widgets/ControlCenter/DisplayTab.qml | 2 +- 4 files changed, 158 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index 037296c5..0b8738d0 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,13 @@ Specifically created for [Niri](https://github.com/YaLTeR/niri). ```bash # Arch -paru -S quickshell-git nerd-fonts ttf-material-symbols-variable-git matugen cliphist cava wl-clipboard +paru -S quickshell-git nerd-fonts ttf-material-symbols-variable-git matugen cliphist cava wl-clipboard ddcutil # Some dependencies are optional # - cava for audio visualizer, without it music will just randomly visualize # - cliphist for clipboard history # - matugen for dynamic themes based on wallpaper +# - ddcutil for brightness changing ``` 2. Configure SwayBG (Optional) diff --git a/Services/BrightnessService.qml b/Services/BrightnessService.qml index 0b4f3482..673dd2da 100644 --- a/Services/BrightnessService.qml +++ b/Services/BrightnessService.qml @@ -7,101 +7,160 @@ pragma ComponentBehavior: Bound Singleton { id: root - property int brightnessLevel: 75 + property list ddcMonitors: [] + readonly property list monitors: variants.instances property bool brightnessAvailable: false + property int brightnessLevel: 75 - // Check if brightness control is available - Process { - id: brightnessAvailabilityChecker - command: ["bash", "-c", "if command -v brightnessctl > /dev/null; then echo 'brightnessctl'; elif command -v xbacklight > /dev/null; then echo 'xbacklight'; else echo 'none'; fi"] - running: true - - stdout: SplitParser { - splitMarker: "\n" - onRead: (data) => { - if (data.trim()) { - let method = data.trim() - if (method === "brightnessctl" || method === "xbacklight") { - root.brightnessAvailable = true - brightnessChecker.running = true - } else { - root.brightnessAvailable = false - console.log("Brightness control not available - no brightnessctl or xbacklight found") - } - } - } - } + function getMonitorForScreen(screen: ShellScreen): var { + return monitors.find(function(m) { return m.modelData === screen; }); } - // Brightness Control - Process { - id: brightnessChecker - command: ["bash", "-c", "if command -v brightnessctl > /dev/null; then brightnessctl get; elif command -v xbacklight > /dev/null; then xbacklight -get | cut -d. -f1; else echo 75; fi"] - running: false - - stdout: SplitParser { - splitMarker: "\n" - onRead: (data) => { - if (data.trim()) { - let brightness = parseInt(data.trim()) || 75 - // brightnessctl returns absolute value, need to convert to percentage - if (brightness > 100) { - brightnessMaxChecker.running = true - } else { - root.brightnessLevel = brightness - } - } - } - } - } - - Process { - id: brightnessMaxChecker - command: ["brightnessctl", "max"] - running: false - - stdout: SplitParser { - splitMarker: "\n" - onRead: (data) => { - if (data.trim()) { - let maxBrightness = parseInt(data.trim()) || 100 - brightnessCurrentChecker.property("maxBrightness", maxBrightness) - brightnessCurrentChecker.running = true - } - } - } - } - - Process { - id: brightnessCurrentChecker - property int maxBrightness: 100 - command: ["brightnessctl", "get"] - running: false - - stdout: SplitParser { - splitMarker: "\n" - onRead: (data) => { - if (data.trim()) { - let currentBrightness = parseInt(data.trim()) || 75 - root.brightnessLevel = Math.round((currentBrightness / maxBrightness) * 100) - } + property var debounceTimer: Timer { + id: debounceTimer + interval: 50 + repeat: false + property int pendingValue: 0 + onTriggered: { + const focusedMonitor = monitors.find(function(m) { return m.modelData === Quickshell.screens[0]; }); + if (focusedMonitor) { + focusedMonitor.setBrightness(pendingValue / 100); } } } function setBrightness(percentage) { - if (!root.brightnessAvailable) { - console.warn("Brightness control not available") - return + root.brightnessLevel = percentage; + debounceTimer.pendingValue = percentage; + debounceTimer.restart(); + } + + function increaseBrightness(): void { + const focusedMonitor = monitors.find(function(m) { return m.modelData === Quickshell.screens[0]; }); + if (focusedMonitor) + focusedMonitor.setBrightness(focusedMonitor.brightness + 0.1); + } + + function decreaseBrightness(): void { + const focusedMonitor = monitors.find(function(m) { return m.modelData === Quickshell.screens[0]; }); + if (focusedMonitor) + focusedMonitor.setBrightness(focusedMonitor.brightness - 0.1); + } + + onMonitorsChanged: { + ddcMonitors = []; + if (ddcAvailable) { + ddcProc.running = true; } - let brightnessSetProcess = Qt.createQmlObject(' - import Quickshell.Io - Process { - command: ["bash", "-c", "if command -v brightnessctl > /dev/null; then brightnessctl set ' + percentage + '%; elif command -v xbacklight > /dev/null; then xbacklight -set ' + percentage + '; fi"] - running: true - onExited: brightnessChecker.running = true + // Update brightness level from first monitor + if (monitors.length > 0) { + root.brightnessLevel = Math.round(monitors[0].brightness * 100); + } + } + + Component.onCompleted: { + ddcAvailabilityChecker.running = true; + } + + Variants { + id: variants + model: Quickshell.screens + Monitor {} + } + + Process { + id: ddcAvailabilityChecker + command: ["which", "ddcutil"] + onExited: function(exitCode) { + root.brightnessAvailable = (exitCode === 0); + if (root.brightnessAvailable) { + ddcProc.running = true; } - ', root) + } + } + + Process { + id: ddcProc + command: ["ddcutil", "detect", "--brief"] + running: false + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + root.ddcMonitors = text.trim().split("\n\n").filter(function(d) { return d.startsWith("Display "); }).map(function(d) { return ({ + model: d.match(/Monitor:.*:(.*):.*/)?.[1] || "Unknown", + busNum: d.match(/I2C bus:[ ]*\/dev\/i2c-([0-9]+)/)?.[1] || "0" + }); }); + } else { + root.ddcMonitors = []; + } + } + } + onExited: function(exitCode) { + if (exitCode !== 0) { + root.ddcMonitors = []; + } + } + } + + component Monitor: QtObject { + id: monitor + + required property ShellScreen modelData + readonly property bool isDdc: root.ddcMonitors.some(function(m) { return m.model === modelData.model; }) + readonly property string busNum: root.ddcMonitors.find(function(m) { return m.model === modelData.model; })?.busNum ?? "" + property real brightness: 0.75 + + readonly property Process initProc: Process { + stdout: StdioCollector { + onStreamFinished: { + if (text.trim()) { + const parts = text.trim().split(" "); + if (parts.length >= 5) { + const current = parseInt(parts[3]) || 75; + const max = parseInt(parts[4]) || 100; + monitor.brightness = current / max; + root.brightnessLevel = Math.round(monitor.brightness * 100); + } + } + } + } + onExited: function(exitCode) { + if (exitCode !== 0) { + monitor.brightness = 0.75; + root.brightnessLevel = 75; + } + } + } + + function setBrightness(value: real): void { + value = Math.max(0, Math.min(1, value)); + const rounded = Math.round(value * 100); + if (Math.round(brightness * 100) === rounded) + return; + + brightness = value; + root.brightnessLevel = rounded; + + if (isDdc && busNum) { + Quickshell.execDetached(["ddcutil", "-b", busNum, "setvcp", "10", rounded.toString()]); + } + } + + onBusNumChanged: { + if (isDdc && busNum) { + initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]; + initProc.running = true; + } + } + + Component.onCompleted: { + Qt.callLater(function() { + if (isDdc && busNum) { + initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"]; + initProc.running = true; + } + }); + } } } \ No newline at end of file diff --git a/Services/ProcessMonitorService.qml b/Services/ProcessMonitorService.qml index 71010cfe..161b8b68 100644 --- a/Services/ProcessMonitorService.qml +++ b/Services/ProcessMonitorService.qml @@ -318,6 +318,9 @@ Singleton { const lines = text.split('\n') let section = 'memory' const coreUsages = [] + let memFree = 0 + let memBuffers = 0 + let memCached = 0 for (let i = 0; i < lines.length; i++) { const line = lines[i].trim() @@ -333,9 +336,12 @@ Singleton { if (section === 'memory') { if (line.startsWith('MemTotal:')) { root.totalMemoryKB = parseInt(line.split(/\s+/)[1]) - } else if (line.startsWith('MemAvailable:')) { - const availableKB = parseInt(line.split(/\s+/)[1]) - root.usedMemoryKB = root.totalMemoryKB - availableKB + } else if (line.startsWith('MemFree:')) { + memFree = parseInt(line.split(/\s+/)[1]) + } else if (line.startsWith('Buffers:')) { + memBuffers = parseInt(line.split(/\s+/)[1]) + } else if (line.startsWith('Cached:')) { + memCached = parseInt(line.split(/\s+/)[1]) } else if (line.startsWith('SwapTotal:')) { root.totalSwapKB = parseInt(line.split(/\s+/)[1]) } else if (line.startsWith('SwapFree:')) { @@ -383,6 +389,9 @@ Singleton { } } + // Calculate used memory as total minus free minus buffers minus cached + root.usedMemoryKB = root.totalMemoryKB - memFree - memBuffers - memCached + // Update per-core usage root.perCoreCpuUsage = coreUsages diff --git a/Widgets/ControlCenter/DisplayTab.qml b/Widgets/ControlCenter/DisplayTab.qml index 0705a6c6..4acb0418 100644 --- a/Widgets/ControlCenter/DisplayTab.qml +++ b/Widgets/ControlCenter/DisplayTab.qml @@ -36,7 +36,7 @@ ScrollView { rightIcon: "brightness_high" enabled: BrightnessService.brightnessAvailable - onSliderValueChanged: (newValue) => { + onSliderValueChanged: function(newValue) { BrightnessService.setBrightness(newValue) } }