diff options
author | Gregor Kleen <gkleen@yggdrasil.li> | 2025-09-12 22:01:51 +0200 |
---|---|---|
committer | Gregor Kleen <gkleen@yggdrasil.li> | 2025-09-12 22:01:51 +0200 |
commit | 666464567055a2e4ba9f6bb310e901cdc27977f7 (patch) | |
tree | 45e626dc591803925880230a3e06d568e6a5fa48 | |
parent | 1ff0e9ecbef79e1b3592cd4a68ce3e90c8536bdb (diff) | |
download | nixos-666464567055a2e4ba9f6bb310e901cdc27977f7.tar nixos-666464567055a2e4ba9f6bb310e901cdc27977f7.tar.gz nixos-666464567055a2e4ba9f6bb310e901cdc27977f7.tar.bz2 nixos-666464567055a2e4ba9f6bb310e901cdc27977f7.tar.xz nixos-666464567055a2e4ba9f6bb310e901cdc27977f7.zip |
...
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")); |
166 | in { | 163 | in { |
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 @@ | |||
1 | import QtQml | ||
2 | import QtQml.Models | ||
3 | import QtQuick | ||
4 | import Quickshell | ||
5 | import Quickshell.Widgets | ||
6 | import Quickshell.Wayland | ||
7 | import qs.Services | ||
8 | import QtQuick.Layouts | ||
9 | import Quickshell.Services.Notifications | ||
10 | |||
11 | Scope { | ||
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 @@ | |||
1 | import Quickshell | ||
2 | import QtQuick | ||
3 | import Quickshell.Widgets | ||
4 | import qs.Services | ||
5 | |||
6 | Item { | ||
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 @@ | |||
1 | pragma Singleton | ||
2 | |||
3 | import QtQml | ||
4 | import Quickshell | ||
5 | import Quickshell.Services.Notifications | ||
6 | |||
7 | Singleton { | ||
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 | } |