Brightness implementation with IPC

This commit is contained in:
Ly-sec 2025-08-15 14:05:02 +02:00
parent 20814007be
commit 22df558e14
6 changed files with 335 additions and 392 deletions

View file

@ -12,6 +12,7 @@ Item {
// Used to avoid opening the pill on Quickshell startup
property bool firstBrightnessReceived: false
property real lastBrightness: -1
function getIcon() {
if (!BrightnessService.available) {
@ -25,18 +26,28 @@ Item {
// Connection used to open the pill when brightness changes
Connections {
target: Brightness
target: BrightnessService.focusedMonitor
function onBrightnessUpdated() {
// console.log("[Bar:Brightness] onBrightnessUpdated")
var currentBrightness = BrightnessService.brightness
// Ignore if this is the first time or if brightness hasn't actually changed
if (!firstBrightnessReceived) {
// Ignore the first brightness change
firstBrightnessReceived = true
} else {
lastBrightness = currentBrightness
return
}
// Only show pill if brightness actually changed (not just loaded from settings)
if (Math.abs(currentBrightness - lastBrightness) > 0.1) {
pill.show()
}
lastBrightness = currentBrightness
}
}
NPill {
id: pill
icon: getIcon()
@ -44,15 +55,15 @@ Item {
collapsedIconColor: Colors.mOnSurface
autoHide: true
text: Math.round(BrightnessService.brightness) + "%"
tooltipText: "Brightness: " + Math.round(BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod + "\nLeft click for advanced settings.\nScroll up/down to change BrightnessService."
tooltipText: "Brightness: " + Math.round(BrightnessService.brightness) + "%\nMethod: " + BrightnessService.currentMethod + "\nLeft click for advanced settings.\nScroll up/down to change brightness."
onWheel: function (angle) {
if (!BrightnessService.available) return
if (angle > 0) {
BrightnessService.increaseBrightness(1)
BrightnessService.increaseBrightness()
} else if (angle < 0) {
BrightnessService.decreaseBrightness(1)
BrightnessService.decreaseBrightness()
}
}
onClicked: {

View file

@ -294,7 +294,7 @@ NLoader {
icon: "brightness_low"
fontPointSize: Style.fontSizeLarge * scaling
onClicked: {
Brightness.decreaseBrightness(1)
Brightness.decreaseBrightness()
}
}
NSlider {
@ -311,7 +311,7 @@ NLoader {
icon: "brightness_high"
fontPointSize: Style.fontSizeLarge * scaling
onClicked: {
Brightness.increaseBrightness(1)
Brightness.increaseBrightness()
}
}
}

View file

@ -47,6 +47,55 @@ Item {
color: Colors.mOnSurface
}
// Brightness Section
ColumnLayout {
spacing: Style.marginSmall * scaling
Layout.fillWidth: true
Layout.topMargin: Style.marginSmall * scaling
NText {
text: "Brightness Step Size"
font.weight: Style.fontWeightBold
color: Colors.mOnSurface
}
NText {
text: "Adjust the step size for brightness changes (scroll wheel, ipc bind)"
font.pointSize: Style.fontSizeSmall * scaling
color: Colors.mOnSurface
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginMedium * scaling
NSlider {
Layout.fillWidth: true
from: 1
to: 50
value: Settings.data.brightness.brightnessStep
stepSize: 1
onMoved: {
Settings.data.brightness.brightnessStep = value
}
}
NText {
text: Settings.data.brightness.brightnessStep + "%"
Layout.alignment: Qt.AlignVCenter
color: Colors.mOnSurface
}
}
}
NDivider {
Layout.fillWidth: true
Layout.topMargin: Style.marginLarge * 2 * scaling
Layout.bottomMargin: Style.marginLarge * scaling
}
Repeater {
model: Quickshell.screens || []
delegate: Rectangle {

View file

@ -1,4 +1,5 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
@ -7,422 +8,282 @@ import Quickshell.Io
Singleton {
id: root
// Public properties
property real brightness: 0
property real maxBrightness: 100
property bool available: false
property string currentMethod: ""
property var detectedDisplays: []
property list<var> ddcMonitors: []
readonly property list<Monitor> monitors: variants.instances
property bool appleDisplayPresent: false
// Private properties
property var _brightnessMethods: []
property var _currentDisplay: null
property bool _initialized: false
property real _targetBrightness: 0
property bool _isSettingBrightness: false
// Public properties for backward compatibility
readonly property real brightness: focusedMonitor ? focusedMonitor.brightness * 100 : Settings.data.brightness.lastBrightness
readonly property bool available: focusedMonitor !== null
readonly property string currentMethod: focusedMonitor ? focusedMonitor.method : Settings.data.brightness.lastMethod
readonly property var detectedDisplays: monitors.map(m => ({
name: m.modelData.name,
type: m.isDdc ? "external" : "internal",
method: m.method,
index: m.busNum
}))
// Signal when brightness changes
signal brightnessUpdated(real newBrightness)
signal methodChanged(string newMethod)
// Initialize the service
Component.onCompleted: {
initializeBrightness()
// Get the currently focused monitor
readonly property Monitor focusedMonitor: {
if (monitors.length === 0) return null
// For now, return the first monitor. Could be enhanced to detect focused monitor
return monitors[0]
}
function initializeBrightness() {
if (_initialized) return
console.log("[Brightness] Initializing brightness service...")
// Start method detection
detectMethods()
_initialized = true
function getMonitorForScreen(screen: ShellScreen): var {
return monitors.find(m => m.modelData === screen)
}
function detectMethods() {
_brightnessMethods = []
// Check for brightnessctl
brightnessctlProcess.running = true
// Check for ddcutil
ddcutilProcess.running = true
// Internal backlight is always available if we can access /sys/class/backlight
backlightCheck.running = true
console.log("[Brightness] Starting method detection...")
}
function checkMethodsComplete() {
// Check if all method detection processes have finished
if (!brightnessctlProcess.running && !ddcutilProcess.running && !backlightCheck.running) {
console.log("[Brightness] Available methods:", _brightnessMethods)
// Now detect displays
detectDisplays()
function increaseBrightness(step = null): void {
if (focusedMonitor) {
var stepSize = step !== null ? step : Settings.data.brightness.brightnessStep
focusedMonitor.setBrightness(focusedMonitor.brightness + (stepSize / 100))
}
}
// Process objects for method detection
Process {
id: brightnessctlProcess
command: ["which", "brightnessctl"]
running: false
onExited: function(exitCode, exitStatus) {
if (exitCode === 0) {
_brightnessMethods.push("brightnessctl")
console.log("[Brightness] brightnessctl available")
}
checkMethodsComplete()
function decreaseBrightness(step = null): void {
if (focusedMonitor) {
var stepSize = step !== null ? step : Settings.data.brightness.brightnessStep
focusedMonitor.setBrightness(focusedMonitor.brightness - (stepSize / 100))
}
}
Process {
id: ddcutilProcess
command: ["which", "ddcutil"]
running: false
onExited: function(exitCode, exitStatus) {
if (exitCode === 0) {
_brightnessMethods.push("ddcutil")
console.log("[Brightness] ddcutil available")
}
checkMethodsComplete()
function setBrightness(newBrightness: real): void {
if (focusedMonitor) {
focusedMonitor.setBrightness(newBrightness / 100)
}
}
Process {
id: backlightCheck
command: ["test", "-d", "/sys/class/backlight"]
running: false
onExited: function(exitCode, exitStatus) {
if (exitCode === 0) {
_brightnessMethods.push("internal")
console.log("[Brightness] Internal backlight available")
}
checkMethodsComplete()
function setBrightnessDebounced(newBrightness: real): void {
if (focusedMonitor) {
focusedMonitor.setBrightnessDebounced(newBrightness / 100)
}
}
function detectDisplays() {
detectedDisplays = []
// Get internal displays
backlightProcess.running = true
// Get external displays via ddcutil
if (_brightnessMethods.indexOf("ddcutil") !== -1) {
ddcutilDetectProcess.running = true
} else {
// If no ddcutil, just check internal displays
checkDisplaysComplete()
}
console.log("[Brightness] Starting display detection...")
// Backward compatibility functions
function updateBrightness(): void {
// No longer needed with the new architecture
}
function checkDisplaysComplete() {
// Check if all display detection processes have finished
var internalFinished = !backlightProcess.running
var externalFinished = _brightnessMethods.indexOf("ddcutil") === -1 || !ddcutilDetectProcess.running
if (internalFinished && externalFinished) {
console.log("[Brightness] Detected displays:", detectedDisplays)
// Set current display to first available
if (detectedDisplays.length > 0) {
_currentDisplay = detectedDisplays[0]
currentMethod = _currentDisplay.method
available = true
console.log("[Brightness] Using display:", _currentDisplay.name, "method:", currentMethod)
// Start initial brightness update
updateBrightness()
} else {
console.warn("[Brightness] No displays detected")
}
}
}
// Process objects for display detection
Process {
id: backlightProcess
command: ["ls", "/sys/class/backlight"]
running: false
stdout: SplitParser {
onRead: function(line) {
var trimmedLine = line.replace(/^\s+|\s+$/g, "")
if (trimmedLine) {
detectedDisplays.push({
name: trimmedLine,
type: "internal",
method: "internal"
})
}
}
}
onExited: function(exitCode, exitStatus) {
checkDisplaysComplete()
}
}
Process {
id: ddcutilDetectProcess
command: ["ddcutil", "detect"]
running: false
stdout: SplitParser {
onRead: function(line) {
console.log("[Brightness] ddcutil detect line:", line)
if (line.indexOf("Display") !== -1) {
// Simple parsing for Display number
var parts = line.split("Display")
if (parts.length > 1) {
var numberPart = parts[1].replace(/^\s+|\s+$/g, "")
var number = numberPart.split(" ")[0]
if (number && !isNaN(number)) {
detectedDisplays.push({
name: "Display " + number,
type: "external",
method: "ddcutil",
index: number
})
console.log("[Brightness] Added external display:", "Display " + number)
}
}
}
// Also look for connector information
if (line.indexOf("DRM connector:") !== -1) {
console.log("[Brightness] Found DRM connector:", line)
}
}
}
onExited: function(exitCode, exitStatus) {
checkDisplaysComplete()
}
}
function updateBrightness() {
if (!_currentDisplay) return
// Prevent multiple simultaneous brightness checks
if (brightnessGetProcess.running) {
console.log("[Brightness] Brightness check already in progress, skipping...")
return
}
// Don't update if we're currently setting brightness
if (_isSettingBrightness) {
console.log("[Brightness] Skipping update while setting brightness...")
return
}
console.log("[Brightness] Updating brightness for display:", _currentDisplay.name)
// Try the brightness script first
if (_currentDisplay.method === "ddcutil" && _currentDisplay.index) {
// For ddcutil, try using the display index directly
brightnessGetProcess.command = ["ddcutil", "--display", _currentDisplay.index, "getvcp", "10"]
} else {
// Use the brightness script
brightnessGetProcess.command = ["sh", "-c", Quickshell.shellDir + "/Bin/brigthness.sh", "get", _currentDisplay.name]
}
brightnessGetProcess.running = true
}
function updateBrightnessDebounced() {
// Use debouncing to prevent excessive updates
debounceTimer.restart()
}
function setBrightness(newBrightness) {
if (!_currentDisplay || !available) {
console.warn("[Brightness] No display available for brightness control")
return false
}
// Clamp brightness to valid range
newBrightness = Math.max(0, Math.min(100, newBrightness))
// Prevent setting if already setting
if (brightnessSetProcess.running) {
console.log("[Brightness] Brightness set already in progress, skipping...")
return false
}
console.log("[Brightness] Setting brightness to:", newBrightness, "for display:", _currentDisplay.name)
// Mark that we're setting brightness
_isSettingBrightness = true
// Try ddcutil directly for external displays
if (_currentDisplay.method === "ddcutil" && _currentDisplay.index) {
brightnessSetProcess.command = ["ddcutil", "--display", _currentDisplay.index, "setvcp", "10", newBrightness.toString()]
} else {
// Use the brightness script for internal displays
brightnessSetProcess.command = ["sh", "-c", Quickshell.shellDir + "/Bin/brigthness.sh", "set", _currentDisplay.name, newBrightness.toString()]
}
brightnessSetProcess.running = true
function setDisplay(displayIndex: int): bool {
// No longer needed with the new architecture
return true
}
function setBrightnessDebounced(newBrightness) {
// Store the target brightness for debounced setting
_targetBrightness = newBrightness
// Update UI immediately for responsiveness
if (brightness !== newBrightness) {
brightness = newBrightness
brightnessUpdated(brightness)
}
setDebounceTimer.restart()
function getDisplayInfo(): var {
return focusedMonitor ? {
name: focusedMonitor.modelData.name,
type: focusedMonitor.isDdc ? "external" : "internal",
method: focusedMonitor.method,
index: focusedMonitor.busNum
} : null
}
// Process objects for brightness control
Process {
id: brightnessGetProcess
running: false
stdout: SplitParser {
onRead: function(line) {
var newBrightness = -1
// Handle ddcutil output format: "current value = X,"
if (line.indexOf("current value =") !== -1) {
var match = line.match(/current value\s*=\s*(\d+)/)
if (match) {
newBrightness = parseFloat(match[1])
}
} else {
// Handle direct numeric output
newBrightness = parseFloat(line.replace(/^\s+|\s+$/g, ""))
}
if (!isNaN(newBrightness) && newBrightness >= 0) {
if (brightness !== newBrightness) {
brightness = newBrightness
brightnessUpdated(brightness)
console.log("[Brightness] Brightness updated to:", brightness)
}
} else {
console.warn("[Brightness] Invalid brightness value:", line)
}
}
}
onExited: function(exitCode, exitStatus) {
// Only log errors
if (exitCode !== 0) {
console.warn("[Brightness] Brightness get process failed with code:", exitCode)
}
}
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
}
Process {
id: brightnessSetProcess
running: false
stdout: SplitParser {
onRead: function(line) {
var result = parseFloat(line.replace(/^\s+|\s+$/g, ""))
if (!isNaN(result) && result >= 0) {
brightness = result
brightnessUpdated(brightness)
console.log("[Brightness] Brightness set to:", brightness)
} else {
console.warn("[Brightness] Failed to set brightness - invalid output:", line)
}
}
}
onExited: function(exitCode, exitStatus) {
if (exitCode === 0) {
// If ddcutil succeeded but didn't output a number, refresh the brightness
if (_currentDisplay.method === "ddcutil") {
// Longer delay to let the display update and avoid conflicts
refreshTimer.interval = 1000
refreshTimer.start()
}
} else {
console.warn("[Brightness] Set brightness process failed with exit code:", exitCode)
}
// Clear the setting flag after a delay
settingCompleteTimer.start()
}
}
// Timer to clear the setting flag
Timer {
id: settingCompleteTimer
interval: 800
repeat: false
onTriggered: {
_isSettingBrightness = false
}
}
// Timer to refresh brightness after setting
Timer {
id: refreshTimer
interval: 500
repeat: false
onTriggered: updateBrightnessDebounced()
}
function increaseBrightness(step = 5) {
return setBrightnessDebounced(brightness + step)
}
function decreaseBrightness(step = 5) {
return setBrightnessDebounced(brightness - step)
}
function setDisplay(displayIndex) {
if (displayIndex >= 0 && displayIndex < detectedDisplays.length) {
_currentDisplay = detectedDisplays[displayIndex]
currentMethod = _currentDisplay.method
methodChanged(currentMethod)
updateBrightness()
return true
}
return false
}
function getDisplayInfo() {
return _currentDisplay || null
}
function getAvailableMethods() {
return _brightnessMethods
}
function getDetectedDisplays() {
function getDetectedDisplays(): list<var> {
return detectedDisplays
}
// Refresh brightness periodically - but less frequently
Timer {
interval: 5000 // Update every 5 seconds instead of 2
running: available && _initialized
repeat: true
onTriggered: updateBrightness()
reloadableId: "brightness"
onMonitorsChanged: {
ddcMonitors = []
ddcProc.running = true
}
// Debounce timer for UI updates
Timer {
id: debounceTimer
interval: 300
repeat: false
onTriggered: updateBrightness()
Variants {
id: variants
model: Quickshell.screens
Monitor {}
}
// Debounce timer for setting brightness
Timer {
id: setDebounceTimer
interval: 100
repeat: false
onTriggered: setBrightness(_targetBrightness)
// 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: {
var displays = text.trim().split("\n\n").filter(d => d.startsWith("Display "))
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: getStoredBrightness()
property real queuedBrightness: NaN
// Signal for brightness changes
signal brightnessUpdated(real newBrightness)
// Initialize brightness
readonly property Process initProc: Process {
stdout: StdioCollector {
onStreamFinished: {
console.log("[BrightnessService] Raw brightness data for", monitor.modelData.name + ":", text.trim())
if (monitor.isAppleDisplay) {
var val = parseInt(text.trim())
if (!isNaN(val)) {
monitor.brightness = val / 101
console.log("[BrightnessService] Apple display brightness:", monitor.brightness)
}
} else if (monitor.isDdc) {
var parts = text.trim().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
console.log("[BrightnessService] DDC brightness:", current + "/" + max + " =", monitor.brightness)
}
}
} else {
// Internal backlight
var parts = text.trim().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
console.log("[BrightnessService] Internal brightness:", current + "/" + max + " =", monitor.brightness)
}
}
}
if (monitor.brightness > 0) {
// Save the detected brightness to settings
monitor.saveBrightness(monitor.brightness)
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 getStoredBrightness(): real {
// Try to get stored brightness for this specific monitor
var stored = Settings.data.brightness.monitorBrightness.find(m => m.name === modelData.name)
if (stored) {
return stored.brightness / 100
}
// Fallback to general last brightness
return Settings.data.brightness.lastBrightness / 100
}
function saveBrightness(value: real): void {
var brightnessPercent = Math.round(value * 100)
// Update general last brightness
Settings.data.brightness.lastBrightness = brightnessPercent
Settings.data.brightness.lastMethod = method
// Update monitor-specific brightness
var monitorIndex = Settings.data.brightness.monitorBrightness.findIndex(m => m.name === modelData.name)
var monitorData = {
name: modelData.name,
brightness: brightnessPercent,
method: method
}
if (monitorIndex >= 0) {
Settings.data.brightness.monitorBrightness[monitorIndex] = monitorData
} else {
Settings.data.brightness.monitorBrightness.push(monitorData)
}
}
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)
// Save to settings
saveBrightness(value)
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 setBrightnessDebounced(value: real): void {
queuedBrightness = value
timer.restart()
}
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
}
onBusNumChanged: initBrightness()
Component.onCompleted: initBrightness()
}
}

View file

@ -48,4 +48,16 @@ Item {
lockScreen.locked = !lockScreen.locked
}
}
IpcHandler {
target: "brightness"
function increase() {
BrightnessService.increaseBrightness()
}
function decrease() {
BrightnessService.decreaseBrightness()
}
}
}

View file

@ -179,6 +179,16 @@ Singleton {
property string fontFamily: "Roboto" // Family for all text
property list<string> monitorsScale: []
}
// brightness
property JsonObject brightness
brightness: JsonObject {
property real lastBrightness: 50.0
property string lastMethod: "internal"
property list<var> monitorBrightness: []
property int brightnessStep: 5
}
}
}
}