summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregor Kleen <gkleen@yggdrasil.li>2025-09-08 20:00:22 +0200
committerGregor Kleen <gkleen@yggdrasil.li>2025-09-08 20:00:22 +0200
commit8a551339cbfaf106ac7d6f1ca5230196be539167 (patch)
tree20dd3cb9f12dd94fa22fcd2d866c3cf586f8931a
parent14d4d05acc235ab7033316d16530783c90e95faa (diff)
downloadnixos-8a551339cbfaf106ac7d6f1ca5230196be539167.tar
nixos-8a551339cbfaf106ac7d6f1ca5230196be539167.tar.gz
nixos-8a551339cbfaf106ac7d6f1ca5230196be539167.tar.bz2
nixos-8a551339cbfaf106ac7d6f1ca5230196be539167.tar.xz
nixos-8a551339cbfaf106ac7d6f1ca5230196be539167.zip
...
-rw-r--r--accounts/gkleen@sif/shell/default.nix6
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt24
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp18
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp21
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp16
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp13
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/default.nix8
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml7
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Clock.qml3
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Lockscreen.qml15
-rw-r--r--accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml24
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml354
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml18
-rw-r--r--accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml129
-rw-r--r--accounts/gkleen@sif/shell/quickshell/shell.qml2
-rw-r--r--overlays/quickshell/default.nix2
-rw-r--r--overlays/quickshell/lock-state-changed.patch12
-rw-r--r--overlays/quickshell/pipewire.patch460
-rw-r--r--user-profiles/yt-dlp.nix4
19 files changed, 1130 insertions, 6 deletions
diff --git a/accounts/gkleen@sif/shell/default.nix b/accounts/gkleen@sif/shell/default.nix
index 85e034d6..5025dd90 100644
--- a/accounts/gkleen@sif/shell/default.nix
+++ b/accounts/gkleen@sif/shell/default.nix
@@ -96,6 +96,12 @@
96 # "${config.programs.niri.package}/share/wayland-sessions/niri.desktop" 96 # "${config.programs.niri.package}/share/wayland-sessions/niri.desktop"
97 ]; 97 ];
98 username = builtins.toJSON config.home.username; 98 username = builtins.toJSON config.home.username;
99 mdi = builtins.toJSON (pkgs.fetchFromGitHub {
100 owner = "Templarian";
101 repo = "MaterialDesign";
102 rev = "2424e748e0cc63ab7b9c095a099b9fe239b737c0";
103 hash = "sha256-QMGl7soAhErrrnY3aKOZpt49yebkSNzy10p/v5OaqQ0=";
104 });
99 }; 105 };
100 }; 106 };
101 }; 107 };
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
index 2123ed35..a7e88fa7 100644
--- a/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
@@ -92,7 +92,7 @@ endfunction()
92cmake_minimum_required(VERSION 3.20) 92cmake_minimum_required(VERSION 3.20)
93project(custom LANGUAGES CXX) 93project(custom LANGUAGES CXX)
94 94
95find_package(Qt6 REQUIRED COMPONENTS Core Qml) 95find_package(Qt6 REQUIRED COMPONENTS Core Qml DBus)
96 96
97qt_standard_project_setup(REQUIRES 6.6) 97qt_standard_project_setup(REQUIRES 6.6)
98 98
@@ -102,16 +102,32 @@ qt6_add_qml_module(customplugin
102 PLUGIN_TARGET customplugin 102 PLUGIN_TARGET customplugin
103) 103)
104 104
105target_sources(customplugin PRIVATE 105set_source_files_properties(org.keepassxc.KeePassXC.MainWindow.xml PROPERTIES
106 Chrono.cpp Chrono.hpp 106 CLASSNAME DBusKeePassXC
107 FileSelector.cpp FileSelector.hpp 107 NO_NAMESPACE TRUE
108) 108)
109 109
110qt_add_dbus_interface(DBUS_INTERFACES
111 org.keepassxc.KeePassXC.MainWindow.xml
112 dbus_keepassxc
113)
114
115include_directories(${CMAKE_SOURCE_DIR}/build)
116
110target_compile_features(customplugin PUBLIC cxx_std_26) 117target_compile_features(customplugin PUBLIC cxx_std_26)
111 118
112target_link_libraries(customplugin PRIVATE 119target_link_libraries(customplugin PRIVATE
113 Qt6::Core 120 Qt6::Core
114 Qt6::Qml 121 Qt6::Qml
122 Qt6::DBus
123)
124
125target_sources(customplugin PRIVATE
126 Chrono.cpp Chrono.hpp
127 FileSelector.cpp FileSelector.hpp
128 KeePassXC.cpp KeePassXC.hpp
129 Systemd.cpp Systemd.hpp
130 ${DBUS_INTERFACES}
115) 131)
116 132
117install_qml_module(customplugin) 133install_qml_module(customplugin)
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp
new file mode 100644
index 00000000..f6e4dd6e
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp
@@ -0,0 +1,18 @@
1#include "KeePassXC.hpp"
2
3#include <QDBusConnection>
4
5KeePassXC::KeePassXC() {
6 this->service = new DBusKeePassXC(DBusKeePassXC::staticInterfaceName(), "/keepassxc", QDBusConnection::sessionBus(), this);
7}
8KeePassXC::~KeePassXC() {
9 if (this->service)
10 delete this->service;
11}
12
13void KeePassXC::lockAllDatabases() {
14 if (!this->service)
15 return;
16
17 this->service->lockAllDatabases();
18}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp
new file mode 100644
index 00000000..c4cd71e0
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp
@@ -0,0 +1,21 @@
1#pragma once
2
3#include "dbus_keepassxc.h"
4
5#include <QObject>
6#include <QtQmlIntegration/qqmlintegration.h>
7
8class KeePassXC : public QObject {
9 Q_OBJECT;
10 QML_SINGLETON;
11 QML_ELEMENT;
12
13public:
14 explicit KeePassXC();
15 ~KeePassXC();
16
17 Q_INVOKABLE void lockAllDatabases();
18
19private:
20 DBusKeePassXC* service = nullptr;
21};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp
new file mode 100644
index 00000000..9ccd8ba0
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp
@@ -0,0 +1,16 @@
1#include "Systemd.hpp"
2
3#include <QDBusConnection>
4#include <QDBusMessage>
5
6void Systemd::stopUserUnit(const QString& unit, const QString& mode) {
7 QDBusMessage m = QDBusMessage::createMethodCall(
8 "org.freedesktop.systemd1",
9 "/org/freedesktop/systemd1",
10 "org.freedesktop.systemd1.Manager",
11 "StopUnit"
12 );
13 m << unit;
14 m << mode;
15 QDBusConnection::sessionBus().send(m);
16}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp
new file mode 100644
index 00000000..883a96f3
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp
@@ -0,0 +1,13 @@
1#pragma once
2
3#include <QObject>
4#include <QtQmlIntegration/qqmlintegration.h>
5
6class Systemd : public QObject {
7 Q_OBJECT;
8 QML_SINGLETON;
9 QML_ELEMENT;
10
11public:
12 Q_INVOKABLE void stopUserUnit(const QString& unit, const QString& mode);
13};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/default.nix b/accounts/gkleen@sif/shell/quickshell-plugins/default.nix
index fafea90e..33b76f61 100644
--- a/accounts/gkleen@sif/shell/quickshell-plugins/default.nix
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/default.nix
@@ -3,11 +3,19 @@
3, cmake 3, cmake
4, qt6 4, qt6
5, fmt 5, fmt
6, keepassxc
7, systemd
6}: 8}:
9
7stdenv.mkDerivation rec { 10stdenv.mkDerivation rec {
8 name = "quickshell-custom"; 11 name = "quickshell-custom";
9 12
10 src = ./.; 13 src = ./.;
14
15 prePatch = ''
16 cp ${keepassxc.src}/src/gui/org.keepassxc.KeePassXC.MainWindow.xml .
17 '';
18
11 nativeBuildInputs = [ cmake qt6.wrapQtAppsHook ]; 19 nativeBuildInputs = [ cmake qt6.wrapQtAppsHook ];
12 buildInputs = [ 20 buildInputs = [
13 qt6.qtbase 21 qt6.qtbase
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
index 38225d74..3652af54 100644
--- a/accounts/gkleen@sif/shell/quickshell/Bar.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -64,6 +64,13 @@ PanelWindow {
64 anchors.verticalCenter: parent.verticalCenter 64 anchors.verticalCenter: parent.verticalCenter
65 spacing: 0 65 spacing: 0
66 66
67 PipewireWidget {}
68
69 Item {
70 height: parent.height
71 width: 4
72 }
73
67 SystemTray {} 74 SystemTray {}
68 75
69 Item { 76 Item {
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml
index 382af168..4644d5e7 100644
--- a/accounts/gkleen@sif/shell/quickshell/Clock.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml
@@ -99,9 +99,10 @@ Item {
99 id: clockTooltipContent 99 id: clockTooltipContent
100 100
101 margin: 8 101 margin: 8
102 leftMargin: 0
103 102
104 ColumnLayout { 103 ColumnLayout {
104 anchors.centerIn: parent
105
105 Text { 106 Text {
106 id: yearLabel 107 id: yearLabel
107 108
diff --git a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
index cc82a275..8e739359 100644
--- a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
@@ -2,6 +2,9 @@ import Quickshell
2import Quickshell.Wayland 2import Quickshell.Wayland
3import Quickshell.Io 3import Quickshell.Io
4import Quickshell.Services.Pam 4import Quickshell.Services.Pam
5import Quickshell.Services.Mpris
6import Custom as Custom
7import qs.Services
5import QtQml 8import QtQml
6 9
7Scope { 10Scope {
@@ -38,9 +41,19 @@ Scope {
38 WlSessionLock { 41 WlSessionLock {
39 id: lock 42 id: lock
40 43
41 onLockedChanged: { 44 onLockStateChanged: {
42 if (!locked && pam.active) 45 if (!locked && pam.active)
43 pam.abort(); 46 pam.abort();
47
48 if (locked) {
49 Custom.KeePassXC.lockAllDatabases();
50 Array.from(Mpris.players.values).forEach(player => {
51 if (player.canPause && player.isPlaying)
52 player.pause();
53 });
54 // Custom.Systemd.stopUserUnit("gpg-agent.service", "replace");
55 GpgAgent.reloadAgent();
56 }
44 } 57 }
45 58
46 WlSessionLockSurface { 59 WlSessionLockSurface {
diff --git a/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
new file mode 100644
index 00000000..387dcc8b
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
@@ -0,0 +1,24 @@
1import QtQuick
2import QtQuick.Effects
3
4Item {
5 id: icon
6
7 required property string icon
8 property color color: "white"
9
10 Image {
11 id: sourceImage
12 source: "file://" + @mdi@ + "/svg/" + icon.icon + ".svg"
13 anchors.fill: parent
14
15 layer.enabled: true
16 layer.effect: MultiEffect {
17 id: effect
18
19 brightness: 1
20 colorization: 1
21 colorizationColor: icon.color
22 }
23 }
24}
diff --git a/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
new file mode 100644
index 00000000..5bf0ac3a
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
@@ -0,0 +1,354 @@
1import QtQuick
2import QtQuick.Layouts
3import QtQuick.Controls.Fusion
4import Quickshell
5import Quickshell.Services.Pipewire
6import Quickshell.Widgets
7
8Item {
9 height: parent.height
10 width: volumeIcon.width + 8
11 anchors.verticalCenter: parent.verticalCenter
12
13 PwObjectTracker {
14 objects: [Pipewire.defaultAudioSink]
15 }
16
17 WrapperMouseArea {
18 id: widgetMouseArea
19
20 anchors.fill: parent
21 hoverEnabled: true
22 cursorShape: Qt.PointingHandCursor
23
24 onClicked: {
25 if (!Pipewire.defaultAudioSink)
26 return;
27 Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted;
28 }
29
30 property real sensitivity: (1 / 20) / 120
31 onWheel: event => {
32 if (!Pipewire.defaultAudioSink)
33 return;
34 Pipewire.defaultAudioSink.audio.volume += event.angleDelta.y * sensitivity;
35 }
36
37 Rectangle {
38 id: volumeWidget
39
40 anchors.fill: parent
41 color: {
42 if (widgetMouseArea.containsMouse)
43 return "#33808080";
44 return "transparent";
45 }
46
47 Item {
48 anchors.fill: parent
49
50 MaterialDesignIcon {
51 id: volumeIcon
52
53 width: 16
54 height: 16
55 anchors.centerIn: parent
56
57 icon: {
58 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
59 return "volume-off";
60 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
61 return "volume-low";
62 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
63 return "volume-medium";
64 return "volume-high";
65 }
66 color: "#555"
67 }
68 }
69 }
70 }
71
72 Loader {
73 id: tooltipLoader
74
75 active: false
76
77 Connections {
78 target: widgetMouseArea
79 function onContainsMouseChanged() {
80 if (widgetMouseArea.containsMouse)
81 tooltipLoader.active = true;
82 }
83 }
84
85 PwObjectTracker {
86 objects: Pipewire.devices
87 }
88 PwObjectTracker {
89 objects: Pipewire.nodes
90 }
91
92 sourceComponent: PopupWindow {
93 id: tooltip
94
95 property bool openPopup: false
96 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup
97
98 anchor {
99 item: widgetMouseArea
100 edges: Edges.Bottom | Edges.Left
101 }
102 visible: false
103
104 onNextVisibleChanged: hangTimer.restart()
105
106 Timer {
107 id: hangTimer
108 interval: 100
109 onTriggered: {
110 tooltip.visible = tooltip.nextVisible;
111 if (!tooltip.visible)
112 tooltipLoader.active = false;
113 }
114 }
115
116 implicitWidth: tooltipContent.width
117 implicitHeight: tooltipContent.height
118 color: "black"
119
120 WrapperMouseArea {
121 id: tooltipMouseArea
122
123 hoverEnabled: true
124 enabled: true
125
126 anchors.fill: parent
127
128 WrapperItem {
129 id: tooltipContent
130
131 margin: 8
132 bottomMargin: 8 + Math.max(0, 200 - tooltipLayout.implicitHeight)
133
134 GridLayout {
135 id: tooltipLayout
136
137 columns: 4
138
139 Repeater {
140 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
141
142 Item {
143 id: descItem
144
145 required property var modelData
146 required property int index
147
148 Layout.column: 0
149 Layout.row: index
150
151 implicitWidth: descText.contentWidth
152 implicitHeight: descText.contentHeight
153
154 Text {
155 id: descText
156
157 color: "white"
158 font.pointSize: 10
159 font.family: "Fira Sans"
160
161 text: descItem.modelData.description
162 }
163 }
164 }
165
166 Repeater {
167 id: defaultSinkRepeater
168
169 model: {
170 Array.from(Pipewire.devices.values)
171 .filter(dev => dev.type == "Audio/Device")
172 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id ));
173 }
174
175 Item {
176 id: defaultSinkItem
177
178 required property var modelData
179 required property int index
180
181 PwObjectTracker {
182 objects: [defaultSinkItem.modelData]
183 }
184
185 Layout.column: 1
186 Layout.row: index
187
188 Layout.fillHeight: true
189
190 implicitWidth: 16 + 8
191
192 WrapperMouseArea {
193 id: defaultSinkMouseArea
194
195 anchors.fill: parent
196 hoverEnabled: true
197 cursorShape: Qt.PointingHandCursor
198
199 onClicked: {
200 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData
201 }
202
203 Rectangle {
204 id: defaultSinkWidget
205
206 anchors.fill: parent
207 color: {
208 if (defaultSinkMouseArea.containsMouse)
209 return "#33808080";
210 return "transparent";
211 }
212
213 MaterialDesignIcon {
214 width: 16
215 height: 16
216 anchors.centerIn: parent
217
218 icon: {
219 if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id)
220 return "speaker";
221 return "speaker-off";
222 }
223 color: icon == "speaker" ? "white" : "#555"
224 }
225 }
226 }
227 }
228 }
229
230 Repeater {
231 id: defaultSourceRepeater
232
233 model: {
234 Array.from(Pipewire.devices.values)
235 .filter(dev => dev.type == "Audio/Device")
236 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id ));
237 }
238
239 Item {
240 id: defaultSourceItem
241
242 required property var modelData
243 required property int index
244
245 PwObjectTracker {
246 objects: [defaultSourceItem.modelData]
247 }
248
249 Layout.column: 2
250 Layout.row: index
251
252 Layout.fillHeight: true
253
254 implicitWidth: 16 + 8
255
256 WrapperMouseArea {
257 id: defaultSourceMouseArea
258
259 anchors.fill: parent
260 hoverEnabled: true
261 cursorShape: Qt.PointingHandCursor
262
263 onClicked: {
264 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData
265 }
266
267 Rectangle {
268 id: defaultSourceWidget
269
270 anchors.fill: parent
271 color: {
272 if (defaultSourceMouseArea.containsMouse)
273 return "#33808080";
274 return "transparent";
275 }
276
277 MaterialDesignIcon {
278 width: 16
279 height: 16
280 anchors.centerIn: parent
281
282 icon: {
283 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id)
284 return "microphone";
285 return "microphone-off";
286 }
287 color: icon == "microphone" ? "white" : "#555"
288 }
289 }
290 }
291 }
292 }
293
294 Repeater {
295 id: profileRepeater
296
297 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
298
299 Item {
300 id: profileItem
301
302 required property var modelData
303 required property int index
304
305 PwObjectTracker {
306 objects: [profileItem.modelData]
307 }
308
309 Layout.column: 3
310 Layout.row: index
311
312 Layout.fillWidth: true
313
314 implicitWidth: Math.max(profileBox.implicitWidth, 300)
315 implicitHeight: profileBox.height
316
317 ComboBox {
318 id: profileBox
319
320 model: profileItem.modelData.profiles
321
322 textRole: "description"
323 valueRole: "index"
324 onActivated: profileItem.modelData.setProfile(currentValue)
325
326 anchors.fill: parent
327
328 implicitContentWidthPolicy: ComboBox.WidestText
329
330 Connections {
331 target: profileItem.modelData
332 function onCurrentProfileChanged() {
333 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
334 }
335 }
336 Component.onCompleted: {
337 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
338 }
339
340 Connections {
341 target: profileBox.popup
342 function onVisibleChanged() {
343 tooltip.openPopup = profileBox.popup.visible
344 }
345 }
346 }
347 }
348 }
349 }
350 }
351 }
352 }
353 }
354}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml b/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml
new file mode 100644
index 00000000..3de69535
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml
@@ -0,0 +1,18 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Io
5
6Singleton {
7 id: root
8
9 Socket {
10 id: agentSocket
11 connected: true
12 path: `${Quickshell.env("XDG_RUNTIME_DIR")}/gnupg/S.gpg-agent`
13 }
14
15 function reloadAgent() {
16 agentSocket.write("RELOADAGENT\n")
17 }
18}
diff --git a/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
new file mode 100644
index 00000000..3a78d91b
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
@@ -0,0 +1,129 @@
1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Services.Pipewire
5import Quickshell.Widgets
6
7Scope {
8 id: root
9
10 property bool show: false
11 property bool inhibited: true
12
13 PwObjectTracker {
14 objects: [ Pipewire.defaultAudioSink ]
15 }
16
17 Connections {
18 target: Pipewire.defaultAudioSink?.audio
19
20 function onVolumeChanged() {
21 root.show = true;
22 hideTimer.reset();
23 }
24 function onMutedChanged() {
25 root.show = true;
26 hideTimer.reset();
27 }
28 }
29
30 onShowChanged: {
31 if (show)
32 hideTimer.restart();
33 }
34
35 Timer {
36 id: hideTimer
37 interval: 2000
38 onTriggered: root.show = false
39 }
40
41 Timer {
42 id: startInhibit
43 interval: 100
44 running: true
45 onTriggered: root.inhibited = false;
46 }
47
48 LazyLoader {
49 active: root.show && !root.inhibited
50
51 Variants {
52 model: Quickshell.screens
53
54 delegate: Scope {
55 id: screenScope
56
57 required property var modelData
58
59 PanelWindow {
60 id: window
61
62 screen: screenScope.modelData
63
64 anchors.top: true
65 margins.top: (screen.height - window.height) / 2
66 exclusiveZone: 0
67
68 implicitWidth: 400
69 implicitHeight: 50
70
71 mask: Region {}
72
73 color: "transparent"
74
75 Rectangle {
76 anchors.fill: parent
77 color: Qt.rgba(0, 0, 0, 0.75)
78 }
79
80 RowLayout {
81 id: layout
82
83 anchors.centerIn: parent
84
85 height: 50 - 8*2
86 width: 400 - 8*2
87
88 MaterialDesignIcon {
89 id: volumeIcon
90
91 implicitWidth: parent.height
92 implicitHeight: parent.height
93
94 icon: {
95 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
96 return "volume-off";
97 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
98 return "volume-low";
99 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
100 return "volume-medium";
101 return "volume-high";
102 }
103 }
104
105 Rectangle {
106 Layout.fillWidth: true
107
108 implicitHeight: 10
109
110 color: "#50ffffff"
111
112 Rectangle {
113 anchors {
114 left: parent.left
115 top: parent.top
116 bottom: parent.bottom
117 }
118
119 color: Pipewire.defaultAudioSink?.audio.muted ? "#70ffffff" : "white"
120
121 implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0)
122 }
123 }
124 }
125 }
126 }
127 }
128 }
129}
diff --git a/accounts/gkleen@sif/shell/quickshell/shell.qml b/accounts/gkleen@sif/shell/quickshell/shell.qml
index 1da9457d..3657f77f 100644
--- a/accounts/gkleen@sif/shell/quickshell/shell.qml
+++ b/accounts/gkleen@sif/shell/quickshell/shell.qml
@@ -41,4 +41,6 @@ ShellRoot {
41 } 41 }
42 42
43 Lockscreen {} 43 Lockscreen {}
44
45 VolumeOSD {}
44} 46}
diff --git a/overlays/quickshell/default.nix b/overlays/quickshell/default.nix
index ac722010..622d69a3 100644
--- a/overlays/quickshell/default.nix
+++ b/overlays/quickshell/default.nix
@@ -3,6 +3,8 @@
3 quickshell = prev.quickshell.overrideAttrs (oldAttrs: { 3 quickshell = prev.quickshell.overrideAttrs (oldAttrs: {
4 patches = (oldAttrs.patches or []) ++ [ 4 patches = (oldAttrs.patches or []) ++ [
5 ./greetd-response.patch 5 ./greetd-response.patch
6 ./lock-state-changed.patch
7 ./pipewire.patch
6 ]; 8 ];
7 }); 9 });
8} 10}
diff --git a/overlays/quickshell/lock-state-changed.patch b/overlays/quickshell/lock-state-changed.patch
new file mode 100644
index 00000000..4be273fa
--- /dev/null
+++ b/overlays/quickshell/lock-state-changed.patch
@@ -0,0 +1,12 @@
1diff --git i/src/wayland/session_lock.cpp w/src/wayland/session_lock.cpp
2index 0ecf9ec..3dbd19b 100644
3--- i/src/wayland/session_lock.cpp
4+++ w/src/wayland/session_lock.cpp
5@@ -127,6 +127,7 @@ void WlSessionLock::realizeLockTarget(WlSessionLock* old) {
6 this->updateSurfaces(false);
7
8 if (!this->manager->lock()) this->lockTarget = false;
9+ emit this->lockStateChanged();
10
11 this->updateSurfaces(true, old);
12 } else {
diff --git a/overlays/quickshell/pipewire.patch b/overlays/quickshell/pipewire.patch
new file mode 100644
index 00000000..33025d8b
--- /dev/null
+++ b/overlays/quickshell/pipewire.patch
@@ -0,0 +1,460 @@
1diff --git i/src/services/pipewire/device.cpp w/src/services/pipewire/device.cpp
2index 616e7d0..0c55008 100644
3--- i/src/services/pipewire/device.cpp
4+++ w/src/services/pipewire/device.cpp
5@@ -3,6 +3,7 @@
6 #include <cstdint>
7 #include <functional>
8 #include <utility>
9+#include <algorithm>
10
11 #include <pipewire/device.h>
12 #include <qcontainerfwd.h>
13@@ -19,6 +20,8 @@
14 #include <spa/pod/pod.h>
15 #include <spa/pod/vararg.h>
16 #include <spa/utils/type.h>
17+#include <spa/monitor/device.h>
18+#include <spa/utils/keys.h>
19
20 #include "../../core/logcat.hpp"
21 #include "core.hpp"
22@@ -46,6 +49,25 @@ void PwDevice::unbindHooks() {
23 this->mWaitingForDevice = false;
24 }
25
26+void PwDevice::initProps(const spa_dict* props) {
27+ if (const auto* deviceName = spa_dict_lookup(props, SPA_KEY_DEVICE_NAME)) {
28+ this->name = deviceName;
29+ }
30+
31+ if (const auto* deviceDesc = spa_dict_lookup(props, SPA_KEY_DEVICE_DESCRIPTION)) {
32+ this->description = deviceDesc;
33+ }
34+
35+ if (const auto* deviceNick = spa_dict_lookup(props, SPA_KEY_DEVICE_NICK)) {
36+ this->nick = deviceNick;
37+ }
38+
39+ if (const auto* mediaClass = spa_dict_lookup(props, SPA_KEY_MEDIA_CLASS)) {
40+ this->type = mediaClass;
41+ }
42+}
43+
44+
45 const pw_device_events PwDevice::EVENTS = {
46 .version = PW_VERSION_DEVICE_EVENTS,
47 .info = &PwDevice::onInfo,
48@@ -71,6 +93,11 @@ void PwDevice::onInfo(void* data, const pw_device_info* info) {
49 }
50
51 break;
52+ } else if (param.id == SPA_PARAM_EnumProfile && param.flags & SPA_PARAM_INFO_READ) {
53+ self->validProfiles.clear();
54+ pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
55+ } else if (param.id == SPA_PARAM_Profile && param.flags & SPA_PARAM_INFO_READ) {
56+ pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
57 }
58 }
59 }
60@@ -97,6 +124,15 @@ void PwDevice::onParam(
61 }
62
63 self->addDeviceIndexPairs(param);
64+ } else if (id == SPA_PARAM_EnumProfile) {
65+ PwProfile profile = PwProfile::parseSpaPod(param);
66+ self->profilesUpdated = true;
67+ self->profiles.insertOrAssign(profile.index, profile);
68+ self->validProfiles.insert(profile.index);
69+ } else if (id == SPA_PARAM_Profile) {
70+ PwProfile profile = PwProfile::parseSpaPod(param);
71+ self->currentProfileUpdated = true;
72+ self->currentProfile = profile;
73 }
74 }
75
76@@ -145,6 +181,21 @@ void PwDevice::polled() {
77 return false;
78 });
79 }
80+ if (this->profilesUpdated) {
81+ this->profiles.removeIf([&](const std::pair<qint32, PwProfile>& entry) {
82+ return !this->validProfiles.contains(entry.first);
83+ });
84+ this->profilesUpdated = false;
85+ QList<PwProfile> profiles = this->profiles.values();
86+ std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; });
87+ emit this->profilesChanged(profiles);
88+ }
89+ if (this->currentProfileUpdated) {
90+ this->currentProfileUpdated = false;
91+ if (this->currentProfile) {
92+ emit this->currentProfileChanged(*this->currentProfile);
93+ }
94+ }
95 }
96
97 bool PwDevice::setVolumes(qint32 routeDevice, const QVector<float>& volumes) {
98@@ -182,6 +233,15 @@ bool PwDevice::setMuted(qint32 routeDevice, bool muted) {
99 });
100 }
101
102+void PwDevice::setProfile(qint32 profileIndex) {
103+ auto buffer = std::array<uint8_t, 1024>();
104+ auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
105+ auto* pod = spa_pod_builder_add_object(&builder,
106+ SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
107+ SPA_PARAM_PROFILE_index, SPA_POD_Int(profileIndex));
108+ pw_device_set_param(this->proxy(), SPA_PARAM_Profile, 0, static_cast<spa_pod*>(pod));
109+}
110+
111 void PwDevice::waitForDevice() { this->mWaitingForDevice = true; }
112 bool PwDevice::waitingForDevice() const { return this->mWaitingForDevice; }
113
114@@ -222,4 +282,24 @@ bool PwDevice::setRouteProps(
115 return true;
116 }
117
118+PwProfile PwProfile::parseSpaPod(const spa_pod* param) {
119+ PwProfile profile;
120+
121+ const auto* indexProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_index);
122+ const auto* descProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_description);
123+ const auto* nameProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_name);
124+
125+ spa_pod_get_int(&indexProp->value, &profile.index);
126+
127+ const char* desc_cstr = nullptr;
128+ spa_pod_get_string(&descProp->value, &desc_cstr);
129+ profile.description = QString(desc_cstr);
130+
131+ const char* name_cstr = nullptr;
132+ spa_pod_get_string(&nameProp->value, &name_cstr);
133+ profile.name = QString(name_cstr);
134+
135+ return profile;
136+}
137+
138 } // namespace qs::service::pipewire
139diff --git i/src/services/pipewire/device.hpp w/src/services/pipewire/device.hpp
140index 1a1f705..ee64858 100644
141--- i/src/services/pipewire/device.hpp
142+++ w/src/services/pipewire/device.hpp
143@@ -1,6 +1,7 @@
144 #pragma once
145
146 #include <functional>
147+#include <optional>
148
149 #include <pipewire/core.h>
150 #include <pipewire/device.h>
151@@ -17,6 +18,20 @@
152
153 namespace qs::service::pipewire {
154
155+struct PwProfile {
156+ Q_GADGET;
157+ Q_PROPERTY(qint32 index MEMBER index)
158+ Q_PROPERTY(QString description MEMBER description)
159+ Q_PROPERTY(QString name MEMBER name)
160+
161+public:
162+ qint32 index;
163+ QString description;
164+ QString name;
165+
166+ static PwProfile parseSpaPod(const spa_pod* param);
167+};
168+
169 class PwDevice;
170
171 class PwDevice: public PwBindable<pw_device, PW_TYPE_INTERFACE_Device, PW_VERSION_DEVICE> {
172@@ -25,6 +40,12 @@ class PwDevice: public PwBindable<pw_device, PW_TYPE_INTERFACE_Device, PW_VERSIO
173 public:
174 void bindHooks() override;
175 void unbindHooks() override;
176+ void initProps(const spa_dict* props) override;
177+
178+ QString name;
179+ QString description;
180+ QString nick;
181+ QString type;
182
183 bool setVolumes(qint32 routeDevice, const QVector<float>& volumes);
184 bool setMuted(qint32 routeDevice, bool muted);
185@@ -32,9 +53,16 @@ public:
186 void waitForDevice();
187 [[nodiscard]] bool waitingForDevice() const;
188
189+ void setProfile(qint32 profileIndex);
190+
191+ QHash<qint32, PwProfile> profiles;
192+ std::optional<PwProfile> currentProfile;
193+
194 signals:
195 void deviceReady();
196 void routeVolumesChanged(qint32 routeDevice, const PwVolumeProps& volumeProps);
197+ void profilesChanged(QList<PwProfile> profiles);
198+ void currentProfileChanged(PwProfile profile);
199
200 private slots:
201 void polled();
202@@ -49,6 +77,11 @@ private:
203 QList<qint32> stagingIndexes;
204 void addDeviceIndexPairs(const spa_pod* param);
205
206+ bool profilesUpdated = false;
207+ QSet<qint32> validProfiles;
208+
209+ bool currentProfileUpdated = false;
210+
211 bool
212 setRouteProps(qint32 routeDevice, const std::function<void*(spa_pod_builder*)>& propsCallback);
213
214diff --git i/src/services/pipewire/qml.cpp w/src/services/pipewire/qml.cpp
215index 9efb17e..921d12a 100644
216--- i/src/services/pipewire/qml.cpp
217+++ w/src/services/pipewire/qml.cpp
218@@ -9,6 +9,9 @@
219 #include <qtypes.h>
220 #include <qvariant.h>
221
222+#include <cstdint>
223+#include <algorithm>
224+
225 #include "../../core/model.hpp"
226 #include "connection.hpp"
227 #include "defaults.hpp"
228@@ -54,6 +57,12 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
229
230 QObject::connect(&connection->registry, &PwRegistry::nodeAdded, this, &Pipewire::onNodeAdded);
231
232+ for (auto* device: connection->registry.devices.values()) {
233+ this->onDeviceAdded(device);
234+ }
235+
236+ QObject::connect(&connection->registry, &PwRegistry::deviceAdded, this, &Pipewire::onDeviceAdded);
237+
238 for (auto* link: connection->registry.links.values()) {
239 this->onLinkAdded(link);
240 }
241@@ -123,6 +132,19 @@ void Pipewire::onNodeRemoved(QObject* object) {
242 this->mNodes.removeObject(iface);
243 }
244
245+ObjectModel<PwDeviceIface>* Pipewire::devices() { return &this->mDevices; }
246+
247+void Pipewire::onDeviceAdded(PwDevice* device) {
248+ auto* iface = PwDeviceIface::instance(device);
249+ QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onDeviceRemoved);
250+ this->mDevices.insertObject(iface);
251+}
252+
253+void Pipewire::onDeviceRemoved(QObject* object) {
254+ auto* iface = static_cast<PwDeviceIface*>(object); // NOLINT
255+ this->mDevices.removeObject(iface);
256+}
257+
258 ObjectModel<PwLinkIface>* Pipewire::links() { return &this->mLinks; }
259
260 void Pipewire::onLinkAdded(PwLink* link) {
261@@ -357,6 +379,8 @@ QVariantMap PwNodeIface::properties() const {
262
263 PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; }
264
265+PwDeviceIface* PwNodeIface::device() const { return PwDeviceIface::instance(this->mNode->device); }
266+
267 PwNodeIface* PwNodeIface::instance(PwNode* node) {
268 if (node == nullptr) return nullptr;
269
270@@ -481,4 +505,42 @@ void PwObjectTracker::objectDestroyed(QObject* object) {
271 emit this->objectsChanged();
272 }
273
274+PwDeviceIface::PwDeviceIface(PwDevice* device): PwObjectIface(device), mDevice(device) {
275+ QObject::connect(device, &PwDevice::profilesChanged, this, &PwDeviceIface::deviceProfilesChanged);
276+ QObject::connect(device, &PwDevice::currentProfileChanged, this, &PwDeviceIface::deviceCurrentProfileChanged);
277+}
278+
279+void PwDeviceIface::deviceProfilesChanged(QList<PwProfile>) { emit this->profilesChanged(); }
280+void PwDeviceIface::deviceCurrentProfileChanged(PwProfile) { emit this->currentProfileChanged(); }
281+
282+quint32 PwDeviceIface::id() const { return this->mDevice->id; }
283+QString PwDeviceIface::name() const { return this->mDevice->name; }
284+QString PwDeviceIface::description() const { return this->mDevice->description; }
285+QString PwDeviceIface::nickname() const { return this->mDevice->nick; }
286+QString PwDeviceIface::type() const { return this->mDevice->type; }
287+QList<PwProfile> PwDeviceIface::profiles() const {
288+ QList<PwProfile> profiles = this->mDevice->profiles.values();
289+ std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; });
290+ return profiles;
291+}
292+qint32 PwDeviceIface::currentProfile() const { return this->mDevice->currentProfile->index; }
293+
294+PwDeviceIface* PwDeviceIface::instance(PwDevice* device) {
295+ if (device == nullptr) return nullptr;
296+
297+ auto v = device->property("iface");
298+ if (v.canConvert<PwDeviceIface*>()) {
299+ return v.value<PwDeviceIface*>();
300+ }
301+
302+ auto* instance = new PwDeviceIface(device);
303+ device->setProperty("iface", QVariant::fromValue(instance));
304+
305+ return instance;
306+}
307+
308+void PwDeviceIface::setProfile(qint32 profileIndex) {
309+ this->mDevice->setProfile(profileIndex);
310+}
311+
312 } // namespace qs::service::pipewire
313diff --git i/src/services/pipewire/qml.hpp w/src/services/pipewire/qml.hpp
314index e3489a1..e5e1891 100644
315--- i/src/services/pipewire/qml.hpp
316+++ w/src/services/pipewire/qml.hpp
317@@ -12,11 +12,13 @@
318 #include "../../core/model.hpp"
319 #include "link.hpp"
320 #include "node.hpp"
321+#include "device.hpp"
322 #include "registry.hpp"
323
324 namespace qs::service::pipewire {
325
326 class PwNodeIface;
327+class PwDeviceIface;
328 class PwLinkIface;
329 class PwLinkGroupIface;
330
331@@ -65,6 +67,8 @@ class Pipewire: public QObject {
332 /// - @@PwNode.audio - if non null the node is an audio node.
333 QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwNodeIface>*);
334 Q_PROPERTY(UntypedObjectModel* nodes READ nodes CONSTANT);
335+ QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwDeviceIface>*);
336+ Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
337 /// All links present in pipewire.
338 ///
339 /// Links connect pipewire nodes to each other, and can be used to determine
340@@ -134,6 +138,7 @@ public:
341 explicit Pipewire(QObject* parent = nullptr);
342
343 [[nodiscard]] ObjectModel<PwNodeIface>* nodes();
344+ [[nodiscard]] ObjectModel<PwDeviceIface>* devices();
345 [[nodiscard]] ObjectModel<PwLinkIface>* links();
346 [[nodiscard]] ObjectModel<PwLinkGroupIface>* linkGroups();
347
348@@ -159,7 +164,9 @@ signals:
349
350 private slots:
351 void onNodeAdded(PwNode* node);
352+ void onDeviceAdded(PwDevice* node);
353 void onNodeRemoved(QObject* object);
354+ void onDeviceRemoved(QObject* object);
355 void onLinkAdded(PwLink* link);
356 void onLinkRemoved(QObject* object);
357 void onLinkGroupAdded(PwLinkGroup* group);
358@@ -167,6 +174,7 @@ private slots:
359
360 private:
361 ObjectModel<PwNodeIface> mNodes {this};
362+ ObjectModel<PwDeviceIface> mDevices {this};
363 ObjectModel<PwLinkIface> mLinks {this};
364 ObjectModel<PwLinkGroupIface> mLinkGroups {this};
365 };
366@@ -315,6 +323,7 @@ class PwNodeIface: public PwObjectIface {
367 /// > [!NOTE] The node may be used before it is fully bound, but some data
368 /// > may be missing or incorrect.
369 Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged);
370+ Q_PROPERTY(qs::service::pipewire::PwDeviceIface* device READ device CONSTANT);
371 QML_NAMED_ELEMENT(PwNode);
372 QML_UNCREATABLE("PwNodes cannot be created directly");
373
374@@ -332,6 +341,7 @@ public:
375 [[nodiscard]] PwNodeType::Flags type() const;
376 [[nodiscard]] QVariantMap properties() const;
377 [[nodiscard]] PwNodeAudioIface* audio() const;
378+ [[nodiscard]] PwDeviceIface* device() const;
379
380 static PwNodeIface* instance(PwNode* node);
381
382@@ -344,6 +354,44 @@ private:
383 PwNodeAudioIface* audioIface = nullptr;
384 };
385
386+class PwDeviceIface: public PwObjectIface {
387+ Q_OBJECT;
388+ Q_PROPERTY(quint32 id READ id CONSTANT);
389+ Q_PROPERTY(QString name READ name CONSTANT);
390+ Q_PROPERTY(QString description READ description CONSTANT);
391+ Q_PROPERTY(QString nickname READ nickname CONSTANT);
392+ Q_PROPERTY(QString type READ type CONSTANT);
393+ Q_PROPERTY(QList<PwProfile> profiles READ profiles NOTIFY profilesChanged);
394+ Q_PROPERTY(qint32 currentProfile READ currentProfile NOTIFY currentProfileChanged);
395+
396+ QML_NAMED_ELEMENT(PwDevice);
397+ QML_UNCREATABLE("PwDevices cannot be created directly");
398+
399+signals:
400+ void profilesChanged();
401+ void currentProfileChanged();
402+
403+public:
404+ explicit PwDeviceIface(PwDevice* node);
405+
406+ [[nodiscard]] quint32 id() const;
407+ [[nodiscard]] QString name() const;
408+ [[nodiscard]] QString description() const;
409+ [[nodiscard]] QString nickname() const;
410+ [[nodiscard]] QString type() const;
411+ QList<PwProfile> profiles() const;
412+ qint32 currentProfile() const;
413+
414+ Q_INVOKABLE void setProfile(qint32 profileIndex);
415+
416+ static PwDeviceIface* instance(PwDevice* node);
417+private:
418+ PwDevice* mDevice;
419+
420+ void deviceProfilesChanged(QList<PwProfile> profiles);
421+ void deviceCurrentProfileChanged(PwProfile profile);
422+};
423+
424 ///! A connection between pipewire nodes.
425 /// Note that there is one link per *channel* of a connection between nodes.
426 /// You usually want @@PwLinkGroup.
427diff --git i/src/services/pipewire/registry.cpp w/src/services/pipewire/registry.cpp
428index c08fc1d..50c6d7a 100644
429--- i/src/services/pipewire/registry.cpp
430+++ w/src/services/pipewire/registry.cpp
431@@ -196,6 +196,7 @@ void PwRegistry::onGlobal(
432 device->initProps(props);
433
434 self->devices.emplace(id, device);
435+ emit self->deviceAdded(device);
436 }
437 }
438
439@@ -211,6 +212,9 @@ void PwRegistry::onGlobalRemoved(void* data, quint32 id) {
440 } else if (auto* node = self->nodes.value(id)) {
441 self->nodes.remove(id);
442 node->safeDestroy();
443+ } else if (auto* device = self->devices.value(id)) {
444+ self->devices.remove(id);
445+ device->safeDestroy();
446 }
447 }
448
449diff --git i/src/services/pipewire/registry.hpp w/src/services/pipewire/registry.hpp
450index 8473f04..87e0766 100644
451--- i/src/services/pipewire/registry.hpp
452+++ w/src/services/pipewire/registry.hpp
453@@ -132,6 +132,7 @@ public:
454
455 signals:
456 void nodeAdded(PwNode* node);
457+ void deviceAdded(PwDevice* node);
458 void linkAdded(PwLink* link);
459 void linkGroupAdded(PwLinkGroup* group);
460 void metadataAdded(PwMetadata* metadata);
diff --git a/user-profiles/yt-dlp.nix b/user-profiles/yt-dlp.nix
index f59a71b9..9e30bba8 100644
--- a/user-profiles/yt-dlp.nix
+++ b/user-profiles/yt-dlp.nix
@@ -11,6 +11,10 @@
11 "best[width<=2560][height<=1440][fps<=60][vcodec!*=av01][vcodec!*=avc1]" 11 "best[width<=2560][height<=1440][fps<=60][vcodec!*=av01][vcodec!*=avc1]"
12 "bestvideo*[vcodec!*=av01][vcodec!*=avc1]+bestaudio" 12 "bestvideo*[vcodec!*=av01][vcodec!*=avc1]+bestaudio"
13 "best[vcodec!*=av01][vcodec!*=avc1]" 13 "best[vcodec!*=av01][vcodec!*=avc1]"
14 "bestvideo*[width<=2560][height<=1440][fps<=60]+bestaudio"
15 "best[width<=2560][height<=1440][fps<=60]"
16 "bestvideo*+bestaudio"
17 "best"
14 ]; 18 ];
15 embed-subs = true; 19 embed-subs = true;
16 embed-thumbnail = true; 20 embed-thumbnail = true;