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 | } |
