summaryrefslogtreecommitdiff
path: root/overlays
diff options
context:
space:
mode:
Diffstat (limited to 'overlays')
-rw-r--r--overlays/niri.nix1
-rw-r--r--overlays/quickshell/default.nix13
-rw-r--r--overlays/quickshell/greetd-response.patch16
-rw-r--r--overlays/quickshell/io.patch13
-rw-r--r--overlays/quickshell/lock-state-changed.patch12
-rw-r--r--overlays/quickshell/pipewire.patch488
-rw-r--r--overlays/swayosd/default.nix13
-rw-r--r--overlays/swayosd/exponential.patch57
8 files changed, 543 insertions, 70 deletions
diff --git a/overlays/niri.nix b/overlays/niri.nix
index 9188ed7d..95a918b0 100644
--- a/overlays/niri.nix
+++ b/overlays/niri.nix
@@ -3,6 +3,7 @@
3 (final: prev: { 3 (final: prev: {
4 niri-unstable = prev.niri-unstable.overrideAttrs (oldAttrs: { 4 niri-unstable = prev.niri-unstable.overrideAttrs (oldAttrs: {
5 buildInputs = (oldAttrs.buildInputs or []) ++ [ final.libgbm ]; 5 buildInputs = (oldAttrs.buildInputs or []) ++ [ final.libgbm ];
6 doCheck = false;
6 }); 7 });
7 }) 8 })
8 final prev 9 final prev
diff --git a/overlays/quickshell/default.nix b/overlays/quickshell/default.nix
new file mode 100644
index 00000000..c01fac20
--- /dev/null
+++ b/overlays/quickshell/default.nix
@@ -0,0 +1,13 @@
1{ final, prev, sources, ... }:
2{
3 quickshell = prev.quickshell.overrideAttrs (oldAttrs: {
4 inherit (sources.quickshell) version src;
5
6 patches = (oldAttrs.patches or []) ++ [
7 ./greetd-response.patch
8 ./lock-state-changed.patch
9 ./pipewire.patch
10 ./io.patch
11 ];
12 });
13}
diff --git a/overlays/quickshell/greetd-response.patch b/overlays/quickshell/greetd-response.patch
new file mode 100644
index 00000000..a0efb562
--- /dev/null
+++ b/overlays/quickshell/greetd-response.patch
@@ -0,0 +1,16 @@
1diff --git c/src/services/greetd/connection.cpp w/src/services/greetd/connection.cpp
2index bf0d1fd..a790ab7 100644
3--- c/src/services/greetd/connection.cpp
4+++ w/src/services/greetd/connection.cpp
5@@ -225,6 +225,11 @@ void GreetdConnection::onSocketReady() {
6
7 this->mResponseRequired = responseRequired;
8 emit this->authMessage(message, error, responseRequired, echoResponse);
9+
10+ if (!responseRequired)
11+ this->sendRequest({
12+ {"type", "post_auth_message_response"}
13+ });
14 } else goto unexpected;
15
16 return;
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..2d98eefc
--- /dev/null
+++ b/overlays/quickshell/pipewire.patch
@@ -0,0 +1,488 @@
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/node.cpp w/src/services/pipewire/node.cpp
215index 3e68149..4721a58 100644
216--- i/src/services/pipewire/node.cpp
217+++ w/src/services/pipewire/node.cpp
218@@ -145,6 +145,10 @@ void PwNode::initProps(const spa_dict* props) {
219 this->type = PwNodeType::VideoSink;
220 } else if (strcmp(mediaClass, "Video/Source") == 0) {
221 this->type = PwNodeType::VideoSource;
222+ } else if (strcmp(mediaClass, "Stream/Output/Video") == 0) {
223+ this->type = PwNodeType::VideoOutStream;
224+ } else if (strcmp(mediaClass, "Stream/Input/Video") == 0) {
225+ this->type = PwNodeType::VideoInStream;
226 }
227 }
228
229diff --git i/src/services/pipewire/node.hpp w/src/services/pipewire/node.hpp
230index 0d4c92e..ee6f223 100644
231--- i/src/services/pipewire/node.hpp
232+++ w/src/services/pipewire/node.hpp
233@@ -144,6 +144,8 @@ public:
234 // This is equivalent to the media class `Video/Sink` and is composed of the
235 // @@PwNodeType.Video and @@PwNodeType.Sink flags.
236 VideoSink = Video | Sink,
237+ VideoOutStream = Video | Sink | Stream,
238+ VideoInStream = Video | Source | Stream,
239 };
240 Q_ENUM(Flag);
241 Q_DECLARE_FLAGS(Flags, Flag);
242diff --git i/src/services/pipewire/qml.cpp w/src/services/pipewire/qml.cpp
243index 9efb17e..921d12a 100644
244--- i/src/services/pipewire/qml.cpp
245+++ w/src/services/pipewire/qml.cpp
246@@ -9,6 +9,9 @@
247 #include <qtypes.h>
248 #include <qvariant.h>
249
250+#include <cstdint>
251+#include <algorithm>
252+
253 #include "../../core/model.hpp"
254 #include "connection.hpp"
255 #include "defaults.hpp"
256@@ -54,6 +57,12 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
257
258 QObject::connect(&connection->registry, &PwRegistry::nodeAdded, this, &Pipewire::onNodeAdded);
259
260+ for (auto* device: connection->registry.devices.values()) {
261+ this->onDeviceAdded(device);
262+ }
263+
264+ QObject::connect(&connection->registry, &PwRegistry::deviceAdded, this, &Pipewire::onDeviceAdded);
265+
266 for (auto* link: connection->registry.links.values()) {
267 this->onLinkAdded(link);
268 }
269@@ -123,6 +132,19 @@ void Pipewire::onNodeRemoved(QObject* object) {
270 this->mNodes.removeObject(iface);
271 }
272
273+ObjectModel<PwDeviceIface>* Pipewire::devices() { return &this->mDevices; }
274+
275+void Pipewire::onDeviceAdded(PwDevice* device) {
276+ auto* iface = PwDeviceIface::instance(device);
277+ QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onDeviceRemoved);
278+ this->mDevices.insertObject(iface);
279+}
280+
281+void Pipewire::onDeviceRemoved(QObject* object) {
282+ auto* iface = static_cast<PwDeviceIface*>(object); // NOLINT
283+ this->mDevices.removeObject(iface);
284+}
285+
286 ObjectModel<PwLinkIface>* Pipewire::links() { return &this->mLinks; }
287
288 void Pipewire::onLinkAdded(PwLink* link) {
289@@ -357,6 +379,8 @@ QVariantMap PwNodeIface::properties() const {
290
291 PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; }
292
293+PwDeviceIface* PwNodeIface::device() const { return PwDeviceIface::instance(this->mNode->device); }
294+
295 PwNodeIface* PwNodeIface::instance(PwNode* node) {
296 if (node == nullptr) return nullptr;
297
298@@ -481,4 +505,42 @@ void PwObjectTracker::objectDestroyed(QObject* object) {
299 emit this->objectsChanged();
300 }
301
302+PwDeviceIface::PwDeviceIface(PwDevice* device): PwObjectIface(device), mDevice(device) {
303+ QObject::connect(device, &PwDevice::profilesChanged, this, &PwDeviceIface::deviceProfilesChanged);
304+ QObject::connect(device, &PwDevice::currentProfileChanged, this, &PwDeviceIface::deviceCurrentProfileChanged);
305+}
306+
307+void PwDeviceIface::deviceProfilesChanged(QList<PwProfile>) { emit this->profilesChanged(); }
308+void PwDeviceIface::deviceCurrentProfileChanged(PwProfile) { emit this->currentProfileChanged(); }
309+
310+quint32 PwDeviceIface::id() const { return this->mDevice->id; }
311+QString PwDeviceIface::name() const { return this->mDevice->name; }
312+QString PwDeviceIface::description() const { return this->mDevice->description; }
313+QString PwDeviceIface::nickname() const { return this->mDevice->nick; }
314+QString PwDeviceIface::type() const { return this->mDevice->type; }
315+QList<PwProfile> PwDeviceIface::profiles() const {
316+ QList<PwProfile> profiles = this->mDevice->profiles.values();
317+ std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; });
318+ return profiles;
319+}
320+qint32 PwDeviceIface::currentProfile() const { return this->mDevice->currentProfile->index; }
321+
322+PwDeviceIface* PwDeviceIface::instance(PwDevice* device) {
323+ if (device == nullptr) return nullptr;
324+
325+ auto v = device->property("iface");
326+ if (v.canConvert<PwDeviceIface*>()) {
327+ return v.value<PwDeviceIface*>();
328+ }
329+
330+ auto* instance = new PwDeviceIface(device);
331+ device->setProperty("iface", QVariant::fromValue(instance));
332+
333+ return instance;
334+}
335+
336+void PwDeviceIface::setProfile(qint32 profileIndex) {
337+ this->mDevice->setProfile(profileIndex);
338+}
339+
340 } // namespace qs::service::pipewire
341diff --git i/src/services/pipewire/qml.hpp w/src/services/pipewire/qml.hpp
342index e3489a1..e5e1891 100644
343--- i/src/services/pipewire/qml.hpp
344+++ w/src/services/pipewire/qml.hpp
345@@ -12,11 +12,13 @@
346 #include "../../core/model.hpp"
347 #include "link.hpp"
348 #include "node.hpp"
349+#include "device.hpp"
350 #include "registry.hpp"
351
352 namespace qs::service::pipewire {
353
354 class PwNodeIface;
355+class PwDeviceIface;
356 class PwLinkIface;
357 class PwLinkGroupIface;
358
359@@ -65,6 +67,8 @@ class Pipewire: public QObject {
360 /// - @@PwNode.audio - if non null the node is an audio node.
361 QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwNodeIface>*);
362 Q_PROPERTY(UntypedObjectModel* nodes READ nodes CONSTANT);
363+ QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwDeviceIface>*);
364+ Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
365 /// All links present in pipewire.
366 ///
367 /// Links connect pipewire nodes to each other, and can be used to determine
368@@ -134,6 +138,7 @@ public:
369 explicit Pipewire(QObject* parent = nullptr);
370
371 [[nodiscard]] ObjectModel<PwNodeIface>* nodes();
372+ [[nodiscard]] ObjectModel<PwDeviceIface>* devices();
373 [[nodiscard]] ObjectModel<PwLinkIface>* links();
374 [[nodiscard]] ObjectModel<PwLinkGroupIface>* linkGroups();
375
376@@ -159,7 +164,9 @@ signals:
377
378 private slots:
379 void onNodeAdded(PwNode* node);
380+ void onDeviceAdded(PwDevice* node);
381 void onNodeRemoved(QObject* object);
382+ void onDeviceRemoved(QObject* object);
383 void onLinkAdded(PwLink* link);
384 void onLinkRemoved(QObject* object);
385 void onLinkGroupAdded(PwLinkGroup* group);
386@@ -167,6 +174,7 @@ private slots:
387
388 private:
389 ObjectModel<PwNodeIface> mNodes {this};
390+ ObjectModel<PwDeviceIface> mDevices {this};
391 ObjectModel<PwLinkIface> mLinks {this};
392 ObjectModel<PwLinkGroupIface> mLinkGroups {this};
393 };
394@@ -315,6 +323,7 @@ class PwNodeIface: public PwObjectIface {
395 /// > [!NOTE] The node may be used before it is fully bound, but some data
396 /// > may be missing or incorrect.
397 Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged);
398+ Q_PROPERTY(qs::service::pipewire::PwDeviceIface* device READ device CONSTANT);
399 QML_NAMED_ELEMENT(PwNode);
400 QML_UNCREATABLE("PwNodes cannot be created directly");
401
402@@ -332,6 +341,7 @@ public:
403 [[nodiscard]] PwNodeType::Flags type() const;
404 [[nodiscard]] QVariantMap properties() const;
405 [[nodiscard]] PwNodeAudioIface* audio() const;
406+ [[nodiscard]] PwDeviceIface* device() const;
407
408 static PwNodeIface* instance(PwNode* node);
409
410@@ -344,6 +354,44 @@ private:
411 PwNodeAudioIface* audioIface = nullptr;
412 };
413
414+class PwDeviceIface: public PwObjectIface {
415+ Q_OBJECT;
416+ Q_PROPERTY(quint32 id READ id CONSTANT);
417+ Q_PROPERTY(QString name READ name CONSTANT);
418+ Q_PROPERTY(QString description READ description CONSTANT);
419+ Q_PROPERTY(QString nickname READ nickname CONSTANT);
420+ Q_PROPERTY(QString type READ type CONSTANT);
421+ Q_PROPERTY(QList<PwProfile> profiles READ profiles NOTIFY profilesChanged);
422+ Q_PROPERTY(qint32 currentProfile READ currentProfile NOTIFY currentProfileChanged);
423+
424+ QML_NAMED_ELEMENT(PwDevice);
425+ QML_UNCREATABLE("PwDevices cannot be created directly");
426+
427+signals:
428+ void profilesChanged();
429+ void currentProfileChanged();
430+
431+public:
432+ explicit PwDeviceIface(PwDevice* node);
433+
434+ [[nodiscard]] quint32 id() const;
435+ [[nodiscard]] QString name() const;
436+ [[nodiscard]] QString description() const;
437+ [[nodiscard]] QString nickname() const;
438+ [[nodiscard]] QString type() const;
439+ QList<PwProfile> profiles() const;
440+ qint32 currentProfile() const;
441+
442+ Q_INVOKABLE void setProfile(qint32 profileIndex);
443+
444+ static PwDeviceIface* instance(PwDevice* node);
445+private:
446+ PwDevice* mDevice;
447+
448+ void deviceProfilesChanged(QList<PwProfile> profiles);
449+ void deviceCurrentProfileChanged(PwProfile profile);
450+};
451+
452 ///! A connection between pipewire nodes.
453 /// Note that there is one link per *channel* of a connection between nodes.
454 /// You usually want @@PwLinkGroup.
455diff --git i/src/services/pipewire/registry.cpp w/src/services/pipewire/registry.cpp
456index c08fc1d..50c6d7a 100644
457--- i/src/services/pipewire/registry.cpp
458+++ w/src/services/pipewire/registry.cpp
459@@ -196,6 +196,7 @@ void PwRegistry::onGlobal(
460 device->initProps(props);
461
462 self->devices.emplace(id, device);
463+ emit self->deviceAdded(device);
464 }
465 }
466
467@@ -211,6 +212,9 @@ void PwRegistry::onGlobalRemoved(void* data, quint32 id) {
468 } else if (auto* node = self->nodes.value(id)) {
469 self->nodes.remove(id);
470 node->safeDestroy();
471+ } else if (auto* device = self->devices.value(id)) {
472+ self->devices.remove(id);
473+ device->safeDestroy();
474 }
475 }
476
477diff --git i/src/services/pipewire/registry.hpp w/src/services/pipewire/registry.hpp
478index 8473f04..87e0766 100644
479--- i/src/services/pipewire/registry.hpp
480+++ w/src/services/pipewire/registry.hpp
481@@ -132,6 +132,7 @@ public:
482
483 signals:
484 void nodeAdded(PwNode* node);
485+ void deviceAdded(PwDevice* node);
486 void linkAdded(PwLink* link);
487 void linkGroupAdded(PwLinkGroup* group);
488 void metadataAdded(PwMetadata* metadata);
diff --git a/overlays/swayosd/default.nix b/overlays/swayosd/default.nix
deleted file mode 100644
index 5e715dae..00000000
--- a/overlays/swayosd/default.nix
+++ /dev/null
@@ -1,13 +0,0 @@
1{ final, prev, sources, ... }: {
2 swayosd = prev.swayosd.overrideAttrs (oldAttrs: rec {
3 inherit (sources.swayosd) version src;
4 cargoDeps = prev.rustPlatform.fetchCargoVendor {
5 inherit (oldAttrs) pname;
6 inherit version src;
7 hash = "sha256-J2sl6/4+bRWlkvaTJtFsMqvvOxYtWLRjJcYWcu0loRE=";
8 };
9 patches = (oldAttrs.patches or []) ++ [
10 ./exponential.patch
11 ];
12 });
13}
diff --git a/overlays/swayosd/exponential.patch b/overlays/swayosd/exponential.patch
deleted file mode 100644
index eb90d739..00000000
--- a/overlays/swayosd/exponential.patch
+++ /dev/null
@@ -1,57 +0,0 @@
1diff --git a/src/brightness_backend/brightnessctl.rs b/src/brightness_backend/brightnessctl.rs
2index ccb0e11..740fdb6 100644
3--- a/src/brightness_backend/brightnessctl.rs
4+++ b/src/brightness_backend/brightnessctl.rs
5@@ -107,10 +107,21 @@ impl VirtualDevice {
6 }
7 }
8
9- fn set_percent(&mut self, mut val: u32) -> anyhow::Result<()> {
10- val = val.clamp(0, 100);
11- self.current = self.max.map(|max| val * max / 100);
12- let _: String = self.run(("set", &*format!("{val}%")))?;
13+ fn val_to_percent(&mut self, val: u32) -> u32 {
14+ return ((val as f64 / self.get_max() as f64).powf(0.25) * 100_f64).round() as u32;
15+ }
16+ fn percent_to_val(&mut self, perc: u32) -> u32 {
17+ return ((perc as f64 / 100_f64).powf(4_f64) * self.get_max() as f64).round() as u32;
18+ }
19+
20+ fn set_percent(&mut self, val: u32) -> anyhow::Result<()> {
21+ let new = self.percent_to_val(val);
22+ self.set_val(new)
23+ }
24+ fn set_val(&mut self, val: u32) -> anyhow::Result<()> {
25+ let curr = val.clamp(0, self.get_max());
26+ self.current = Some(curr);
27+ let _: String = self.run(("set", &*format!("{curr}")))?;
28 Ok(())
29 }
30 }
31@@ -134,20 +145,18 @@ impl BrightnessBackend for BrightnessCtl {
32
33 fn lower(&mut self, by: u32) -> anyhow::Result<()> {
34 let curr = self.get_current();
35- let max = self.get_max();
36-
37- let curr = curr * 100 / max;
38+ let mut new = self.device.val_to_percent(curr).saturating_sub(by);
39+ new = self.device.percent_to_val(new).min(curr.saturating_sub(1));
40
41- self.device.set_percent(curr.saturating_sub(by))
42+ self.device.set_val(new)
43 }
44
45 fn raise(&mut self, by: u32) -> anyhow::Result<()> {
46 let curr = self.get_current();
47- let max = self.get_max();
48-
49- let curr = curr * 100 / max;
50+ let mut new = self.device.val_to_percent(curr) + by;
51+ new = self.device.percent_to_val(new).max(curr + 1);
52
53- self.device.set_percent(curr + by)
54+ self.device.set_val(new)
55 }
56
57 fn set(&mut self, val: u32) -> anyhow::Result<()> {