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.nix988
1 files changed, 988 insertions, 0 deletions
diff --git a/accounts/gkleen@sif/niri.nix b/accounts/gkleen@sif/niri.nix
new file mode 100644
index 00000000..e3a08b52
--- /dev/null
+++ b/accounts/gkleen@sif/niri.nix
@@ -0,0 +1,988 @@
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 }))
518
519 (node "output" ["eDP-1"] [
520 (sleaf "scale" 1.5)
521 (sleaf "position" { x = 0; y = 0; })
522 ])
523 (node "output" ["Ancor Communications Inc ASUS PB287Q 0x0000DD9B"] [
524 (sleaf "scale" 1.5)
525 (sleaf "position" { x = 2560; y = 0; })
526 ])
527 (node "output" ["HP Inc. HP 727pu CN4417143K"] [
528 (sleaf "mode" "2560x1440@119.998")
529 (sleaf "scale" 1)
530 (sleaf "position" { x = 2560; y = 0; })
531 (flag "variable-refresh-rate")
532 ])
533
534 (plain "debug" [
535 (sleaf "render-drm-device" "/dev/dri/by-path/pci-0000:00:02.0-render")
536 ])
537
538 (plain "animations" [
539 (sleaf "slowdown" 0.5)
540 (plain "workspace-switch" [(flag "off")])
541 ])
542
543 (plain "layout" [
544 (sleaf "gaps" 8)
545 (plain "struts" [
546 (sleaf "left" 26)
547 (sleaf "right" 26)
548 (sleaf "top" 0)
549 (sleaf "bottom" 0)
550 ])
551 (plain "border" [
552 (sleaf "width" 2)
553 (sleaf "active-gradient" {
554 from = "hsla(195 100% 45% 1)";
555 to = "hsla(155 100% 37.5% 1)";
556 angle = 29;
557 relative-to = "workspace-view";
558 })
559 (sleaf "inactive-gradient" {
560 from = "hsla(0 0% 27.7% 1)";
561 to = "hsla(0 0% 23% 1)";
562 angle = 29;
563 relative-to = "workspace-view";
564 })
565 ])
566 (plain "focus-ring" [
567 (flag "off")
568 ])
569
570 (plain "preset-column-widths" (map (prop: sleaf "proportion" prop) [
571 (1. / 4.) (1. / 3.) (1. / 2.) (2. / 3.) (3. / 4.) (1.)
572 ]))
573 (plain "default-column-width" [ (sleaf "proportion" (1. / 2.)) ])
574 (plain "preset-window-heights" (map (prop: sleaf "proportion" prop) [
575 (1. / 3.) (1. / 2.) (2. / 3.) (1.)
576 ]))
577
578 (flag "always-center-single-column")
579
580 (plain "tab-indicator" [
581 (sleaf "gap" 4)
582 (sleaf "width" 8)
583 (sleaf "gaps-between-tabs" 4)
584 (flag "place-within-column")
585 (sleaf "length" { total-proportion = 1.; })
586 (sleaf "active-gradient" {
587 from = "hsla(195 100% 60% 0.75)";
588 to = "hsla(155 100% 50% 0.75)";
589 angle = 29;
590 relative-to = "workspace-view";
591 })
592 (sleaf "inactive-gradient" {
593 from = "hsla(0 0% 42% 0.66)";
594 to = "hsla(0 0% 35% 0.66)";
595 angle = 29;
596 relative-to = "workspace-view";
597 })
598 ])
599 ])
600
601 (plain "cursor" [
602 (flag "hide-when-typing")
603 ])
604
605 (map (name:
606 (node "workspace" [name] [
607 (sleaf "open-on-output" "eDP-1")
608 ])
609 ) (map ({name, ...}: name) cfg.scratchspaces))
610 (map (name:
611 (sleaf "workspace" name)
612 ) ["comm" "web" "vid" "bmr"])
613
614 (plain "window-rule" [
615 (sleaf "clip-to-geometry" true)
616 ])
617
618 (plain "window-rule" [
619 (sleaf "match" { is-floating = true; })
620 (sleaf "geometry-corner-radius" 8)
621 (plain "shadow" [ (flag "on") ])
622 ])
623
624 (plain "window-rule" [
625 (sleaf "match" { app-id = "^org\\.keepassxc\\.KeePassXC$"; })
626 (sleaf "block-out-from" "screencast")
627 ])
628 (plain "window-rule" (normalize-nodes [
629 (map (title:
630 (sleaf "match" { app-id = "^org\\.keepassxc\\.KeePassXC$"; inherit title; })
631 ) ["^Unlock Database.*" "^Access Request.*" ".*Passkey credentials$" "Browser Access Request$"])
632 (sleaf "open-focused" true)
633 (sleaf "open-floating" true)
634 ]))
635
636 (map ({ name, match, exclude, windowRuleExtra, ... }:
637 (optional-node (match != []) (plain "window-rule" (normalize-nodes [
638 (map (sleaf "match") match)
639 (map (sleaf "exclude") exclude)
640 (sleaf "open-on-workspace" name)
641 (sleaf "open-maximized" true)
642 windowRuleExtra
643 ])))
644 ) cfg.scratchspaces)
645
646 (plain "window-rule" [
647 (sleaf "match" { app-id = "^emacs$"; })
648 (sleaf "match" { app-id = "^firefox$"; })
649 (plain "default-column-width" [(sleaf "proportion" (2. / 3.))])
650 ])
651 (plain "window-rule" [
652 (sleaf "match" { app-id = "^kitty$"; })
653 (sleaf "match" { app-id = "^kitty-play$"; })
654 (plain "default-column-width" [(sleaf "proportion" (1. / 3.))])
655 ])
656
657 (plain "window-rule" [
658 (sleaf "match" { app-id = "^thunderbird$"; })
659 (sleaf "match" { app-id = "^Element$"; })
660 (sleaf "match" { app-id = "^chrome-web\.openrainbow\.com__-Default$"; })
661 (sleaf "open-on-workspace" "comm")
662 ])
663 (plain "window-rule" [
664 (sleaf "match" { app-id = "^firefox$"; })
665 (sleaf "open-on-workspace" "web")
666 (sleaf "open-maximized" true)
667 ])
668 (plain "window-rule" [
669 (sleaf "match" { app-id = "^mpv$"; })
670 (sleaf "open-on-workspace" "vid")
671 (plain "default-column-width" [(sleaf "proportion" 1.)])
672 ])
673 (plain "window-rule" [
674 (sleaf "match" { app-id = "^kitty-play$"; })
675 (sleaf "open-on-workspace" "vid")
676 (sleaf "open-focused" false)
677 ])
678 (plain "window-rule" [
679 (sleaf "match" { app-id = "^chrome-audiobookshelf\.yggdrasil\.li__-Default$"; })
680 (sleaf "match" { app-id = "^YouTube Music Desktop App$"; })
681 (sleaf "open-on-workspace" "vid")
682 ])
683 (plain "window-rule" [
684 (sleaf "match" { app-id = "^pdfpc$"; })
685 (plain "default-column-width" [(sleaf "proportion" 1.)])
686 ])
687 (plain "window-rule" [
688 (sleaf "match" { app-id = "^pdfpc$"; title = "^.*presentation.*$"; })
689 (plain "default-column-width" [(sleaf "proportion" 1.)])
690 (sleaf "open-fullscreen" true)
691 (sleaf "open-on-workspace" "bmr")
692 (sleaf "open-focused" false)
693 ])
694 (plain "window-rule" (normalize-nodes [
695 (map (sleaf "match") [
696 { app-id = "^Gimp-"; title = "^Quit GIMP$"; }
697 { app-id = "^org\\.kde\\.polkit-kde-authentication-agent-1$"; }
698 { app-id = "^xdg-desktop-portal-gtk$"; }
699 ])
700 (sleaf "open-floating" true)
701 ]))
702 (plain "window-rule" [
703 (sleaf "match" { app-id = "^org\\.pwmt\\.zathura$"; })
704 (sleaf "match" { app-id = "^evince$"; })
705 (sleaf "match" { app-id = "^org\\.gnome\\.Papers$"; })
706 (sleaf "default-column-display" "tabbed")
707 ])
708
709 (plain "layer-rule" [
710 (sleaf "match" { namespace = "^notifications$"; })
711 (sleaf "match" { namespace = "^bar$"; })
712 (sleaf "match" { namespace = "^launcher$"; })
713 (sleaf "block-out-from" "screencast")
714 ])
715
716 (plain "recent-windows" [
717 (flag "off")
718 ])
719
720 (plain "binds"
721 (let
722 bind = name: cfg: node name [(lib.removeAttrs cfg ["action"])] (lib.mapAttrsToList leaf (lib.removeAttrs cfg.action ["__functor"]));
723
724 move-column-to-workspace = kdl.magic-leaf "move-column-to-workspace";
725 screenshot = kdl.magic-leaf "screenshot";
726 screenshot-window = kdl.magic-leaf "screenshot-window";
727 screenshot-screen = kdl.magic-leaf "screenshot-screen";
728 in
729 normalize-nodes [
730 (lib.mapAttrsToList bind (with config.lib.niri.actions; {
731 "Mod+Slash".action = show-hotkey-overlay;
732
733 "Mod+Return".action = spawn terminal;
734 "Mod+Shift+Return".action =
735 let
736 nushellKitty = pkgs.symlinkJoin {
737 name = "nushell-kitty";
738 paths = [ config.programs.kitty.package ];
739 buildInputs = [ pkgs.makeWrapper ];
740 postBuild = ''
741 wrapProgram $out/bin/kitty \
742 --add-flags "--config ${pkgs.writeText "kitty.conf" ''
743 include $HOME/${config.xdg.configFile."kitty/kitty.conf".target}
744 shell ${lib.getExe config.programs.nushell.package}
745 ''}"
746 '';
747 };
748 in spawn (lib.getExe' nushellKitty "kitty");
749 "Mod+Q".action = close-window;
750 "Mod+O".action = spawn (lib.getExe config.programs.fuzzel.package);
751 "Mod+Shift+O".action = spawn (lib.getExe config.programs.fuzzel.package) "--list-executables-in-path";
752
753 "Mod+Alt+E".action = spawn (lib.getExe' config.services.emacs.package "emacsclient") "-c";
754 "Mod+Alt+Y".action = spawn (lib.getExe (pkgs.writeShellApplication {
755 name = "queue-yt-dlp";
756 runtimeInputs = with pkgs; [ wl-clipboard-rs socat ];
757 text = ''
758 socat STDIO UNIX-CONNECT:"$XDG_RUNTIME_DIR"/yt-dlp.sock <<<$'{ "urls": ["'"$(wl-paste)"$'"] }'
759 '';
760 }));
761 "Mod+Alt+L".action = spawn (lib.getExe (pkgs.writeShellApplication {
762 name = "queue-yt-dlp";
763 runtimeInputs = with pkgs; [ wl-clipboard-rs config.programs.kitty.package ];
764 text = ''
765 exec -- kitty --app-id kitty-play --directory "$HOME"/media mpv "$(wl-paste)"
766 '';
767 }));
768 "Mod+Alt+M".action = spawn (lib.getExe' pkgs.screen-message "sm") "-n" "Fira Mono" "-a" "1" "-f" "#fff" "-b" "#000";
769
770 "Mod+U".action = spawn (lib.getExe (pkgs.writeShellApplication {
771 name = "qalc-fuzzel";
772 runtimeInputs = with pkgs; [ wl-clipboard-rs libqalculate config.programs.fuzzel.package coreutils findutils libnotify gnugrep ];
773 text = ''
774 RESULTS_DIR="$HOME/.cache/qalc-fuzzel"
775 prev() {
776 FOUND=false
777 while IFS= read -r line; do
778 [[ -n "$line" ]] || continue
779 FOUND=true
780 echo "$line"
781 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)
782 $FOUND || echo
783 }
784 FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> " --width=60) || exit $?
785 if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then
786 QALC_RES="$FUZZEL_RES"
787 QALC_RET=0
788 else
789 QALC_RES=$(qalc -set "autocalc off" "$FUZZEL_RES" 2>&1)
790 QALC_RET=$?
791 fi
792 [[ -n "$QALC_RES" ]] || exit 1
793 EXISTING=false
794 set +o pipefail
795 grep -Fxrl "$QALC_RES" "$RESULTS_DIR" | xargs -r touch
796 [[ ''${PIPESTATUS[0]} -eq 0 ]] && EXISTING=true
797 set -o pipefail
798 if [[ $QALC_RET -eq 0 ]] && ! $EXISTING; then
799 set +o pipefail
800 RES_FILE="$RESULTS_DIR"/$(date -uIs).$(tr -Cd 'a-zA-Z0-9' </dev/random | head -c 10)
801 set -o pipefail
802 cat >"$RES_FILE" <<<"$QALC_RES"
803 fi
804 [[ "$QALC_RES" =~ .*\ =\ (.*) ]] && QALC_RES="''${BASH_REMATCH[1]}"
805 [[ $QALC_RET -eq 0 ]] && wl-copy "$QALC_RES"
806 notify-send "$QALC_RES"
807 '';
808 }));
809 "Mod+Shift+U".action =
810 let
811 qalcKitty = pkgs.symlinkJoin {
812 name = "qalc-kitty";
813 paths = [ config.programs.kitty.package ];
814 buildInputs = [ pkgs.makeWrapper ];
815 postBuild = ''
816 wrapProgram $out/bin/kitty \
817 --add-flags "--config ${pkgs.writeText "kitty.conf" ''
818 include $HOME/${config.xdg.configFile."kitty/kitty.conf".target}
819 shell ${lib.getExe pkgs.libqalculate}
820 ''}"
821 '';
822 };
823 in spawn (lib.getExe' qalcKitty "kitty");
824 "Mod+E".action = spawn (lib.getExe (pkgs.writeShellApplication {
825 name = "emoji-fuzzel";
826 runtimeInputs = with pkgs; [ config.programs.fuzzel.package wtype wl-clipboard-rs ];
827 text = ''
828 FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " --cache "$HOME"/.cache/fuzzel-emoji --width=60 <"$HOME"/.local/share/emoji-data/list.txt) || exit $?
829 [[ -n "$FUZZEL_RES" ]] || exit 1
830 wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste
831 '';
832 }));
833 "Print".action = screenshot;
834 "Control+Print".action = screenshot-window;
835 "Shift+Print".action = screenshot-screen;
836 "Mod+B".action = with-select-window-action ".workspace_id == ($active_workspace | tonumber)" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
837 "Mod+Shift+B".action = with-select-window-action "true" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
838
839 "Mod+Escape" = {
840 allow-inhibiting = false;
841 action = toggle-keyboard-shortcuts-inhibit;
842 };
843
844 "Mod+H".action = focus-column-left;
845 "Mod+T".action = focus-window-down;
846 "Mod+N".action = focus-window-up;
847 "Mod+S".action = focus-column-right;
848
849 "Mod+Shift+H".action = move-column-left;
850 "Mod+Shift+T".action = move-window-down;
851 "Mod+Shift+N".action = move-window-up;
852 "Mod+Shift+S".action = move-column-right;
853
854 "Mod+Control+H".action = focus-monitor-left;
855 "Mod+Control+T".action = focus-monitor-down;
856 "Mod+Control+N".action = focus-monitor-up;
857 "Mod+Control+S".action = focus-monitor-right;
858
859 "Mod+Shift+Control+H".action = move-workspace-to-monitor-left;
860 "Mod+Shift+Control+T".action = move-workspace-to-monitor-down;
861 "Mod+Shift+Control+N".action = move-workspace-to-monitor-up;
862 "Mod+Shift+Control+S".action = move-workspace-to-monitor-right;
863
864 "Mod+G".action = focus-adjacent-workspace "down";
865 "Mod+C".action = focus-adjacent-workspace "up";
866
867 "Mod+Shift+G".action = move-column-to-adjacent-workspace "down";
868 "Mod+Shift+C".action = move-column-to-adjacent-workspace "up";
869
870 "Mod+Shift+Control+G".action = move-workspace-down;
871 "Mod+Shift+Control+C".action = move-workspace-up;
872
873 "Mod+ParenLeft".action = focus-workspace "comm";
874 "Mod+Shift+ParenLeft".action = move-column-to-workspace "comm";
875
876 "Mod+ParenRight".action = focus-workspace "web";
877 "Mod+Shift+ParenRight".action = move-column-to-workspace "web";
878
879 "Mod+BraceRight".action = focus-workspace "read";
880 "Mod+Shift+BraceRight".action = move-column-to-workspace "read";
881
882 "Mod+BraceLeft".action = focus-workspace "mon";
883 "Mod+Shift+BraceLeft".action = move-column-to-workspace "mon";
884
885 "Mod+Asterisk".action = focus-workspace "vid";
886 "Mod+Shift+Asterisk".action = move-column-to-workspace "vid";
887
888 "Mod+Plus".action = with-unnamed-workspace-action ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
889 "Mod+Shift+Plus".action = with-unnamed-workspace-action ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}, "focus": true}}}'';
890
891 "Mod+M".action = consume-or-expel-window-left;
892 "Mod+W".action = consume-or-expel-window-right;
893
894 "Mod+Shift+M".action = toggle-column-tabbed-display;
895
896 "Mod+L".action = maximize-window-to-edges;
897 "Mod+R".action = switch-preset-column-width;
898 "Mod+Shift+R".action = maximize-column;
899 "Mod+Shift+Ctrl+R".action = switch-preset-window-height;
900 "Mod+F".action = center-column;
901 "Mod+Shift+F".action = toggle-windowed-fullscreen;
902 "Mod+Ctrl+Shift+F".action = fullscreen-window;
903
904 "Mod+V".action = switch-focus-between-floating-and-tiling;
905 "Mod+Shift+V".action = toggle-window-floating;
906
907 "Mod+Left".action = set-column-width "-10%";
908 "Mod+Down".action = set-window-height "-10%";
909 "Mod+Up".action = set-window-height "+10%";
910 "Mod+Right".action = set-column-width "+10%";
911
912 "Mod+Shift+Z" = {
913 action = power-off-monitors;
914 allow-when-locked = true;
915 };
916 "Mod+Shift+E".action = quit;
917
918 # "Mod+Semicolon".action = spawn makoctl "dismiss" "--group";
919 # "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all";
920 # "Mod+Period".action = spawn makoctl "menu" "--" (lib.getExe config.programs.fuzzel.package) "--dmenu";
921 # "Mod+Comma".action = spawn makoctl "restore";
922
923 "Mod+Control+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}";
924 "Mod+Control+Shift+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"MoveColumnToWorkspace\":{\"reference\":{\"Id\": $workspace_id}, \"focus\": true}}}";
925
926 "Mod+X".action = set-dynamic-cast-window;
927 "Mod+Shift+X".action = set-dynamic-cast-monitor;
928 "Mod+Control+Shift+X".action = clear-dynamic-cast-target;
929
930 "Mod+D".action = with-urgent-window-action "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
931 "Mod+Shift+D".action = with-focused-window-action "{\"Action\":{\"UnsetUrgent\":{\"id\": .id}}}";
932
933 "Mod+K".action = spawn (lib.getExe' pkgs.worktime "worktime-ui");
934 "Mod+Shift+K".action = spawn (lib.getExe' pkgs.worktime "worktime-stop");
935 }))
936 (lib.mapAttrsToList (name: cfg: node name [(lib.removeAttrs cfg ["action"])] [cfg.action]) (let
937 shell = obj: leaf "send-unix" [
938 { path = ''''${XDG_RUNTIME_DIR}/shell.sock''; }
939 (builtins.toJSON obj + "\n")
940 ];
941 in {
942 "XF86AudioRaiseVolume" = {
943 allow-when-locked = true;
944 action = shell { Volume.volume = "up"; };
945 };
946 "XF86AudioLowerVolume" = {
947 allow-when-locked = true;
948 action = shell { Volume.volume = "down"; };
949 };
950 "XF86AudioMute" = {
951 allow-when-locked = true;
952 action = shell { Volume.muted = "toggle"; };
953 };
954 "XF86AudioMicMute" = {
955 allow-when-locked = true;
956 action = shell { Volume."mic-muted" = "toggle"; };
957 };
958 "XF86MonBrightnessUp" = {
959 action = shell { Brightness = "up"; };
960 allow-when-locked = true;
961 };
962 "XF86MonBrightnessDown" = {
963 action = shell { Brightness = "down"; };
964 allow-when-locked = true;
965 };
966 "Mod+Shift+L".action = shell { LockSession = {}; };
967 "Mod+Shift+Minus" = {
968 action = shell { Suspend = {}; };
969 allow-when-locked = true;
970 };
971 "Mod+Shift+Control+Minus" = {
972 action = shell { Hibernate = {}; };
973 allow-when-locked = true;
974 };
975 "Mod+Shift+P" = {
976 action = shell { Mpris = { PauseAll = {}; }; };
977 allow-when-locked = true;
978 };
979 "Mod+Semicolon".action = shell { Notifications = { DismissGroup = {}; }; };
980 "Mod+Shift+Semicolon".action = shell { Notifications = { DismissAll = {}; }; };
981 }))
982 (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)
983 (map ({ name, moveKey, ...}: if moveKey != null then bind moveKey { action = move-column-to-workspace name; } else null) cfg.scratchspaces)
984 ]
985 ))
986 ];
987 };
988}