1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 07:52:50 -05:00

brightness: remove brightnessctl + ddcutil dependencies

- Switches to DMS API v13+ dependency
This commit is contained in:
bbedward
2025-11-02 20:22:45 -05:00
parent 66e3cc77c5
commit 976ff108b3
6 changed files with 277 additions and 788 deletions

View File

@@ -47,6 +47,16 @@ Rectangle {
} }
} }
const backlight = DisplayService.devices.find(d => d.class === "backlight")
if (backlight) {
return backlight.name
}
const ddc = DisplayService.devices.find(d => d.class === "ddc")
if (ddc) {
return ddc.name
}
return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : "" return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : ""
} }
@@ -224,8 +234,10 @@ Rectangle {
if (deviceClass === "backlight" || deviceClass === "ddc") { if (deviceClass === "backlight" || deviceClass === "ddc") {
const brightness = DisplayService.getDeviceBrightness(modelData.name) const brightness = DisplayService.getDeviceBrightness(modelData.name)
if (brightness <= 33) return "brightness_low" if (brightness <= 33)
if (brightness <= 66) return "brightness_medium" return "brightness_low"
if (brightness <= 66)
return "brightness_medium"
return "brightness_high" return "brightness_high"
} else if (deviceName.includes("kbd")) { } else if (deviceName.includes("kbd")) {
return "keyboard" return "keyboard"
@@ -277,9 +289,12 @@ Rectangle {
StyledText { StyledText {
text: { text: {
const deviceClass = modelData.class || "" const deviceClass = modelData.class || ""
if (deviceClass === "backlight") return "Backlight device" if (deviceClass === "backlight")
if (deviceClass === "ddc") return "DDC/CI monitor" return "Backlight device"
if (deviceClass === "leds") return "LED device" if (deviceClass === "ddc")
return "DDC/CI monitor"
if (deviceClass === "leds")
return "LED device"
return deviceClass return deviceClass
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
@@ -299,7 +314,6 @@ Rectangle {
deviceNameChanged(modelData.name) deviceNameChanged(modelData.name)
} }
} }
} }
} }
} }

View File

