add volume and brightness control
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user