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
|
|
@ -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