pragma Singleton import Quickshell import Quickshell.Io import QtQuick import qs.config // The display backlight, driven through brightnessctl. // // Writing to /sys/class/backlight needs privileges a session usually does not // have, so the write goes through brightnessctl, which asks logind when it // cannot open the file itself. Reads do not: actual_brightness is world // readable, so the current value comes straight off sysfs rather than from a // process spawned every couple of seconds. // // sysfs attributes do not raise inotify events, so `refresh()` is polled by // whoever is displaying the value — only the controls panel, and only while it // is on screen. Nothing here runs with the panel closed. Singleton { id: root // Device name as brightnessctl knows it, e.g. "intel_backlight", or "" on a // machine with no backlight — a desktop, or a laptop whose panel is not // controllable. Without one there is no brightness row. property string device: "" property int max: 0 property int current: 0 readonly property bool available: Config.showBrightness && root.device !== "" && root.max > 0 readonly property real value: root.max > 0 ? root.current / root.max : 0 // Never all the way off: a backlight at zero is indistinguishable from a // broken panel, and on most laptops the only way back is the hardware key. readonly property int minimum: Math.max(1, Math.round(root.max * Config.minBrightness)) // What was last asked for, and whether a request landed while the previous // one was still in flight. Dragging the slider produces far more requests // than brightnessctl can service, so they coalesce onto the newest. property int pending: 0 property bool queued: false function set(fraction: real): void { if (!root.available) return; const raw = Math.max(root.minimum, Math.min(root.max, Math.round(fraction * root.max))); if (raw === root.current && !write.running) return; // Optimistic, like the keyboard button: the slider has to track the // finger, not the process. The next refresh overwrites this with what // the hardware actually did. root.current = raw; root.pending = raw; if (write.running) root.queued = true; else root.dispatch(); } function step(delta: real): void { root.set(root.value + delta); } function dispatch(): void { write.command = ["brightnessctl", "-m", "-q", "-c", "backlight", "-d", root.device, "set", String(root.pending)]; write.running = true; } function refresh(): void { // A write in flight is the newer truth: sysfs still holds the value the // drag has already moved past. if (root.available && !write.running && !root.queued) actual.reload(); } // `brightnessctl -m` prints one CSV line per device: // device,class,current,percent,max function parseInfo(text: string): void { const fields = text.trim().split("\n")[0].split(","); if (fields.length < 5) return; const max = parseInt(fields[4]); if (!(max > 0)) return; root.device = fields[0]; root.max = max; root.current = parseInt(fields[2]) || 0; } Process { id: write } // Guarded by `command -v` so a machine without brightnessctl gets a quiet // "no backlight" rather than a failed spawn on every start. Process { running: Config.showBrightness command: ["sh", "-c", "command -v brightnessctl >/dev/null 2>&1 && brightnessctl -m -c backlight || true"] stdout: StdioCollector { onStreamFinished: root.parseInfo(this.text) } } // actual_brightness, not brightness: what the hardware settled on, which is // also what the hardware keys move. FileView { id: actual path: root.available ? `/sys/class/backlight/${root.device}/actual_brightness` : "" printErrors: false onLoaded: { if (write.running || root.queued) return; const value = parseInt(this.text()); if (value >= 0) root.current = value; } } Connections { target: write function onExited(exitCode: int, exitStatus: int): void { if (root.queued) { root.queued = false; root.dispatch(); return; } // What the hardware settled on, which is the value the next step // has to count from — and, when the write was refused (no logind, // or a device that will not take the value), the number that goes // back under the slider. actual.reload(); } } }