aboutsummaryrefslogtreecommitdiff
path: root/modules/system/quickshell/MicInput.qml
diff options
context:
space:
mode:
authorLeander Scherer <leander@schererleander.de>2026-05-30 15:35:27 +0200
committerLeander Scherer <leander@schererleander.de>2026-05-30 15:35:27 +0200
commitd2747e2ca1e211a32e91e44010f40a00e0ac97e4 (patch)
treefb229d6a18541c7a5f1944390b21edde028955f9 /modules/system/quickshell/MicInput.qml
parent51b3cbd50b92d026549ce3ebff17ca9b3344f441 (diff)
feat(quickshell): add popup controls and privacy indicators
Diffstat (limited to 'modules/system/quickshell/MicInput.qml')
-rw-r--r--modules/system/quickshell/MicInput.qml204
1 files changed, 204 insertions, 0 deletions
diff --git a/modules/system/quickshell/MicInput.qml b/modules/system/quickshell/MicInput.qml
new file mode 100644
index 0000000..2a1f653
--- /dev/null
+++ b/modules/system/quickshell/MicInput.qml
@@ -0,0 +1,204 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import Quickshell.Services.Pipewire
+
+Item {
+ id: root
+ width: indicator.visible ? indicator.width : 0
+ height: parent.height
+ visible: root.micInUse
+
+ readonly property PwNode source: Pipewire.defaultAudioSource
+ readonly property bool muted: source && source.audio ? source.audio.muted : true
+ readonly property real volume: source && source.audio ? source.audio.volume : 0
+ readonly property bool micInUse: {
+ const nodes = Pipewire.nodes?.values || [];
+ for (const node of nodes) {
+ if (node && (node.type & PwNodeType.AudioInStream))
+ return true;
+ }
+ return false;
+ }
+
+ onMicInUseChanged: {
+ if (!micInUse && GlobalState.activePopup === "MicInput")
+ GlobalState.close();
+ }
+
+ PwObjectTracker {
+ objects: root.source ? [root.source] : []
+ }
+
+ function setVolume(value) {
+ if (source?.ready && source?.audio) {
+ source.audio.muted = false;
+ source.audio.volume = value;
+ }
+ }
+
+ function toggleMute() {
+ if (source?.ready && source?.audio) {
+ source.audio.muted = !source.audio.muted;
+ }
+ }
+
+ function getDeviceIcon(node) {
+ return node?.properties?.["device.icon-name"] ?? "audio-input-microphone";
+ }
+
+ Squircle {
+ id: indicator
+ anchors.verticalCenter: parent.verticalCenter
+ width: 28
+ height: 22
+ cornerRadius: 8
+ fillColor: "#FFFF9500"
+
+ Image {
+ anchors.centerIn: parent
+ width: 16
+ height: 16
+ source: Quickshell.iconPath(root.muted ? "microphone-sensitivity-muted-symbolic" : "audio-input-microphone-symbolic")
+ sourceSize: Qt.size(width, height)
+ smooth: true
+ mipmap: true
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ cursorShape: Qt.PointingHandCursor
+ onClicked: mouse => {
+ if (mouse.button === Qt.RightButton)
+ root.toggleMute();
+ else
+ GlobalState.toggle("MicInput");
+ }
+ onWheel: wheel => {
+ const step = 0.05;
+ const next = wheel.angleDelta.y > 0 ? root.volume + step : root.volume - step;
+ root.setVolume(Math.max(0.0, Math.min(1.0, next)));
+ }
+ }
+
+ AnchoredPopup {
+ popupName: "MicInput"
+ anchorWindow: barWindow
+ anchorItem: root
+
+ PopupCard {
+ width: 280
+ margins: 14
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: 8
+
+ Text {
+ text: "Microphone"
+ color: Theme.text
+ font {
+ family: Theme.mainFont
+ pixelSize: 14
+ weight: Font.DemiBold
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ Toggle {
+ checked: !root.muted
+ enabled: root.source !== null
+ onToggled: root.toggleMute()
+ }
+ }
+
+ PillSlider {
+ id: micSlider
+ Layout.fillWidth: true
+ icon: "audio-input-microphone-symbolic"
+ value: root.volume
+ enabled: root.source !== null
+ colorProgress: "#FFFF9500"
+ onMoved: root.setVolume(value)
+
+ Binding {
+ target: micSlider
+ property: "value"
+ value: root.volume
+ when: !micSlider.pressed
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Theme.border
+ }
+
+ Text {
+ text: "Input Devices"
+ color: Theme.textMuted
+ font {
+ family: Theme.mainFont
+ pixelSize: 12
+ weight: Font.DemiBold
+ }
+ Layout.leftMargin: 4
+ }
+
+ Repeater {
+ model: Pipewire.nodes
+
+ delegate: Squircle {
+ id: inputItem
+ required property var modelData
+ readonly property bool isInput: modelData && (modelData.type & PwNodeType.AudioSource) && !modelData.isStream && modelData.name !== "Dummy-Driver"
+
+ visible: isInput
+ Layout.fillWidth: true
+ Layout.preferredHeight: visible ? 40 : 0
+ cornerRadius: 6
+ fillColor: inputArea.containsMouse ? Theme.surfaceLighter : Theme.transparent
+
+ RowLayout {
+ anchors {
+ fill: parent
+ margins: 8
+ }
+ spacing: 12
+
+ IconCircle {
+ size: 24
+ source: root.getDeviceIcon(inputItem.modelData)
+ active: root.source && root.source.id === inputItem.modelData.id
+ }
+
+ Text {
+ Layout.fillWidth: true
+ text: inputItem.modelData.description || inputItem.modelData.nickname || inputItem.modelData.name
+ color: (root.source && root.source.id === inputItem.modelData.id) ? Theme.text : Theme.textDim
+ elide: Text.ElideRight
+ font {
+ family: Theme.mainFont
+ pixelSize: 13
+ }
+ }
+ }
+
+ MouseArea {
+ id: inputArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: Pipewire.preferredDefaultAudioSource = inputItem.modelData
+ }
+ }
+ }
+ }
+ }
+}