LockKeysService: use Qml file system model

Forking shell processes is very very expensive. Replace with a more
efficient Qml model.

This should automatically instantiate watchers for hotplugged
devices, as FolderListModel uses a file system watcher.

Quickshell’s FileView does not get notified of changes because
that’s how sysfs works, but it does handle the content conparison
for us. Convenient!
This commit is contained in:
Mia Herkt
2026-03-20 18:27:47 +01:00
parent e2ba46ed12
commit 4230ae5207
+66 -69
View File
@@ -1,6 +1,8 @@
pragma Singleton
import QtQuick
import QtQml.Models
import Qt.labs.folderlistmodel 2.10
import Quickshell
import Quickshell.Io
import qs.Commons
@@ -36,84 +38,79 @@ Singleton {
// Flag to track if this is the initial check to avoid OSD triggers
property bool initialCheckDone: false
Process {
id: stateCheckProcess
Instantiator {
model: FolderListModel {
id: folderModel
folder: Qt.resolvedUrl("/sys/class/leds")
showFiles: false
showOnlyReadable: true
}
delegate: Component {
FileView {
id: fileView
path: filePath + "/brightness"
onTextChanged: () => {
if (!this.isWanted)
return
if (!this.initialCheckDone) {
this.initialCheckDone = true
return
}
property string checkCommand: " \
caps=0; cat /sys/class/leds/input*::capslock/brightness 2>/dev/null | grep -q 1 && caps=1; echo \"caps:${caps}\"; \
num=0; cat /sys/class/leds/input*::numlock/brightness 2>/dev/null | grep -q 1 && num=1; echo \"num:${num}\"; \
scroll=0; cat /sys/class/leds/input*::scrolllock/brightness 2>/dev/null | grep -q 1 && scroll=1; echo \"scroll:${scroll}\"; \
"
command: ["sh", "-c", stateCheckProcess.checkCommand]
var state = !this.text().startsWith("0")
switch (fileName.split("::")[1]) {
case "numlock":
root.numLockOn = state
root.numLockChanged(state)
Logger.i("LockKeysService", "Num Lock:", state, this.path);
break
case "capslock":
root.capsLockOn = state
root.capsLockChanged(state)
Logger.i("LockKeysService", "Caps Lock:", state, this.path);
break
case "scrolllock":
root.scrollLockOn = state
root.scrollLockChanged(state)
Logger.i("LockKeysService", "Scroll Lock:", state, this.path);
break
}
}
stdout: StdioCollector {
onStreamFinished: {
var lines = this.text.trim().split('\n');
for (var i = 0; i < lines.length; i++) {
var parts = lines[i].split(':');
if (parts.length === 2) {
var key = parts[0];
var newState = (parts[1] === '1');
// FolderListModel only provides filters for file names, not folders
property bool isWanted: {
if (fileName.startsWith("input") && fileName.includes("::")) {
switch (fileName.split("::")[1]) {
case "numlock":
case "capslock":
case "scrolllock":
return true
}
}
Logger.i("LockKeysService", "ignoring:", this.path);
return false
}
if (key === "caps") {
if (root.capsLockOn !== newState) {
root.capsLockOn = newState;
if (root.initialCheckDone) {
root.capsLockChanged(newState);
}
Logger.i("LockKeysService", "Caps Lock:", capsLockOn);
}
} else if (key === "num") {
if (root.numLockOn !== newState) {
root.numLockOn = newState;
if (root.initialCheckDone) {
root.numLockChanged(newState);
}
Logger.i("LockKeysService", "Num Lock:", numLockOn);
}
} else if (key === "scroll") {
if (root.scrollLockOn !== newState) {
root.scrollLockOn = newState;
if (root.initialCheckDone) {
root.scrollLockChanged(newState);
}
Logger.i("LockKeysService", "Scroll Lock:", scrollLockOn);
}
// Skip first OSD event if one fires immediately after enabling
property bool initialCheckDone: false
property variant connections: Connections {
target: root
function onShouldRunChanged() {
if (root.shouldRun) {
this.initialCheckDone = false
}
}
}
// Set initialCheckDone to true after the first check is complete
if (!root.initialCheckDone) {
root.initialCheckDone = true;
// sysfs does not provide change notifications
property variant refreshTimer: Timer {
interval: 200
running: root.shouldRun && fileView.isWanted
repeat: true
onTriggered: fileView.reload()
}
}
}
stderr: StdioCollector {
onStreamFinished: {
if (this.text.trim().length > 0)
Logger.i("LockKeysService", "Error running state check:", this.text.trim());
}
}
}
onShouldRunChanged: {
if (shouldRun) {
// Reset initial check so first poll after re-enable doesn't trigger OSD
root.initialCheckDone = false;
stateCheckProcess.running = true;
}
}
Timer {
id: pollTimer
interval: 200
running: root.shouldRun
repeat: true
onTriggered: {
if (!stateCheckProcess.running) {
stateCheckProcess.running = true;
}
}
}
Component.onCompleted: {