Files
att_menu/services/Brightness.qml
T

147 lines
4.8 KiB
QML

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();
}
}
}