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