quickshell: native volume/brightness OSD, drop swayosd + wob
Sixth surface in the SDF chrome shader, melting out of the bottom frame. Volume is reactive via Pipewire so the OSD also fires for pavucontrol/mixer changes; brightness routes through a new osd IPC handler since sysfs backlight only notifies via POLLPRI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3b0133ff3c
commit
efd1331823
3 changed files with 160 additions and 42 deletions
|
|
@ -57,26 +57,6 @@
|
||||||
scale = "auto";
|
scale = "auto";
|
||||||
}];
|
}];
|
||||||
|
|
||||||
# wob reads 0-100 integers from a FIFO and shows a progress bar overlay
|
|
||||||
systemd.user.services.wob = {
|
|
||||||
Unit = {
|
|
||||||
Description = "Wayland OSD Bar";
|
|
||||||
After = [ "graphical-session.target" ];
|
|
||||||
PartOf = [ "graphical-session.target" ];
|
|
||||||
};
|
|
||||||
Service = {
|
|
||||||
Type = "simple";
|
|
||||||
ExecStart = "${pkgs.writeShellScript "wob-start" ''
|
|
||||||
rm -f "%t/wob.fifo"
|
|
||||||
mkfifo "%t/wob.fifo"
|
|
||||||
tail -f "%t/wob.fifo" | ${pkgs.wob}/bin/wob
|
|
||||||
''}";
|
|
||||||
ExecStopPost = "${pkgs.coreutils}/bin/rm -f %t/wob.fifo";
|
|
||||||
Restart = "on-failure";
|
|
||||||
};
|
|
||||||
Install.WantedBy = [ "graphical-session.target" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.user.services.nemo-warmup = {
|
systemd.user.services.nemo-warmup = {
|
||||||
Unit = {
|
Unit = {
|
||||||
Description = "Pre-warm Nemo file manager libraries";
|
Description = "Pre-warm Nemo file manager libraries";
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,6 @@ in
|
||||||
wl-clipboard
|
wl-clipboard
|
||||||
cliphist
|
cliphist
|
||||||
brightnessctl
|
brightnessctl
|
||||||
swayosd
|
|
||||||
playerctl
|
playerctl
|
||||||
hypridle
|
hypridle
|
||||||
hyprshot
|
hyprshot
|
||||||
|
|
@ -196,18 +195,8 @@ in
|
||||||
|
|
||||||
extraConfig =
|
extraConfig =
|
||||||
let
|
let
|
||||||
kbdBrightUp = pkgs.writeShellScript "kbd-bright-up" ''
|
wpctl = "${pkgs.wireplumber}/bin/wpctl";
|
||||||
${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight set +10%
|
qs = "${pkgs.quickshell}/bin/qs";
|
||||||
brightness=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight get)
|
|
||||||
max=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight max)
|
|
||||||
echo $(( brightness * 100 / max )) > "$XDG_RUNTIME_DIR/wob.fifo"
|
|
||||||
'';
|
|
||||||
kbdBrightDown = pkgs.writeShellScript "kbd-bright-down" ''
|
|
||||||
${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight set 10%-
|
|
||||||
brightness=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight get)
|
|
||||||
max=$(${pkgs.brightnessctl}/bin/brightnessctl -d smc::kbd_backlight max)
|
|
||||||
echo $(( brightness * 100 / max )) > "$XDG_RUNTIME_DIR/wob.fifo"
|
|
||||||
'';
|
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
-- Environment
|
-- Environment
|
||||||
|
|
@ -244,7 +233,7 @@ in
|
||||||
hl.exec_cmd("wl-paste --type text --watch cliphist store")
|
hl.exec_cmd("wl-paste --type text --watch cliphist store")
|
||||||
hl.exec_cmd("wl-paste --type image --watch cliphist store")
|
hl.exec_cmd("wl-paste --type image --watch cliphist store")
|
||||||
hl.exec_cmd("hyprctl setcursor Bibata-Modern-Ice 24")
|
hl.exec_cmd("hyprctl setcursor Bibata-Modern-Ice 24")
|
||||||
hl.exec_cmd("swayosd-server")
|
-- swayosd removed; volume/brightness OSD handled by quickshell
|
||||||
${lib.optionalString isMacbook ''hl.exec_cmd("hypridle")''}
|
${lib.optionalString isMacbook ''hl.exec_cmd("hypridle")''}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -363,15 +352,18 @@ in
|
||||||
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
|
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
|
||||||
hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
|
hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
|
||||||
|
|
||||||
-- Volume / brightness (repeating)
|
-- Volume / brightness (repeating). The OSD overlay is quickshell's
|
||||||
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("swayosd-client --output-volume raise"), { repeating = true })
|
-- (see settings/quickshell.nix) — swayosd and wob are both gone.
|
||||||
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("swayosd-client --output-volume lower"), { repeating = true })
|
-- Volume needs no IPC: quickshell watches Pipewire, so the OSD
|
||||||
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("swayosd-client --output-volume mute-toggle"), { repeating = true })
|
-- also shows for changes made in pavucontrol or a mixer.
|
||||||
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("swayosd-client --brightness raise"), { repeating = true })
|
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("${wpctl} set-volume @DEFAULT_AUDIO_SINK@ 5%+ -l 1.0"), { repeating = true })
|
||||||
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("swayosd-client --brightness lower"), { repeating = true })
|
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("${wpctl} set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { repeating = true })
|
||||||
|
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("${wpctl} set-mute @DEFAULT_AUDIO_SINK@ toggle"))
|
||||||
|
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("${qs} ipc call osd brightnessUp"), { repeating = true })
|
||||||
|
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("${qs} ipc call osd brightnessDown"), { repeating = true })
|
||||||
${lib.optionalString isMacbook ''
|
${lib.optionalString isMacbook ''
|
||||||
hl.bind("XF86KbdBrightnessUp", hl.dsp.exec_cmd("${kbdBrightUp}"), { repeating = true })
|
hl.bind("XF86KbdBrightnessUp", hl.dsp.exec_cmd("${qs} ipc call osd kbdUp"), { repeating = true })
|
||||||
hl.bind("XF86KbdBrightnessDown", hl.dsp.exec_cmd("${kbdBrightDown}"), { repeating = true })
|
hl.bind("XF86KbdBrightnessDown", hl.dsp.exec_cmd("${qs} ipc call osd kbdDown"), { repeating = true })
|
||||||
''}
|
''}
|
||||||
|
|
||||||
-- Media keys (locked — work through lockscreen)
|
-- Media keys (locked — work through lockscreen)
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ in
|
||||||
vec4 toast; // cx, cy, hw, hh — toast (hw <= 0: none)
|
vec4 toast; // cx, cy, hw, hh — toast (hw <= 0: none)
|
||||||
vec4 session; // cx, cy, hw, hh — session menu (hw <= 0: none)
|
vec4 session; // cx, cy, hw, hh — session menu (hw <= 0: none)
|
||||||
vec4 launcher; // cx, cy, hw, hh — bottom launcher (hw <= 0: none)
|
vec4 launcher; // cx, cy, hw, hh — bottom launcher (hw <= 0: none)
|
||||||
|
vec4 osd; // cx, cy, hw, hh — volume/brightness OSD (hw <= 0: none)
|
||||||
vec4 fillColor; // straight (non-premultiplied) rgba
|
vec4 fillColor; // straight (non-premultiplied) rgba
|
||||||
vec4 borderColor;
|
vec4 borderColor;
|
||||||
vec2 res;
|
vec2 res;
|
||||||
|
|
@ -104,6 +105,8 @@ in
|
||||||
d = smin(d, sdRoundedBox(p, session.xy, session.zw, panelR), meltK);
|
d = smin(d, sdRoundedBox(p, session.xy, session.zw, panelR), meltK);
|
||||||
if (launcher.z > 0.5)
|
if (launcher.z > 0.5)
|
||||||
d = smin(d, sdRoundedBox(p, launcher.xy, launcher.zw, panelR), meltK);
|
d = smin(d, sdRoundedBox(p, launcher.xy, launcher.zw, panelR), meltK);
|
||||||
|
if (osd.z > 0.5)
|
||||||
|
d = smin(d, sdRoundedBox(p, osd.xy, osd.zw, panelR), meltK);
|
||||||
|
|
||||||
float fw = fwidth(d);
|
float fw = fwidth(d);
|
||||||
// 1 inside the union, 0 outside (antialiased)
|
// 1 inside the union, 0 outside (antialiased)
|
||||||
|
|
@ -463,6 +466,44 @@ in
|
||||||
function unpin(): void { if (root.mainBar) root.mainBar.setScreenshotPin(false); }
|
function unpin(): void { if (root.mainBar) root.mainBar.setScreenshotPin(false); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── OSD brightness (replaces swayosd). Volume needs nothing
|
||||||
|
// here — the bar watches Pipewire directly. Backlight has no
|
||||||
|
// watchable change event, so the keybind routes through here:
|
||||||
|
// brightnessctl -m prints the resulting level, which becomes
|
||||||
|
// the OSD value. Bars pick this up on the focused monitor.
|
||||||
|
signal osdBrightness(string icon, real value)
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: brightProc
|
||||||
|
property string osdIcon: "brightness_6"
|
||||||
|
stdout: StdioCollector {
|
||||||
|
// machine-readable: device,class,current,percent%,max
|
||||||
|
onStreamFinished: {
|
||||||
|
let f = text.trim().split("\n")[0].split(",");
|
||||||
|
if (f.length < 5) return;
|
||||||
|
root.osdBrightness(brightProc.osdIcon, parseInt(f[3]) / 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setBrightness(args, icon) {
|
||||||
|
// Key repeat outpaces the process; dropping a step is
|
||||||
|
// invisible, rewriting a live command is not.
|
||||||
|
if (brightProc.running) return;
|
||||||
|
brightProc.osdIcon = icon;
|
||||||
|
brightProc.command = ["brightnessctl", "-m"].concat(args);
|
||||||
|
brightProc.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
IpcHandler {
|
||||||
|
target: "osd"
|
||||||
|
// floor 1%: `set 0%` turns the panel fully off
|
||||||
|
function brightnessUp(): void { root.setBrightness(["set", "5%+"], "brightness_6"); }
|
||||||
|
function brightnessDown(): void { root.setBrightness(["-n", "1", "set", "5%-"], "brightness_6"); }
|
||||||
|
function kbdUp(): void { root.setBrightness(["-d", "smc::kbd_backlight", "set", "10%+"], "keyboard"); }
|
||||||
|
function kbdDown(): void { root.setBrightness(["-d", "smc::kbd_backlight", "set", "10%-"], "keyboard"); }
|
||||||
|
}
|
||||||
|
|
||||||
// ── Night light: quickshell owns the schedule, the hyprsunset
|
// ── Night light: quickshell owns the schedule, the hyprsunset
|
||||||
// daemon just applies the gamma (hyprctl hyprsunset …).
|
// daemon just applies the gamma (hyprctl hyprsunset …).
|
||||||
// sunrise/sunset default to 7:00/21:00 and are replaced with
|
// sunrise/sunset default to 7:00/21:00 and are replaced with
|
||||||
|
|
@ -946,6 +987,11 @@ in
|
||||||
? Qt.vector4d(launcherPanel.x + launcherPanel.width / 2, (launcherPanel.y + launchBot) / 2,
|
? Qt.vector4d(launcherPanel.x + launcherPanel.width / 2, (launcherPanel.y + launchBot) / 2,
|
||||||
launcherPanel.width / 2, (launchBot - launcherPanel.y) / 2)
|
launcherPanel.width / 2, (launchBot - launcherPanel.y) / 2)
|
||||||
: Qt.vector4d(0, 0, 0, 0)
|
: Qt.vector4d(0, 0, 0, 0)
|
||||||
|
// OSD shares the launcher's bottom melt line.
|
||||||
|
property vector4d osd: osdItem.visible
|
||||||
|
? Qt.vector4d(osdItem.x + osdItem.width / 2, (osdItem.y + launchBot) / 2,
|
||||||
|
osdItem.width / 2, (launchBot - osdItem.y) / 2)
|
||||||
|
: Qt.vector4d(0, 0, 0, 0)
|
||||||
property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a)
|
property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a)
|
||||||
property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
|
property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
|
||||||
property vector2d res: Qt.vector2d(width, height)
|
property vector2d res: Qt.vector2d(width, height)
|
||||||
|
|
@ -3187,6 +3233,106 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── OSD: volume / brightness overlay melting out of the
|
||||||
|
// bottom frame, on the focused monitor only. Volume is
|
||||||
|
// reactive — Pipewire reports changes from any source (media
|
||||||
|
// keys, pavucontrol, a hardware mixer), so no IPC is needed.
|
||||||
|
// Brightness arrives via shellRoot's signal instead: sysfs
|
||||||
|
// backlight notifies with POLLPRI, which inotify never sees.
|
||||||
|
// Deliberately absent from the window `mask` above, unlike
|
||||||
|
// every other surface — a transient overlay must not eat
|
||||||
|
// clicks on whatever it covers.
|
||||||
|
Item {
|
||||||
|
id: osdItem
|
||||||
|
readonly property bool isFocused: Hyprland.focusedMonitor
|
||||||
|
&& Hyprland.focusedMonitor.name === bar.screen.name
|
||||||
|
// Every binding evaluates once at load; arm on a delay so
|
||||||
|
// a shell restart doesn't flash an OSD on screen.
|
||||||
|
property bool armed: false
|
||||||
|
property bool shown: false
|
||||||
|
property string icon: "volume_up"
|
||||||
|
property real value: 0
|
||||||
|
property color fill: Theme.base0D
|
||||||
|
|
||||||
|
width: 260
|
||||||
|
height: shown ? 52 : 0
|
||||||
|
x: Math.round((bar.width - width) / 2)
|
||||||
|
y: bar.height - Theme.frameWidth - height
|
||||||
|
visible: height > 0.5
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(ic, v, f) {
|
||||||
|
if (!armed || !isFocused) return;
|
||||||
|
icon = ic;
|
||||||
|
value = v;
|
||||||
|
fill = f;
|
||||||
|
shown = true;
|
||||||
|
osdTimer.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer { id: osdTimer; interval: 1600; onTriggered: osdItem.shown = false }
|
||||||
|
Timer { id: osdArm; interval: 1500; running: true; onTriggered: osdItem.armed = true }
|
||||||
|
|
||||||
|
function showVolume() {
|
||||||
|
// The volume dropdown already shows the level live.
|
||||||
|
if (bar.activeDropdown === volDropdown) return;
|
||||||
|
show(volWidget.volIcon, volWidget.vol / 100,
|
||||||
|
volWidget.muted ? Theme.base03 : Theme.base0D);
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: volWidget.sink && volWidget.sink.audio ? volWidget.sink.audio : null
|
||||||
|
// `volume` notifies through volumesChanged, not
|
||||||
|
// volumeChanged — it is a view over the per-channel
|
||||||
|
// volumes vector.
|
||||||
|
function onVolumesChanged() { osdItem.showVolume(); }
|
||||||
|
function onMutedChanged() { osdItem.showVolume(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: bar.shellRoot
|
||||||
|
function onOsdBrightness(icon, value) {
|
||||||
|
osdItem.show(icon, value, Theme.base0A);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - 4 * Theme.panelGap
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
SIcon {
|
||||||
|
id: osdIcon
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 22
|
||||||
|
text: osdItem.icon
|
||||||
|
color: Theme.base05
|
||||||
|
font.pixelSize: 20
|
||||||
|
}
|
||||||
|
|
||||||
|
PillSlider {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - osdIcon.width - osdPct.width - 2 * parent.spacing
|
||||||
|
value: osdItem.value
|
||||||
|
fillColor: osdItem.fill
|
||||||
|
}
|
||||||
|
|
||||||
|
SText {
|
||||||
|
id: osdPct
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 34
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: Math.round(osdItem.value * 100) + "%"
|
||||||
|
color: Theme.base04
|
||||||
|
font.pixelSize: 12
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Notification Toast (only on primary screen) ──
|
// ── Notification Toast (only on primary screen) ──
|
||||||
Item {
|
Item {
|
||||||
id: toastItem
|
id: toastItem
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue