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