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";
|
||||
}];
|
||||
|
||||
# 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 = {
|
||||
Unit = {
|
||||
Description = "Pre-warm Nemo file manager libraries";
|
||||
|
|
|
|||
|
|
@ -50,7 +50,6 @@ in
|
|||
wl-clipboard
|
||||
cliphist
|
||||
brightnessctl
|
||||
swayosd
|
||||
playerctl
|
||||
hypridle
|
||||
hyprshot
|
||||
|
|
@ -196,18 +195,8 @@ in
|
|||
|
||||
extraConfig =
|
||||
let
|
||||
kbdBrightUp = pkgs.writeShellScript "kbd-bright-up" ''
|
||||
${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"
|
||||
'';
|
||||
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"
|
||||
'';
|
||||
wpctl = "${pkgs.wireplumber}/bin/wpctl";
|
||||
qs = "${pkgs.quickshell}/bin/qs";
|
||||
in
|
||||
''
|
||||
-- Environment
|
||||
|
|
@ -244,7 +233,7 @@ in
|
|||
hl.exec_cmd("wl-paste --type text --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("swayosd-server")
|
||||
-- swayosd removed; volume/brightness OSD handled by quickshell
|
||||
${lib.optionalString isMacbook ''hl.exec_cmd("hypridle")''}
|
||||
end)
|
||||
|
||||
|
|
@ -363,15 +352,18 @@ in
|
|||
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
|
||||
hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
|
||||
|
||||
-- Volume / brightness (repeating)
|
||||
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("swayosd-client --output-volume raise"), { repeating = true })
|
||||
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("swayosd-client --output-volume lower"), { repeating = true })
|
||||
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("swayosd-client --output-volume mute-toggle"), { repeating = true })
|
||||
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("swayosd-client --brightness raise"), { repeating = true })
|
||||
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("swayosd-client --brightness lower"), { repeating = true })
|
||||
-- Volume / brightness (repeating). The OSD overlay is quickshell's
|
||||
-- (see settings/quickshell.nix) — swayosd and wob are both gone.
|
||||
-- Volume needs no IPC: quickshell watches Pipewire, so the OSD
|
||||
-- also shows for changes made in pavucontrol or a mixer.
|
||||
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("${wpctl} set-volume @DEFAULT_AUDIO_SINK@ 5%+ -l 1.0"), { 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 ''
|
||||
hl.bind("XF86KbdBrightnessUp", hl.dsp.exec_cmd("${kbdBrightUp}"), { repeating = true })
|
||||
hl.bind("XF86KbdBrightnessDown", hl.dsp.exec_cmd("${kbdBrightDown}"), { repeating = true })
|
||||
hl.bind("XF86KbdBrightnessUp", hl.dsp.exec_cmd("${qs} ipc call osd kbdUp"), { repeating = true })
|
||||
hl.bind("XF86KbdBrightnessDown", hl.dsp.exec_cmd("${qs} ipc call osd kbdDown"), { repeating = true })
|
||||
''}
|
||||
|
||||
-- Media keys (locked — work through lockscreen)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ in
|
|||
vec4 toast; // cx, cy, hw, hh — toast (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 osd; // cx, cy, hw, hh — volume/brightness OSD (hw <= 0: none)
|
||||
vec4 fillColor; // straight (non-premultiplied) rgba
|
||||
vec4 borderColor;
|
||||
vec2 res;
|
||||
|
|
@ -104,6 +105,8 @@ in
|
|||
d = smin(d, sdRoundedBox(p, session.xy, session.zw, panelR), meltK);
|
||||
if (launcher.z > 0.5)
|
||||
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);
|
||||
// 1 inside the union, 0 outside (antialiased)
|
||||
|
|
@ -463,6 +466,44 @@ in
|
|||
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
|
||||
// daemon just applies the gamma (hyprctl hyprsunset …).
|
||||
// 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,
|
||||
launcherPanel.width / 2, (launchBot - launcherPanel.y) / 2)
|
||||
: 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 borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
|
||||
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) ──
|
||||
Item {
|
||||
id: toastItem
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue