// 05-osd-volume/shell.qml // OSD volume: appears for 1s when the default sink's volume changes. // Pattern: Scope (shared state) + Connections (signal) + Timer (debounce) + LazyLoader (memory-aware) + PanelWindow (default layer). // Run with: qs -p examples/05-osd-volume/shell.qml // Try: `wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.5` in another terminal to trigger. import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Services.Pipewire import Quickshell.Widgets Scope { id: root readonly property var sinkAudio: Pipewire.defaultAudioSink ? Pipewire.defaultAudioSink.audio : null // Keep the default sink tracked. The Connections below needs it to be live. PwObjectTracker { objects: [ Pipewire.defaultAudioSink ] } // Show OSD for 1s after every volume change. Connections { target: root.sinkAudio function onVolumeChanged() { root.shouldShowOsd = true hideTimer.restart() } function onMutedChanged() { root.shouldShowOsd = true hideTimer.restart() } } property bool shouldShowOsd: false Timer { id: hideTimer interval: 1000 onTriggered: root.shouldShowOsd = false } // LazyLoader: only exists when shouldShowOsd. Frees memory between events. // PanelWindow, not FloatingWindow, provides layer-shell placement. LazyLoader { active: root.shouldShowOsd PanelWindow { // The compositor picks the current focused monitor when no screen is set. anchors.bottom: true margins.bottom: screen.height / 5 exclusiveZone: 0 implicitWidth: 400 implicitHeight: 60 color: "transparent" // Empty region = click-through. Clicks pass to whatever's behind. mask: Region {} Rectangle { anchors.fill: parent radius: height / 2 color: "#aa000000" RowLayout { anchors { fill: parent leftMargin: 14 rightMargin: 18 } spacing: 12 IconImage { implicitSize: 28 source: Quickshell.iconPath( root.sinkAudio && root.sinkAudio.muted ? "audio-volume-muted-symbolic" : "audio-volume-high-symbolic" ) } Rectangle { Layout.fillWidth: true implicitHeight: 8 radius: 4 color: "#33ffffff" Rectangle { anchors { left: parent.left top: parent.top bottom: parent.bottom } implicitWidth: parent.width * Math.min(1, root.sinkAudio ? root.sinkAudio.volume : 0) radius: 4 color: "white" } } Text { text: `${Math.floor((root.sinkAudio ? root.sinkAudio.volume : 0) * 100)}%` color: "white" font.pointSize: 13 Layout.preferredWidth: 50 } } } } } }