summaryrefslogtreecommitdiff
path: root/overlays/quickshell
diff options
context:
space:
mode:
Diffstat (limited to 'overlays/quickshell')
-rw-r--r--overlays/quickshell/close-stdin.patch13
-rw-r--r--overlays/quickshell/default.nix25
-rw-r--r--overlays/quickshell/io.patch13
-rw-r--r--overlays/quickshell/lock-state-changed.patch12
-rw-r--r--overlays/quickshell/pipewire.patch490
5 files changed, 553 insertions, 0 deletions
diff --git a/overlays/quickshell/close-stdin.patch b/overlays/quickshell/close-stdin.patch
new file mode 100644
index 00000000..230c602c
--- /dev/null
+++ b/overlays/quickshell/close-stdin.patch
@@ -0,0 +1,13 @@
1diff --git i/src/io/process.cpp w/src/io/process.cpp
2index 6055e2c..f528438 100644
3--- i/src/io/process.cpp
4+++ w/src/io/process.cpp
5@@ -210,7 +210,7 @@ void Process::startProcessIfReady() {
6
7 if (this->mStdoutParser == nullptr) this->process->closeReadChannel(QProcess::StandardOutput);
8 if (this->mStderrParser == nullptr) this->process->closeReadChannel(QProcess::StandardError);
9- if (!this->mStdinEnabled) this->process->closeWriteChannel();
10+ if (!this->mStdinEnabled) this->process->setStandardInputFile(QProcess::nullDevice());
11
12 this->setupEnvironment(this->process);
13 this->process->start(cmd, args);
diff --git a/overlays/quickshell/default.nix b/overlays/quickshell/default.nix
new file mode 100644
index 00000000..9aefeeb4
--- /dev/null
+++ b/overlays/quickshell/default.nix
@@ -0,0 +1,25 @@
1{ final, prev, sources, ... }:
2{
3 quickshell = prev.quickshell.overrideAttrs (oldAttrs: {
4 inherit (sources.quickshell) version src;
5
6 buildInputs = (oldAttrs.buildInputs or []) ++ [
7 (prev.lib.getDev final.polkit)
8 (final.cpptrace.overrideAttrs (oldAttrs: {
9 buildInputs = (oldAttrs.buildInputs or []) ++ [
10 (prev.lib.getDev final.libunwind)
11 ];
12 cmakeFlags = (oldAttrs.cmakeFlags or []) ++ [
13 (prev.lib.cmakeBool "CPPTRACE_UNWIND_WITH_LIBUNWIND" true)
14 ];
15 }))
16 ];
17
18 patches = (oldAttrs.patches or []) ++ [
19 ./lock-state-changed.patch
20 ./pipewire.patch
21 ./io.patch
22 ./close-stdin.patch
23 ];
24 });
25}
diff --git a/overlays/quickshell/io.patch b/overlays/quickshell/io.patch
new file mode 100644
index 00000000..961bdcaf
--- /dev/null
+++ b/overlays/quickshell/io.patch
@@ -0,0 +1,13 @@
1diff --git i/src/io/socket.cpp w/src/io/socket.cpp
2index 371f687..d12eaeb 100644
3--- i/src/io/socket.cpp
4+++ w/src/io/socket.cpp
5@@ -66,7 +66,7 @@ void Socket::onSocketDisconnected() {
6 }
7
8 void Socket::onSocketError(QLocalSocket::LocalSocketError error) {
9- qCWarning(logSocket) << "Socket error for" << this << error;
10+ // qCWarning(logSocket) << "Socket error for" << this << error;
11 emit this->error(error);
12 }
13
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..b94a2691
--- /dev/null
+++ b/overlays/quickshell/pipewire.patch
@@ -0,0 +1,490 @@
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,10 @@
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+#include <spa/pod/parser.h>
20+#include <spa/pod/iter.h>
21
22 #include "../../core/logcat.hpp"
23 #include "core.hpp"
24@@ -46,6 +49,25 @@ void PwDevice::unbindHooks() {
25 this->mWaitingForDevice = false;
26 }
27
28+void PwDevice::initProps(const spa_dict* props) {
29+ if (const auto* deviceName = spa_dict_lookup(props, SPA_KEY_DEVICE_NAME)) {
30+ this->name = deviceName;
31+ }
32+
33+ if (const auto* deviceDesc = spa_dict_lookup(props, SPA_KEY_DEVICE_DESCRIPTION)) {
34+ this->description = deviceDesc;
35+ }
36+
37+ if (const auto* deviceNick = spa_dict_lookup(props, SPA_KEY_DEVICE_NICK)) {
38+ this->nick = deviceNick;
39+ }
40+
41+ if (const auto* mediaClass = spa_dict_lookup(props, SPA_KEY_MEDIA_CLASS)) {
42+ this->type = mediaClass;
43+ }
44+}
45+
46+
47 const pw_device_events PwDevice::EVENTS = {
48 .version = PW_VERSION_DEVICE_EVENTS,
49 .info = &PwDevice::onInfo,
50@@ -71,6 +93,11 @@ void PwDevice::onInfo(void* data, const pw_device_info* info) {
51 }
52
53 break;
54+ } else if (param.id == SPA_PARAM_EnumProfile && param.flags & SPA_PARAM_INFO_READ) {
55+ self->validProfiles.clear();
56+ pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
57+ } else if (param.id == SPA_PARAM_Profile && param.flags & SPA_PARAM_INFO_READ) {
58+ pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
59 }
60 }
61 }
62@@ -97,6 +124,15 @@ void PwDevice::onParam(
63 }
64
65 self->addDeviceIndexPairs(param);
66+ } else if (id == SPA_PARAM_EnumProfile) {
67+ PwProfile profile = PwProfile::parseSpaPod(param);
68+ self->profilesUpdated = true;
69+ self->profiles.insertOrAssign(profile.index, profile);
70+ self->validProfiles.insert(profile.index);
71+ } else if (id == SPA_PARAM_Profile) {
72+ PwProfile profile = PwProfile::parseSpaPod(param);
73+ self->currentProfileUpdated = true;
74+ self->currentProfile = profile;
75 }
76 }
77
78@@ -145,6 +181,21 @@ void PwDevice::polled() {
79 return false;
80 });
81 }
82+ if (this->profilesUpdated) {
83+ this->profiles.removeIf([&](const std::pair<qint32, PwProfile>& entry) {
84+ return !this->validProfiles.contains(entry.first);
85+ });
86+ this->profilesUpdated = false;
87+ QList<PwProfile> profiles = this->profiles.values();
88+ std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; });
89+ emit this->profilesChanged(profiles);
90+ }
91+ if (this->currentProfileUpdated) {
92+ this->currentProfileUpdated = false;
93+ if (this->currentProfile) {
94+ emit this->currentProfileChanged(*this->currentProfile);
95+ }
96+ }
97 }
98
99 bool PwDevice::setVolumes(qint32 routeDevice, const QVector<float>& volumes) {
100@@ -182,6 +233,15 @@ bool PwDevice::setMuted(qint32 routeDevice, bool muted) {
101 });
102 }
103
104+void PwDevice::setProfile(qint32 profileIndex) {
105+ auto buffer = std::array<uint8_t, 1024>();
106+ auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
107+ auto* pod = spa_pod_builder_add_object(&builder,
108+ SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
109+ SPA_PARAM_PROFILE_index, SPA_POD_Int(profileIndex));
110+ pw_device_set_param(this->proxy(), SPA_PARAM_Profile, 0, static_cast<spa_pod*>(pod));
111+}
112+
113 void PwDevice::waitForDevice() { this->mWaitingForDevice = true; }
114 bool PwDevice::waitingForDevice() const { return this->mWaitingForDevice; }
115
116@@ -222,4 +282,24 @@ bool PwDevice::setRouteProps(
117 return true;
118 }
119
120+PwProfile PwProfile::parseSpaPod(const spa_pod* param) {
121+ PwProfile profile;
122+
123+ const auto* indexProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_index);
124+ const auto* descProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_description);
125+ const auto* nameProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_name);
126+
127+ spa_pod_get_int(&indexProp->value, &profile.index);
128+
129+ const char* desc_cstr = nullptr;
130+ spa_pod_get_string(&descProp->value, &desc_cstr);
131+ profile.description = QString(desc_cstr);
132+
133+ const char* name_cstr = nullptr;
134+ spa_pod_get_string(&nameProp->value, &name_cstr);
135+ profile.name = QString(name_cstr);
136+
137+ return profile;
138+}
139+
140 } // namespace qs::service::pipewire
141diff --git i/src/services/pipewire/device.hpp w/src/services/pipewire/device.hpp
142index 1a1f705..ee64858 100644
143--- i/src/services/pipewire/device.hpp
144+++ w/src/services/pipewire/device.hpp
145@@ -1,6 +1,7 @@
146 #pragma once
147
148 #include <functional>
149+#include <optional>
150
151 #include <pipewire/core.h>
152 #include <pipewire/device.h>
153@@ -17,6 +18,20 @@
154
155 namespace qs::service::pipewire {
156
157+struct PwProfile {
158+ Q_GADGET;
159+ Q_PROPERTY(qint32 index MEMBER index)
160+ Q_PROPERTY(QString description MEMBER description)
161+ Q_PROPERTY(QString name MEMBER name)
162+
163+public:
164+ qint32 index;
165+ QString description;
166+ QString name;
167+
168+ static PwProfile parseSpaPod(const spa_pod* param);
169+};
170+
171 class PwDevice;
172
173 class PwDevice: public PwBindable<pw_device, PW_TYPE_INTERFACE_Device, PW_VERSION_DEVICE> {
174@@ -25,6 +40,12 @@ class PwDevice: public PwBindable<pw_device, PW_TYPE_INTERFACE_Device, PW_VERSIO
175 public:
176 void bindHooks() override;
177 void unbindHooks() override;
178+ void initProps(const spa_dict* props) override;
179+
180+ QString name;
181+ QString description;
182+ QString nick;
183+ QString type;
184
185 bool setVolumes(qint32 routeDevice, const QVector<float>& volumes);
186 bool setMuted(qint32 routeDevice, bool muted);
187@@ -32,9 +53,16 @@ public:
188 void waitForDevice();
189 [[nodiscard]] bool waitingForDevice() const;
190
191+ void setProfile(qint32 profileIndex);
192+
193+ QHash<qint32, PwProfile> profiles;
194+ std::optional<PwProfile> currentProfile;
195+
196 signals:
197 void deviceReady();
198 void routeVolumesChanged(qint32 routeDevice, const PwVolumeProps& volumeProps);
199+ void profilesChanged(QList<PwProfile> profiles);
200+ void currentProfileChanged(PwProfile profile);
201
202 private slots:
203 void polled();
204@@ -49,6 +77,11 @@ private:
205 QList<qint32> stagingIndexes;
206 void addDeviceIndexPairs(const spa_pod* param);
207
208+ bool profilesUpdated = false;
209+ QSet<qint32> validProfiles;
210+
211+ bool currentProfileUpdated = false;
212+
213 bool
214 setRouteProps(qint32 routeDevice, const std::function<void*(spa_pod_builder*)>& propsCallback);
215
216diff --git i/src/services/pipewire/node.cpp w/src/services/pipewire/node.cpp
217index 3e68149..4721a58 100644
218--- i/src/services/pipewire/node.cpp
219+++ w/src/services/pipewire/node.cpp
220@@ -145,6 +145,10 @@ void PwNode::initProps(const spa_dict* props) {
221 this->type = PwNodeType::VideoSink;
222 } else if (strcmp(mediaClass, "Video/Source") == 0) {
223 this->type = PwNodeType::VideoSource;
224+ } else if (strcmp(mediaClass, "Stream/Output/Video") == 0) {
225+ this->type = PwNodeType::VideoOutStream;
226+ } else if (strcmp(mediaClass, "Stream/Input/Video") == 0) {
227+ this->type = PwNodeType::VideoInStream;
228 }
229 }
230
231diff --git i/src/services/pipewire/node.hpp w/src/services/pipewire/node.hpp
232index 0d4c92e..ee6f223 100644
233--- i/src/services/pipewire/node.hpp
234+++ w/src/services/pipewire/node.hpp
235@@ -144,6 +144,8 @@ public:
236 // This is equivalent to the media class `Video/Sink` and is composed of the
237 // @@PwNodeType.Video and @@PwNodeType.Sink flags.
238 VideoSink = Video | Sink,
239+ VideoOutStream = Video | Sink | Stream,
240+ VideoInStream = Video | Source | Stream,
241 };
242 Q_ENUM(Flag);
243 Q_DECLARE_FLAGS(Flags, Flag);
244diff --git i/src/services/pipewire/qml.cpp w/src/services/pipewire/qml.cpp
245index 9efb17e..921d12a 100644
246--- i/src/services/pipewire/qml.cpp
247+++ w/src/services/pipewire/qml.cpp
248@@ -9,6 +9,9 @@
249 #include <qtypes.h>
250 #include <qvariant.h>
251
252+#include <cstdint>
253+#include <algorithm>
254+
255 #include "../../core/model.hpp"
256 #include "connection.hpp"
257 #include "defaults.hpp"
258@@ -54,6 +57,12 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
259
260 QObject::connect(&connection->registry, &PwRegistry::nodeAdded, this, &Pipewire::onNodeAdded);
261
262+ for (auto* device: connection->registry.devices.values()) {
263+ this->onDeviceAdded(device);
264+ }
265+
266+ QObject::connect(&connection->registry, &PwRegistry::deviceAdded, this, &Pipewire::onDeviceAdded);
267+
268 for (auto* link: connection->registry.links.values()) {
269 this->onLinkAdded(link);
270 }
271@@ -123,6 +132,19 @@ void Pipewire::onNodeRemoved(QObject* object) {
272 this->mNodes.removeObject(iface);
273 }
274
275+ObjectModel<PwDeviceIface>* Pipewire::devices() { return &this->mDevices; }
276+
277+void Pipewire::onDeviceAdded(PwDevice* device) {
278+ auto* iface = PwDeviceIface::instance(device);
279+ QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onDeviceRemoved);
280+ this->mDevices.insertObject(iface);
281+}
282+
283+void Pipewire::onDeviceRemoved(QObject* object) {
284+ auto* iface = static_cast<PwDeviceIface*>(object); // NOLINT
285+ this->mDevices.removeObject(iface);
286+}
287+
288 ObjectModel<PwLinkIface>* Pipewire::links() { return &this->mLinks; }
289
290 void Pipewire::onLinkAdded(PwLink* link) {
291@@ -357,6 +379,8 @@ QVariantMap PwNodeIface::properties() const {
292
293 PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; }
294
295+PwDeviceIface* PwNodeIface::device() const { return PwDeviceIface::instance(this->mNode->device); }
296+
297 PwNodeIface* PwNodeIface::instance(PwNode* node) {
298 if (node == nullptr) return nullptr;
299
300@@ -481,4 +505,42 @@ void PwObjectTracker::objectDestroyed(QObject* object) {
301 emit this->objectsChanged();
302 }
303
304+PwDeviceIface::PwDeviceIface(PwDevice* device): PwObjectIface(device), mDevice(device) {
305+ QObject::connect(device, &PwDevice::profilesChanged, this, &PwDeviceIface::deviceProfilesChanged);
306+ QObject::connect(device, &PwDevice::currentProfileChanged, this, &PwDeviceIface::deviceCurrentProfileChanged);
307+}
308+
309+void PwDeviceIface::deviceProfilesChanged(QList<PwProfile>) { emit this->profilesChanged(); }
310+void PwDeviceIface::deviceCurrentProfileChanged(PwProfile) { emit this->currentProfileChanged(); }
311+
312+quint32 PwDeviceIface::id() const { return this->mDevice->id; }
313+QString PwDeviceIface::name() const { return this->mDevice->name; }
314+QString PwDeviceIface::description() const { return this->mDevice->description; }
315+QString PwDeviceIface::nickname() const { return this->mDevice->nick; }
316+QString PwDeviceIface::type() const { return this->mDevice->type; }
317+QList<PwProfile> PwDeviceIface::profiles() const {
318+ QList<PwProfile> profiles = this->mDevice->profiles.values();
319+ std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; });
320+ return profiles;
321+}
322+qint32 PwDeviceIface::currentProfile() const { return this->mDevice->currentProfile->index; }
323+
324+PwDeviceIface* PwDeviceIface::instance(PwDevice* device) {
325+ if (device == nullptr) return nullptr;
326+
327+ auto v = device->property("iface");
328+ if (v.canConvert<PwDeviceIface*>()) {
329+ return v.value<PwDeviceIface*>();
330+ }
331+
332+ auto* instance = new PwDeviceIface(device);
333+ device->setProperty("iface", QVariant::fromValue(instance));
334+
335+ return instance;
336+}
337+
338+void PwDeviceIface::setProfile(qint32 profileIndex) {
339+ this->mDevice->setProfile(profileIndex);
340+}
341+
342 } // namespace qs::service::pipewire
343diff --git i/src/services/pipewire/qml.hpp w/src/services/pipewire/qml.hpp
344index e3489a1..e5e1891 100644
345--- i/src/services/pipewire/qml.hpp
346+++ w/src/services/pipewire/qml.hpp
347@@ -12,11 +12,13 @@
348 #include "../../core/model.hpp"
349 #include "link.hpp"
350 #include "node.hpp"
351+#include "device.hpp"
352 #include "registry.hpp"
353
354 namespace qs::service::pipewire {
355
356 class PwNodeIface;
357+class PwDeviceIface;
358 class PwLinkIface;
359 class PwLinkGroupIface;
360
361@@ -65,6 +67,8 @@ class Pipewire: public QObject {
362 /// - @@PwNode.audio - if non null the node is an audio node.
363 QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwNodeIface>*);
364 Q_PROPERTY(UntypedObjectModel* nodes READ nodes CONSTANT);
365+ QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwDeviceIface>*);
366+ Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
367 /// All links present in pipewire.
368 ///
369 /// Links connect pipewire nodes to each other, and can be used to determine
370@@ -134,6 +138,7 @@ public:
371 explicit Pipewire(QObject* parent = nullptr);
372
373 [[nodiscard]] ObjectModel<PwNodeIface>* nodes();
374+ [[nodiscard]] ObjectModel<PwDeviceIface>* devices();
375 [[nodiscard]] ObjectModel<PwLinkIface>* links();
376 [[nodiscard]] ObjectModel<PwLinkGroupIface>* linkGroups();
377
378@@ -159,7 +164,9 @@ signals:
379
380 private slots:
381 void onNodeAdded(PwNode* node);
382+ void onDeviceAdded(PwDevice* node);
383 void onNodeRemoved(QObject* object);
384+ void onDeviceRemoved(QObject* object);
385 void onLinkAdded(PwLink* link);
386 void onLinkRemoved(QObject* object);
387 void onLinkGroupAdded(PwLinkGroup* group);
388@@ -167,6 +174,7 @@ private slots:
389
390 private:
391 ObjectModel<PwNodeIface> mNodes {this};
392+ ObjectModel<PwDeviceIface> mDevices {this};
393 ObjectModel<PwLinkIface> mLinks {this};
394 ObjectModel<PwLinkGroupIface> mLinkGroups {this};
395 };
396@@ -315,6 +323,7 @@ class PwNodeIface: public PwObjectIface {
397 /// > [!NOTE] The node may be used before it is fully bound, but some data
398 /// > may be missing or incorrect.
399 Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged);
400+ Q_PROPERTY(qs::service::pipewire::PwDeviceIface* device READ device CONSTANT);
401 QML_NAMED_ELEMENT(PwNode);
402 QML_UNCREATABLE("PwNodes cannot be created directly");
403
404@@ -332,6 +341,7 @@ public:
405 [[nodiscard]] PwNodeType::Flags type() const;
406 [[nodiscard]] QVariantMap properties() const;
407 [[nodiscard]] PwNodeAudioIface* audio() const;
408+ [[nodiscard]] PwDeviceIface* device() const;
409
410 static PwNodeIface* instance(PwNode* node);
411
412@@ -344,6 +354,44 @@ private:
413 PwNodeAudioIface* audioIface = nullptr;
414 };
415
416+class PwDeviceIface: public PwObjectIface {
417+ Q_OBJECT;
418+ Q_PROPERTY(quint32 id READ id CONSTANT);
419+ Q_PROPERTY(QString name READ name CONSTANT);
420+ Q_PROPERTY(QString description READ description CONSTANT);
421+ Q_PROPERTY(QString nickname READ nickname CONSTANT);
422+ Q_PROPERTY(QString type READ type CONSTANT);
423+ Q_PROPERTY(QList<PwProfile> profiles READ profiles NOTIFY profilesChanged);
424+ Q_PROPERTY(qint32 currentProfile READ currentProfile NOTIFY currentProfileChanged);
425+
426+ QML_NAMED_ELEMENT(PwDevice);
427+ QML_UNCREATABLE("PwDevices cannot be created directly");
428+
429+signals:
430+ void profilesChanged();
431+ void currentProfileChanged();
432+
433+public:
434+ explicit PwDeviceIface(PwDevice* node);
435+
436+ [[nodiscard]] quint32 id() const;
437+ [[nodiscard]] QString name() const;
438+ [[nodiscard]] QString description() const;
439+ [[nodiscard]] QString nickname() const;
440+ [[nodiscard]] QString type() const;
441+ QList<PwProfile> profiles() const;
442+ qint32 currentProfile() const;
443+
444+ Q_INVOKABLE void setProfile(qint32 profileIndex);
445+
446+ static PwDeviceIface* instance(PwDevice* node);
447+private:
448+ PwDevice* mDevice;
449+
450+ void deviceProfilesChanged(QList<PwProfile> profiles);
451+ void deviceCurrentProfileChanged(PwProfile profile);
452+};
453+
454 ///! A connection between pipewire nodes.
455 /// Note that there is one link per *channel* of a connection between nodes.
456 /// You usually want @@PwLinkGroup.
457diff --git i/src/services/pipewire/registry.cpp w/src/services/pipewire/registry.cpp
458index c08fc1d..50c6d7a 100644
459--- i/src/services/pipewire/registry.cpp
460+++ w/src/services/pipewire/registry.cpp
461@@ -196,6 +196,7 @@ void PwRegistry::onGlobal(
462 device->initProps(props);
463
464 self->devices.emplace(id, device);
465+ emit self->deviceAdded(device);
466 }
467 }
468
469@@ -211,6 +212,9 @@ void PwRegistry::onGlobalRemoved(void* data, quint32 id) {
470 } else if (auto* node = self->nodes.value(id)) {
471 self->nodes.remove(id);
472 node->safeDestroy();
473+ } else if (auto* device = self->devices.value(id)) {
474+ self->devices.remove(id);
475+ device->safeDestroy();
476 }
477 }
478
479diff --git i/src/services/pipewire/registry.hpp w/src/services/pipewire/registry.hpp
480index 8473f04..87e0766 100644
481--- i/src/services/pipewire/registry.hpp
482+++ w/src/services/pipewire/registry.hpp
483@@ -132,6 +132,7 @@ public:
484
485 signals:
486 void nodeAdded(PwNode* node);
487+ void deviceAdded(PwDevice* node);
488 void linkAdded(PwLink* link);
489 void linkGroupAdded(PwLinkGroup* group);
490 void metadataAdded(PwMetadata* metadata);