diff --git a/quickshell/Common/KeybindActions.js b/quickshell/Common/KeybindActions.js index fcfacacf..afd0be4e 100644 --- a/quickshell/Common/KeybindActions.js +++ b/quickshell/Common/KeybindActions.js @@ -59,6 +59,7 @@ const DMS_ACTIONS = [ { id: "spawn dms ipc call audio decrement 10", label: "Volume Down (10%)" }, { id: "spawn dms ipc call audio mute", label: "Volume Mute Toggle" }, { id: "spawn dms ipc call audio micmute", label: "Microphone Mute Toggle" }, + { id: "spawn dms ipc call audio cycleoutput", label: "Audio Output: Cycle" }, { id: "spawn dms ipc call brightness increment", label: "Brightness Up" }, { id: "spawn dms ipc call brightness increment 1", label: "Brightness Up (1%)" }, { id: "spawn dms ipc call brightness increment 5", label: "Brightness Up (5%)" }, diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 80279ac7..2bcd81e7 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -313,6 +313,7 @@ Singleton { property bool osdMicMuteEnabled: true property bool osdCapsLockEnabled: true property bool osdPowerProfileEnabled: true + property bool osdAudioOutputEnabled: true property bool powerActionConfirm: true property int powerActionHoldDuration: 1 diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 5e559b30..535226c8 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -212,6 +212,7 @@ var SPEC = { osdMicMuteEnabled: { def: true }, osdCapsLockEnabled: { def: true }, osdPowerProfileEnabled: { def: false }, + osdAudioOutputEnabled: { def: true }, powerActionConfirm: { def: true }, powerActionHoldDuration: { def: 1 }, diff --git a/quickshell/DMSShell.qml b/quickshell/DMSShell.qml index 19016109..4d3a428f 100644 --- a/quickshell/DMSShell.qml +++ b/quickshell/DMSShell.qml @@ -700,6 +700,14 @@ Item { } } + Variants { + model: SettingsData.getFilteredScreens("osd") + + delegate: AudioOutputOSD { + modelData: item + } + } + LazyLoader { id: hyprlandOverviewLoader active: CompositorService.isHyprland diff --git a/quickshell/Modules/OSD/AudioOutputOSD.qml b/quickshell/Modules/OSD/AudioOutputOSD.qml new file mode 100644 index 00000000..33a36acb --- /dev/null +++ b/quickshell/Modules/OSD/AudioOutputOSD.qml @@ -0,0 +1,80 @@ +import QtQuick +import qs.Common +import qs.Services +import qs.Widgets + +DankOSD { + id: root + + property string deviceName: "" + property string deviceIcon: "speaker" + + osdWidth: Math.min(Math.max(120, Theme.iconSize + textMetrics.width + Theme.spacingS * 4), Screen.width - Theme.spacingM * 2) + osdHeight: 40 + Theme.spacingS * 2 + autoHideInterval: 2500 + enableMouseInteraction: false + + TextMetrics { + id: textMetrics + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + font.family: Theme.fontFamily + text: root.deviceName + } + + function getIconForSink(sink) { + if (!sink) + return "speaker"; + const name = sink.name || ""; + if (name.includes("bluez")) + return "headset"; + if (name.includes("hdmi")) + return "tv"; + if (name.includes("usb")) + return "headset"; + return "speaker"; + } + + Connections { + target: AudioService + + function onAudioOutputCycled(name) { + if (!SettingsData.osdAudioOutputEnabled) + return; + root.deviceName = name; + root.deviceIcon = getIconForSink(AudioService.sink); + root.show(); + } + } + + content: Item { + property int gap: Theme.spacingS + + anchors.centerIn: parent + width: parent.width - Theme.spacingS * 2 + height: 40 + + DankIcon { + id: iconItem + width: Theme.iconSize + height: Theme.iconSize + x: parent.gap + anchors.verticalCenter: parent.verticalCenter + name: root.deviceIcon + size: Theme.iconSize + color: Theme.primary + } + + StyledText { + id: textItem + x: parent.gap * 2 + Theme.iconSize + width: parent.width - Theme.iconSize - parent.gap * 3 + anchors.verticalCenter: parent.verticalCenter + text: root.deviceName + font.pixelSize: Theme.fontSizeMedium + font.weight: Font.Medium + color: Theme.surfaceText + elide: Text.ElideRight + } + } +} diff --git a/quickshell/Modules/Settings/OSDTab.qml b/quickshell/Modules/Settings/OSDTab.qml index 878ce0fa..be4cee6e 100644 --- a/quickshell/Modules/Settings/OSDTab.qml +++ b/quickshell/Modules/Settings/OSDTab.qml @@ -141,6 +141,13 @@ Item { checked: SettingsData.osdPowerProfileEnabled onToggled: checked => SettingsData.set("osdPowerProfileEnabled", checked) } + + SettingsToggleRow { + text: I18n.tr("Audio Output Switch") + description: I18n.tr("Show on-screen display when cycling audio output devices") + checked: SettingsData.osdAudioOutputEnabled + onToggled: checked => SettingsData.set("osdAudioOutputEnabled", checked) + } } } } diff --git a/quickshell/Services/AudioService.qml b/quickshell/Services/AudioService.qml index 1e60b1de..3dff34f6 100644 --- a/quickshell/Services/AudioService.qml +++ b/quickshell/Services/AudioService.qml @@ -30,6 +30,33 @@ Singleton { property var mediaDevicesConnections: null signal micMuteChanged + signal audioOutputCycled(string deviceName) + + function getAvailableSinks() { + return Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream); + } + + function cycleAudioOutput() { + const sinks = getAvailableSinks(); + if (sinks.length < 2) + return null; + + const currentSink = root.sink; + let currentIndex = -1; + for (let i = 0; i < sinks.length; i++) { + if (sinks[i] === currentSink) { + currentIndex = i; + break; + } + } + + const nextIndex = (currentIndex + 1) % sinks.length; + const nextSink = sinks[nextIndex]; + Pipewire.preferredDefaultAudioSink = nextSink; + const name = displayName(nextSink); + audioOutputCycled(name); + return name; + } Connections { target: root.sink?.audio ?? null @@ -595,6 +622,13 @@ Singleton { return result; } + + function cycleoutput(): string { + const result = root.cycleAudioOutput(); + if (!result) + return "Only one audio output available"; + return `Switched to: ${result}`; + } } Connections {