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