| 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: Settings.osdTimeout; 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 | } |