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.nix923
1 files changed, 0 insertions, 923 deletions
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
deleted file mode 100644
index 924d3843..00000000
--- a/accounts/gkleen@sif/niri/default.nix
+++ /dev/null
@@ -1,923 +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 = spawn terminal (lib.getExe config.programs.nushell.package);
715 "Mod+Q".action = close-window;
716 "Mod+O".action = spawn (lib.getExe config.programs.fuzzel.package);
717 "Mod+Shift+O".action = spawn (lib.getExe config.programs.fuzzel.package) "--list-executables-in-path";
718
719 "Mod+Alt+E".action = spawn (lib.getExe' config.services.emacs.package "emacsclient") "-c";
720 "Mod+Alt+Y".action = spawn (lib.getExe (pkgs.writeShellApplication {
721 name = "queue-yt-dlp";
722 runtimeInputs = with pkgs; [ wl-clipboard-rs socat ];
723 text = ''
724 socat STDIO UNIX-CONNECT:"$XDG_RUNTIME_DIR"/yt-dlp.sock <<<$'{ "urls": ["'"$(wl-paste)"$'"] }'
725 '';
726 }));
727 "Mod+Alt+L".action = spawn (lib.getExe (pkgs.writeShellApplication {
728 name = "queue-yt-dlp";
729 runtimeInputs = with pkgs; [ wl-clipboard-rs config.programs.kitty.package ];
730 text = ''
731 exec -- kitty --app-id kitty-play --directory "$HOME"/media mpv "$(wl-paste)"
732 '';
733 }));
734 "Mod+Alt+M".action = spawn (lib.getExe' pkgs.screen-message "sm") "-n" "Fira Mono" "-a" "1" "-f" "#fff" "-b" "#000";
735
736 "Mod+U".action = spawn (lib.getExe (pkgs.writeShellApplication {
737 name = "qalc-fuzzel";
738 runtimeInputs = with pkgs; [ wl-clipboard-rs libqalculate config.programs.fuzzel.package coreutils findutils libnotify gnugrep ];
739 text = ''
740 RESULTS_DIR="$HOME/.cache/qalc-fuzzel"
741 prev() {
742 FOUND=false
743 while IFS= read -r line; do
744 [[ -n "$line" ]] || continue
745 FOUND=true
746 echo "$line"
747 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)
748 $FOUND || echo
749 }
750 FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> ") || exit $?
751 if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then
752 QALC_RES="$FUZZEL_RES"
753 QALC_RET=0
754 else
755 QALC_RES=$(qalc "$FUZZEL_RES" 2>&1)
756 QALC_RET=$?
757 fi
758 [[ -n "$QALC_RES" ]] || exit 1
759 EXISTING=false
760 set +o pipefail
761 grep -Fxrl "$QALC_RES" "$RESULTS_DIR" | xargs -r touch
762 [[ ''${PIPESTATUS[0]} -eq 0 ]] && EXISTING=true
763 set -o pipefail
764 if [[ $QALC_RET -eq 0 ]] && ! $EXISTING; then
765 set +o pipefail
766 RES_FILE="$RESULTS_DIR"/$(date -uIs).$(tr -Cd 'a-zA-Z0-9' </dev/random | head -c 10)
767 set -o pipefail
768 cat >"$RES_FILE" <<<"$QALC_RES"
769 fi
770 [[ "$QALC_RES" =~ .*\ =\ (.*) ]] && QALC_RES="''${BASH_REMATCH[1]}"
771 [[ $QALC_RET -eq 0 ]] && wl-copy "$QALC_RES"
772 notify-send "$QALC_RES"
773 '';
774 }));
775 "Mod+E".action = spawn (lib.getExe (pkgs.writeShellApplication {
776 name = "emoji-fuzzel";
777 runtimeInputs = with pkgs; [ config.programs.fuzzel.package wtype wl-clipboard-rs ];
778 text = ''
779 FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " <"$HOME"/.local/share/emoji-data/list.txt) || exit $?
780 [[ -n "$FUZZEL_RES" ]] || exit 1
781 wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste
782 '';
783 }));
784 "Print".action = screenshot;
785 "Control+Print".action = screenshot-window;
786 "Shift+Print".action = kdl.magic-leaf "screenshot-screen";
787 "Mod+B".action = with-select-window-action ".workspace_id == ($active_workspace | tonumber)" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
788 "Mod+Shift+B".action = with-select-window-action "true" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
789
790 "Mod+Escape" = {
791 allow-inhibiting = false;
792 action = toggle-keyboard-shortcuts-inhibit;
793 };
794
795 "Mod+H".action = focus-column-left;
796 "Mod+T".action = focus-window-down;
797 "Mod+N".action = focus-window-up;
798 "Mod+S".action = focus-column-right;
799
800 "Mod+Shift+H".action = move-column-left;
801 "Mod+Shift+T".action = move-window-down;
802 "Mod+Shift+N".action = move-window-up;
803 "Mod+Shift+S".action = move-column-right;
804
805 "Mod+Control+H".action = focus-monitor-left;
806 "Mod+Control+T".action = focus-monitor-down;
807 "Mod+Control+N".action = focus-monitor-up;
808 "Mod+Control+S".action = focus-monitor-right;
809
810 "Mod+Shift+Control+H".action = move-workspace-to-monitor-left;
811 "Mod+Shift+Control+T".action = move-workspace-to-monitor-down;
812 "Mod+Shift+Control+N".action = move-workspace-to-monitor-up;
813 "Mod+Shift+Control+S".action = move-workspace-to-monitor-right;
814
815 "Mod+G".action = focus-adjacent-workspace "down";
816 "Mod+C".action = focus-adjacent-workspace "up";
817
818 "Mod+Shift+G".action = move-column-to-adjacent-workspace "down";
819 "Mod+Shift+C".action = move-column-to-adjacent-workspace "up";
820
821 "Mod+Shift+Control+G".action = move-workspace-down;
822 "Mod+Shift+Control+C".action = move-workspace-up;
823
824 "Mod+ParenLeft".action = focus-workspace "comm";
825 "Mod+Shift+ParenLeft".action = kdl.magic-leaf "move-column-to-workspace" "comm";
826
827 "Mod+ParenRight".action = focus-workspace "web";
828 "Mod+Shift+ParenRight".action = kdl.magic-leaf "move-column-to-workspace" "web";
829
830 "Mod+BraceRight".action = focus-workspace "read";
831 "Mod+Shift+BraceRight".action = kdl.magic-leaf "move-column-to-workspace" "read";
832
833 "Mod+BraceLeft".action = focus-workspace "mon";
834 "Mod+Shift+BraceLeft".action = kdl.magic-leaf "move-column-to-workspace" "mon";
835
836 "Mod+Asterisk".action = focus-workspace "vid";
837 "Mod+Shift+Asterisk".action = kdl.magic-leaf "move-column-to-workspace" "vid";
838
839 "Mod+Plus".action = with-unnamed-workspace-action ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
840 "Mod+Shift+Plus".action = with-unnamed-workspace-action ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}}}}'';
841
842 "Mod+M".action = consume-or-expel-window-left;
843 "Mod+W".action = consume-or-expel-window-right;
844
845 "Mod+Shift+M".action = toggle-column-tabbed-display;
846
847 "Mod+R".action = switch-preset-column-width;
848 "Mod+Shift+R".action = maximize-column;
849 "Mod+Shift+Ctrl+R".action = switch-preset-window-height;
850 "Mod+F".action = center-column;
851 "Mod+Shift+F".action = toggle-windowed-fullscreen;
852 "Mod+Ctrl+Shift+F".action = fullscreen-window;
853
854 "Mod+V".action = switch-focus-between-floating-and-tiling;
855 "Mod+Shift+V".action = toggle-window-floating;
856
857 "Mod+Left".action = set-column-width "-10%";
858 "Mod+Down".action = set-window-height "-10%";
859 "Mod+Up".action = set-window-height "+10%";
860 "Mod+Right".action = set-column-width "+10%";
861
862 "Mod+Shift+Z" = {
863 action = spawn (lib.getExe niri) "msg" "action" "power-off-monitors";
864 allow-when-locked = true;
865 };
866 "Mod+Shift+L".action = spawn loginctl "lock-session";
867 "Mod+Shift+E".action = quit;
868 "Mod+Shift+Minus" = {
869 action = spawn systemctl "suspend";
870 allow-when-locked = true;
871 };
872 "Mod+Shift+Control+Minus" = {
873 action = spawn systemctl "hibernate";
874 allow-when-locked = true;
875 };
876 "Mod+Shift+P" = {
877 action = spawn (lib.getExe pkgs.playerctl) "-a" "pause";
878 allow-when-locked = true;
879 };
880
881 "XF86MonBrightnessUp" = {
882 action = spawn swayosd-client "--brightness" "raise";
883 allow-when-locked = true;
884 };
885 "XF86MonBrightnessDown" = {
886 action = spawn swayosd-client "--brightness" "lower";
887 allow-when-locked = true;
888 };
889 "XF86AudioRaiseVolume" = {
890 action = spawn swayosd-client "--output-volume" "raise";
891 allow-when-locked = true;
892 };
893 "XF86AudioLowerVolume" = {
894 action = spawn swayosd-client "--output-volume" "lower";
895 allow-when-locked = true;
896 };
897 "XF86AudioMute" = {
898 action = spawn swayosd-client "--output-volume" "mute-toggle";
899 allow-when-locked = true;
900 };
901 "XF86AudioMicMute" = {
902 action = spawn swayosd-client "--input-volume" "mute-toggle";
903 allow-when-locked = true;
904 };
905
906 "Mod+Semicolon".action = spawn makoctl "dismiss" "--group";
907 "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all";
908 "Mod+Period".action = spawn makoctl "menu" "--" (lib.getExe config.programs.fuzzel.package) "--dmenu";
909 "Mod+Comma".action = spawn makoctl "restore";
910
911 "Mod+Control+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}";
912 "Mod+Control+Shift+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"MoveColumnToWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}";
913
914 "Mod+X".action = set-dynamic-cast-window;
915 "Mod+Shift+X".action = set-dynamic-cast-monitor;
916 "Mod+Control+Shift+X".action = clear-dynamic-cast-target;
917 }))
918 (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)
919 ]
920 ))
921 ];
922 };
923}