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