@@ -13,7 +13,7 @@ Row {
property string screenName: "" property string screenName: ""
property var parentScreen: null property var parentScreen: null
signal iconClicked() signal iconClicked
height: 40 height: 40
spacing: 0 spacing: 0
@@ -36,13 +36,27 @@ Row {
if (deviceName && deviceName.length > 0) { if (deviceName && deviceName.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === deviceName) const found = DisplayService.devices.find(dev => dev.name === deviceName)
return found ? found.name : "" if (found) {
return found.name
}
} }
const currentDeviceName = DisplayService.currentDevice const currentDeviceName = DisplayService.currentDevice
if (currentDeviceName) { if (currentDeviceName) {
const found = DisplayService.devices.find(dev => dev.name === currentDeviceName) const found = DisplayService.devices.find(dev => dev.name === currentDeviceName)
return found ? found.name : "" if (found) {
return found.name
}
}
const backlight = DisplayService.devices.find(d => d.class === "backlight")
if (backlight) {
return backlight.name
}
const ddc = DisplayService.devices.find(d => d.class === "ddc")
if (ddc) {
return ddc.name
} }
return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : "" return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : ""
@@ -69,9 +83,7 @@ Row {
height: Theme.iconSize + Theme.spacingS * 2 height: Theme.iconSize + Theme.spacingS * 2
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
radius: (Theme.iconSize + Theme.spacingS * 2) / 2 radius: (Theme.iconSize + Theme.spacingS * 2) / 2
color: iconArea.containsMouse color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.primary, 0)
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
: Theme.withAlpha(Theme.primary, 0)
MouseArea { MouseArea {
id: iconArea id: iconArea
@@ -112,8 +124,10 @@ Row {
if (targetDevice.class === "backlight" || targetDevice.class === "ddc") { if (targetDevice.class === "backlight" || targetDevice.class === "ddc") {
const brightness = targetBrightness const brightness = targetBrightness
if (brightness <= 33) return "brightness_low" if (brightness <= 33)
if (brightness <= 66) return "brightness_medium" return "brightness_low"
if (brightness <= 66)
return "brightness_medium"
return "brightness_high" return "brightness_high"
} else if (targetDevice.name.includes("kbd")) { } else if (targetDevice.name.includes("kbd")) {
return "keyboard" return "keyboard"
@@ -134,7 +148,7 @@ Row {
minimum: 1 minimum: 1
maximum: 100 maximum: 100
value: targetBrightness value: targetBrightness
onSliderValueChanged: function(newValue) { onSliderValueChanged: function (newValue) {
if (DisplayService.brightnessAvailable && targetDeviceName) { if (DisplayService.brightnessAvailable && targetDeviceName) {
DisplayService.setBrightness(newValue, targetDeviceName, true) DisplayService.setBrightness(newValue, targetDeviceName, true)
} }
@@ -148,4 +162,4 @@ Row {
active: false active: false
sourceComponent: DankTooltip {} sourceComponent: DankTooltip {}
} }
} }

View File

@@ -1,6 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtCore import QtCore
import QtQuick import QtQuick
@@ -34,16 +34,17 @@ Singleton {
signal searchResultsReceived(var plugins) signal searchResultsReceived(var plugins)
signal operationSuccess(string message) signal operationSuccess(string message)
signal operationError(string error) signal operationError(string error)
signal connectionStateChanged() signal connectionStateChanged
signal networkStateUpdate(var data) signal networkStateUpdate(var data)
signal cupsStateUpdate(var data) signal cupsStateUpdate(var data)
signal loginctlStateUpdate(var data) signal loginctlStateUpdate(var data)
signal loginctlEvent(var event) signal loginctlEvent(var event)
signal capabilitiesReceived() signal capabilitiesReceived
signal credentialsRequest(var data) signal credentialsRequest(var data)
signal bluetoothPairingRequest(var data) signal bluetoothPairingRequest(var data)
signal dwlStateUpdate(var data) signal dwlStateUpdate(var data)
signal brightnessStateUpdate(var data)
Component.onCompleted: { Component.onCompleted: {
if (socketPath && socketPath.length > 0) { if (socketPath && socketPath.length > 0) {
@@ -227,11 +228,7 @@ Singleton {
if (response.error.includes("unknown method") && response.error.includes("subscribe")) { if (response.error.includes("unknown method") && response.error.includes("subscribe")) {
if (!shownOutdatedError) { if (!shownOutdatedError) {
console.error("DMSService: Server does not support subscribe method") console.error("DMSService: Server does not support subscribe method")
ToastService.showError( ToastService.showError(I18n.tr("DMS out of date"), I18n.tr("To update, run the following command:"), updateCommand)
I18n.tr("DMS out of date"),
I18n.tr("To update, run the following command:"),
updateCommand
)
shownOutdatedError = true shownOutdatedError = true
} }
} }
@@ -272,6 +269,8 @@ Singleton {
cupsStateUpdate(data) cupsStateUpdate(data)
} else if (service === "dwl") { } else if (service === "dwl") {
dwlStateUpdate(data) dwlStateUpdate(data)
} else if (service === "brightness") {
brightnessStateUpdate(data)
} }
} }
@@ -280,8 +279,8 @@ Singleton {
console.warn("DMSService.sendRequest: Not connected, method:", method) console.warn("DMSService.sendRequest: Not connected, method:", method)
if (callback) { if (callback) {
callback({ callback({
"error": "not connected to DMS socket" "error": "not connected to DMS socket"
}) })
} }
return return
} }

View File

@@ -1,6 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -12,14 +12,9 @@ Singleton {
property bool brightnessAvailable: devices.length > 0 property bool brightnessAvailable: devices.length > 0
property var devices: [] property var devices: []
property var ddcDevices: []
property var deviceBrightness: ({}) property var deviceBrightness: ({})
property var ddcPendingInit: ({})
property string currentDevice: "" property string currentDevice: ""
property string lastIpcDevice: "" property string lastIpcDevice: ""
property bool ddcAvailable: false
property var ddcInitQueue: []
property bool skipDdcRead: false
property int brightnessLevel: { property int brightnessLevel: {
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice) const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
if (!deviceToUse) { if (!deviceToUse) {
@@ -40,36 +35,75 @@ Singleton {
property bool automationAvailable: false property bool automationAvailable: false
property bool gammaControlAvailable: false property bool gammaControlAvailable: false
function setBrightnessInternal(percentage, device) { function updateFromBrightnessState(state) {
const clampedValue = Math.max(1, Math.min(100, percentage)) if (!state || !state.devices) {
const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice()) return
if (actualDevice) {
const newBrightness = Object.assign({}, deviceBrightness)
newBrightness[actualDevice] = clampedValue
deviceBrightness = newBrightness
} }
const deviceInfo = getCurrentDeviceInfoByName(actualDevice) devices = state.devices.map(d => ({
"id": d.id,
"name": d.id,
"class": d.class,
"current": d.current,
"percentage": d.currentPercent,
"max": d.max,
"backend": d.backend
}))
if (deviceInfo && deviceInfo.class === "ddc") { const newBrightness = {}
ddcBrightnessSetProcess.command = ["ddcutil", "setvcp", "-d", String(deviceInfo.ddcDisplay), "10", String(clampedValue)] for (const device of state.devices) {
ddcBrightnessSetProcess.running = true newBrightness[device.id] = device.currentPercent
} else { }
if (device) { deviceBrightness = newBrightness
brightnessSetProcess.command = ["brightnessctl", "-d", device, "set", `${clampedValue}%`]
brightnessAvailable = devices.length > 0
if (devices.length > 0 && !currentDevice) {
const lastDevice = SessionData.lastBrightnessDevice || ""
const deviceExists = devices.some(d => d.id === lastDevice)
if (deviceExists) {
setCurrentDevice(lastDevice, false)
} else { } else {
brightnessSetProcess.command = ["brightnessctl", "set", `${clampedValue}%`] const backlight = devices.find(d => d.class === "backlight")
const nonKbdDevice = devices.find(d => !d.id.includes("kbd"))
const defaultDevice = backlight || nonKbdDevice || devices[0]
setCurrentDevice(defaultDevice.id, false)
} }
brightnessSetProcess.running = true }
if (!brightnessInitialized) {
brightnessInitialized = true
} }
} }
function setBrightness(percentage, device, suppressOsd) { function setBrightness(percentage, device, suppressOsd) {
setBrightnessInternal(percentage, device) const clampedValue = Math.max(1, Math.min(100, percentage))
if (!suppressOsd) { const actualDevice = device === "" ? getDefaultDevice() : (device || currentDevice || getDefaultDevice())
brightnessChanged()
if (!actualDevice) {
console.warn("DisplayService: No device selected for brightness change")
return
} }
if (!DMSService.isConnected) {
console.warn("DisplayService: Not connected to DMS")
return
}
DMSService.sendRequest("brightness.setBrightness", {
"device": actualDevice,
"percent": clampedValue
}, response => {
if (response.error) {
console.error("DisplayService: Failed to set brightness:", response.error)
ToastService.showError("Failed to set brightness: " + response.error)
return
}
if (!suppressOsd) {
brightnessChanged()
}
})
} }
function setCurrentDevice(deviceName, saveToSession = false) { function setCurrentDevice(deviceName, saveToSession = false) {
@@ -85,79 +119,27 @@ Singleton {
} }
deviceSwitched() deviceSwitched()
const deviceInfo = getCurrentDeviceInfoByName(deviceName)
if (deviceInfo && deviceInfo.class === "ddc") {
return
} else {
brightnessGetProcess.command = ["brightnessctl", "-m", "-d", deviceName, "get"]
brightnessGetProcess.running = true
}
}
function refreshDevices() {
deviceListProcess.running = true
}
function refreshDevicesInternal() {
const allDevices = [...devices, ...ddcDevices]
allDevices.sort((a, b) => {
if (a.class === "backlight" && b.class !== "backlight") {
return -1
}
if (a.class !== "backlight" && b.class === "backlight") {
return 1
}
if (a.class === "ddc" && b.class !== "ddc" && b.class !== "backlight") {
return -1
}
if (a.class !== "ddc" && b.class === "ddc" && a.class !== "backlight") {
return 1
}
return a.name.localeCompare(b.name)
})
devices = allDevices
if (devices.length > 0 && !currentDevice) {
const lastDevice = SessionData.lastBrightnessDevice || ""
const deviceExists = devices.some(d => d.name === lastDevice)
if (deviceExists) {
setCurrentDevice(lastDevice, false)
} else {
const nonKbdDevice = devices.find(d => !d.name.includes("kbd")) || devices[0]
setCurrentDevice(nonKbdDevice.name, false)
}
}
} }
function getDeviceBrightness(deviceName) { function getDeviceBrightness(deviceName) {
if (!deviceName) { if (!deviceName) {
return
} 50
const deviceInfo = getCurrentDeviceInfoByName(deviceName)
if (!deviceInfo) {
return 50 return 50
} }
if (deviceInfo.class === "ddc") { if (deviceName in deviceBrightness) {
return deviceBrightness[deviceName] || 50 return deviceBrightness[deviceName]
} }
return deviceBrightness[deviceName] || deviceInfo.percentage || 50 return 50
} }
function getDefaultDevice() { function getDefaultDevice() {
for (const device of devices) { for (const device of devices) {
if (device.class === "backlight") { if (device.class === "backlight") {
return device.name return device.id
} }
} }
return devices.length > 0 ? devices[0].name : "" return devices.length > 0 ? devices[0].id : ""
} }
function getCurrentDeviceInfo() { function getCurrentDeviceInfo() {
@@ -167,7 +149,7 @@ Singleton {
} }
for (const device of devices) { for (const device of devices) {
if (device.name === deviceToUse) { if (device.id === deviceToUse) {
return device return device
} }
} }
@@ -176,15 +158,7 @@ Singleton {
function isCurrentDeviceReady() { function isCurrentDeviceReady() {
const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice) const deviceToUse = lastIpcDevice === "" ? getDefaultDevice() : (lastIpcDevice || currentDevice)
if (!deviceToUse) { return deviceToUse !== ""
return false
}
if (ddcPendingInit[deviceToUse]) {
return false
}
return true
} }
function getCurrentDeviceInfoByName(deviceName) { function getCurrentDeviceInfoByName(deviceName) {
@@ -193,23 +167,13 @@ Singleton {
} }
for (const device of devices) { for (const device of devices) {
if (device.name === deviceName) { if (device.id === deviceName) {
return device return device
} }
} }
return null return null
} }
function processNextDdcInit() {
if (ddcInitQueue.length === 0 || ddcInitialBrightnessProcess.running) {
return
}
const displayId = ddcInitQueue.shift()
ddcInitialBrightnessProcess.command = ["ddcutil", "getvcp", "-d", String(displayId), "10", "--brief"]
ddcInitialBrightnessProcess.running = true
}
// Night Mode Functions - Simplified // Night Mode Functions - Simplified
function enableNightMode() { function enableNightMode() {
if (!gammaControlAvailable) { if (!gammaControlAvailable) {
@@ -221,22 +185,22 @@ Singleton {
SessionData.setNightModeEnabled(true) SessionData.setNightModeEnabled(true)
DMSService.sendRequest("wayland.gamma.setEnabled", { DMSService.sendRequest("wayland.gamma.setEnabled", {
"enabled": true "enabled": true
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to enable gamma control:", response.error) console.error("DisplayService: Failed to enable gamma control:", response.error)
ToastService.showError("Failed to enable night mode: " + response.error) ToastService.showError("Failed to enable night mode: " + response.error)
nightModeEnabled = false nightModeEnabled = false
SessionData.setNightModeEnabled(false) SessionData.setNightModeEnabled(false)
return return
} }
if (SessionData.nightModeAutoEnabled) { if (SessionData.nightModeAutoEnabled) {
startAutomation() startAutomation()
} else { } else {
applyNightModeDirectly() applyNightModeDirectly()
} }
}) })
} }
function disableNightMode() { function disableNightMode() {
@@ -248,13 +212,13 @@ Singleton {
} }
DMSService.sendRequest("wayland.gamma.setEnabled", { DMSService.sendRequest("wayland.gamma.setEnabled", {
"enabled": false "enabled": false
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to disable gamma control:", response.error) console.error("DisplayService: Failed to disable gamma control:", response.error)
ToastService.showError("Failed to disable night mode: " + response.error) ToastService.showError("Failed to disable night mode: " + response.error)
} }
}) })
} }
function toggleNightMode() { function toggleNightMode() {
@@ -269,32 +233,32 @@ Singleton {
const temperature = SessionData.nightModeTemperature || 4000 const temperature = SessionData.nightModeTemperature || 4000
DMSService.sendRequest("wayland.gamma.setManualTimes", { DMSService.sendRequest("wayland.gamma.setManualTimes", {
"sunrise": null, "sunrise": null,
"sunset": null "sunset": null
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to clear manual times:", response.error) console.error("DisplayService: Failed to clear manual times:", response.error)
return return
} }
DMSService.sendRequest("wayland.gamma.setUseIPLocation", { DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": false "use": false
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to disable IP location:", response.error) console.error("DisplayService: Failed to disable IP location:", response.error)
return return
} }
DMSService.sendRequest("wayland.gamma.setTemperature", { DMSService.sendRequest("wayland.gamma.setTemperature", {
"temp": temperature "temp": temperature
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to set temperature:", response.error) console.error("DisplayService: Failed to set temperature:", response.error)
ToastService.showError("Failed to set night mode temperature: " + response.error) ToastService.showError("Failed to set night mode temperature: " + response.error)
} }
}) })
}) })
}) })
} }
function startAutomation() { function startAutomation() {
@@ -326,34 +290,34 @@ Singleton {
const sunset = `${String(sunsetHour).padStart(2, '0')}:${String(sunsetMinute).padStart(2, '0')}` const sunset = `${String(sunsetHour).padStart(2, '0')}:${String(sunsetMinute).padStart(2, '0')}`
DMSService.sendRequest("wayland.gamma.setUseIPLocation", { DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": false "use": false
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to disable IP location:", response.error) console.error("DisplayService: Failed to disable IP location:", response.error)
return return
} }
DMSService.sendRequest("wayland.gamma.setTemperature", { DMSService.sendRequest("wayland.gamma.setTemperature", {
"low": temperature, "low": temperature,
"high": highTemp "high": highTemp
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to set temperature:", response.error) console.error("DisplayService: Failed to set temperature:", response.error)
ToastService.showError("Failed to set night mode temperature: " + response.error) ToastService.showError("Failed to set night mode temperature: " + response.error)
return return
} }
DMSService.sendRequest("wayland.gamma.setManualTimes", { DMSService.sendRequest("wayland.gamma.setManualTimes", {
"sunrise": sunrise, "sunrise": sunrise,
"sunset": sunset "sunset": sunset
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to set manual times:", response.error) console.error("DisplayService: Failed to set manual times:", response.error)
ToastService.showError("Failed to set night mode schedule: " + response.error) ToastService.showError("Failed to set night mode schedule: " + response.error)
} }
}) })
}) })
}) })
} }
function startLocationBasedMode() { function startLocationBasedMode() {
@@ -361,57 +325,57 @@ Singleton {
const highTemp = SessionData.nightModeHighTemperature || 6500 const highTemp = SessionData.nightModeHighTemperature || 6500
DMSService.sendRequest("wayland.gamma.setManualTimes", { DMSService.sendRequest("wayland.gamma.setManualTimes", {
"sunrise": null, "sunrise": null,
"sunset": null "sunset": null
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to clear manual times:", response.error) console.error("DisplayService: Failed to clear manual times:", response.error)
return return
} }
DMSService.sendRequest("wayland.gamma.setTemperature", { DMSService.sendRequest("wayland.gamma.setTemperature", {
"low": temperature, "low": temperature,
"high": highTemp "high": highTemp
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to set temperature:", response.error) console.error("DisplayService: Failed to set temperature:", response.error)
ToastService.showError("Failed to set night mode temperature: " + response.error) ToastService.showError("Failed to set night mode temperature: " + response.error)
return return
} }
if (SessionData.nightModeUseIPLocation) { if (SessionData.nightModeUseIPLocation) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", { DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": true "use": true
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to enable IP location:", response.error) console.error("DisplayService: Failed to enable IP location:", response.error)
ToastService.showError("Failed to enable IP location: " + response.error) ToastService.showError("Failed to enable IP location: " + response.error)
} }
}) })
} else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) { } else if (SessionData.latitude !== 0.0 && SessionData.longitude !== 0.0) {
DMSService.sendRequest("wayland.gamma.setUseIPLocation", { DMSService.sendRequest("wayland.gamma.setUseIPLocation", {
"use": false "use": false
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to disable IP location:", response.error) console.error("DisplayService: Failed to disable IP location:", response.error)
return return
} }
DMSService.sendRequest("wayland.gamma.setLocation", { DMSService.sendRequest("wayland.gamma.setLocation", {
"latitude": SessionData.latitude, "latitude": SessionData.latitude,
"longitude": SessionData.longitude "longitude": SessionData.longitude
}, response => { }, response => {
if (response.error) { if (response.error) {
console.error("DisplayService: Failed to set location:", response.error) console.error("DisplayService: Failed to set location:", response.error)
ToastService.showError("Failed to set night mode location: " + response.error) ToastService.showError("Failed to set night mode location: " + response.error)
} }
}) })
}) })
} else { } else {
console.warn("DisplayService: Location mode selected but no coordinates set and IP location disabled") console.warn("DisplayService: Location mode selected but no coordinates set and IP location disabled")
} }
}) })
}) })
} }
function setNightModeAutomationMode(mode) { function setNightModeAutomationMode(mode) {
@@ -450,32 +414,32 @@ Singleton {
} }
DMSService.sendRequest("wayland.gamma.getState", null, response => { DMSService.sendRequest("wayland.gamma.getState", null, response => {
if (response.error) { if (response.error) {
gammaControlAvailable = false gammaControlAvailable = false
automationAvailable = false automationAvailable = false
console.error("DisplayService: Gamma control not available:", response.error) console.error("DisplayService: Gamma control not available:", response.error)
} else { } else {
gammaControlAvailable = true gammaControlAvailable = true
automationAvailable = true automationAvailable = true
if (nightModeEnabled) { if (nightModeEnabled) {
DMSService.sendRequest("wayland.gamma.setEnabled", { DMSService.sendRequest("wayland.gamma.setEnabled", {
"enabled": true "enabled": true
}, enableResponse => { }, enableResponse => {
if (enableResponse.error) { if (enableResponse.error) {
console.error("DisplayService: Failed to enable gamma control on startup:", enableResponse.error) console.error("DisplayService: Failed to enable gamma control on startup:", enableResponse.error)
return return
} }
if (SessionData.nightModeAutoEnabled) { if (SessionData.nightModeAutoEnabled) {
startAutomation() startAutomation()
} else { } else {
applyNightModeDirectly() applyNightModeDirectly()
} }
}) })
} }
} }
}) })
} }
Timer { Timer {
@@ -495,274 +459,9 @@ Singleton {
} }
Component.onCompleted: { Component.onCompleted: {
ddcDetectionProcess.running = true
refreshDevices()
nightModeEnabled = SessionData.nightModeEnabled nightModeEnabled = SessionData.nightModeEnabled
} if (DMSService.isConnected) {
checkGammaControlAvailability()
Process {
id: ddcDetectionProcess
command: ["which", "ddcutil"]
running: false
onExited: function (exitCode) {
ddcAvailable = (exitCode === 0)
if (ddcAvailable) {
ddcDisplayDetectionProcess.running = true
} else {
console.info("DisplayService: ddcutil not available")
}
}
}
Process {
id: ddcDisplayDetectionProcess
command: ["bash", "-c", "ddcutil detect --brief 2>/dev/null | grep '^Display [0-9]' | awk '{print \"{\\\"display\\\":\" $2 \",\\\"name\\\":\\\"ddc-\" $2 \"\\\",\\\"class\\\":\\\"ddc\\\"}\"}' | tr '\\n' ',' | sed 's/,$//' | sed 's/^/[/' | sed 's/$/]/' || echo '[]'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (!text.trim()) {
ddcDevices = []
return
}
try {
const parsedDevices = JSON.parse(text.trim())
const newDdcDevices = []
for (const device of parsedDevices) {
if (device.display && device.class === "ddc") {
newDdcDevices.push({
"name": device.name,
"class": "ddc",
"current": 50,
"percentage": 50,
"max": 100,
"ddcDisplay": device.display
})
}
}
ddcDevices = newDdcDevices
console.info("DisplayService: Found", ddcDevices.length, "DDC displays")
// Queue initial brightness readings for DDC devices
ddcInitQueue = []
for (const device of ddcDevices) {
ddcInitQueue.push(device.ddcDisplay)
// Mark DDC device as pending initialization
ddcPendingInit[device.name] = true
}
// Start processing the queue
processNextDdcInit()
// Refresh device list to include DDC devices
refreshDevicesInternal()
// Retry setting last device now that DDC devices are available
const lastDevice = SessionData.lastBrightnessDevice || ""
if (lastDevice) {
const deviceExists = devices.some(d => d.name === lastDevice)
if (deviceExists && (!currentDevice || currentDevice !== lastDevice)) {
setCurrentDevice(lastDevice, false)
}
}
} catch (error) {
console.warn("DisplayService: Failed to parse DDC devices:", error)
ddcDevices = []
}
}
}
onExited: function (exitCode) {
if (exitCode !== 0) {
console.warn("DisplayService: Failed to detect DDC displays:", exitCode)
ddcDevices = []
}
}
}
Process {
id: deviceListProcess
command: ["brightnessctl", "-m", "-l"]
onExited: function (exitCode) {
if (exitCode !== 0) {
console.warn("DisplayService: Failed to list devices:", exitCode)
brightnessAvailable = false
}
}
stdout: StdioCollector {
onStreamFinished: {
if (!text.trim()) {
console.warn("DisplayService: No devices found")
return
}
const lines = text.trim().split("\n")
const newDevices = []
for (const line of lines) {
const parts = line.split(",")
if (parts.length >= 5) {
newDevices.push({
"name": parts[0],
"class": parts[1],
"current": parseInt(parts[2]),
"percentage": parseInt(parts[3]),
"max": parseInt(parts[4])
})
}
}
// Store brightnessctl devices separately
devices = newDevices
// Always refresh to combine with DDC devices and set up device selection
refreshDevicesInternal()
}
}
}
Process {
id: brightnessSetProcess
running: false
onExited: function (exitCode) {
if (exitCode !== 0) {
console.warn("DisplayService: Failed to set brightness:", exitCode)
}
}
}
Process {
id: ddcBrightnessSetProcess
running: false
onExited: function (exitCode) {
if (exitCode !== 0) {
console.warn("DisplayService: Failed to set DDC brightness:", exitCode)
}
}
}
Process {
id: ddcInitialBrightnessProcess
running: false
onExited: function (exitCode) {
if (exitCode !== 0) {
console.warn("DisplayService: Failed to get initial DDC brightness:", exitCode)
}
processNextDdcInit()
}
stdout: StdioCollector {
onStreamFinished: {
if (!text.trim())
return
const parts = text.trim().split(" ")
if (parts.length >= 5) {
const current = parseInt(parts[3]) || 50
const max = parseInt(parts[4]) || 100
const brightness = Math.round((current / max) * 100)
const commandParts = ddcInitialBrightnessProcess.command
if (commandParts && commandParts.length >= 4) {
const displayId = commandParts[3]
const deviceName = "ddc-" + displayId
var newBrightness = Object.assign({}, deviceBrightness)
newBrightness[deviceName] = brightness
deviceBrightness = newBrightness
var newPending = Object.assign({}, ddcPendingInit)
delete newPending[deviceName]
ddcPendingInit = newPending
console.log("DisplayService: Initial DDC Device", deviceName, "brightness:", brightness + "%")
}
}
}
}
}
Process {
id: brightnessGetProcess
running: false
onExited: function (exitCode) {
if (exitCode !== 0) {
console.warn("DisplayService: Failed to get brightness:", exitCode)
}
}
stdout: StdioCollector {
onStreamFinished: {
if (!text.trim())
return
const parts = text.trim().split(",")
if (parts.length >= 5) {
const current = parseInt(parts[2])
const max = parseInt(parts[4])
maxBrightness = max
const brightness = Math.round((current / max) * 100)
// Update the device brightness cache
if (currentDevice) {
var newBrightness = Object.assign({}, deviceBrightness)
newBrightness[currentDevice] = brightness
deviceBrightness = newBrightness
}
brightnessInitialized = true
console.log("DisplayService: Device", currentDevice, "brightness:", brightness + "%")
brightnessChanged()
}
}
}
}
Process {
id: ddcBrightnessGetProcess
running: false
onExited: function (exitCode) {
if (exitCode !== 0) {
console.warn("DisplayService: Failed to get DDC brightness:", exitCode)
}
}
stdout: StdioCollector {
onStreamFinished: {
if (!text.trim())
return
// Parse ddcutil getvcp output format: "VCP 10 C 50 100"
const parts = text.trim().split(" ")
if (parts.length >= 5) {
const current = parseInt(parts[3]) || 50
const max = parseInt(parts[4]) || 100
maxBrightness = max
const brightness = Math.round((current / max) * 100)
// Update the device brightness cache
if (currentDevice) {
var newBrightness = Object.assign({}, deviceBrightness)
newBrightness[currentDevice] = brightness
deviceBrightness = newBrightness
}
brightnessInitialized = true
console.log("DisplayService: DDC Device", currentDevice, "brightness:", brightness + "%")
brightnessChanged()
}
}
} }
} }
@@ -773,6 +472,7 @@ Singleton {
if (DMSService.isConnected) { if (DMSService.isConnected) {
checkGammaControlAvailability() checkGammaControlAvailability()
} else { } else {
brightnessAvailable = false
gammaControlAvailable = false gammaControlAvailable = false
automationAvailable = false automationAvailable = false
} }
@@ -781,6 +481,10 @@ Singleton {
function onCapabilitiesReceived() { function onCapabilitiesReceived() {
checkGammaControlAvailability() checkGammaControlAvailability()
} }
function onBrightnessStateUpdate(data) {
updateFromBrightnessState(data)
}
} }
// Session Data Connections // Session Data Connections
@@ -842,8 +546,7 @@ Singleton {
const clampedValue = Math.max(1, Math.min(100, value)) const clampedValue = Math.max(1, Math.min(100, value))
const targetDevice = device || "" const targetDevice = device || ""
// Ensure device exists if specified if (targetDevice && !root.devices.some(d => d.id === targetDevice)) {
if (targetDevice && !root.devices.some(d => d.name === targetDevice)) {
return "Device not found: " + targetDevice return "Device not found: " + targetDevice
} }
@@ -868,8 +571,7 @@ Singleton {
const targetDevice = device || "" const targetDevice = device || ""
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
// Ensure device exists if (actualDevice && !root.devices.some(d => d.id === actualDevice)) {
if (actualDevice && !root.devices.some(d => d.name === actualDevice)) {
return "Device not found: " + actualDevice return "Device not found: " + actualDevice
} }
@@ -898,8 +600,7 @@ Singleton {
const targetDevice = device || "" const targetDevice = device || ""
const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice const actualDevice = targetDevice === "" ? root.getDefaultDevice() : targetDevice
// Ensure device exists if (actualDevice && !root.devices.some(d => d.id === actualDevice)) {
if (actualDevice && !root.devices.some(d => d.name === actualDevice)) {
return "Device not found: " + actualDevice return "Device not found: " + actualDevice
} }
@@ -935,7 +636,7 @@ Singleton {
let result = "Available devices:\\n" let result = "Available devices:\\n"
for (const device of root.devices) { for (const device of root.devices) {
result += device.name + " (" + device.class + ")\\n" result += device.id + " (" + device.class + ")\\n"
} }
return result return result
} }

