diff options
Diffstat (limited to 'accounts')
-rw-r--r-- | accounts/gkleen@sif/niri/default.nix | 32 | ||||
-rw-r--r-- | accounts/gkleen@sif/niri/mako.nix | 2 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/Bar.qml | 6 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/Lockscreen.qml | 11 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/NiriIdle.qml | 4 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml | 332 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml | 266 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml | 151 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/UnixIPC.qml | 58 | ||||
-rw-r--r-- | accounts/gkleen@sif/shell/quickshell/shell.qml | 2 |
10 files changed, 811 insertions, 53 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..426ed78c 100644 --- a/accounts/gkleen@sif/shell/quickshell/Bar.qml +++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml | |||
@@ -66,9 +66,9 @@ PanelWindow { | |||
66 | anchors.verticalCenter: parent.verticalCenter | 66 | anchors.verticalCenter: parent.verticalCenter |
67 | spacing: 0 | 67 | spacing: 0 |
68 | 68 | ||
69 | WorktimeWidget { command: "time"; } | 69 | // WorktimeWidget { command: "time"; } |
70 | 70 | ||
71 | WorktimeWidget { command: "today"; } | 71 | // WorktimeWidget { command: "today"; } |
72 | 72 | ||
73 | KeyboardLayout {} | 73 | KeyboardLayout {} |
74 | 74 | ||
@@ -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..996fd41b 100644 --- a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml +++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml | |||
@@ -70,6 +70,12 @@ Scope { | |||
70 | mode: Custom.SystemdInhibitorParams.Delay | 70 | mode: Custom.SystemdInhibitorParams.Delay |
71 | } | 71 | } |
72 | 72 | ||
73 | Binding { | ||
74 | target: NotificationManager | ||
75 | property: "lockscreenActive" | ||
76 | value: lock.locked | ||
77 | } | ||
78 | |||
73 | WlSessionLock { | 79 | WlSessionLock { |
74 | id: lock | 80 | id: lock |
75 | 81 | ||
@@ -78,7 +84,7 @@ Scope { | |||
78 | pam.abort(); | 84 | pam.abort(); |
79 | 85 | ||
80 | if (locked) { | 86 | if (locked) { |
81 | NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }); | 87 | NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {}); |
82 | Custom.KeePassXC.lockAllDatabases(); | 88 | Custom.KeePassXC.lockAllDatabases(); |
83 | Array.from(MprisProxy.players).forEach(player => { | 89 | Array.from(MprisProxy.players).forEach(player => { |
84 | if (player.canPause && player.isPlaying) | 90 | if (player.canPause && player.isPlaying) |
@@ -88,8 +94,7 @@ Scope { | |||
88 | GpgAgent.reloadAgent(); | 94 | GpgAgent.reloadAgent(); |
89 | } | 95 | } |
90 | } | 96 | } |
91 | 97 | Component.onCompleted: { (_ => {})(MprisProxy.players); } | |
92 | Binding { target: MprisProxy; } | ||
93 | 98 | ||
94 | onSecureStateChanged: Custom.Systemd.lockedHint = lock.secure | 99 | onSecureStateChanged: Custom.Systemd.lockedHint = lock.secure |
95 | 100 | ||
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..03406687 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml | |||
@@ -0,0 +1,332 @@ | |||
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.active ? [] : 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 | (_ => {})(notifsRepeater.objectAt(i).modelData); | ||
52 | res += notifsRepeater.objectAt(i).height + 8; | ||
53 | } | ||
54 | return res; | ||
55 | } | ||
56 | |||
57 | margins { | ||
58 | right: 26 + 8 | ||
59 | top: 8 + spaceAbove | ||
60 | } | ||
61 | |||
62 | color: "transparent" | ||
63 | |||
64 | implicitHeight: Math.max(notifCount.visible ? notifCount.contentHeight : 0, notifSummary.contentHeight) + (notifBody.visible ? 8 + notifBody.contentHeight : 0) + (notifActions.visible ? 8 + notifActions.height : 0) + (notifTime.visible ? 8 + notifTime.contentHeight : 0) + 16 | ||
65 | implicitWidth: 400 | ||
66 | |||
67 | WrapperMouseArea { | ||
68 | enabled: true | ||
69 | |||
70 | anchors.fill: parent | ||
71 | |||
72 | cursorShape: Qt.PointingHandCursor | ||
73 | |||
74 | onClicked: { | ||
75 | for (const notif of notifWindow.modelData) | ||
76 | notif.dismiss(); | ||
77 | } | ||
78 | |||
79 | property real angleRem: 0 | ||
80 | property real sensitivity: 1 / 120 | ||
81 | onWheel: event => { | ||
82 | angleRem += event.angleDelta.y; | ||
83 | const d = Math.round(angleRem * sensitivity); | ||
84 | angleRem -= d / sensitivity; | ||
85 | notifWindow.activeIx = ((notifWindow.modelData?.length ?? 1) + notifWindow.activeIx - d) % (notifWindow.modelData?.length ?? 1); | ||
86 | } | ||
87 | |||
88 | Rectangle { | ||
89 | color: notifWindow.backgroundColor | ||
90 | anchors.fill: parent | ||
91 | border { | ||
92 | color: Qt.hsla(195/360, 1, 0.45, 1) | ||
93 | width: 2 | ||
94 | } | ||
95 | |||
96 | GridLayout { | ||
97 | id: notifLayout | ||
98 | |||
99 | width: 400 - 16 | ||
100 | anchors.fill: parent | ||
101 | anchors.margins: 8 | ||
102 | columnSpacing: 8 | ||
103 | rowSpacing: 8 | ||
104 | |||
105 | columns: notifImage.visible ? 3 : 2 | ||
106 | rows: { | ||
107 | var res = 1; | ||
108 | if (notifBody.visible) | ||
109 | res += 1; | ||
110 | if (notifActions.visible) | ||
111 | res += 1; | ||
112 | if (notifTime.visible) | ||
113 | res += 1; | ||
114 | return res; | ||
115 | } | ||
116 | |||
117 | Text { | ||
118 | id: notifCount | ||
119 | |||
120 | visible: notifWindow.modelData?.length > 1 ?? false | ||
121 | text: `${notifWindow.activeIx + 1}/${notifWindow.modelData?.length ?? ""}` | ||
122 | |||
123 | font.pointSize: 10 | ||
124 | font.family: "Fira Sans" | ||
125 | font.bold: true | ||
126 | font.features: { "tnum": 1 } | ||
127 | color: notifWindow.textColor | ||
128 | maximumLineCount: 1 | ||
129 | |||
130 | Layout.fillWidth: false | ||
131 | Layout.row: 0 | ||
132 | Layout.column: notifImage.visible ? 1 : 0 | ||
133 | } | ||
134 | |||
135 | Text { | ||
136 | id: notifSummary | ||
137 | |||
138 | text: notifWindow.modelData?.[notifWindow.activeIx]?.summary ?? "" | ||
139 | |||
140 | font.pointSize: 10 | ||
141 | font.family: "Fira Sans" | ||
142 | font.italic: true | ||
143 | color: notifWindow.textColor | ||
144 | maximumLineCount: 1 | ||
145 | elide: Text.ElideRight | ||
146 | |||
147 | Layout.fillWidth: true | ||
148 | Layout.row: 0 | ||
149 | Layout.column: (notifCount.visible ? 1 : 0) + (notifImage.visible ? 1 : 0) | ||
150 | Layout.columnSpan: notifCount.visible ? 1 : 2 | ||
151 | } | ||
152 | |||
153 | Image { | ||
154 | id: notifImage | ||
155 | |||
156 | visible: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? false | ||
157 | |||
158 | onStatusChanged: { | ||
159 | if (notifImage.status == Image.Error) | ||
160 | notifImage.visible = false; | ||
161 | } | ||
162 | |||
163 | source: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? "" | ||
164 | fillMode: Image.PreserveAspectFit | ||
165 | asynchronous: true | ||
166 | smooth: true | ||
167 | mipmap: true | ||
168 | |||
169 | Layout.maximumWidth: 50 | ||
170 | Layout.column: 0 | ||
171 | Layout.row: 0 | ||
172 | Layout.fillHeight: true | ||
173 | Layout.rowSpan: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) | ||
174 | } | ||
175 | |||
176 | Text { | ||
177 | id: notifBody | ||
178 | |||
179 | visible: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? false | ||
180 | text: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? "" | ||
181 | textFormat: Text.RichText | ||
182 | wrapMode: Text.Wrap | ||
183 | |||
184 | font.pointSize: 10 | ||
185 | font.family: "Fira Sans" | ||
186 | color: notifWindow.textColor | ||
187 | |||
188 | Layout.fillWidth: true | ||
189 | Layout.row: 1 | ||
190 | Layout.column: notifImage.visible ? 1 : 0 | ||
191 | Layout.columnSpan: notifCount.visible ? 2 : 1 | ||
192 | } | ||
193 | |||
194 | Text { | ||
195 | id: notifTime | ||
196 | |||
197 | Connections { | ||
198 | target: NotificationManager.clock | ||
199 | function onDateChanged() { | ||
200 | notifTime.text = NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime); | ||
201 | } | ||
202 | } | ||
203 | |||
204 | visible: notifTime.text && notifTime.text !== "now" | ||
205 | text: NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime) | ||
206 | |||
207 | font.pointSize: 8 | ||
208 | font.family: "Fira Sans" | ||
209 | font.italic: true | ||
210 | color: "#555" | ||
211 | maximumLineCount: 1 | ||
212 | horizontalAlignment: Text.AlignRight | ||
213 | |||
214 | Layout.fillWidth: true | ||
215 | Layout.row: notifBody.visible ? 2 : 1 | ||
216 | Layout.column: notifImage.visible ? 1 : 0 | ||
217 | Layout.columnSpan: 2 | ||
218 | } | ||
219 | |||
220 | RowLayout { | ||
221 | id: notifActions | ||
222 | |||
223 | visible: notifWindow.modelData?.[notifWindow.activeIx]?.actions.length > 0 ?? false | ||
224 | |||
225 | spacing: 8 | ||
226 | uniformCellSizes: true | ||
227 | |||
228 | width: 400 - 16 | ||
229 | Layout.row: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0) | ||
230 | Layout.column: 0 | ||
231 | Layout.columnSpan: 2 + (notifImage.visible ? 1 : 0) | ||
232 | |||
233 | Repeater { | ||
234 | model: ScriptModel { | ||
235 | values: notifWindow.modelData?.[notifWindow.activeIx]?.actions | ||
236 | } | ||
237 | |||
238 | delegate: WrapperMouseArea { | ||
239 | id: actionMouseArea | ||
240 | |||
241 | required property var modelData | ||
242 | |||
243 | height: actionLabelWrapper.implicitHeight | ||
244 | Layout.fillWidth: true | ||
245 | Layout.horizontalStretchFactor: 1 | ||
246 | |||
247 | hoverEnabled: true | ||
248 | cursorShape: Qt.PointingHandCursor | ||
249 | |||
250 | onClicked: actionMouseArea.modelData?.invoke() | ||
251 | |||
252 | Rectangle { | ||
253 | anchors.fill: parent | ||
254 | |||
255 | color: actionMouseArea.containsMouse ? "#20ffffff" : "transparent" | ||
256 | |||
257 | border { | ||
258 | width: 2 | ||
259 | color: "#20ffffff" | ||
260 | } | ||
261 | |||
262 | WrapperItem { | ||
263 | id: actionLabelWrapper | ||
264 | |||
265 | margin: 8 | ||
266 | anchors.centerIn: parent | ||
267 | |||
268 | RowLayout { | ||
269 | id: actionLabelLayout | ||
270 | |||
271 | spacing: 8 | ||
272 | |||
273 | IconImage { | ||
274 | id: actionIcon | ||
275 | |||
276 | visible: notifWindow.modelData?.[notifWindow.activeIx]?.hasActionIcons | ||
277 | |||
278 | onStatusChanged: { | ||
279 | if (actionIcon.status == Image.Error) | ||
280 | actionIcon.visible = false; | ||
281 | } | ||
282 | |||
283 | implicitSize: 16 | ||
284 | source: { | ||
285 | if (!actionIcon.visible) | ||
286 | return ""; | ||
287 | |||
288 | let icon = actionMouseArea.modelData?.identifier ?? "" | ||
289 | if (icon.includes("?path=")) { | ||
290 | const split = icon.split("?path=") | ||
291 | if (split.length !== 2) | ||
292 | return icon | ||
293 | const name = split[0] | ||
294 | const path = split[1] | ||
295 | const fileName = name.substring( | ||
296 | name.lastIndexOf("/") + 1) | ||
297 | return `file://${path}/${fileName}` | ||
298 | } | ||
299 | return icon | ||
300 | } | ||
301 | asynchronous: true | ||
302 | smooth: true | ||
303 | mipmap: true | ||
304 | |||
305 | Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter | ||
306 | } | ||
307 | |||
308 | Text { | ||
309 | id: actionLabel | ||
310 | |||
311 | visible: actionMouseArea.modelData?.text ?? false | ||
312 | |||
313 | text: actionMouseArea.modelData?.text ?? "" | ||
314 | |||
315 | font.pointSize: 10 | ||
316 | font.family: "Fira Sans" | ||
317 | color: notifWindow.textColor | ||
318 | maximumLineCount: 1 | ||
319 | elide: Text.ElideRight | ||
320 | } | ||
321 | } | ||
322 | } | ||
323 | } | ||
324 | } | ||
325 | } | ||
326 | } | ||
327 | } | ||
328 | } | ||
329 | } | ||
330 | } | ||
331 | } | ||
332 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml new file mode 100644 index 00000000..b58467b3 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml | |||
@@ -0,0 +1,266 @@ | |||
1 | import Quickshell | ||
2 | import QtQuick | ||
3 | import Quickshell.Widgets | ||
4 | import qs.Services | ||
5 | import QtQuick.Controls | ||
6 | import QtQuick.Layouts | ||
7 | import QtQuick.Shapes | ||
8 | |||
9 | Item { | ||
10 | id: root | ||
11 | |||
12 | width: icon.width + 8 | ||
13 | height: parent.height | ||
14 | anchors.verticalCenter: parent.verticalCenter | ||
15 | |||
16 | WrapperMouseArea { | ||
17 | id: widgetMouseArea | ||
18 | |||
19 | anchors.fill: parent | ||
20 | |||
21 | hoverEnabled: true | ||
22 | cursorShape: Qt.PointingHandCursor | ||
23 | |||
24 | onClicked: NotificationManager.displayInhibited = !NotificationManager.displayInhibited | ||
25 | |||
26 | Rectangle { | ||
27 | anchors.fill: parent | ||
28 | color: { | ||
29 | if (widgetMouseArea.containsMouse) { | ||
30 | return "#33808080"; | ||
31 | } | ||
32 | return "transparent"; | ||
33 | } | ||
34 | |||
35 | Item { | ||
36 | anchors.fill: parent | ||
37 | |||
38 | MaterialDesignIcon { | ||
39 | id: icon | ||
40 | |||
41 | implicitSize: 14 | ||
42 | anchors.centerIn: parent | ||
43 | |||
44 | icon: NotificationManager.active ? "message" : "message-off" | ||
45 | color: { | ||
46 | if (!NotificationManager.active && !NotificationManager.displayInhibited) | ||
47 | return "#f28a21"; | ||
48 | if (NotificationManager.displayInhibited) | ||
49 | return "white"; | ||
50 | return "#555"; | ||
51 | } | ||
52 | } | ||
53 | } | ||
54 | } | ||
55 | } | ||
56 | |||
57 | Loader { | ||
58 | id: tooltipLoader | ||
59 | |||
60 | active: false | ||
61 | |||
62 | Connections { | ||
63 | target: widgetMouseArea | ||
64 | function onContainsMouseChanged() { | ||
65 | if (widgetMouseArea.containsMouse) | ||
66 | tooltipLoader.active = true; | ||
67 | } | ||
68 | } | ||
69 | |||
70 | sourceComponent: PopupWindow { | ||
71 | id: tooltip | ||
72 | |||
73 | property bool nextVisible: NotificationManager.active && (widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse) | ||
74 | |||
75 | anchor { | ||
76 | item: widgetMouseArea | ||
77 | edges: Edges.Bottom | Edges.Left | ||
78 | } | ||
79 | visible: false | ||
80 | |||
81 | onNextVisibleChanged: hangTimer.restart() | ||
82 | |||
83 | Timer { | ||
84 | id: hangTimer | ||
85 | interval: tooltip.visible ? 100 : 500 | ||
86 | onTriggered: { | ||
87 | tooltip.visible = tooltip.nextVisible; | ||
88 | if (!tooltip.visible) | ||
89 | tooltipLoader.active = false; | ||
90 | } | ||
91 | } | ||
92 | |||
93 | implicitWidth: 400 | ||
94 | implicitHeight: Math.min(tooltip.screen.height * 0.66, Math.max(100, scroll.contentHeight + 16)) | ||
95 | color: "black" | ||
96 | |||
97 | WrapperMouseArea { | ||
98 | id: tooltipMouseArea | ||
99 | |||
100 | hoverEnabled: true | ||
101 | enabled: true | ||
102 | |||
103 | anchors.fill: parent | ||
104 | |||
105 | WrapperItem { | ||
106 | margin: 8 | ||
107 | |||
108 | ScrollView { | ||
109 | id: scroll | ||
110 | |||
111 | contentWidth: availableWidth | ||
112 | // ScrollBar.vertical.policy: ScrollBar.AlwaysOn | ||
113 | |||
114 | ColumnLayout { | ||
115 | id: historyLayout | ||
116 | anchors { | ||
117 | left: parent.left | ||
118 | right: parent.right | ||
119 | } | ||
120 | |||
121 | spacing: 8 | ||
122 | |||
123 | Repeater { | ||
124 | model: ScriptModel { | ||
125 | values: [...NotificationManager.history].reverse().map(o => o.notification) | ||
126 | } | ||
127 | |||
128 | delegate: GridLayout { | ||
129 | id: notif | ||
130 | |||
131 | Layout.fillWidth: true | ||
132 | Layout.preferredHeight: notifSummary.contentHeight + (notifBody.visible ? notifBody.contentHeight + 8 : 0) + (notifSep.visible ? notifSep.height + 8 : 0) + notifTime.contentHeight + 8 | ||
133 | |||
134 | required property var modelData | ||
135 | required property int index | ||
136 | |||
137 | columnSpacing: 8 | ||
138 | rowSpacing: 8 | ||
139 | |||
140 | columns: notifImage.visible ? 2 : 1 | ||
141 | rows: { | ||
142 | var res = 2; | ||
143 | if (notifBody.visible) | ||
144 | res += 1; | ||
145 | if (notifSep.visible) | ||
146 | res += 1; | ||
147 | return res; | ||
148 | } | ||
149 | |||
150 | Shape { | ||
151 | id: notifSep | ||
152 | |||
153 | visible: notif.index != 0 | ||
154 | |||
155 | height: 2 | ||
156 | width: 400 - 32 | ||
157 | |||
158 | ShapePath { | ||
159 | strokeWidth: 2 | ||
160 | strokeColor: "#20ffffff" | ||
161 | startX: 0; startY: 0; | ||
162 | PathLine { x: 400 - 32; y: 0; } | ||
163 | } | ||
164 | |||
165 | Layout.row: 0 | ||
166 | Layout.column: 0 | ||
167 | Layout.columnSpan: notifImage.visible ? 2 : 1 | ||
168 | Layout.alignment: Qt.AlignHCenter | ||
169 | } | ||
170 | |||
171 | Text { | ||
172 | id: notifSummary | ||
173 | |||
174 | text: notif.modelData?.summary ?? "" | ||
175 | |||
176 | font.pointSize: 10 | ||
177 | font.family: "Fira Sans" | ||
178 | font.italic: true | ||
179 | color: "white" | ||
180 | maximumLineCount: 1 | ||
181 | elide: Text.ElideRight | ||
182 | |||
183 | Layout.fillWidth: true | ||
184 | Layout.row: notifSep.visible ? 1 : 0 | ||
185 | Layout.column: notifImage.visible ? 1 : 0 | ||
186 | } | ||
187 | |||
188 | Image { | ||
189 | id: notifImage | ||
190 | |||
191 | visible: (notif.modelData?.image || notif.modelData?.appIcon) ?? false | ||
192 | |||
193 | onStatusChanged: { | ||
194 | if (notifImage.status == Image.Error) | ||
195 | notifImage.visible = false; | ||
196 | } | ||
197 | |||
198 | source: (notif.modelData?.image || notif.modelData?.appIcon) ?? "" | ||
199 | fillMode: Image.PreserveAspectFit | ||
200 | asynchronous: true | ||
201 | smooth: true | ||
202 | mipmap: true | ||
203 | |||
204 | Layout.maximumWidth: 50 | ||
205 | Layout.column: 0 | ||
206 | Layout.row: notifSep.visible ? 1 : 0 | ||
207 | Layout.fillHeight: true | ||
208 | Layout.rowSpan: notifBody.visible ? 3 : 2 | ||
209 | } | ||
210 | |||
211 | Text { | ||
212 | id: notifBody | ||
213 | |||
214 | visible: notif.modelData?.body ?? false | ||
215 | text: notif.modelData?.body ?? "" | ||
216 | textFormat: Text.RichText | ||
217 | wrapMode: Text.Wrap | ||
218 | |||
219 | font.pointSize: 10 | ||
220 | font.family: "Fira Sans" | ||
221 | color: "white" | ||
222 | |||
223 | Layout.fillWidth: true | ||
224 | Layout.row: notifSep.visible ? 2 : 1 | ||
225 | Layout.column: notifImage.visible ? 1 : 0 | ||
226 | } | ||
227 | |||
228 | Text { | ||
229 | id: notifTime | ||
230 | |||
231 | Connections { | ||
232 | target: NotificationManager.clock | ||
233 | function onDateChanged() { | ||
234 | notifTime.text = NotificationManager.formatTime(notif.modelData?.receivedTime); | ||
235 | } | ||
236 | } | ||
237 | |||
238 | text: NotificationManager.formatTime(notif.modelData?.receivedTime) | ||
239 | |||
240 | font.pointSize: 8 | ||
241 | font.family: "Fira Sans" | ||
242 | font.italic: true | ||
243 | color: "#555" | ||
244 | maximumLineCount: 1 | ||
245 | horizontalAlignment: Text.AlignRight | ||
246 | |||
247 | Layout.fillWidth: true | ||
248 | Layout.row: { | ||
249 | var res = 1; | ||
250 | if (notifSep.visible) | ||
251 | res += 1; | ||
252 | if (notifBody.visible) | ||
253 | res += 1; | ||
254 | return res; | ||
255 | } | ||
256 | Layout.column: notifImage.visible ? 1 : 0 | ||
257 | } | ||
258 | } | ||
259 | } | ||
260 | } | ||
261 | } | ||
262 | } | ||
263 | } | ||
264 | } | ||
265 | } | ||
266 | } | ||
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..001ffcf5 --- /dev/null +++ b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml | |||
@@ -0,0 +1,151 @@ | |||
1 | pragma Singleton | ||
2 | |||
3 | import QtQml | ||
4 | import Quickshell | ||
5 | import Quickshell.Services.Notifications | ||
6 | |||
7 | Singleton { | ||
8 | id: root | ||
9 | |||
10 | readonly property bool active: !lockscreenActive && !displayInhibited | ||
11 | property bool lockscreenActive: false | ||
12 | property bool displayInhibited: false | ||
13 | property alias trackedNotifications: server.trackedNotifications | ||
14 | readonly property var groups: { | ||
15 | function matchesGroupKey(notif, groupKey) { | ||
16 | var matches = true; | ||
17 | for (const prop in groupKey.test) { | ||
18 | if (notif[prop] !== groupKey.test[prop]) { | ||
19 | matches = false; | ||
20 | break; | ||
21 | } | ||
22 | } | ||
23 | return matches; | ||
24 | } | ||
25 | |||
26 | var groups = new Map(); | ||
27 | var notifs = new Array(); | ||
28 | for (const [ix, notif] of server.trackedNotifications.values.entries()) { | ||
29 | var didGroup = false; | ||
30 | for (const groupKey of root.groupKeys) { | ||
31 | if (!matchesGroupKey(notif, groupKey)) | ||
32 | continue; | ||
33 | |||
34 | const key = JSON.stringify({ | ||
35 | "key": groupKey, | ||
36 | "values": Object.assign({}, ...(Array.from(groupKey["group-by"]).map(prop => { | ||
37 | var res = {}; | ||
38 | res[prop] = notif[prop]; | ||
39 | return res; | ||
40 | }))) | ||
41 | }); | ||
42 | if (!groups.has(key)) | ||
43 | groups.set(key, new Array()); | ||
44 | groups.get(key).push({ "ix": ix, "notif": notif }); | ||
45 | didGroup = true; | ||
46 | break; | ||
47 | } | ||
48 | |||
49 | if (!didGroup) | ||
50 | notifs.push([{ "ix": ix, "notif": notif }]); | ||
51 | } | ||
52 | notifs.push(...groups.values()); | ||
53 | notifs.sort((as, bs) => Math.min(...(as.map(o => o.ix))) - Math.min(...(bs.map(o => o.ix)))); | ||
54 | return notifs.map(ns => ns.map(n => n.notif)); | ||
55 | } | ||
56 | |||
57 | property var groupKeys: [ | ||
58 | { "test": { "appName": "Element" }, "group-by": [ "summary" ] } | ||
59 | ]; | ||
60 | |||
61 | property var history: [] | ||
62 | |||
63 | Component { | ||
64 | id: expirationTimer | ||
65 | |||
66 | QtObject { | ||
67 | id: timer | ||
68 | |||
69 | required property QtObject parent | ||
70 | required property int expirationTime | ||
71 | property list<QtObject> data: [ | ||
72 | Timer { | ||
73 | running: root.active | ||
74 | interval: timer.expirationTime | ||
75 | onTriggered: timer.parent.expire() | ||
76 | } | ||
77 | ] | ||
78 | } | ||
79 | } | ||
80 | |||
81 | Component { | ||
82 | id: notificationLock | ||
83 | |||
84 | RetainableLock {} | ||
85 | } | ||
86 | |||
87 | readonly property SystemClock clock: SystemClock { | ||
88 | precision: SystemClock.Minutes | ||
89 | } | ||
90 | |||
91 | function formatTime(time) { | ||
92 | const now = root.clock.date; | ||
93 | const diff = now - time; | ||
94 | const minutes = Math.ceil(diff / 60000); | ||
95 | const hours = Math.floor(minutes / 60); | ||
96 | |||
97 | if (hours < 1) { | ||
98 | if (minutes < 1) | ||
99 | return "now"; | ||
100 | if (minutes == 1) | ||
101 | return "1 minute"; | ||
102 | return `${minutes} minutes`; | ||
103 | } | ||
104 | |||
105 | const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate()) | ||
106 | const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate()) | ||
107 | const days = Math.floor((nowDate - timeDate) / (1000 * 86400)) | ||
108 | |||
109 | const timeStr = time.toLocaleTimeString(Qt.locale(), "HH:mm"); | ||
110 | |||
111 | if (days === 0) | ||
112 | return timeStr; | ||
113 | if (days === 1) | ||
114 | return `yesterday ${timeStr}`; | ||
115 | |||
116 | const dateStr = time.toLocaleTimeString(Qt.locale(), "YYYY-MM-DD"); | ||
117 | return `${dateStr} ${timeStr}`; | ||
118 | } | ||
119 | |||
120 | NotificationServer { | ||
121 | id: server | ||
122 | |||
123 | bodySupported: true | ||
124 | actionsSupported: true | ||
125 | actionIconsSupported: true | ||
126 | imageSupported: true | ||
127 | bodyMarkupSupported: true | ||
128 | bodyImagesSupported: true | ||
129 | |||
130 | onNotification: notification => { | ||
131 | var timeout = notification.expireTimeout * 1000; | ||
132 | if (notification.appName == "poweralertd") | ||
133 | timeout = 2000; | ||
134 | if (timeout > 0) { | ||
135 | Object.defineProperty(notification, "expirationTimer", { configurable: true, enumerable: true, writable: true }); | ||
136 | notification.expirationTimer = expirationTimer.createObject(notification, { parent: notification, expirationTime: timeout }); | ||
137 | } | ||
138 | Object.defineProperty(notification, "receivedTime", { configurable: true, enumerable: true, writable: true }); | ||
139 | notification.receivedTime = root.clock.date; | ||
140 | notification.closed.connect((reason) => server.onNotificationClosed(notification, reason)); | ||
141 | notification.tracked = true; | ||
142 | } | ||
143 | |||
144 | function onNotificationClosed(notification, reason) { | ||
145 | root.history.push({ | ||
146 | lock: notificationLock.createObject(root, { locked: true, object: notification }), | ||
147 | notification: notification | ||
148 | }); | ||
149 | } | ||
150 | } | ||
151 | } | ||
diff --git a/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml index 4ec5186c..05a40dbc 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.active) { | ||
88 | if (NotificationManager.groups.length > 0) | ||
89 | for (const notif of [...NotificationManager.groups[0]]) | ||
90 | notif.dismiss(); | ||
91 | } | ||
92 | if (command.DismissAll && NotificationManager.active) { | ||
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 | } |