Krolyxon
Commit 5f6633a

Branches

Refs →

aurora5f6633a

aurora: add volume/mic/brightness OSD

Author
krolyxon
Date
2026-10-17 14:55 +0530
Parent
7822c82
Commit
5f6633a73b1e8af1d9a735e669054d1fef95b30d

+139 −0

3 files changed

  1. M .config/quickshell/aurora/Bright.qml +17 −0
  2. A .config/quickshell/aurora/Osd.qml +110 −0
  3. M .config/quickshell/aurora/shell.qml +12 −0
M .config/quickshell/aurora/Bright.qml View
@@ -10,7 +10,19 @@ Singleton {
1010 readonly property bool available: percent >= 0
1111 property int pending: -1
1212
13+ property bool stepping: false
14+ signal stepped() // a step() finished and percent is fresh (drives the OSD)
15+
1316 function refresh() { getProc.running = true }
17+ // media keys: ~/.local/bin/brictl picks laptop backlight vs ddcutil for the focused monitor;
18+ // plain brightnessctl if the script is missing
19+ function step(dir) {
20+ if (stepProc.running) return
21+ stepProc.command = ["sh", "-c",
22+ '[ -x "$1" ] && exec "$1" "$2"; exec brightnessctl -e4 -n2 set 5%"$2"',
23+ "sh", Quickshell.env("HOME") + "/.local/bin/brictl", dir > 0 ? "+" : "-"]
24+ stepProc.running = true
25+ }
1426 function setPercent(v) {
1527 percent = Math.round(v)
1628 pending = percent
@@ -27,6 +39,10 @@ Singleton {
2739 }
2840 }
2941 Process { id: setProc }
42+ Process {
43+ id: stepProc
44+ onExited: { root.stepping = true; getProc.running = true }
45+ }
3046 Process {
3147 id: getProc
3248 command: ["brightnessctl", "-m"]
@@ -35,6 +51,7 @@ Singleton {
3551 onStreamFinished: {
3652 var p = text.trim().split(",")
3753 if (p.length >= 4) root.percent = parseInt(p[3])
54+ if (root.stepping) { root.stepping = false; root.stepped() }
3855 }
3956 }
4057 }
A .config/quickshell/aurora/Osd.qml View
@@ -0,0 +1,110 @@
1+import QtQuick
2+import Quickshell
3+import Quickshell.Wayland
4+import Quickshell.Services.Pipewire
5+
6+// Volume / mic / brightness popup: bottom-centre, click-through, auto-hides.
7+// Volume + mute changes are picked up natively from Pipewire (any source: keys, wpctl, panel).
8+// Brightness is pushed in via show() from the `bright` IPC call.
9+PanelWindow {
10+ id: win
11+ property bool suppressed: false // hidden while the control panel is open
12+ property bool shown: false
13+ property string icon: ""
14+ property real value: 0
15+ property bool muted: false
16+ property string valueText: ""
17+
18+ function show(icon, value, muted, valueText) {
19+ win.icon = icon
20+ win.value = value
21+ win.muted = muted
22+ win.valueText = valueText !== undefined ? valueText : Math.round(value * 100) + "%"
23+ shown = true
24+ hide.restart()
25+ }
26+
27+ visible: shown && !suppressed
28+ anchors.bottom: true
29+ margins.bottom: 80
30+ implicitWidth: 280
31+ implicitHeight: 40
32+ exclusiveZone: 0
33+ color: "transparent"
34+ mask: Region {}
35+ WlrLayershell.layer: WlrLayer.Overlay
36+ WlrLayershell.namespace: "aurora-osd"
37+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
38+
39+ Timer { id: hide; interval: 1500; onTriggered: win.shown = false }
40+
41+ // ---- pipewire watchers ----
42+ // ignore the burst of change signals on startup and when the default device switches
43+ property bool armed: false
44+ Timer { id: arm; interval: 1000; running: true; onTriggered: win.armed = true }
45+ function rearm() { armed = false; arm.restart() }
46+
47+ readonly property var sink: Pipewire.defaultAudioSink
48+ readonly property var source: Pipewire.defaultAudioSource
49+ onSinkChanged: rearm()
50+ onSourceChanged: rearm()
51+
52+ function showVol() {
53+ var a = sink ? sink.audio : null
54+ if (!armed || !a) return
55+ show(Theme.volIcon(a.volume, a.muted), a.volume, a.muted)
56+ }
57+ Connections {
58+ target: win.sink ? win.sink.audio : null
59+ function onVolumeChanged() { win.showVol() }
60+ function onMutedChanged() { win.showVol() }
61+ }
62+ Connections {
63+ target: win.source ? win.source.audio : null
64+ function onMutedChanged() {
65+ var a = win.source.audio
66+ if (!win.armed) return
67+ win.show(a.muted ? Theme.iMicOff : Theme.iMic, a.volume, a.muted, a.muted ? "muted" : "on")
68+ }
69+ }
70+
71+ // ---- ui ----
72+ Rectangle {
73+ anchors.fill: parent
74+ color: Theme.bgA
75+ border.width: 1
76+ border.color: Theme.accent
77+
78+ Row {
79+ anchors.fill: parent
80+ anchors.leftMargin: 12
81+ anchors.rightMargin: 12
82+ spacing: 10
83+
84+ Text {
85+ anchors.verticalCenter: parent.verticalCenter
86+ width: 20
87+ text: win.icon
88+ color: win.muted ? Theme.dim : Theme.fg
89+ font.family: Theme.font
90+ font.pixelSize: 16
91+ }
92+ BarSlider {
93+ anchors.verticalCenter: parent.verticalCenter
94+ width: parent.width - 20 - 44 - parent.spacing * 2
95+ value: win.value
96+ muted: win.muted
97+ }
98+ Text {
99+ anchors.verticalCenter: parent.verticalCenter
100+ width: 44
101+ horizontalAlignment: Text.AlignRight
102+ text: win.valueText
103+ color: win.muted ? Theme.dim : Theme.fg
104+ font.family: Theme.font
105+ font.pixelSize: Theme.fontSize
106+ font.bold: true
107+ }
108+ }
109+ }
110+}
M .config/quickshell/aurora/shell.qml View
@@ -68,6 +68,17 @@ ShellRoot {
6868
6969 NotifPopups { screen: root.focusedScr; suppressed: root.opened }
7070
71+ Osd { id: osd; screen: root.focusedScr; suppressed: root.opened }
72+ Connections {
73+ target: Bright
74+ // brightnessctl only knows the laptop panel; external monitors go through ddcutil (no readback)
75+ function onStepped() {
76+ var m = Hyprland.focusedMonitor
77+ if (Bright.available && (!m || m.name.indexOf("eDP") === 0))
78+ osd.show(Theme.iBright, Bright.percent / 100, false)
79+ }
80+ }
81+
7182 IpcHandler {
7283 target: "shell"
7384 function toggle(): void { if (root.opened) root.close(); else root.openTab(root.tabNames[root.tab]) }
@@ -77,5 +88,6 @@ ShellRoot {
7788 function dnd(): void { Notifs.dnd = !Notifs.dnd }
7889 function shot(mode: string): void { root.close(); Shot.take(mode) } // region | window | output | ocr
7990 function shotsave(): void { Shot.save = !Shot.save }
91+ function bright(dir: string): void { Bright.step(dir === "down" ? -1 : 1) } // up | down
8092 }
8193 }