View File

@@ -10,8 +10,6 @@ import qs.Common
Singleton { Singleton {
id: root id: root
readonly property string shellDir: Paths.strip(Qt.resolvedUrl(".").toString()).replace("/Services/", "")
property string scriptPath: `${shellDir}/scripts/hyprland_keybinds.py`
readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation) readonly property string _configUrl: StandardPaths.writableLocation(StandardPaths.ConfigLocation)
readonly property string _configDir: Paths.strip(_configUrl) readonly property string _configDir: Paths.strip(_configUrl)
property string hyprConfigPath: `${_configDir}/hypr` property string hyprConfigPath: `${_configDir}/hypr`
@@ -20,7 +18,7 @@ Singleton {
Process { Process {
id: getKeybinds id: getKeybinds
running: false running: false
command: [root.scriptPath, "--path", root.hyprConfigPath] command: ["dms", "hyprland", "keybinds", "--path", root.hyprConfigPath]
stdout: SplitParser { stdout: SplitParser {
onRead: data => { onRead: data => {

View File

@@ -1,237 +0,0 @@
#!/usr/bin/env python3
# Based on end-4 dots-hyprland get_keybinds.py
# https://github.com/end-4/dots-hyprland/blob/main/.config/quickshell/ii/scripts/hyprland/get_keybinds.py
import argparse
import re
import os
import glob
from os.path import expandvars as os_expandvars
from typing import Dict, List
TITLE_REGEX = "#+!"
HIDE_COMMENT = "[hidden]"
MOD_SEPARATORS = ['+', ' ']
COMMENT_BIND_PATTERN = "#/#"
parser = argparse.ArgumentParser(description='Hyprland keybind reader')
parser.add_argument('--path', type=str, default="$HOME/.config/hypr", help='path to hyprland config directory')
args = parser.parse_args()
content_lines = []
reading_line = 0
Variables: Dict[str, str] = {}
class KeyBinding(dict):
def __init__(self, mods, key, dispatcher, params, comment) -> None:
self["mods"] = mods
self["key"] = key
self["dispatcher"] = dispatcher
self["params"] = params
self["comment"] = comment
class Section(dict):
def __init__(self, children, keybinds, name) -> None:
self["children"] = children
self["keybinds"] = keybinds
self["name"] = name
def read_content(directory: str) -> str:
expanded_dir = os.path.expanduser(os.path.expandvars(directory))
if not os.path.isdir(expanded_dir):
return "error"
conf_files = glob.glob(os.path.join(expanded_dir, "*.conf"))
if not conf_files:
return "error"
combined_content = []
for conf_file in sorted(conf_files):
if os.access(conf_file, os.R_OK):
with open(conf_file, "r") as file:
combined_content.append(file.read())
return "\n".join(combined_content) if combined_content else "error"
def autogenerate_comment(dispatcher: str, params: str = "") -> str:
match dispatcher:
case "resizewindow":
return "Resize window"
case "movewindow":
if(params == ""):
return "Move window"
else:
return "Window: move in {} direction".format({
"l": "left",
"r": "right",
"u": "up",
"d": "down",
}.get(params, "null"))
case "pin":
return "Window: pin (show on all workspaces)"
case "splitratio":
return "Window split ratio {}".format(params)
case "togglefloating":
return "Float/unfloat window"
case "resizeactive":
return "Resize window by {}".format(params)
case "killactive":
return "Close window"
case "fullscreen":
return "Toggle {}".format(
{
"0": "fullscreen",
"1": "maximization",
"2": "fullscreen on Hyprland's side",
}.get(params, "null")
)
case "fakefullscreen":
return "Toggle fake fullscreen"
case "workspace":
if params == "+1":
return "Workspace: focus right"
elif params == "-1":
return "Workspace: focus left"
return "Focus workspace {}".format(params)
case "movefocus":
return "Window: move focus {}".format(
{
"l": "left",
"r": "right",
"u": "up",
"d": "down",
}.get(params, "null")
)
case "swapwindow":
return "Window: swap in {} direction".format(
{
"l": "left",
"r": "right",
"u": "up",
"d": "down",
}.get(params, "null")
)
case "movetoworkspace":
if params == "+1":
return "Window: move to right workspace (non-silent)"
elif params == "-1":
return "Window: move to left workspace (non-silent)"
return "Window: move to workspace {} (non-silent)".format(params)
case "movetoworkspacesilent":
if params == "+1":
return "Window: move to right workspace"
elif params == "-1":
return "Window: move to right workspace"
return "Window: move to workspace {}".format(params)
case "togglespecialworkspace":
return "Workspace: toggle special"
case "exec":
return "Execute: {}".format(params)
case _:
return ""
def get_keybind_at_line(line_number, line_start = 0):
global content_lines
line = content_lines[line_number]
_, keys = line.split("=", 1)
keys, *comment = keys.split("#", 1)
mods, key, dispatcher, *params = list(map(str.strip, keys.split(",", 4)))
params = "".join(map(str.strip, params))
# Remove empty spaces
comment = list(map(str.strip, comment))
# Add comment if it exists, else generate it
if comment:
comment = comment[0]
if comment.startswith("[hidden]"):
return None
else:
comment = autogenerate_comment(dispatcher, params)
if mods:
modstring = mods + MOD_SEPARATORS[0] # Add separator at end to ensure last mod is read
mods = []
p = 0
for index, char in enumerate(modstring):
if(char in MOD_SEPARATORS):
if(index - p > 1):
mods.append(modstring[p:index])
p = index+1
else:
mods = []
return KeyBinding(mods, key, dispatcher, params, comment)
def get_binds_recursive(current_content, scope):
global content_lines
global reading_line
# print("get_binds_recursive({0}, {1}) [@L{2}]".format(current_content, scope, reading_line + 1))
while reading_line < len(content_lines): # TODO: Adjust condition
line = content_lines[reading_line]
heading_search_result = re.search(TITLE_REGEX, line)
# print("Read line {0}: {1}\tisHeading: {2}".format(reading_line + 1, content_lines[reading_line], "[{0}, {1}, {2}]".format(heading_search_result.start(), heading_search_result.start() == 0, ((heading_search_result != None) and (heading_search_result.start() == 0))) if heading_search_result != None else "No"))
if ((heading_search_result != None) and (heading_search_result.start() == 0)): # Found title
# Determine scope
heading_scope = line.find('!')
# Lower? Return
if(heading_scope <= scope):
reading_line -= 1
return current_content
section_name = line[(heading_scope+1):].strip()
# print("[[ Found h{0} at line {1} ]] {2}".format(heading_scope, reading_line+1, content_lines[reading_line]))
reading_line += 1
current_content["children"].append(get_binds_recursive(Section([], [], section_name), heading_scope))
elif line.startswith(COMMENT_BIND_PATTERN):
keybind = get_keybind_at_line(reading_line, line_start=len(COMMENT_BIND_PATTERN))
if(keybind != None):
current_content["keybinds"].append(keybind)
elif line == "" or not line.lstrip().startswith("bind"): # Comment, ignore
pass
else: # Normal keybind
keybind = get_keybind_at_line(reading_line)
if(keybind != None):
current_content["keybinds"].append(keybind)
reading_line += 1
return current_content;
def parse_keys(path: str) -> Dict[str, List[KeyBinding]]:
global content_lines
content_lines = read_content(path).splitlines()
if content_lines[0] == "error":
return "error"
return get_binds_recursive(Section([], [], ""), 0)
if __name__ == "__main__":
import json
ParsedKeys = parse_keys(args.path)
print(json.dumps(ParsedKeys))