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