147 lines
4.8 KiB
QML
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();
|
|
}
|
|
}
|
|
}
|