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