summaryrefslogtreecommitdiff
path: root/accounts/gkleen@sif/niri
diff options
context:
space:
mode:
Diffstat (limited to 'accounts/gkleen@sif/niri')
-rw-r--r--accounts/gkleen@sif/niri/default.nix1057
-rw-r--r--accounts/gkleen@sif/niri/mako.nix113
-rw-r--r--accounts/gkleen@sif/niri/swayosd.nix66
-rw-r--r--accounts/gkleen@sif/niri/waybar.nix160
4 files changed, 1260 insertions, 136 deletions
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
index 6a8d10a0..8752f3e3 100644
--- a/accounts/gkleen@sif/niri/default.nix
+++ b/accounts/gkleen@sif/niri/default.nix
@@ -1,15 +1,234 @@
1{ config, pkgs, lib, ... }: 1{ config, hostConfig, pkgs, lib, flakeInputs, ... }:
2let 2let
3 niri = config.programs.niri.package; 3 cfg = config.programs.niri;
4
5 kdl = flakeInputs.niri-flake.lib.kdl;
6
7 niri = cfg.package;
4 terminal = lib.getExe config.programs.kitty.package; 8 terminal = lib.getExe config.programs.kitty.package;
5 lightctl = lib.getExe' config.services.avizo.package "lightctl"; 9 makoctl = lib.getExe' config.services.mako.package "makoctl";
6 volumectl = lib.getExe' config.services.avizo.package "volumectl"; 10 loginctl = lib.getExe' hostConfig.systemd.package "loginctl";
7 dunstctl = lib.getExe' config.services.dunst.package "dunstctl"; 11 systemctl = lib.getExe' hostConfig.systemd.package "systemctl";
12 swayosd-client = lib.getExe' config.services.swayosd.package "swayosd-client";
13
14 focus_or_spawn = pkgs.writeShellApplication {
15 name = "focus-or-spawn";
16 runtimeInputs = [ niri pkgs.gojq pkgs.gnugrep pkgs.socat ];
17 text = ''
18 window_select="$1"
19 shift
20 workspace_name="$1"
21 shift
22
23 workspaces_json="$(niri msg -j workspaces)"
24 workspace_output="$(jq -r --arg workspace_name "$workspace_name" '.[] | select(.name == $workspace_name) | .output' <<<"$workspaces_json")"
25 # active_workspace="$(jq -r --arg workspace_output "$workspace_output" '.[] | select(.output == $workspace_output and .is_active) | .id' <<<"$workspaces_json")"
26 active_output="$(jq -r '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
27 if [[ $workspace_output != "$active_output" ]]; then
28 niri msg action move-workspace-to-monitor --reference "$workspace_name" "$active_output"
29 # socat STDIO "$NIRI_SOCKET" <<<'{"Action":{"FocusWorkspace":{"reference":{"Id":'"''${active_workspace}"'}}}}'
30 # niri msg action move-workspace-to-index --reference "$workspace_name" 1
31 fi
32
33 while IFS=$'\n' read -r window_json; do
34 if [[ -n $(jq -c "$window_select" <<<"$window_json") ]]; then
35 if jq -e '.is_focused' <<<"$window_json" >/dev/null; then
36 niri msg action focus-workspace-previous
37 else
38 if [[ $(jq -r --arg workspace_name "$workspace_name" 'map(select(.name == $workspace_name)) | .[0].is_focused' <<<"$workspaces_json") != "true" ]] && [[ $(jq -r --arg workspace_name "$workspace_name" 'map(select(.name == $workspace_name)) | .[0].id' <<<"$workspaces_json") = $(jq -r '.workspace_id' <<<"$window_json") ]]; then
39 niri msg action focus-workspace "$workspace_name"
40 else
41 niri msg action focus-window --id "$(jq -r '.id' <<<"$window_json")"
42 fi
43 fi
44 exit 0
45 fi
46 done < <(niri msg -j windows | jq -c '.[]')
47
48 exec "$@"
49 '';
50 };
51 focus-or-spawn-action = config.lib.niri.actions.spawn (lib.getExe focus_or_spawn);
52
53 with_adjacent_workspace = pkgs.writeShellApplication {
54 name = "with-adjacent-workspace";
55 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
56 text = ''
57 blacklist="$1"
58 shift
59 direction="$1"
60 shift
61 action="$1"
62 shift
63
64 workspaces_json="$(niri msg -j workspaces)"
65 active_workspace="$(jq -r '.[] | select(.is_focused) | .id' <<<"$workspaces_json")"
66 workspace_output="$(jq -r --arg active_workspace "$active_workspace" '.[] | select(.id == ($active_workspace | tonumber)) | .output' <<<"$workspaces_json")"
67 workspace_idx="$(jq -r '.[] | select(.is_focused) | .idx' <<<"$workspaces_json")"
68
69 jq_script='map(select('
70 case "$direction" in
71 down)
72 # shellcheck disable=SC2016
73 jq_script=''${jq_script}'.idx > ($workspace_idx | tonumber)';;
74 up)
75 # shellcheck disable=SC2016
76 jq_script=''${jq_script}'.idx < ($workspace_idx | tonumber)';;
77 esac
78 # shellcheck disable=SC2016
79 jq_script=''${jq_script}' and .output == $workspace_output and ((.name == null) or (.name | test($blacklist) | not)))) | sort_by(.idx)'
80 [[ $direction == "up" ]] && jq_script=''${jq_script}' | reverse'
81 jq_script=''${jq_script}' | .[0]'
82
83 workspace_json=$(jq -c --arg blacklist "$blacklist" --arg workspace_output "$workspace_output" --arg workspace_idx "$workspace_idx" "$jq_script" <<<"$workspaces_json")
84 [[ -n $workspace_json && $workspace_json != null ]] || exit 0
85 jq --arg active_workspace "$active_workspace" -c "$action" <<<"$workspace_json" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
86 '';
87 };
88 with-adjacent-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_adjacent_workspace) "^${lib.concatMapStringsSep "|" ({ name, ...}: name) cfg.scratchspaces}$";
89 focus-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
90 move-column-to-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}, "focus": true}}}'';
91
92 with_unnamed_workspace = pkgs.writeShellApplication {
93 name = "with-unnamed-workspace";
94 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
95 text = ''
96 action="$1"
97 shift
98
99 workspaces_json="$(niri msg -j workspaces)"
100 active_output="$(jq -r '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
101 active_workspace="$(jq -r '.[] | select(.is_focused) | .id' <<<"$workspaces_json")"
102
103 history_json="$(socat STDIO UNIX-CONNECT:"$XDG_RUNTIME_DIR"/niri-workspace-history.sock)"
104 workspace_json="$(jq -c --arg active_output "$active_output" --argjson history "$history_json" 'map(select(.output == $active_output and .name == null)) | map({"value": ., "history_idx": ((. as $workspace | ($history[$active_output] | index($workspace | .id))) as $active_idx | if $active_idx then $active_idx else ($history[$active_output] | length) + 1 end)}) | sort_by(.history_idx, .value.idx) | map(.value) | .[0]' <<<"$workspaces_json")"
105 [[ -n $workspace_json && $workspace_json != null ]] || exit 0
106 jq --arg active_workspace "$active_workspace" -c "$action" <<<"$workspace_json" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
107 '';
108 };
109 with-unnamed-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_unnamed_workspace);
110
111 with_empty_unnamed_workspace = pkgs.writeShellApplication {
112 name = "with-empty-unnamed-workspace";
113 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
114 text = ''
115 action="$1"
116 shift
117
118 workspaces_json="$(niri msg -j workspaces)"
119 active_output="$(jq '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
120 target_workspace_id="$(jq --argjson active_output "$active_output" 'map(select(.active_window_id == null and .name == null and .output == $active_output)) | sort_by(.idx) | .[0].id' <<<"$workspaces_json")"
121 jq --argjson workspace_id "$target_workspace_id" -nc "$action" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
122 '';
123 };
124 with-empty-unnamed-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_empty_unnamed_workspace);
125
126 with_select_window = pkgs.writeShellApplication {
127 name = "with-select-window";
128 runtimeInputs = [ niri pkgs.gojq pkgs.socat config.programs.fuzzel.package pkgs.gawk ];
129 text = ''
130 window_select="$1"
131 shift
132 action="$1"
133 shift
134
135 windows_json="$(niri msg -j windows)"
136 active_workspace="$(jq -r '.[] | select(.is_focused) | .workspace_id' <<<"$windows_json")"
137 window_ix="$(gojq -r --arg active_workspace "$active_workspace" '.[] | select('"$window_select"') | "\(.title)\u0000icon\u001f\(.app_id)"' <<<"$windows_json" | fuzzel --width=60 --log-level=warning --dmenu --index)"
138 # shellcheck disable=SC2016
139 window_json="$(gojq -rc --arg active_workspace "$active_workspace" --arg window_ix "$window_ix" 'map(select('"$window_select"')) | .[($window_ix | tonumber)]' <<<"$windows_json")"
140
141 [[ -z "$window_json" ]] && exit 1
142
143 jq -c "$action" <<<"$window_json" | socat STDIO "$NIRI_SOCKET"
144 '';
145 };
146 with-select-window-action = config.lib.niri.actions.spawn (lib.getExe with_select_window);
147
148 with_predicate_window = pred: pkgs.writeShellApplication {
149 name = "with-predicate-window";
150 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
151 text = ''
152 action="$1"
153 shift
154
155 windows_json="$(niri msg -j windows)"
156 window_json="$(gojq -rc 'map(select(${pred})) | .[0]' <<<"$windows_json")"
157
158 [[ -z "$window_json" || $window_json = "null" ]] && exit 1
159
160 jq -c "$action" <<<"$window_json" | socat STDIO "$NIRI_SOCKET"
161 '';
162 };
163
164 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"));
8in { 166in {
9 imports = [ 167 imports = [
10 ./waybar.nix 168 ./waybar.nix
169 ./mako.nix
170 ./swayosd.nix
11 ]; 171 ];
12 172
173 options = {
174 programs.niri.scratchspaces = lib.mkOption {
175 type = lib.types.listOf (lib.types.submodule ({ config, ... }: {
176 options = {
177 name = lib.mkOption {
178 type = lib.types.str;
179 };
180 match = lib.mkOption {
181 type = lib.types.listOf (lib.types.attrsOf kdl.types.kdl-args);
182 default = [];
183 };
184 exclude = lib.mkOption {
185 type = lib.types.listOf (lib.types.attrsOf kdl.types.kdl-args);
186 default = [];
187 };
188 windowRuleExtra = lib.mkOption {
189 type = kdl.types.kdl-nodes;
190 default = [];
191 };
192 key = lib.mkOption {
193 type = lib.types.nullOr lib.types.str;
194 default = null;
195 };
196 moveKey = lib.mkOption {
197 type = lib.types.nullOr lib.types.str;
198 default = let
199 keys = lib.splitString "+" config.key;
200 defMoveKey = lib.concatStringsSep "+" (lib.flatten [
201 (lib.take (lib.length keys - 1) keys)
202 ["Shift"]
203 (lib.takeEnd 1 keys)
204 ]);
205 in if config.key == null then null else defMoveKey;
206 };
207 spawn = lib.mkOption {
208 type = lib.types.nullOr (lib.types.listOf lib.types.str);
209 default = null;
210 };
211 app-id = lib.mkOption {
212 type = lib.types.nullOr lib.types.str;
213 default = null;
214 };
215 selector = lib.mkOption {
216 type = lib.types.nullOr lib.types.str;
217 default = null;
218 };
219 };
220
221 config = lib.mkMerge [
222 (lib.mkIf (config.app-id != null) {
223 match = lib.mkDefault [ { app-id = "^${lib.escapeRegex config.app-id}$"; } ];
224 selector = lib.mkDefault "select(.app_id == \"${config.app-id}\")";
225 })
226 ];
227 }));
228 default = [];
229 };
230 };
231
13 config = { 232 config = {
14 systemd.user.services.xwayland-satellite = { 233 systemd.user.services.xwayland-satellite = {
15 Unit = { 234 Unit = {
@@ -21,7 +240,9 @@ in {
21 Service = { 240 Service = {
22 Type = "notify"; 241 Type = "notify";
23 NotifyAccess = "all"; 242 NotifyAccess = "all";
24 ExecStart = lib.getExe pkgs.xwayland-satellite-unstable; 243 Environment = [ "DISPLAY=:0" ];
244 ExecStart = ''${lib.getExe pkgs.xwayland-satellite-unstable} ''${DISPLAY}'';
245 ExecStartPre = "${systemctl} --user import-environment DISPLAY";
25 StandardOutput = "journal"; 246 StandardOutput = "journal";
26 }; 247 };
27 Install = { 248 Install = {
@@ -29,111 +250,765 @@ in {
29 }; 250 };
30 }; 251 };
31 252
32 programs.niri.settings = { 253 services.swayidle = {
33 prefer-no-csd = true; 254 events = [
34 screenshot-path = "${config.home.homeDirectory}/screenshots"; 255 { event = "after-resume"; command = "${lib.getExe niri} msg action power-on-monitors"; }
256 ];
257 timeouts = [
258 { timeout = 540;
259 command = "${lib.getExe niri} msg action power-off-monitors";
260 }
261 ];
262 };
35 263
36 input = { 264 systemd.user.sockets.niri-workspace-history = {
37 keyboard.xkb = { 265 Socket = {
38 layout = "us,"; 266 ListenStream = "%t/niri-workspace-history.sock";
39 variant = "dvp,"; 267 SocketMode = "0600";
40 options = "compose:caps,grp:win_space_toggle";
41 };
42 }; 268 };
43 269 };
44 environment = { 270 systemd.user.services.niri-workspace-history = {
45 NIXOS_OZONE_WL = "1"; 271 Unit = {
46 QT_QPA_PLATFORM = "wayland"; 272 BindsTo = [ "niri.service" ];
47 GDK_BACKEND = "wayland"; 273 After = [ "niri.service" ];
48 SDL_VIDEODRIVER = "wayland";
49 }; 274 };
275 Install = {
276 WantedBy = [ "niri.service" ];
277 };
278 Service = {
279 Type = "simple";
280 Sockets = [ "niri-workspace-history.socket" ];
281 ExecStart = pkgs.writers.writePython3 "niri-workspace-history" { flakeIgnore = ["E501"]; } ''
282 import os
283 import socket
284 import json
285 # import sys
286 from collections import defaultdict
287 from threading import Thread, Lock
288 from socketserver import StreamRequestHandler, ThreadingTCPServer
289 from contextlib import contextmanager
290 from io import TextIOWrapper
50 291
51 cursor.hide-when-typing = true;
52 292
53 binds = with config.lib.niri.actions; { 293 @contextmanager
54 "Mod+Return".action = spawn terminal; 294 def detaching(thing):
55 "Mod+Q".action = close-window; 295 try:
56 "Mod+D".action = spawn (lib.getExe config.programs.fuzzel.package); 296 yield thing
57 "Mod+Shift+D".action = spawn (lib.getExe config.programs.fuzzel.package) "--list-executables-in-path"; 297 finally:
298 thing.detach()
58 299
59 "Mod+H".action = focus-column-left;
60 "Mod+T".action = focus-window-down;
61 "Mod+N".action = focus-window-up;
62 "Mod+S".action = focus-column-right;
63 300
64 "Mod+Shift+H".action = move-column-left; 301 workspace_history = defaultdict(list)
65 "Mod+Shift+T".action = move-window-down; 302 history_lock = Lock()
66 "Mod+Shift+N".action = move-window-up;
67 "Mod+Shift+S".action = move-column-right;
68 303
69 "Mod+Control+H".action = focus-monitor-left;
70 "Mod+Control+T".action = focus-monitor-down;
71 "Mod+Control+N".action = focus-monitor-up;
72 "Mod+Control+S".action = focus-monitor-right;
73 304
74 "Mod+Shift+Control+H".action = move-workspace-to-monitor-left; 305 def monitor_niri():
75 "Mod+Shift+Control+T".action = move-workspace-to-monitor-down; 306 workspaces = list()
76 "Mod+Shift+Control+N".action = move-workspace-to-monitor-up;
77 "Mod+Shift+Control+S".action = move-workspace-to-monitor-right;
78 307
79 "Mod+G".action = focus-workspace-down; 308 def focus_workspace(output, workspace):
80 "Mod+C".action = focus-workspace-up; 309 with history_lock:
310 workspace_history[output] = [workspace] + [ws for ws in workspace_history[output] if ws != workspace]
311 # print(json.dumps(workspace_history), file=sys.stderr)
81 312
82 "Mod+Control+G".action = move-column-to-workspace-down; 313 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
83 "Mod+Control+C".action = move-column-to-workspace-up; 314 sock.connect(os.environ["NIRI_SOCKET"])
315 sock.send(b"\"EventStream\"\n")
316 for line in sock.makefile(buffering=1, encoding='utf-8'):
317 if line_json := json.loads(line):
318 if "WorkspacesChanged" in line_json:
319 workspaces = line_json["WorkspacesChanged"]["workspaces"]
320 for ws in workspaces:
321 if ws["is_focused"]:
322 focus_workspace(ws["output"], ws["id"])
323 if "WorkspaceActivated" in line_json:
324 for ws in workspaces:
325 if ws["id"] != line_json["WorkspaceActivated"]["id"]:
326 continue
327 focus_workspace(ws["output"], ws["id"])
328 break
84 329
85 "Mod+Shift+G".action = move-workspace-down;
86 "Mod+Shift+C".action = move-workspace-up;
87 330
88 "Mod+M".action = consume-window-into-column; 331 class RequestHandler(StreamRequestHandler):
89 "Mod+W".action = expel-window-from-column; 332 def handle(self):
333 with detaching(TextIOWrapper(self.wfile, encoding='utf-8', write_through=True)) as out:
334 with history_lock:
335 json.dump(workspace_history, out)
90 336
91 "Mod+F".action = maximize-column;
92 "Mod+Shift+F".action = fullscreen-window;
93 337
94 "Mod+Space".action = switch-focus-between-floating-and-tiling; 338 class Server(ThreadingTCPServer):
95 "Mod+Shift+Space".action = toggle-window-floating; 339 def __init__(self):
340 ThreadingTCPServer.__init__(self, ("", 8000), RequestHandler, bind_and_activate=False)
341 self.socket = socket.fromfd(3, self.address_family, self.socket_type)
96 342
97 "Mod+Left".action = set-column-width "-10%";
98 "Mod+Down".action = set-window-height "-10%";
99 "Mod+Up".action = set-window-height "+10%";
100 "Mod+Right".action = set-column-width "+10%";
101 343
102 "Mod+Shift+Z" = { 344 def run_server():
103 action = spawn (lib.getExe niri) "msg" "action" "power-off-monitors"; 345 with Server() as server:
104 allow-when-locked = true; 346 server.serve_forever()
105 };
106 347
107 "XF86MonBrightnessUp" = {
108 action = spawn lightctl "-d" "-e4" "-n1" "up";
109 allow-when-locked = true;
110 };
111 "XF86MonBrightnessDown" = {
112 action = spawn lightctl "-d" "-e4" "-n1" "down";
113 allow-when-locked = true;
114 };
115 "XF86AudioRaiseVolume" = {
116 action = spawn volumectl "-d" "-u" "up";
117 allow-when-locked = true;
118 };
119 "XF86AudioLowerVolume" = {
120 action = spawn volumectl "-d" "-u" "down";
121 allow-when-locked = true;
122 };
123 "XF86AudioMute" = {
124 action = spawn volumectl "-d" "toggle-mute";
125 allow-when-locked = true;
126 };
127 "XF86AudioMicMute" = {
128 action = spawn volumectl "-d" "-m" "toggle-mute";
129 allow-when-locked = true;
130 };
131 348
132 "Mod+Semicolon".action = spawn dunstctl "close"; 349 niri = Thread(target=monitor_niri)
133 "Mod+Shift+Semicolon".action = spawn dunstctl "close-all"; 350 niri.daemon = True
134 "Mod+Period".action = spawn dunstctl "context"; 351 niri.start()
135 "Mod+Comma".action = spawn dunstctl "history-pop"; 352
353 server_thread = Thread(target=run_server)
354 server_thread.daemon = True
355 server_thread.start()
356
357 while True:
358 server_thread.join(timeout=0.5)
359 niri.join(timeout=0.5)
360
361 if not (niri.is_alive() and server_thread.is_alive()):
362 break
363 '';
136 }; 364 };
137 }; 365 };
366 systemd.user.services.niri-workspace-sort = {
367 Unit = {
368 BindsTo = [ "niri.service" ];
369 After = [ "niri.service" ];
370 };
371 Install = {
372 WantedBy = [ "niri.service" ];
373 };
374 Service = {
375 Type = "simple";
376 ExecStart = pkgs.writers.writePython3 "niri-workspace-sort" { flakeIgnore = ["E501"]; } ''
377 import os
378 import sys
379 import socket
380 import json
381
382 outputs = None
383 only = {'HDMI-A-1': {'bmr'}, 'eDP-1': {'vid'}}
384
385
386 class Niri(socket.socket):
387 def __init__(self):
388 super().__init__(socket.AF_UNIX, socket.SOCK_STREAM)
389 super().connect(os.environ["NIRI_SOCKET"])
390 self.fh = super().makefile(mode='rw', buffering=1, encoding='utf-8')
391
392 def cmd(self, obj):
393 print(json.dumps(obj, separators=(',', ':')), flush=True, file=self.fh)
394
395 def event_stream(self):
396 self.cmd("EventStream")
397 return self.fh
398
399
400 with Niri() as niri, Niri().event_stream() as niri_stream:
401 for line in niri_stream:
402 workspaces = None
403 if line_json := json.loads(line):
404 if "WorkspacesChanged" in line_json:
405 workspaces = line_json["WorkspacesChanged"]["workspaces"]
406
407 if workspaces is None:
408 continue
409
410 old_outputs = outputs
411 outputs = {ws["output"] for ws in workspaces}
412 if old_outputs is None:
413 print("Initial outputs: {}".format(outputs), file=sys.stderr)
414 continue
415
416 new_outputs = outputs - old_outputs
417 if not new_outputs:
418 continue
419 print("New outputs: {}".format(new_outputs), file=sys.stderr)
420
421 relevant_workspaces = list(filter(lambda ws: (ws["name"] is not None) or (ws["active_window_id"] is not None), workspaces))
422 target_output = next(iter(outputs - set(only.keys())))
423 if not target_output:
424 continue
425 for ws in relevant_workspaces:
426 ws_ident = ws["name"] if ws["name"] is not None else (ws["output"], ws["idx"])
427 if ws["output"] not in set(only.keys()):
428 continue
429 if ws_ident in only[ws["output"]]:
430 continue
431
432 print("{} -> {}".format(ws_ident, target_output), file=sys.stderr)
433 niri.cmd({"Action": {"MoveWorkspaceToMonitor": {"reference": {"Id": ws["id"]}, "output": target_output}}})
434 '';
435 Restart = "on-failure";
436 RestartSec = 10;
437 };
438 };
439
440 programs.niri.scratchspaces = [
441 { name = "pwctl";
442 key = "Mod+Control+A";
443 spawn = ["pwvucontrol"];
444 app-id = "com.saivert.pwvucontrol";
445 }
446 { name = "kpxc";
447 exclude = [
448 { title = "^Unlock Database.*"; }
449 { title = "^Access Request.*"; }
450 { title = ".*Passkey credentials$"; }
451 ];
452 windowRuleExtra = with kdl; [
453 (kdl.leaf "open-focused" false)
454 ];
455 key = "Mod+Control+P";
456 app-id = "org.keepassxc.KeePassXC";
457 spawn = [ "keepassxc" ];
458 }
459 { name = "bmgr";
460 key = "Mod+Control+B";
461 app-id = ".blueman-manager-wrapped";
462 spawn = [ "blueman-manager" ];
463 }
464 { name = "term";
465 key = "Mod+Control+Return";
466 app-id = "kitty-scratch";
467 spawn = [ "kitty" "--app-id" "kitty-scratch" ];
468 }
469 { name = "edit";
470 match = [ { title = "^scratch$"; app-id = "^emacs$"; } ];
471 key = "Mod+Control+E";
472 selector = "select(.app_id == \"emacs\" and .title == \"scratch\")";
473 spawn = [ "emacsclient" "-c" "--frame-parameters=(quote (name . \"scratch\"))" ];
474 }
475 { name = "eff";
476 key = "Mod+Control+O";
477 app-id = "com.github.wwmm.easyeffects";
478 spawn = [ "easyeffects" ];
479 }
480 { name = "time";
481 key = "Mod+Control+K";
482 app-id = "chrome-kimai.yggdrasil.li__-Default";
483 spawn = [ (toString (pkgs.resholve.writeScript "kimai" {
484 interpreter = pkgs.runtimeShell;
485 inputs = [ pkgs.dex ];
486 execer = [ "cannot:${lib.getExe pkgs.dex}" ];
487 } ''
488 exec dex $HOME/.local/state/nix/profile/share/applications/kimai.desktop
489 '')) ];
490 windowRuleExtra = with kdl; [
491 (leaf "block-out-from" "screencast")
492 ];
493 }
494 ];
495 programs.niri.config =
496 let
497 inherit (kdl) node plain leaf flag;
498 optional-node = cond: v:
499 if cond
500 then v
501 else null;
502 opt-props = lib.filterAttrs (lib.const (value: value != null));
503 in
504 [ (flag "prefer-no-csd")
505
506 (leaf "screenshot-path" "~/screenshots/%Y-%m-%dT%H:%M:%S.png")
507
508 (plain "hotkey-overlay" [
509 (flag "skip-at-startup")
510 ])
511
512 (plain "input" [
513 (plain "keyboard" [
514 (leaf "repeat-delay" 300)
515 (leaf "repeat-rate" 50)
516
517 (plain "xkb" [
518 (leaf "layout" "us,us")
519 (leaf "variant" "dvp,")
520 (leaf "options" "compose:caps,grp:win_space_toggle")
521 ])
522 ])
523
524 (flag "workspace-auto-back-and-forth")
525 # (leaf "focus-follows-mouse" {})
526 # (flag "warp-mouse-to-focus")
527
528 # (plain "touchpad" [ (flag "off") ])
529 (plain "trackball" [
530 (leaf "scroll-method" "on-button-down")
531 (leaf "scroll-button" 278)
532 ])
533 (plain "touch" [
534 (leaf "map-to-output" "eDP-1")
535 ])
536 ])
537
538 (plain "gestures" [
539 (plain "hot-corners" [(flag "off")])
540 ])
541
542 (plain "environment" (lib.mapAttrsToList leaf {
543 NIXOS_OZONE_WL = "1";
544 QT_QPA_PLATFORM = "wayland";
545 QT_WAYLAND_DISABLE_WINDOWDECORATION = "1";
546 GDK_BACKEND = "wayland";
547 SDL_VIDEODRIVER = "wayland";
548 DISPLAY = ":0";
549 ELECTRON_OZONE_PLATFORM_HINT = "auto";
550 SSH_ASKPASS_REQUIRE = "prefer";
551 SSH_ASKPASS = lib.getExe pkgs.kdePackages.ksshaskpass;
552 SUDO_ASKPASS = lib.getExe pkgs.kdePackages.ksshaskpass;
553 }))
554
555 (node "output" "eDP-1" [
556 (leaf "scale" 1.5)
557 (leaf "position" { x = 0; y = 0; })
558 ])
559 (node "output" "Ancor Communications Inc ASUS PB287Q 0x0000DD9B" [
560 (leaf "scale" 1.5)
561 (leaf "position" { x = 2560; y = 0; })
562 ])
563 (node "output" "HP Inc. HP 727pu CN4417143K" [
564 (leaf "mode" "2560x1440@119.998")
565 (leaf "scale" 1)
566 (leaf "position" { x = 2560; y = 0; })
567 (flag "variable-refresh-rate")
568 ])
569
570 (plain "debug" [
571 (leaf "render-drm-device" "/dev/dri/by-path/pci-0000:00:02.0-render")
572 ])
573
574 (plain "animations" [
575 (leaf "slowdown" 0.5)
576 (plain "workspace-switch" [(flag "off")])
577 ])
578
579 (plain "layout" [
580 (leaf "gaps" 8)
581 (plain "struts" [
582 (leaf "left" 26)
583 (leaf "right" 26)
584 (leaf "top" 0)
585 (leaf "bottom" 0)
586 ])
587 (plain "border" [
588 (leaf "width" 2)
589 (leaf "active-gradient" {
590 from = "hsla(195 100% 45% 1)";
591 to = "hsla(155 100% 37.5% 1)";
592 angle = 29;
593 relative-to = "workspace-view";
594 })
595 (leaf "inactive-gradient" {
596 from = "hsla(0 0% 27.7% 1)";
597 to = "hsla(0 0% 23% 1)";
598 angle = 29;
599 relative-to = "workspace-view";
600 })
601 ])
602 (plain "focus-ring" [
603 (flag "off")
604 ])
605
606 (plain "preset-column-widths" (map (prop: leaf "proportion" prop) [
607 (1. / 4.) (1. / 3.) (1. / 2.) (2. / 3.) (3. / 4.) (1.)
608 ]))
609 (plain "default-column-width" [ (leaf "proportion" (1. / 2.)) ])
610 (plain "preset-window-heights" (map (prop: leaf "proportion" prop) [
611 (1. / 3.) (1. / 2.) (2. / 3.) (1.)
612 ]))
613
614 (flag "always-center-single-column")
615
616 (plain "tab-indicator" [
617 (leaf "gap" 4)
618 (leaf "width" 8)
619 (leaf "gaps-between-tabs" 4)
620 (flag "place-within-column")
621 (leaf "length" { total-proportion = 1.; })
622 (leaf "active-gradient" {
623 from = "hsla(195 100% 60% 0.75)";
624 to = "hsla(155 100% 50% 0.75)";
625 angle = 29;
626 relative-to = "workspace-view";
627 })
628 (leaf "inactive-gradient" {
629 from = "hsla(0 0% 42% 0.66)";
630 to = "hsla(0 0% 35% 0.66)";
631 angle = 29;
632 relative-to = "workspace-view";
633 })
634 ])
635 ])
636
637 (plain "cursor" [
638 (flag "hide-when-typing")
639 ])
640
641 (map (name:
642 (node "workspace" name [
643 (leaf "open-on-output" "eDP-1")
644 ])
645 ) (map ({name, ...}: name) cfg.scratchspaces))
646 (map (name:
647 (leaf "workspace" name)
648 ) ["comm" "web" "vid" "bmr"])
649
650 (plain "window-rule" [
651 (leaf "clip-to-geometry" true)
652 ])
653
654 (plain "window-rule" [
655 (leaf "match" { is-floating = true; })
656 (leaf "geometry-corner-radius" 8)
657 (plain "shadow" [ (flag "on") ])
658 ])
659
660 (plain "window-rule" [
661 (leaf "match" { app-id = "^org\\.keepassxc\\.KeePassXC$"; })
662 (leaf "block-out-from" "screencast")
663 ])
664 (plain "window-rule" [
665 (map (title:
666 (leaf "match" { app-id = "^org\\.keepassxc\\.KeePassXC$"; inherit title; })
667 ) ["^Unlock Database.*" "^Access Request.*" ".*Passkey credentials$" "Browser Access Request$"])
668 (leaf "open-focused" true)
669 (leaf "open-floating" true)
670 ])
671
672 (map ({ name, match, exclude, windowRuleExtra, ... }:
673 (optional-node (match != []) (plain "window-rule" [
674 (map (leaf "match") match)
675 (map (leaf "exclude") exclude)
676 (leaf "open-on-workspace" name)
677 (leaf "open-maximized" true)
678 windowRuleExtra
679 ]))
680 ) cfg.scratchspaces)
681
682 (plain "window-rule" [
683 (leaf "match" { app-id = "^emacs$"; })
684 (leaf "match" { app-id = "^firefox$"; })
685 (plain "default-column-width" [(leaf "proportion" (2. / 3.))])
686 ])
687 (plain "window-rule" [
688 (leaf "match" { app-id = "^kitty$"; })
689 (leaf "match" { app-id = "^kitty-play$"; })
690 (plain "default-column-width" [(leaf "proportion" (1. / 3.))])
691 ])
692
693 (plain "window-rule" [
694 (leaf "match" { app-id = "^thunderbird$"; })
695 (leaf "match" { app-id = "^Element$"; })
696 (leaf "match" { app-id = "^chrome-web\.openrainbow\.com__-Default$"; })
697 (leaf "open-on-workspace" "comm")
698 ])
699 (plain "window-rule" [
700 (leaf "match" { app-id = "^firefox$"; })
701 (leaf "open-on-workspace" "web")
702 (leaf "open-maximized" true)
703 ])
704 (plain "window-rule" [
705 (leaf "match" { app-id = "^mpv$"; })
706 (leaf "open-on-workspace" "vid")
707 (plain "default-column-width" [(leaf "proportion" 1.)])
708 ])
709 (plain "window-rule" [
710 (leaf "match" { app-id = "^kitty-play$"; })
711 (leaf "open-on-workspace" "vid")
712 (leaf "open-focused" false)
713 ])
714 (plain "window-rule" [
715 (leaf "match" { app-id = "^chrome-audiobookshelf\.yggdrasil\.li__-Default$"; })
716 (leaf "match" { app-id = "^YouTube Music Desktop App$"; })
717 (leaf "open-on-workspace" "vid")
718 ])
719 (plain "window-rule" [
720 (leaf "match" { app-id = "^pdfpc$"; })
721 (plain "default-column-width" [(leaf "proportion" 1.)])
722 ])
723 (plain "window-rule" [
724 (leaf "match" { app-id = "^pdfpc$"; title = "^.*presentation.*$"; })
725 (plain "default-column-width" [(leaf "proportion" 1.)])
726 (leaf "open-fullscreen" true)
727 (leaf "open-on-workspace" "bmr")
728 (leaf "open-focused" false)
729 ])
730 (plain "window-rule" [
731 (map (leaf "match") [
732 { app-id = "^Gimp-"; title = "^Quit GIMP$"; }
733 { app-id = "^org\\.kde\\.polkit-kde-authentication-agent-1$"; }
734 { app-id = "^xdg-desktop-portal-gtk$"; }
735 ])
736 (leaf "open-floating" true)
737 ])
738 (plain "window-rule" [
739 (leaf "match" { app-id = "^org\\.pwmt\\.zathura$"; })
740 (leaf "match" { app-id = "^evince$"; })
741 (leaf "match" { app-id = "^org\\.gnome\\.Papers$"; })
742 (leaf "default-column-display" "tabbed")
743 ])
744
745 (plain "layer-rule" [
746 (leaf "match" { namespace = "^notifications$"; })
747 (leaf "match" { namespace = "^waybar$"; })
748 (leaf "match" { namespace = "^launcher$"; })
749 (leaf "block-out-from" "screencast")
750 ])
751
752 (plain "binds"
753 (let
754 bind = name: cfg: node name (opt-props {
755 cooldown-ms = cfg.cooldown-ms or null;
756 }
757 // (lib.optionalAttrs (!(cfg.repeat or true)) {
758 repeat = false;
759 })
760 // (lib.optionalAttrs (cfg.allow-when-locked or false) {
761 allow-when-locked = true;
762 })) (lib.mapAttrsToList leaf (lib.removeAttrs cfg.action ["__functor"]));
763 in
764 [
765 (lib.mapAttrsToList bind (with config.lib.niri.actions; {
766 "Mod+Slash".action = show-hotkey-overlay;
767
768 "Mod+Return".action = spawn terminal;
769 "Mod+Shift+Return".action =
770 let
771 nushellKitty = pkgs.symlinkJoin {
772 name = "nushell-kitty";
773 paths = [ config.programs.kitty.package ];
774 buildInputs = [ pkgs.makeWrapper ];
775 postBuild = ''
776 wrapProgram $out/bin/kitty \
777 --add-flags "--config ${pkgs.writeText "kitty.conf" ''
778 include $HOME/${config.xdg.configFile."kitty/kitty.conf".target}
779 shell ${lib.getExe config.programs.nushell.package}
780 ''}"
781 '';
782 };
783 in spawn (lib.getExe' nushellKitty "kitty");
784 "Mod+Q".action = close-window;
785 "Mod+O".action = spawn (lib.getExe config.programs.fuzzel.package);
786 "Mod+Shift+O".action = spawn (lib.getExe config.programs.fuzzel.package) "--list-executables-in-path";
787
788 "Mod+Alt+E".action = spawn (lib.getExe' config.services.emacs.package "emacsclient") "-c";
789 "Mod+Alt+Y".action = spawn (lib.getExe (pkgs.writeShellApplication {
790 name = "queue-yt-dlp";
791 runtimeInputs = with pkgs; [ wl-clipboard-rs socat ];
792 text = ''
793 socat STDIO UNIX-CONNECT:"$XDG_RUNTIME_DIR"/yt-dlp.sock <<<$'{ "urls": ["'"$(wl-paste)"$'"] }'
794 '';
795 }));
796 "Mod+Alt+L".action = spawn (lib.getExe (pkgs.writeShellApplication {
797 name = "queue-yt-dlp";
798 runtimeInputs = with pkgs; [ wl-clipboard-rs config.programs.kitty.package ];
799 text = ''
800 exec -- kitty --app-id kitty-play --directory "$HOME"/media mpv "$(wl-paste)"
801 '';
802 }));
803 "Mod+Alt+M".action = spawn (lib.getExe' pkgs.screen-message "sm") "-n" "Fira Mono" "-a" "1" "-f" "#fff" "-b" "#000";
804
805 "Mod+U".action = spawn (lib.getExe (pkgs.writeShellApplication {
806 name = "qalc-fuzzel";
807 runtimeInputs = with pkgs; [ wl-clipboard-rs libqalculate config.programs.fuzzel.package coreutils findutils libnotify gnugrep ];
808 text = ''
809 RESULTS_DIR="$HOME/.cache/qalc-fuzzel"
810 prev() {
811 FOUND=false
812 while IFS= read -r line; do
813 [[ -n "$line" ]] || continue
814 FOUND=true
815 echo "$line"
816 done < <(export LC_ALL=C.UTF-8; echo; find "$RESULTS_DIR" -type f -printf $'%T@ %p\n' | sort -n | cut -d' ' -f2- | xargs -r cat)
817 $FOUND || echo
818 }
819 FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> " --width=60) || exit $?
820 if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then
821 QALC_RES="$FUZZEL_RES"
822 QALC_RET=0
823 else
824 QALC_RES=$(qalc -set "autocalc off" "$FUZZEL_RES" 2>&1)
825 QALC_RET=$?
826 fi
827 [[ -n "$QALC_RES" ]] || exit 1
828 EXISTING=false
829 set +o pipefail
830 grep -Fxrl "$QALC_RES" "$RESULTS_DIR" | xargs -r touch
831 [[ ''${PIPESTATUS[0]} -eq 0 ]] && EXISTING=true
832 set -o pipefail
833 if [[ $QALC_RET -eq 0 ]] && ! $EXISTING; then
834 set +o pipefail
835 RES_FILE="$RESULTS_DIR"/$(date -uIs).$(tr -Cd 'a-zA-Z0-9' </dev/random | head -c 10)
836 set -o pipefail
837 cat >"$RES_FILE" <<<"$QALC_RES"
838 fi
839 [[ "$QALC_RES" =~ .*\ =\ (.*) ]] && QALC_RES="''${BASH_REMATCH[1]}"
840 [[ $QALC_RET -eq 0 ]] && wl-copy "$QALC_RES"
841 notify-send "$QALC_RES"
842 '';
843 }));
844 "Mod+Shift+U".action =
845 let
846 qalcKitty = pkgs.symlinkJoin {
847 name = "qalc-kitty";
848 paths = [ config.programs.kitty.package ];
849 buildInputs = [ pkgs.makeWrapper ];
850 postBuild = ''
851 wrapProgram $out/bin/kitty \
852 --add-flags "--config ${pkgs.writeText "kitty.conf" ''
853 include $HOME/${config.xdg.configFile."kitty/kitty.conf".target}
854 shell ${lib.getExe pkgs.libqalculate}
855 ''}"
856 '';
857 };
858 in spawn (lib.getExe' qalcKitty "kitty");
859 "Mod+E".action = spawn (lib.getExe (pkgs.writeShellApplication {
860 name = "emoji-fuzzel";
861 runtimeInputs = with pkgs; [ config.programs.fuzzel.package wtype wl-clipboard-rs ];
862 text = ''
863 FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " --cache "$HOME"/.cache/fuzzel-emoji --width=60 <"$HOME"/.local/share/emoji-data/list.txt) || exit $?
864 [[ -n "$FUZZEL_RES" ]] || exit 1
865 wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste
866 '';
867 }));
868 "Print".action = screenshot;
869 "Control+Print".action = screenshot-window;
870 "Shift+Print".action = kdl.magic-leaf "screenshot-screen";
871 "Mod+B".action = with-select-window-action ".workspace_id == ($active_workspace | tonumber)" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
872 "Mod+Shift+B".action = with-select-window-action "true" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
873
874 "Mod+Escape" = {
875 allow-inhibiting = false;
876 action = toggle-keyboard-shortcuts-inhibit;
877 };
878
879 "Mod+H".action = focus-column-left;
880 "Mod+T".action = focus-window-down;
881 "Mod+N".action = focus-window-up;
882 "Mod+S".action = focus-column-right;
883
884 "Mod+Shift+H".action = move-column-left;
885 "Mod+Shift+T".action = move-window-down;
886 "Mod+Shift+N".action = move-window-up;
887 "Mod+Shift+S".action = move-column-right;
888
889 "Mod+Control+H".action = focus-monitor-left;
890 "Mod+Control+T".action = focus-monitor-down;
891 "Mod+Control+N".action = focus-monitor-up;
892 "Mod+Control+S".action = focus-monitor-right;
893
894 "Mod+Shift+Control+H".action = move-workspace-to-monitor-left;
895 "Mod+Shift+Control+T".action = move-workspace-to-monitor-down;
896 "Mod+Shift+Control+N".action = move-workspace-to-monitor-up;
897 "Mod+Shift+Control+S".action = move-workspace-to-monitor-right;
898
899 "Mod+G".action = focus-adjacent-workspace "down";
900 "Mod+C".action = focus-adjacent-workspace "up";
901
902 "Mod+Shift+G".action = move-column-to-adjacent-workspace "down";
903 "Mod+Shift+C".action = move-column-to-adjacent-workspace "up";
904
905 "Mod+Shift+Control+G".action = move-workspace-down;
906 "Mod+Shift+Control+C".action = move-workspace-up;
907
908 "Mod+ParenLeft".action = focus-workspace "comm";
909 "Mod+Shift+ParenLeft".action = kdl.magic-leaf "move-column-to-workspace" "comm";
910
911 "Mod+ParenRight".action = focus-workspace "web";
912 "Mod+Shift+ParenRight".action = kdl.magic-leaf "move-column-to-workspace" "web";
913
914 "Mod+BraceRight".action = focus-workspace "read";
915 "Mod+Shift+BraceRight".action = kdl.magic-leaf "move-column-to-workspace" "read";
916
917 "Mod+BraceLeft".action = focus-workspace "mon";
918 "Mod+Shift+BraceLeft".action = kdl.magic-leaf "move-column-to-workspace" "mon";
919
920 "Mod+Asterisk".action = focus-workspace "vid";
921 "Mod+Shift+Asterisk".action = kdl.magic-leaf "move-column-to-workspace" "vid";
922
923 "Mod+Plus".action = with-unnamed-workspace-action ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
924 "Mod+Shift+Plus".action = with-unnamed-workspace-action ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}, "focus": true}}}'';
925
926 "Mod+M".action = consume-or-expel-window-left;
927 "Mod+W".action = consume-or-expel-window-right;
928
929 "Mod+Shift+M".action = toggle-column-tabbed-display;
930
931 "Mod+R".action = switch-preset-column-width;
932 "Mod+Shift+R".action = maximize-column;
933 "Mod+Shift+Ctrl+R".action = switch-preset-window-height;
934 "Mod+F".action = center-column;
935 "Mod+Shift+F".action = toggle-windowed-fullscreen;
936 "Mod+Ctrl+Shift+F".action = fullscreen-window;
937
938 "Mod+V".action = switch-focus-between-floating-and-tiling;
939 "Mod+Shift+V".action = toggle-window-floating;
940
941 "Mod+Left".action = set-column-width "-10%";
942 "Mod+Down".action = set-window-height "-10%";
943 "Mod+Up".action = set-window-height "+10%";
944 "Mod+Right".action = set-column-width "+10%";
945
946 "Mod+Shift+Z" = {
947 action = spawn (lib.getExe niri) "msg" "action" "power-off-monitors";
948 allow-when-locked = true;
949 };
950 "Mod+Shift+L".action = spawn loginctl "lock-session";
951 "Mod+Shift+E".action = quit;
952 "Mod+Shift+Minus" = {
953 action = spawn systemctl "suspend";
954 allow-when-locked = true;
955 };
956 "Mod+Shift+Control+Minus" = {
957 action = spawn systemctl "hibernate";
958 allow-when-locked = true;
959 };
960 "Mod+Shift+P" = {
961 action = spawn (lib.getExe pkgs.playerctl) "-a" "pause";
962 allow-when-locked = true;
963 };
964
965 "XF86MonBrightnessUp" = {
966 action = spawn swayosd-client "--brightness" "raise";
967 allow-when-locked = true;
968 };
969 "XF86MonBrightnessDown" = {
970 action = spawn swayosd-client "--brightness" "lower";
971 allow-when-locked = true;
972 };
973 "XF86AudioRaiseVolume" = {
974 action = spawn swayosd-client "--output-volume" "raise";
975 allow-when-locked = true;
976 };
977 "XF86AudioLowerVolume" = {
978 action = spawn swayosd-client "--output-volume" "lower";
979 allow-when-locked = true;
980 };
981 "XF86AudioMute" = {
982 action = spawn swayosd-client "--output-volume" "mute-toggle";
983 allow-when-locked = true;
984 };
985 "XF86AudioMicMute" = {
986 action = spawn swayosd-client "--input-volume" "mute-toggle";
987 allow-when-locked = true;
988 };
989
990 "Mod+Semicolon".action = spawn makoctl "dismiss" "--group";
991 "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all";
992 "Mod+Period".action = spawn makoctl "menu" "--" (lib.getExe config.programs.fuzzel.package) "--dmenu";
993 "Mod+Comma".action = spawn makoctl "restore";
994
995 "Mod+Control+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}";
996 "Mod+Control+Shift+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"MoveColumnToWorkspace\":{\"reference\":{\"Id\": $workspace_id}, \"focus\": true}}}";
997
998 "Mod+X".action = set-dynamic-cast-window;
999 "Mod+Shift+X".action = set-dynamic-cast-monitor;
1000 "Mod+Control+Shift+X".action = clear-dynamic-cast-target;
1001
1002 "Mod+D".action = with-urgent-window-action "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
1003 "Mod+Shift+D".action = with-focused-window-action "{\"Action\":{\"UnsetUrgent\":{\"id\": .id}}}";
1004
1005 "Mod+K".action = spawn (lib.getExe' pkgs.worktime "worktime-ui");
1006 "Mod+Shift+K".action = spawn (lib.getExe' pkgs.worktime "worktime-stop");
1007 }))
1008 (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)
1009 (map ({ name, moveKey, ...}: if moveKey != null then bind moveKey { action = kdl.magic-leaf "move-column-to-workspace" name; } else null) cfg.scratchspaces)
1010 ]
1011 ))
1012 ];
138 }; 1013 };
139} 1014}
diff --git a/accounts/gkleen@sif/niri/mako.nix b/accounts/gkleen@sif/niri/mako.nix
new file mode 100644
index 00000000..eba26caa
--- /dev/null
+++ b/accounts/gkleen@sif/niri/mako.nix
@@ -0,0 +1,113 @@
1{ config, lib, pkgs, ... }:
2{
3 config = {
4 services.mako = {
5 enable = true;
6 settings = {
7 font = "Fira Sans 10";
8 format = "<i>%s</i>\\n%b";
9 margin = "2";
10 max-visible = -1;
11 background-color = "#000000dd";
12 progress-color = "source #223544ff";
13 width = 384;
14 outer-margin = 1;
15 max-history = 100;
16 max-icon-size = 48;
17 };
18 criteria = {
19 grouped.format = "<b>(%g)</b> <i>%s</i>\\n%b";
20 "urgency=low".text-color = "#999999ff";
21 "urgency=critical".background-color = "#900000dd";
22 "app-name=Element".group-by = "summary";
23 "app-name=poweralertd" = {
24 history = false;
25 ignore-timeout = true;
26 default-timeout = 2000;
27 };
28 "app-name=worktime".history = false;
29 "mode=silent".invisible = true;
30 };
31 package = pkgs.symlinkJoin {
32 name = "${pkgs.mako.name}-wrapped";
33 paths = with pkgs; [ mako ];
34 inherit (pkgs.mako) meta;
35 postBuild = ''
36 rm -r $out/share/dbus-1
37 '';
38 };
39 };
40 systemd.user.services.mako = {
41 Unit = {
42 Description = "Mako notification daemon";
43 PartOf = [ "graphical-session.target" ];
44 };
45 Install = {
46 WantedBy = [ "graphical-session.target" ];
47 };
48 Service = {
49 Type = "dbus";
50 BusName = "org.freedesktop.Notifications";
51 ExecStart = lib.getExe config.services.mako.package;
52 RestartSec = 5;
53 Restart = "always";
54 };
55 };
56
57 systemd.user.services.mako-follows-focus = {
58 Unit = {
59 BindsTo = [ "niri.service" "mako.service" ];
60 After = [ "niri.service" "mako.service" ];
61 };
62 Service = {
63 Type = "simple";
64 Restart = "always";
65 ExecStart = pkgs.writers.writePython3 "mako-follows-focus" {
66 libraries = with pkgs.python3Packages; [];
67 } ''
68 import os
69 import socket
70 import json
71 import subprocess
72
73
74 current_output = None
75 workspaces = []
76
77
78 def output_changed(new_output):
79 global current_output
80
81 if current_output == new_output:
82 return
83
84 current_output = new_output
85 subprocess.run(["makoctl", "reload"])
86
87
88 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
89 sock.connect(os.environ["NIRI_SOCKET"])
90 sock.send(b"\"EventStream\"\n")
91 for line in sock.makefile(buffering=1, encoding='utf-8'):
92 if line_json := json.loads(line):
93 if "WorkspacesChanged" in line_json:
94 workspaces = line_json["WorkspacesChanged"]["workspaces"]
95 for workspace in workspaces:
96 if not workspace["is_focused"]:
97 continue
98 output_changed(workspace["output"])
99 break
100 if "WorkspaceActivated" in line_json and line_json["WorkspaceActivated"]["focused"]: # noqa: E501
101 for workspace in workspaces:
102 if not workspace["id"] == line_json["WorkspaceActivated"]["id"]: # noqa: E501
103 continue
104 output_changed(workspace["output"])
105 break
106 '';
107 };
108 Install = {
109 WantedBy = [ "mako.service" ];
110 };
111 };
112 };
113}
diff --git a/accounts/gkleen@sif/niri/swayosd.nix b/accounts/gkleen@sif/niri/swayosd.nix
new file mode 100644
index 00000000..54ebb302
--- /dev/null
+++ b/accounts/gkleen@sif/niri/swayosd.nix
@@ -0,0 +1,66 @@
1{ pkgs, ... }:
2{
3 config = {
4 services.swayosd = {
5 enable = true;
6 topMargin = 0.4769706078;
7 stylePath = pkgs.runCommand "style.css" {
8 passAsFile = [ "src" ];
9 src = ''
10 window#osd {
11 padding: 12px 20px;
12 border-radius: 999px;
13 border: none;
14 background: rgba(0, 0, 0, 0.87);
15
16 #container {
17 margin: 16px;
18 }
19
20 image,
21 label {
22 color: rgb(255, 255, 255);
23
24 &:disabled {
25 opacity: 1;
26 color: rgb(84, 84, 84);
27 }
28 }
29
30 progressbar {
31 min-height: 6px;
32 border-radius: 999px;
33 background: transparent;
34 border: none;
35
36 trough, progress {
37 min-height: inherit;
38 border-radius: inherit;
39 border: none;
40 }
41
42 trough {
43 background: rgb(127, 127, 127);
44 }
45 progress {
46 background: rgb(255, 255, 255);
47 }
48
49 &:disabled {
50 opacity: 1;
51
52 trough {
53 background: rgb(19, 19, 19);
54 }
55 progress {
56 background: rgb(38, 38, 38);
57 }
58 }
59 }
60 }
61 '';
62 buildInputs = with pkgs; [sass];
63 } "scss -C --sourcemap=none --style=compact $srcPath $out";
64 };
65 };
66}
diff --git a/accounts/gkleen@sif/niri/waybar.nix b/accounts/gkleen@sif/niri/waybar.nix
index 1a25b581..c02a9a76 100644
--- a/accounts/gkleen@sif/niri/waybar.nix
+++ b/accounts/gkleen@sif/niri/waybar.nix
@@ -1,5 +1,7 @@
1{ lib, pkgs, ... }: 1{ lib, config, pkgs, ... }:
2{ 2let
3 swayosd-client = lib.getExe' config.services.swayosd.package "swayosd-client";
4in {
3 config = { 5 config = {
4 programs.waybar = { 6 programs.waybar = {
5 enable = true; 7 enable = true;
@@ -18,20 +20,76 @@
18 { 20 {
19 layer = "top"; 21 layer = "top";
20 position = "top"; 22 position = "top";
21 height = 14; 23 height = 21;
22 output = [ "eDP-1" "DP-2" "DP-3" ]; 24 output = [ "eDP-1" "DP-2" "DP-3" ];
23 modules-left = [ "niri/workspaces" ]; 25 modules-left = [ "niri/workspaces" ];
24 modules-center = [ "niri/window" ]; 26 modules-center = [ "niri/window" ];
25 modules-right = [ # "custom/worktime" "custom/worktime-today" 27 modules-right = [ "custom/worktime" "custom/worktime-today"
26 "custom/weather" 28 "custom/weather"
27 # "custom/keymap" 29 "custom/keymap"
28 "privacy" "tray" "wireplumber" "backlight" "battery" "idle_inhibitor" "clock" ]; 30 "privacy" "tray" "wireplumber" "backlight" "battery" "idle_inhibitor" "custom/mako" "custom/lid_inhibitor" "clock" ];
31
32 "custom/lid_inhibitor" = {
33 format = "{}";
34 return-type = "json";
35 exec = lib.getExe pkgs.waybar-systemd-inhibit;
36 on-click = lib.getExe' pkgs.waybar-systemd-inhibit "waybar-systemd-inhibit-toggle";
37 };
38 "custom/mako" = {
39 format = "{}";
40 return-type = "json";
41 exec = pkgs.writers.writePython3 "mako-silent" { libraries = [ pkgs.python3Packages.dbus-next ]; } ''
42 from dbus_next.aio import MessageBus
43
44 import asyncio
45
46 import json
47
48
49 loop = asyncio.new_event_loop()
50 asyncio.set_event_loop(loop)
51
52
53 async def main():
54 bus = await MessageBus().connect()
55 # the introspection xml would normally be included in your project, but
56 # this is convenient for development
57 introspection = await bus.introspect('org.freedesktop.Notifications', '/fr/emersion/Mako') # noqa: E501
58
59 obj = bus.get_proxy_object('org.freedesktop.Notifications', '/fr/emersion/Mako', introspection) # noqa: E501
60 mako = obj.get_interface('fr.emersion.Mako')
61 properties = obj.get_interface('org.freedesktop.DBus.Properties')
62
63 async def print_mode():
64 modes = await mako.get_modes()
65 is_silent = "silent" in modes
66 icon = "&#xf009b;" if is_silent else "&#xf009a;"
67 text = f"<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>" # noqa: E501
68 if is_silent:
69 text = f"<span color=\"#ffffff\">{text}</span>"
70 print(json.dumps({'text': text, 'tooltip': ', '.join(modes)}, separators=(',', ':')), flush=True) # noqa: E501
71
72 async def on_properties_changed(interface_name, changed_properties, invalidated_properties): # noqa: E501
73 if "Modes" not in invalidated_properties:
74 return
75
76 await print_mode()
77
78 properties.on_properties_changed(on_properties_changed)
79 await print_mode()
29 80
81 await loop.create_future()
82
83
84 loop.run_until_complete(main())
85 '';
86 on-click = "makoctl mode -t silent";
87 };
30 "custom/weather" = { 88 "custom/weather" = {
31 format = "{}"; 89 format = "{}";
32 tooltip = true; 90 tooltip = true;
33 interval = 3600; 91 interval = 3600;
34 exec = "${lib.getExe pkgs.wttrbar} --hide-conditions --nerd --custom-indicator \"<span font=\\\"Symbols Nerd Font Mono\\\" size=\\\"120%\\\">{ICON}</span> {FeelsLikeC}°\""; 92 exec = "${lib.getExe pkgs.wttrbar} --hide-conditions --nerd --custom-indicator \"<span font=\\\"Symbols Nerd Font Mono\\\" size=\\\"100%\\\">{ICON}</span> {FeelsLikeC}°\"";
35 return-type = "json"; 93 return-type = "json";
36 }; 94 };
37 "custom/keymap" = { 95 "custom/keymap" = {
@@ -41,8 +99,6 @@
41 exec = pkgs.writers.writePython3 "keymap" {} '' 99 exec = pkgs.writers.writePython3 "keymap" {} ''
42 import os 100 import os
43 import socket 101 import socket
44 import re
45 import subprocess
46 import json 102 import json
47 103
48 104
@@ -55,33 +111,33 @@
55 print(json.dumps({'text': short, 'tooltip': keymap}, separators=(',', ':')), flush=True) # noqa: E501 111 print(json.dumps({'text': short, 'tooltip': keymap}, separators=(',', ':')), flush=True) # noqa: E501
56 112
57 113
58 r = subprocess.run(["hyprctl", "devices", "-j"], check=True, stdout=subprocess.PIPE, text=True) # noqa: E501 114 keyboard_layouts = []
59 for keyboard in json.loads(r.stdout)['keyboards']:
60 if keyboard['name'] != "at-translated-set-2-keyboard":
61 continue
62 output(keyboard['active_keymap'])
63 115
64 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 116 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
65 sock.connect(os.environ["XDG_RUNTIME_DIR"] + "/hypr/" + os.environ["HYPRLAND_INSTANCE_SIGNATURE"] + "/.socket2.sock") # noqa: E501 117 sock.connect(os.environ["NIRI_SOCKET"])
66 expected = re.compile(r'^activelayout>>at-translated-set-2-keyboard,(?P<keymap>.+)$') # noqa: E501 118 sock.send(b"\"EventStream\"\n")
67 for line in sock.makefile(buffering=1, encoding='utf-8'): 119 for line in sock.makefile(buffering=1, encoding='utf-8'):
68 if match := expected.match(line): 120 if line_json := json.loads(line):
69 output(match.group("keymap")) 121 if "KeyboardLayoutsChanged" in line_json:
122 keyboard_layouts = line_json["KeyboardLayoutsChanged"]["keyboard_layouts"]["names"] # noqa: E501
123 output(keyboard_layouts[line_json["KeyboardLayoutsChanged"]["keyboard_layouts"]["current_idx"]]) # noqa: E501
124 if "KeyboardLayoutSwitched" in line_json:
125 output(keyboard_layouts[line_json["KeyboardLayoutSwitched"]["idx"]]) # noqa: E501
70 ''; 126 '';
71 on-click = "hyprctl switchxkblayout at-translated-set-2-keyboard next"; 127 on-click = "niri msg action switch-layout next";
72 }; 128 };
73 "custom/worktime" = { 129 "custom/worktime" = {
74 interval = 60; 130 interval = 60;
75 exec = lib.getExe pkgs.worktime; 131 exec = "${lib.getExe pkgs.worktime} time --waybar";
76 tooltip = false; 132 return-type = "json";
77 }; 133 };
78 "custom/worktime-today" = { 134 "custom/worktime-today" = {
79 interval = 60; 135 interval = 60;
80 exec = "${lib.getExe pkgs.worktime} today"; 136 exec = "${lib.getExe pkgs.worktime} today --waybar";
81 tooltip = false; 137 return-type = "json";
82 }; 138 };
83 "niri/workspaces" = { 139 "niri/workspaces" = {
84 all-outputs = true; 140 ignore = map ({ name, ... }: name) config.programs.niri.scratchspaces;
85 }; 141 };
86 "niri/window" = { 142 "niri/window" = {
87 separate-outputs = true; 143 separate-outputs = true;
@@ -142,8 +198,8 @@
142 icon-size = iconSize; 198 icon-size = iconSize;
143 tooltip-format = "{percent}%"; 199 tooltip-format = "{percent}%";
144 format-icons = ["&#xf00da;" "&#xf00db;" "&#xf00dc;" "&#xf00dd;" "&#xf00de;" "&#xf00df;" "&#xf00e0;"]; 200 format-icons = ["&#xf00da;" "&#xf00db;" "&#xf00dc;" "&#xf00dd;" "&#xf00de;" "&#xf00df;" "&#xf00e0;"];
145 on-scroll-up = "lightctl -d -e4 -n1 up"; 201 on-scroll-up = "${swayosd-client} --brightness raise";
146 on-scroll-down = "lightctl -d -e4 -n1 down"; 202 on-scroll-down = "${swayosd-client} --brightness lower";
147 }; 203 };
148 wireplumber = { 204 wireplumber = {
149 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>"; 205 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
@@ -152,22 +208,22 @@
152 format-icons = ["&#xf057f;" "&#xf0580;" "&#xf057e;"]; 208 format-icons = ["&#xf057f;" "&#xf0580;" "&#xf057e;"];
153 format-muted = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">&#xf075f;</span>"; 209 format-muted = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">&#xf075f;</span>";
154 # ignored-sinks = ["Easy Effects Sink"]; 210 # ignored-sinks = ["Easy Effects Sink"];
155 on-scroll-up = "volumectl -d -u up"; 211 on-scroll-up = "${swayosd-client} --output-volume raise";
156 on-scroll-down = "volumectl -d -u down"; 212 on-scroll-down = "${swayosd-client} --output-volume lower";
157 on-click = "volumectl -d toggle-mute"; 213 on-click = "${swayosd-client} --output-volume mute-toggle";
158 }; 214 };
159 } 215 }
160 { 216 {
161 layer = "top"; 217 layer = "top";
162 position = "top"; 218 position = "top";
163 height = 14; 219 height = 14;
164 output = [ "!eDP-1" "!DP-2" "!DP-3" ]; 220 output = [ "!eDP-1" "!DP-2" "!DP-3" "*" ];
165 modules-left = [ "niri/workspaces" ]; 221 modules-left = [ "niri/workspaces" ];
166 modules-center = [ "niri/window" ]; 222 modules-center = [ "niri/window" ];
167 modules-right = [ "clock" ]; 223 modules-right = [ "clock" ];
168 224
169 "niri/workspaces" = { 225 "niri/workspaces" = {
170 all-outputs = false; 226 ignore = map ({ name, ... }: name) config.programs.niri.scratchspaces;
171 }; 227 };
172 "niri/window" = { 228 "niri/window" = {
173 separate-outputs = true; 229 separate-outputs = true;
@@ -193,7 +249,7 @@
193 249
194 * { 250 * {
195 border: none; 251 border: none;
196 font-family: "Fira Sans Nerd Font"; 252 font-family: "Fira Sans";
197 font-size: 10pt; 253 font-size: 10pt;
198 min-height: 0; 254 min-height: 0;
199 } 255 }
@@ -204,10 +260,10 @@
204 } 260 }
205 261
206 .modules-left { 262 .modules-left {
207 margin-left: 9px; 263 margin-left: 38px;
208 } 264 }
209 .modules-right { 265 .modules-right {
210 margin-right: 9px; 266 margin-right: 38px;
211 } 267 }
212 268
213 .module { 269 .module {
@@ -215,13 +271,11 @@
215 } 271 }
216 272
217 #workspaces button { 273 #workspaces button {
218 color: @grey;
219 }
220 #workspaces button.hosting-monitor {
221 color: @white; 274 color: @white;
275 padding: 2px 5px;
222 } 276 }
223 #workspaces button.visible { 277 #workspaces button.empty {
224 color: @blue; 278 color: @grey;
225 } 279 }
226 #workspaces button.active { 280 #workspaces button.active {
227 color: @green; 281 color: @green;
@@ -234,22 +288,31 @@
234 color: @grey; 288 color: @grey;
235 margin: 0 5px; 289 margin: 0 5px;
236 } 290 }
237 #custom-weather, #custom-worktime-today { 291 #custom-weather {
238 margin-right: 3px; 292 margin-right: 3px;
239 } 293 }
240 #custom-keymap, #custom-weather { 294 #custom-keymap {
241 margin-left: 3px; 295 margin-left: 3px;
296 margin-right: 3px;
242 } 297 }
243 298
244 #tray { 299 #tray {
245 margin: 0; 300 margin: 0;
246 } 301 }
247 #battery, #idle_inhibitor, #backlight, #wireplumber { 302 #battery, #idle_inhibitor, #backlight, #wireplumber, #custom-mako, #custom-lid_inhibitor {
248 color: @grey; 303 color: @grey;
249 margin: 0 5px 0 2px; 304 margin: 0 5px 0 2px;
250 } 305 }
251 #idle_inhibitor { 306 #idle_inhibitor {
252 margin-right: 2px; 307 margin-right: 4px;
308 margin-left: 6px;
309 }
310 #custom-mako {
311 margin-right: 4px;
312 margin-left: 3px;
313 }
314 #custom-lid_inhibitor {
315 margin-right: 3px;
253 margin-left: 3px; 316 margin-left: 3px;
254 } 317 }
255 #battery { 318 #battery {
@@ -270,17 +333,24 @@
270 #idle_inhibitor.activated { 333 #idle_inhibitor.activated {
271 color: @white; 334 color: @white;
272 } 335 }
336 #custom-worktime.running, #custom-worktime-today.running {
337 color: @white;
338 }
339 #custom-worktime.over, #custom-worktime-today.over {
340 color: @orange;
341 }
273 342
274 #idle_inhibitor { 343 #idle_inhibitor, #custom-lid_inhibitor {
275 padding-top: 1px; 344 padding-top: 1px;
276 } 345 }
277 346
278 #privacy { 347 #privacy {
279 color: @red; 348 color: @red;
280 margin: -1px 2px 0px 5px; 349 margin: -1px 4px 0px 3px;
281 } 350 }
282 #clock { 351 #clock {
283 /* margin-right: 5px; */ 352 /* margin-right: 5px; */
353 font-feature-settings: "tnum";
284 } 354 }
285 ''; 355 '';
286 }; 356 };