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