summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGregor Kleen <gkleen@yggdrasil.li>2025-09-12 22:01:51 +0200
committerGregor Kleen <gkleen@yggdrasil.li>2025-09-12 22:01:51 +0200
commit666464567055a2e4ba9f6bb310e901cdc27977f7 (patch)
tree45e626dc591803925880230a3e06d568e6a5fa48
parent1ff0e9ecbef79e1b3592cd4a68ce3e90c8536bdb (diff)
downloadnixos-666464567055a2e4ba9f6bb310e901cdc27977f7.tar
nixos-666464567055a2e4ba9f6bb310e901cdc27977f7.tar.gz
nixos-666464567055a2e4ba9f6bb310e901cdc27977f7.tar.bz2
nixos-666464567055a2e4ba9f6bb310e901cdc27977f7.tar.xz
nixos-666464567055a2e4ba9f6bb310e901cdc27977f7.zip
...
-rw-r--r--accounts/gkleen@sif/niri/default.nix32
-rw-r--r--accounts/gkleen@sif/niri/mako.nix2
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml2
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Lockscreen.qml5
-rw-r--r--accounts/gkleen@sif/shell/quickshell/NiriIdle.qml4
-rw-r--r--accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml302
-rw-r--r--accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml47
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml98
-rw-r--r--accounts/gkleen@sif/shell/quickshell/UnixIPC.qml58
-rw-r--r--accounts/gkleen@sif/shell/quickshell/shell.qml2
10 files changed, 501 insertions, 51 deletions
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
index 3c29b83c..5ae372c1 100644
--- a/accounts/gkleen@sif/niri/default.nix
+++ b/accounts/gkleen@sif/niri/default.nix
@@ -7,9 +7,6 @@ let
7 7
8 niri = cfg.package; 8 niri = cfg.package;
9 terminal = lib.getExe config.programs.kitty.package; 9 terminal = lib.getExe config.programs.kitty.package;
10 makoctl = lib.getExe' config.services.mako.package "makoctl";
11 loginctl = lib.getExe' hostConfig.systemd.package "loginctl";
12 systemctl = lib.getExe' hostConfig.systemd.package "systemctl";
13 10
14 focus_or_spawn = pkgs.writeShellApplication { 11 focus_or_spawn = pkgs.writeShellApplication {
15 name = "focus-or-spawn"; 12 name = "focus-or-spawn";
@@ -164,10 +161,6 @@ let
164 with-urgent-window-action = config.lib.niri.actions.spawn (lib.getExe (with_predicate_window ".is_urgent")); 161 with-urgent-window-action = config.lib.niri.actions.spawn (lib.getExe (with_predicate_window ".is_urgent"));
165 with-focused-window-action = config.lib.niri.actions.spawn (lib.getExe (with_predicate_window ".is_focused")); 162 with-focused-window-action = config.lib.niri.actions.spawn (lib.getExe (with_predicate_window ".is_focused"));
166in { 163in {
167 imports = [
168 ./mako.nix
169 ];
170
171 options = { 164 options = {
172 programs.niri.scratchspaces = lib.mkOption { 165 programs.niri.scratchspaces = lib.mkOption {
173 type = lib.types.listOf (lib.types.submodule ({ config, ... }: { 166 type = lib.types.listOf (lib.types.submodule ({ config, ... }: {
@@ -910,25 +903,12 @@ in {
910 action = power-off-monitors; 903 action = power-off-monitors;
911 allow-when-locked = true; 904 allow-when-locked = true;
912 }; 905 };
913 # "Mod+Shift+L".action = spawn loginctl "lock-session";
914 "Mod+Shift+E".action = quit; 906 "Mod+Shift+E".action = quit;
915 # "Mod+Shift+Minus" = { 907
916 # action = spawn systemctl "suspend"; 908 # "Mod+Semicolon".action = spawn makoctl "dismiss" "--group";
917 # allow-when-locked = true; 909 # "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all";
918 # }; 910 # "Mod+Period".action = spawn makoctl "menu" "--" (lib.getExe config.programs.fuzzel.package) "--dmenu";
919 # "Mod+Shift+Control+Minus" = { 911 # "Mod+Comma".action = spawn makoctl "restore";
920 # action = spawn systemctl "hibernate";
921 # allow-when-locked = true;
922 # };
923 # "Mod+Shift+P" = {
924 # action = spawn (lib.getExe pkgs.playerctl) "-a" "pause";
925 # allow-when-locked = true;
926 # };
927
928 "Mod+Semicolon".action = spawn makoctl "dismiss" "--group";
929 "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all";
930 "Mod+Period".action = spawn makoctl "menu" "--" (lib.getExe config.programs.fuzzel.package) "--dmenu";
931 "Mod+Comma".action = spawn makoctl "restore";
932 912
933 "Mod+Control+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}"; 913 "Mod+Control+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}";
934 "Mod+Control+Shift+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"MoveColumnToWorkspace\":{\"reference\":{\"Id\": $workspace_id}, \"focus\": true}}}"; 914 "Mod+Control+Shift+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"MoveColumnToWorkspace\":{\"reference\":{\"Id\": $workspace_id}, \"focus\": true}}}";
@@ -986,6 +966,8 @@ in {
986 action = shell { Mpris = { PauseAll = {}; }; }; 966 action = shell { Mpris = { PauseAll = {}; }; };
987 allow-when-locked = true; 967 allow-when-locked = true;
988 }; 968 };
969 "Mod+Semicolon".action = shell { Notifications = { DismissGroup = {}; }; };
970 "Mod+Shift+Semicolon".action = shell { Notifications = { DismissAll = {}; }; };
989 })) 971 }))
990 (map ({ name, selector, spawn, key, ...}: if key != null && selector != null && spawn != null then bind key { action = focus-or-spawn-action selector name spawn; } else null) cfg.scratchspaces) 972 (map ({ name, selector, spawn, key, ...}: if key != null && selector != null && spawn != null then bind key { action = focus-or-spawn-action selector name spawn; } else null) cfg.scratchspaces)
991 (map ({ name, moveKey, ...}: if moveKey != null then bind moveKey { action = kdl.magic-leaf "move-column-to-workspace" name; } else null) cfg.scratchspaces) 973 (map ({ name, moveKey, ...}: if moveKey != null then bind moveKey { action = kdl.magic-leaf "move-column-to-workspace" name; } else null) cfg.scratchspaces)
diff --git a/accounts/gkleen@sif/niri/mako.nix b/accounts/gkleen@sif/niri/mako.nix
index 703d5f7b..3d246d96 100644
--- a/accounts/gkleen@sif/niri/mako.nix
+++ b/accounts/gkleen@sif/niri/mako.nix
@@ -1,6 +1,6 @@
1{ config, lib, pkgs, ... }: 1{ config, lib, pkgs, ... }:
2{ 2{
3 config = { 3 config = lib.mkIf false {
4 services.mako = { 4 services.mako = {
5 enable = true; 5 enable = true;
6 settings = { 6 settings = {
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
index 7f97bd75..dd0feb4b 100644
--- a/accounts/gkleen@sif/shell/quickshell/Bar.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -100,6 +100,8 @@ PanelWindow {
100 window: bar 100 window: bar
101 } 101 }
102 102
103 NotificationInhibitorWidget {}
104
103 LidSwitchInhibitorWidget {} 105 LidSwitchInhibitorWidget {}
104 106
105 Item { 107 Item {
diff --git a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
index 124f441b..880e6614 100644
--- a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
+++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
@@ -78,7 +78,7 @@ Scope {
78 pam.abort(); 78 pam.abort();
79 79
80 if (locked) { 80 if (locked) {
81 NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }); 81 NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {});
82 Custom.KeePassXC.lockAllDatabases(); 82 Custom.KeePassXC.lockAllDatabases();
83 Array.from(MprisProxy.players).forEach(player => { 83 Array.from(MprisProxy.players).forEach(player => {
84 if (player.canPause && player.isPlaying) 84 if (player.canPause && player.isPlaying)
@@ -88,8 +88,7 @@ Scope {
88 GpgAgent.reloadAgent(); 88 GpgAgent.reloadAgent();
89 } 89 }
90 } 90 }
91 91 Component.onCompleted: { (_ => {})(MprisProxy.players); }
92 Binding { target: MprisProxy; }
93 92
94 onSecureStateChanged: Custom.Systemd.lockedHint = lock.secure 93 onSecureStateChanged: Custom.Systemd.lockedHint = lock.secure
95 94
diff --git a/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml b/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml
index d65711e2..beff205c 100644
--- a/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml
+++ b/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml
@@ -17,14 +17,14 @@ Scope {
17 17
18 onIsIdleChanged: { 18 onIsIdleChanged: {
19 if (idleMonitor540.isIdle) 19 if (idleMonitor540.isIdle)
20 NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }); 20 NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {});
21 } 21 }
22 } 22 }
23 Connections { 23 Connections {
24 target: Custom.Systemd 24 target: Custom.Systemd
25 function onSleep(before: bool) { 25 function onSleep(before: bool) {
26 if (!before) 26 if (!before)
27 NiriService.sendCommand({ "Action": { "PowerOnMonitors": {} } }); 27 NiriService.sendCommand({ "Action": { "PowerOnMonitors": {} } }, _ => {});
28 } 28 }
29 } 29 }
30} 30}
diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml
new file mode 100644
index 00000000..589c36e5
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml
@@ -0,0 +1,302 @@
1import QtQml
2import QtQml.Models
3import QtQuick
4import Quickshell
5import Quickshell.Widgets
6import Quickshell.Wayland
7import qs.Services
8import QtQuick.Layouts
9import Quickshell.Services.Notifications
10
11Scope {
12 readonly property ShellScreen activeScreen: Array.from(Quickshell.screens).find(screen => screen.name === Array.from(NiriService.workspaces).find(ws => ws.is_focused)?.output) ?? null
13
14 Instantiator {
15 id: notifsRepeater
16
17 model: ScriptModel {
18 values: NotificationManager.displayInhibited ? [] : [...NotificationManager.groups]
19 }
20
21 delegate: PanelWindow {
22 id: notifWindow
23
24 required property var modelData
25 required property var index
26
27 property int activeIx: modelData.length - 1
28 onModelDataChanged: {
29 notifWindow.activeIx = modelData.length - 1;
30 }
31
32 property color textColor: {
33 if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Low)
34 return "#ff999999";
35 return "white";
36 }
37 property color backgroundColor: {
38 if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Critical)
39 return "#dd900000";
40 return "black";
41 }
42
43 anchors {
44 right: true
45 top: true
46 }
47
48 readonly property real spaceAbove: {
49 var res = 0;
50 for (let i = 0; i < notifWindow.index; i++) {
51 res += notifsRepeater.objectAt(i).height + 8;
52 }
53 return res;
54 }
55
56 margins {
57 right: 26 + 8
58 top: 8 + spaceAbove
59 }
60
61 color: "transparent"
62
63 implicitHeight: Math.max(notifCount.visible ? notifCount.contentHeight : 0, notifSummary.contentHeight) + (notifBody.visible ? 8 + notifBody.contentHeight : 0) + (notifActions.visible ? 8 + notifActions.height : 0) + 16
64 implicitWidth: 400
65
66 WrapperMouseArea {
67 enabled: true
68
69 anchors.fill: parent
70
71 cursorShape: Qt.PointingHandCursor
72
73 onClicked: {
74 for (const notif of notifWindow.modelData)
75 notif.dismiss();
76 }
77
78 property real angleRem: 0
79 property real sensitivity: 1 / 120
80 onWheel: event => {
81 angleRem += event.angleDelta.y;
82 const d = Math.round(angleRem * sensitivity);
83 angleRem -= d / sensitivity;
84 notifWindow.activeIx = ((notifWindow.modelData?.length ?? 1) + notifWindow.activeIx - d) % (notifWindow.modelData?.length ?? 1);
85 }
86
87 Rectangle {
88 color: notifWindow.backgroundColor
89 anchors.fill: parent
90 border {
91 color: Qt.hsla(195/360, 1, 0.45, 1)
92 width: 2
93 }
94
95 GridLayout {
96 id: notifLayout
97
98 width: 400 - 16
99 anchors.fill: parent
100 anchors.margins: 8
101 columnSpacing: 8
102 rowSpacing: 8
103
104 columns: notifImage.visible ? 3 : 2
105 rows: {
106 var res = 1;
107 if (notifBody.visible)
108 res += 1;
109 if (notifActions.visible)
110 res += 1;
111 return res;
112 }
113
114 Text {
115 id: notifCount
116
117 visible: notifWindow.modelData?.length > 1 ?? false
118 text: `${notifWindow.activeIx + 1}/${notifWindow.modelData?.length ?? ""}`
119
120 font.pointSize: 10
121 font.family: "Fira Sans"
122 font.bold: true
123 font.features: { "tnum": 1 }
124 color: notifWindow.textColor
125 maximumLineCount: 1
126
127 Layout.fillWidth: false
128 Layout.row: 0
129 Layout.column: notifImage.visible ? 1 : 0
130 }
131
132 Text {
133 id: notifSummary
134
135 text: notifWindow.modelData?.[notifWindow.activeIx]?.summary ?? ""
136
137 font.pointSize: 10
138 font.family: "Fira Sans"
139 font.italic: true
140 color: notifWindow.textColor
141 maximumLineCount: 1
142 elide: Text.ElideRight
143
144 Layout.fillWidth: true
145 Layout.row: 0
146 Layout.column: (notifCount.visible ? 1 : 0) + (notifImage.visible ? 1 : 0)
147 Layout.columnSpan: notifCount.visible ? 1 : 2
148 }
149
150 Image {
151 id: notifImage
152
153 visible: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? false
154
155 onStatusChanged: {
156 if (notifImage.status == Image.Error)
157 notifImage.visible = false;
158 }
159
160 source: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? ""
161 fillMode: Image.PreserveAspectFit
162 asynchronous: true
163 smooth: true
164 mipmap: true
165
166 Layout.maximumWidth: 50
167 Layout.column: 0
168 Layout.row: 0
169 Layout.fillHeight: true
170 Layout.rowSpan: notifBody.visible ? 2 : 1
171 }
172
173 Text {
174 id: notifBody
175
176 visible: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? false
177 text: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? ""
178 textFormat: Text.RichText
179
180 font.pointSize: 10
181 font.family: "Fira Sans"
182 color: notifWindow.textColor
183
184 Layout.fillWidth: true
185 Layout.row: 1
186 Layout.column: notifImage.visible ? 1 : 0
187 Layout.columnSpan: notifCount.visible ? 2 : 1
188 }
189
190 RowLayout {
191 id: notifActions
192
193 visible: notifWindow.modelData?.[notifWindow.activeIx]?.actions.length > 0 ?? false
194
195 spacing: 8
196 uniformCellSizes: true
197
198 width: 400 - 16
199 Layout.row: notifBody.visible ? 2 : 1
200 Layout.column: 0
201 Layout.columnSpan: notifImage.visible ? 3 : 2
202
203 Repeater {
204 model: ScriptModel {
205 values: notifWindow.modelData?.[notifWindow.activeIx]?.actions
206 }
207
208 delegate: WrapperMouseArea {
209 id: actionMouseArea
210
211 required property var modelData
212
213 height: actionLabelWrapper.implicitHeight
214 Layout.fillWidth: true
215 Layout.horizontalStretchFactor: 1
216
217 hoverEnabled: true
218 cursorShape: Qt.PointingHandCursor
219
220 onClicked: actionMouseArea.modelData?.invoke()
221
222 Rectangle {
223 anchors.fill: parent
224
225 color: actionMouseArea.containsMouse ? "#20ffffff" : "transparent"
226
227 border {
228 width: 2
229 color: "#20ffffff"
230 }
231
232 WrapperItem {
233 id: actionLabelWrapper
234
235 margin: 8
236 anchors.centerIn: parent
237
238 RowLayout {
239 id: actionLabelLayout
240
241 spacing: 8
242
243 IconImage {
244 id: actionIcon
245
246 visible: notifWindow.modelData?.[notifWindow.activeIx]?.hasActionIcons
247
248 onStatusChanged: {
249 if (actionIcon.status == Image.Error)
250 actionIcon.visible = false;
251 }
252
253 implicitSize: 16
254 source: {
255 if (!actionIcon.visible)
256 return "";
257
258 let icon = actionMouseArea.modelData?.identifier ?? ""
259 if (icon.includes("?path=")) {
260 const split = icon.split("?path=")
261 if (split.length !== 2)
262 return icon
263 const name = split[0]
264 const path = split[1]
265 const fileName = name.substring(
266 name.lastIndexOf("/") + 1)
267 return `file://${path}/${fileName}`
268 }
269 return icon
270 }
271 asynchronous: true
272 smooth: true
273 mipmap: true
274
275 Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
276 }
277
278 Text {
279 id: actionLabel
280
281 visible: actionMouseArea.modelData?.text ?? false
282
283 text: actionMouseArea.modelData?.text ?? ""
284
285 font.pointSize: 10
286 font.family: "Fira Sans"
287 color: notifWindow.textColor
288 maximumLineCount: 1
289 elide: Text.ElideRight
290 }
291 }
292 }
293 }
294 }
295 }
296 }
297 }
298 }
299 }
300 }
301 }
302}
diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml
new file mode 100644
index 00000000..3dadbc69
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml
@@ -0,0 +1,47 @@
1import Quickshell
2import QtQuick
3import Quickshell.Widgets
4import qs.Services
5
6Item {
7 id: root
8
9 width: icon.width + 8
10 height: parent.height
11 anchors.verticalCenter: parent.verticalCenter
12
13 WrapperMouseArea {
14 id: widgetMouseArea
15
16 anchors.fill: parent
17
18 hoverEnabled: true
19 cursorShape: Qt.PointingHandCursor
20
21 onClicked: NotificationManager.displayInhibited = !NotificationManager.displayInhibited
22
23 Rectangle {
24 anchors.fill: parent
25 color: {
26 if (widgetMouseArea.containsMouse) {
27 return "#33808080";
28 }
29 return "transparent";
30 }
31
32 Item {
33 anchors.fill: parent
34
35 MaterialDesignIcon {
36 id: icon
37
38 implicitSize: 14
39 anchors.centerIn: parent
40
41 icon: NotificationManager.displayInhibited ? "message-off" : "message"
42 color: NotificationManager.displayInhibited ? "white" : "#555"
43 }
44 }
45 }
46 }
47}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml
new file mode 100644
index 00000000..778cdc2a
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml
@@ -0,0 +1,98 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Services.Notifications
6
7Singleton {
8 id: root
9
10 property bool displayInhibited: false
11 property alias trackedNotifications: server.trackedNotifications
12 readonly property var groups: {
13 function matchesGroupKey(notif, groupKey) {
14 var matches = true;
15 for (const prop in groupKey.test) {
16 if (notif[prop] !== groupKey.test[prop]) {
17 matches = false;
18 break;
19 }
20 }
21 return matches;
22 }
23
24 var groups = new Map();
25 var notifs = new Array();
26 for (const [ix, notif] of server.trackedNotifications.values.entries()) {
27 var didGroup = false;
28 for (const groupKey of root.groupKeys) {
29 if (!matchesGroupKey(notif, groupKey))
30 continue;
31
32 const key = JSON.stringify({
33 "key": groupKey,
34 "values": Object.assign({}, ...(Array.from(groupKey["group-by"]).map(prop => {
35 var res = {};
36 res[prop] = notif[prop];
37 return res;
38 })))
39 });
40 if (!groups.has(key))
41 groups.set(key, new Array());
42 groups.get(key).push({ "ix": ix, "notif": notif });
43 didGroup = true;
44 break;
45 }
46
47 if (!didGroup)
48 notifs.push([{ "ix": ix, "notif": notif }]);
49 }
50 notifs.push(...groups.values());
51 notifs.sort((as, bs) => Math.min(...(as.map(o => o.ix))) - Math.min(...(bs.map(o => o.ix))));
52 return notifs.map(ns => ns.map(n => n.notif));
53 }
54
55 property var groupKeys: [
56 { "test": { "appName": "Element" }, "group-by": [ "summary" ] }
57 ];
58
59 Component {
60 id: expirationTimer
61
62 QtObject {
63 id: timer
64
65 required property QtObject parent
66 required property int expirationTime
67 property list<QtObject> data: [
68 Timer {
69 running: !root.displayInhibited
70 interval: timer.expirationTime
71 onTriggered: timer.parent.expire()
72 }
73 ]
74 }
75 }
76
77 NotificationServer {
78 id: server
79
80 bodySupported: true
81 actionsSupported: true
82 actionIconsSupported: true
83 imageSupported: true
84 bodyMarkupSupported: true
85 bodyImagesSupported: true
86
87 onNotification: notification => {
88 var timeout = notification.expireTimeout * 1000;
89 if (notification.appName == "poweralertd")
90 timeout = 2000;
91 if (timeout > 0) {
92 Object.defineProperty(notification, "expirationTimer", { configurable: true, enumerable: true, writable: true });
93 notification.expirationTimer = expirationTimer.createObject(notification, { parent: notification, expirationTime: timeout });
94 }
95 notification.tracked = true;
96 }
97 }
98}
diff --git a/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml
index 4ec5186c..e19ccfb1 100644
--- a/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml
+++ b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml
@@ -15,26 +15,32 @@ Scope {
15 handler: Socket { 15 handler: Socket {
16 parser: SplitParser { 16 parser: SplitParser {
17 onRead: line => { 17 onRead: line => {
18 try { 18 const command = (() => {
19 const command = JSON.parse(line); 19 try {
20 return JSON.parse(line);
21 } catch (e) {
22 console.warn("UnixIPC: Failed to parse command:", line, e);
23 }
24 })();
25 if (!command)
26 return;
20 27
21 if (command.Volume) 28 if (command.Volume)
22 root.onCommandVolume(command.Volume); 29 root.onCommandVolume(command.Volume);
23 else if (command.Brightness) 30 else if (command.Brightness)
24 root.onCommandBrightness(command.Brightness); 31 root.onCommandBrightness(command.Brightness);
25 else if (command.LockSession) 32 else if (command.LockSession)
26 Custom.Systemd.lockSession(); 33 Custom.Systemd.lockSession();
27 else if (command.Suspend) 34 else if (command.Suspend)
28 Custom.Systemd.suspend(); 35 Custom.Systemd.suspend();
29 else if (command.Hibernate) 36 else if (command.Hibernate)
30 Custom.Systemd.hibernate(); 37 Custom.Systemd.hibernate();
31 else if (command.Mpris) 38 else if (command.Mpris)
32 root.onCommandMpris(command.Mpris); 39 root.onCommandMpris(command.Mpris);
33 else 40 else if (command.Notifications)
34 console.warn("UnixIPC: Command not handled:", JSON.stringify(command)); 41 root.onCommandNotifications(command.Notifications);
35 } catch (e) { 42 else
36 console.warn("UnixIPC: Failed to parse command:", line, e); 43 console.warn("UnixIPC: Command not handled:", JSON.stringify(command));
37 }
38 } 44 }
39 } 45 }
40 46
@@ -75,5 +81,17 @@ Scope {
75 player.pause(); 81 player.pause();
76 }); 82 });
77 } 83 }
78 Binding { target: MprisProxy; } 84 Component.onCompleted: { (_ => {})(MprisProxy.players); }
85
86 function onCommandNotifications(command) {
87 if (command.DismissGroup && !NotificationManager.displayInhibited) {
88 if (NotificationManager.groups.length > 0)
89 for (const notif of [...NotificationManager.groups[0]])
90 notif.dismiss();
91 }
92 if (command.DismissAll && !NotificationManager.displayInhibited) {
93 for (const notif of [...NotificationManager.trackedNotifications.values])
94 notif.dismiss();
95 }
96 }
79} 97}
diff --git a/accounts/gkleen@sif/shell/quickshell/shell.qml b/accounts/gkleen@sif/shell/quickshell/shell.qml
index 10c2eff6..1a5875f0 100644
--- a/accounts/gkleen@sif/shell/quickshell/shell.qml
+++ b/accounts/gkleen@sif/shell/quickshell/shell.qml
@@ -47,5 +47,7 @@ ShellRoot {
47 VolumeOSD {} 47 VolumeOSD {}
48 BrightnessOSD {} 48 BrightnessOSD {}
49 49
50 NotificationDisplay {}
51
50 UnixIPC {} 52 UnixIPC {}
51} 53}