diff --git a/hosts/FredOS-Macbook.nix b/hosts/FredOS-Macbook.nix index 7d075db..e5a2dd8 100644 --- a/hosts/FredOS-Macbook.nix +++ b/hosts/FredOS-Macbook.nix @@ -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"; diff --git a/settings/hyprland.nix b/settings/hyprland.nix index 236e588..730773c 100644 --- a/settings/hyprland.nix +++ b/settings/hyprland.nix @@ -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) diff --git a/settings/quickshell.nix b/settings/quickshell.nix index a5ff2fe..2b0a611 100644 --- a/settings/quickshell.nix +++ b/settings/quickshell.nix @@ -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