summaryrefslogtreecommitdiff
path: root/accounts/gkleen@sif/niri
diff options
context:
space:
mode:
Diffstat (limited to 'accounts/gkleen@sif/niri')
-rw-r--r--accounts/gkleen@sif/niri/default.nix1051
-rw-r--r--accounts/gkleen@sif/niri/mako.nix112
-rw-r--r--accounts/gkleen@sif/niri/waybar.nix288
3 files changed, 1057 insertions, 394 deletions
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
index 6a8d10a0..5ae372c1 100644
--- a/accounts/gkleen@sif/niri/default.nix
+++ b/accounts/gkleen@sif/niri/default.nix
@@ -1,139 +1,978 @@
1{ config, 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"; 10
6 volumectl = lib.getExe' config.services.avizo.package "volumectl"; 11 focus_or_spawn = pkgs.writeShellApplication {
7 dunstctl = lib.getExe' config.services.dunst.package "dunstctl"; 12 name = "focus-or-spawn";
13 runtimeInputs = [ niri pkgs.gojq pkgs.gnugrep pkgs.socat ];
14 text = ''
15 window_select="$1"
16 shift
17 workspace_name="$1"
18 shift
19
20 workspaces_json="$(niri msg -j workspaces)"
21 workspace_output="$(jq -r --arg workspace_name "$workspace_name" '.[] | select(.name == $workspace_name) | .output' <<<"$workspaces_json")"
22 # active_workspace="$(jq -r --arg workspace_output "$workspace_output" '.[] | select(.output == $workspace_output and .is_active) | .id' <<<"$workspaces_json")"
23 active_output="$(jq -r '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
24 if [[ $workspace_output != "$active_output" ]]; then
25 niri msg action move-workspace-to-monitor --reference "$workspace_name" "$active_output"
26 # socat STDIO "$NIRI_SOCKET" <<<'{"Action":{"FocusWorkspace":{"reference":{"Id":'"''${active_workspace}"'}}}}'
27 # niri msg action move-workspace-to-index --reference "$workspace_name" 1
28 fi
29
30 while IFS=$'\n' read -r window_json; do
31 if [[ -n $(jq -c "$window_select" <<<"$window_json") ]]; then
32 if jq -e '.is_focused' <<<"$window_json" >/dev/null; then
33 niri msg action focus-workspace-previous
34 else
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
40 fi
41 exit 0
42 fi
43 done < <(niri msg -j windows | jq -c '.[]')
44
45 exec "$@"
46 '';
47 };
48 focus-or-spawn-action = config.lib.niri.actions.spawn (lib.getExe focus_or_spawn);
49
50 with_adjacent_workspace = pkgs.writeShellApplication {
51 name = "with-adjacent-workspace";
52 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
53 text = ''
54 blacklist="$1"
55 shift
56 direction="$1"
57 shift
58 action="$1"
59 shift
60
61 workspaces_json="$(niri msg -j workspaces)"
62 active_workspace="$(jq -r '.[] | select(.is_focused) | .id' <<<"$workspaces_json")"
63 workspace_output="$(jq -r --arg active_workspace "$active_workspace" '.[] | select(.id == ($active_workspace | tonumber)) | .output' <<<"$workspaces_json")"
64 workspace_idx="$(jq -r '.[] | select(.is_focused) | .idx' <<<"$workspaces_json")"
65
66 jq_script='map(select('
67 case "$direction" in
68 down)
69 # shellcheck disable=SC2016
70 jq_script=''${jq_script}'.idx > ($workspace_idx | tonumber)';;
71 up)
72 # shellcheck disable=SC2016
73 jq_script=''${jq_script}'.idx < ($workspace_idx | tonumber)';;
74 esac
75 # shellcheck disable=SC2016
76 jq_script=''${jq_script}' and .output == $workspace_output and ((.name == null) or (.name | test($blacklist) | not)))) | sort_by(.idx)'
77 [[ $direction == "up" ]] && jq_script=''${jq_script}' | reverse'
78 jq_script=''${jq_script}' | .[0]'
79
80 workspace_json=$(jq -c --arg blacklist "$blacklist" --arg workspace_output "$workspace_output" --arg workspace_idx "$workspace_idx" "$jq_script" <<<"$workspaces_json")
81 [[ -n $workspace_json && $workspace_json != null ]] || exit 0
82 jq --arg active_workspace "$active_workspace" -c "$action" <<<"$workspace_json" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
83 '';
84 };
85 with-adjacent-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_adjacent_workspace) "^${lib.concatMapStringsSep "|" ({ name, ...}: name) cfg.scratchspaces}$";
86 focus-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
87 move-column-to-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}, "focus": true}}}'';
88
89 with_unnamed_workspace = pkgs.writeShellApplication {
90 name = "with-unnamed-workspace";
91 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
92 text = ''
93 action="$1"
94 shift
95
96 workspaces_json="$(niri msg -j workspaces)"
97 active_output="$(jq -r '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
98 active_workspace="$(jq -r '.[] | select(.is_focused) | .id' <<<"$workspaces_json")"
99
100 history_json="$(socat STDIO UNIX-CONNECT:"$XDG_RUNTIME_DIR"/niri-workspace-history.sock)"
101 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")"
102 [[ -n $workspace_json && $workspace_json != null ]] || exit 0
103 jq --arg active_workspace "$active_workspace" -c "$action" <<<"$workspace_json" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
104 '';
105 };
106 with-unnamed-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_unnamed_workspace);
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
123 with_select_window = pkgs.writeShellApplication {
124 name = "with-select-window";
125 runtimeInputs = [ niri pkgs.gojq pkgs.socat config.programs.fuzzel.package pkgs.gawk ];
126 text = ''
127 window_select="$1"
128 shift
129 action="$1"
130 shift
131
132 windows_json="$(niri msg -j windows)"
133 active_workspace="$(jq -r '.[] | select(.is_focused) | .workspace_id' <<<"$windows_json")"
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)"
135 # shellcheck disable=SC2016
136 window_json="$(gojq -rc --arg active_workspace "$active_workspace" --arg window_ix "$window_ix" 'map(select('"$window_select"')) | .[($window_ix | tonumber)]' <<<"$windows_json")"
137
138 [[ -z "$window_json" ]] && exit 1
139
140 jq -c "$action" <<<"$window_json" | socat STDIO "$NIRI_SOCKET"
141 '';
142 };
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"));
8in { 163in {
9 imports = [ 164 options = {
10 ./waybar.nix 165 programs.niri.scratchspaces = lib.mkOption {
11 ]; 166 type = lib.types.listOf (lib.types.submodule ({ config, ... }: {
167 options = {
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 };
211
212 config = lib.mkMerge [
213 (lib.mkIf (config.app-id != null) {
214 match = lib.mkDefault [ { app-id = "^${lib.escapeRegex config.app-id}$"; } ];
215 selector = lib.mkDefault "select(.app_id == \"${config.app-id}\")";
216 })
217 ];
218 }));
219 default = [];
220 };
221 };
12 222
13 config = { 223 config = {
14 systemd.user.services.xwayland-satellite = { 224 home.packages = [ pkgs.xwayland-satellite-unstable ];
225
226 systemd.user.sockets.niri-workspace-history = {
227 Socket = {
228 ListenStream = "%t/niri-workspace-history.sock";
229 SocketMode = "0600";
230 };
231 };
232 systemd.user.services.niri-workspace-history = {
15 Unit = { 233 Unit = {
16 BindsTo = [ "graphical-session.target" ]; 234 BindsTo = [ "niri.service" ];
17 PartOf = [ "graphical-session.target" ]; 235 After = [ "niri.service" ];
18 After = [ "graphical-session.target" ]; 236 };
19 Requisite = [ "graphical-session.target" ]; 237 Install = {
238 WantedBy = [ "niri.service" ];
20 }; 239 };
21 Service = { 240 Service = {
22 Type = "notify"; 241 Type = "simple";
23 NotifyAccess = "all"; 242 Sockets = [ "niri-workspace-history.socket" ];
24 ExecStart = lib.getExe pkgs.xwayland-satellite-unstable; 243 ExecStart = pkgs.writers.writePython3 "niri-workspace-history" { flakeIgnore = ["E501"]; } ''
25 StandardOutput = "journal"; 244 import os
245 import socket
246 import json
247 # import sys
248 from collections import defaultdict
249 from threading import Thread, Lock
250 from socketserver import StreamRequestHandler, ThreadingTCPServer
251 from contextlib import contextmanager
252 from io import TextIOWrapper
253
254
255 @contextmanager
256 def detaching(thing):
257 try:
258 yield thing
259 finally:
260 thing.detach()
261
262
263 workspace_history = defaultdict(list)
264 history_lock = Lock()
265
266
267 def monitor_niri():
268 workspaces = list()
269
270 def focus_workspace(output, workspace):
271 with history_lock:
272 workspace_history[output] = [workspace] + [ws for ws in workspace_history[output] if ws != workspace]
273 # print(json.dumps(workspace_history), file=sys.stderr)
274
275 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
276 sock.connect(os.environ["NIRI_SOCKET"])
277 sock.send(b"\"EventStream\"\n")
278 for line in sock.makefile(buffering=1, encoding='utf-8'):
279 if line_json := json.loads(line):
280 if "WorkspacesChanged" in line_json:
281 workspaces = line_json["WorkspacesChanged"]["workspaces"]
282 for ws in workspaces:
283 if ws["is_focused"]:
284 focus_workspace(ws["output"], ws["id"])
285 if "WorkspaceActivated" in line_json:
286 for ws in workspaces:
287 if ws["id"] != line_json["WorkspaceActivated"]["id"]:
288 continue
289 focus_workspace(ws["output"], ws["id"])
290 break
291
292
293 class RequestHandler(StreamRequestHandler):
294 def handle(self):
295 with detaching(TextIOWrapper(self.wfile, encoding='utf-8', write_through=True)) as out:
296 with history_lock:
297 json.dump(workspace_history, out)
298
299
300 class Server(ThreadingTCPServer):
301 def __init__(self):
302 ThreadingTCPServer.__init__(self, ("", 8000), RequestHandler, bind_and_activate=False)
303 self.socket = socket.fromfd(3, self.address_family, self.socket_type)
304
305
306 def run_server():
307 with Server() as server:
308 server.serve_forever()
309
310
311 niri = Thread(target=monitor_niri)
312 niri.daemon = True
313 niri.start()
314
315 server_thread = Thread(target=run_server)
316 server_thread.daemon = True
317 server_thread.start()
318
319 while True:
320 server_thread.join(timeout=0.5)
321 niri.join(timeout=0.5)
322
323 if not (niri.is_alive() and server_thread.is_alive()):
324 break
325 '';
326 };
327 };
328 systemd.user.services.niri-workspace-sort = {
329 Unit = {
330 BindsTo = [ "niri.service" ];
331 After = [ "niri.service" ];
26 }; 332 };
27 Install = { 333 Install = {
28 WantedBy = [ "graphical-session.target" ]; 334 WantedBy = [ "niri.service" ];
29 }; 335 };
30 }; 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
31 343
32 programs.niri.settings = { 344 outputs = None
33 prefer-no-csd = true; 345 only = {'HDMI-A-1': {'bmr'}, 'eDP-1': {'vid'}}
34 screenshot-path = "${config.home.homeDirectory}/screenshots"; 346
347
348 class Niri(socket.socket):
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')
353
354 def cmd(self, obj):
355 print(json.dumps(obj, separators=(',', ':')), flush=True, file=self.fh)
356
357 def event_stream(self):
358 self.cmd("EventStream")
359 return self.fh
35 360
36 input = {
37 keyboard.xkb = {
38 layout = "us,";
39 variant = "dvp,";
40 options = "compose:caps,grp:win_space_toggle";
41 };
42 };
43 361
44 environment = { 362 with Niri() as niri, Niri().event_stream() as niri_stream:
45 NIXOS_OZONE_WL = "1"; 363 for line in niri_stream:
46 QT_QPA_PLATFORM = "wayland"; 364 workspaces = None
47 GDK_BACKEND = "wayland"; 365 if line_json := json.loads(line):
48 SDL_VIDEODRIVER = "wayland"; 366 if "WorkspacesChanged" in line_json:
367 workspaces = line_json["WorkspacesChanged"]["workspaces"]
368
369 if workspaces is None:
370 continue
371
372 old_outputs = outputs
373 outputs = {ws["output"] for ws in workspaces}
374 if old_outputs is None:
375 print("Initial outputs: {}".format(outputs), file=sys.stderr)
376 continue
377
378 new_outputs = outputs - old_outputs
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;
49 }; 399 };
400 };
50 401
51 cursor.hide-when-typing = true; 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")
52 469
53 binds = with config.lib.niri.actions; { 470 (sleaf "screenshot-path" "~/screenshots/%Y-%m-%dT%H:%M:%S.png")
54 "Mod+Return".action = spawn terminal;
55 "Mod+Q".action = close-window;
56 "Mod+D".action = spawn (lib.getExe config.programs.fuzzel.package);
57 "Mod+Shift+D".action = spawn (lib.getExe config.programs.fuzzel.package) "--list-executables-in-path";
58 471
59 "Mod+H".action = focus-column-left; 472 (plain "hotkey-overlay" [
60 "Mod+T".action = focus-window-down; 473 (flag "skip-at-startup")
61 "Mod+N".action = focus-window-up; 474 ])
62 "Mod+S".action = focus-column-right;
63 475
64 "Mod+Shift+H".action = move-column-left; 476 (plain "input" [
65 "Mod+Shift+T".action = move-window-down; 477 (plain "keyboard" [
66 "Mod+Shift+N".action = move-window-up; 478 (sleaf "repeat-delay" 300)
67 "Mod+Shift+S".action = move-column-right; 479 (sleaf "repeat-rate" 50)
68 480
69 "Mod+Control+H".action = focus-monitor-left; 481 (plain "xkb" [
70 "Mod+Control+T".action = focus-monitor-down; 482 (sleaf "layout" "us,us")
71 "Mod+Control+N".action = focus-monitor-up; 483 (sleaf "variant" "dvp,")
72 "Mod+Control+S".action = focus-monitor-right; 484 (sleaf "options" "compose:caps,grp:win_space_toggle")
485 ])
486 ])
73 487
74 "Mod+Shift+Control+H".action = move-workspace-to-monitor-left; 488 (flag "workspace-auto-back-and-forth")
75 "Mod+Shift+Control+T".action = move-workspace-to-monitor-down; 489 # (sleaf "focus-follows-mouse" {})
76 "Mod+Shift+Control+N".action = move-workspace-to-monitor-up; 490 # (flag "warp-mouse-to-focus")
77 "Mod+Shift+Control+S".action = move-workspace-to-monitor-right;
78 491
79 "Mod+G".action = focus-workspace-down; 492 # (plain "touchpad" [ (flag "off") ])
80 "Mod+C".action = focus-workspace-up; 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 ])
81 501
82 "Mod+Control+G".action = move-column-to-workspace-down; 502 (plain "gestures" [
83 "Mod+Control+C".action = move-column-to-workspace-up; 503 (plain "hot-corners" [(flag "off")])
504 ])
84 505
85 "Mod+Shift+G".action = move-workspace-down; 506 (plain "environment" (lib.mapAttrsToList sleaf {
86 "Mod+Shift+C".action = move-workspace-up; 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 }))
87 518
88 "Mod+M".action = consume-window-into-column; 519 (node "output" ["eDP-1"] [
89 "Mod+W".action = expel-window-from-column; 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 ])
90 533
91 "Mod+F".action = maximize-column; 534 (plain "debug" [
92 "Mod+Shift+F".action = fullscreen-window; 535 (sleaf "render-drm-device" "/dev/dri/by-path/pci-0000:00:02.0-render")
536 ])
93 537
94 "Mod+Space".action = switch-focus-between-floating-and-tiling; 538 (plain "animations" [
95 "Mod+Shift+Space".action = toggle-window-floating; 539 (sleaf "slowdown" 0.5)
540 (plain "workspace-switch" [(flag "off")])
541 ])
96 542
97 "Mod+Left".action = set-column-width "-10%"; 543 (plain "layout" [
98 "Mod+Down".action = set-window-height "-10%"; 544 (sleaf "gaps" 8)
99 "Mod+Up".action = set-window-height "+10%"; 545 (plain "struts" [
100 "Mod+Right".action = set-column-width "+10%"; 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 ])
101 569
102 "Mod+Shift+Z" = { 570 (plain "preset-column-widths" (map (prop: sleaf "proportion" prop) [
103 action = spawn (lib.getExe niri) "msg" "action" "power-off-monitors"; 571 (1. / 4.) (1. / 3.) (1. / 2.) (2. / 3.) (3. / 4.) (1.)
104 allow-when-locked = true; 572 ]))
105 }; 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 ]))
106 577
107 "XF86MonBrightnessUp" = { 578 (flag "always-center-single-column")
108 action = spawn lightctl "-d" "-e4" "-n1" "up";
109 allow-when-locked = true;
110 };
111 "XF86MonBrightnessDown" = {
112 action = spawn lightctl "-d" "-e4" "-n1" "down";
113 allow-when-locked = true;
114 };
115 "XF86AudioRaiseVolume" = {
116 action = spawn volumectl "-d" "-u" "up";
117 allow-when-locked = true;
118 };
119 "XF86AudioLowerVolume" = {
120 action = spawn volumectl "-d" "-u" "down";
121 allow-when-locked = true;
122 };
123 "XF86AudioMute" = {
124 action = spawn volumectl "-d" "toggle-mute";
125 allow-when-locked = true;
126 };
127 "XF86AudioMicMute" = {
128 action = spawn volumectl "-d" "-m" "toggle-mute";
129 allow-when-locked = true;
130 };
131 579
132 "Mod+Semicolon".action = spawn dunstctl "close"; 580 (plain "tab-indicator" [
133 "Mod+Shift+Semicolon".action = spawn dunstctl "close-all"; 581 (sleaf "gap" 4)
134 "Mod+Period".action = spawn dunstctl "context"; 582 (sleaf "width" 8)
135 "Mod+Comma".action = spawn dunstctl "history-pop"; 583 (sleaf "gaps-between-tabs" 4)
136 }; 584 (flag "place-within-column")
137 }; 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 ];
138 }; 977 };
139} 978}
diff --git a/accounts/gkleen@sif/niri/mako.nix b/accounts/gkleen@sif/niri/mako.nix
new file mode 100644
index 00000000..3d246d96
--- /dev/null
+++ b/accounts/gkleen@sif/niri/mako.nix
@@ -0,0 +1,112 @@
1{ config, lib, pkgs, ... }:
2{
3 config = lib.mkIf false {
4 services.mako = {
5 enable = true;
6 settings = {
7 font = "Fira Sans 10";
8 format = "<i>%s</i>\\n%b";
9 margin = "2";
10 max-visible = -1;
11 background-color = "#000000dd";
12 progress-color = "source #223544ff";
13 width = 384;
14 outer-margin = 1;
15 max-history = 100;
16 max-icon-size = 48;
17
18 grouped.format = "<b>(%g)</b> <i>%s</i>\\n%b";
19 "urgency=low".text-color = "#999999ff";
20 "urgency=critical".background-color = "#900000dd";
21 "app-name=Element".group-by = "summary";
22 "app-name=poweralertd" = {
23 history = false;
24 ignore-timeout = true;
25 default-timeout = 2000;
26 };
27 "app-name=worktime".history = false;
28 "mode=silent".invisible = true;
29 };
30 package = pkgs.symlinkJoin {
31 name = "${pkgs.mako.name}-wrapped";
32 paths = with pkgs; [ mako ];
33 inherit (pkgs.mako) meta;
34 postBuild = ''
35 rm -r $out/share/dbus-1
36 '';
37 };
38 };
39 systemd.user.services.mako = {
40 Unit = {
41 Description = "Mako notification daemon";
42 PartOf = [ "graphical-session.target" ];
43 };
44 Install = {
45 WantedBy = [ "graphical-session.target" ];
46 };
47 Service = {
48 Type = "dbus";
49 BusName = "org.freedesktop.Notifications";
50 ExecStart = lib.getExe config.services.mako.package;
51 RestartSec = 5;
52 Restart = "always";
53 };
54 };
55
56 systemd.user.services.mako-follows-focus = {
57 Unit = {
58 BindsTo = [ "niri.service" "mako.service" ];
59 After = [ "niri.service" "mako.service" ];
60 };
61 Service = {
62 Type = "simple";
63 Restart = "always";
64 ExecStart = pkgs.writers.writePython3 "mako-follows-focus" {
65 libraries = with pkgs.python3Packages; [];
66 } ''
67 import os
68 import socket
69 import json
70 import subprocess
71
72
73 current_output = None
74 workspaces = []
75
76
77 def output_changed(new_output):
78 global current_output
79
80 if current_output == new_output:
81 return
82
83 current_output = new_output
84 subprocess.run(["makoctl", "reload"])
85
86
87 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
88 sock.connect(os.environ["NIRI_SOCKET"])
89 sock.send(b"\"EventStream\"\n")
90 for line in sock.makefile(buffering=1, encoding='utf-8'):
91 if line_json := json.loads(line):
92 if "WorkspacesChanged" in line_json:
93 workspaces = line_json["WorkspacesChanged"]["workspaces"]
94 for workspace in workspaces:
95 if not workspace["is_focused"]:
96 continue
97 output_changed(workspace["output"])
98 break
99 if "WorkspaceActivated" in line_json and line_json["WorkspaceActivated"]["focused"]: # noqa: E501
100 for workspace in workspaces:
101 if not workspace["id"] == line_json["WorkspaceActivated"]["id"]: # noqa: E501
102 continue
103 output_changed(workspace["output"])
104 break
105 '';
106 };
107 Install = {
108 WantedBy = [ "mako.service" ];
109 };
110 };
111 };
112}
diff --git a/accounts/gkleen@sif/niri/waybar.nix b/accounts/gkleen@sif/niri/waybar.nix
deleted file mode 100644
index 1a25b581..00000000
--- a/accounts/gkleen@sif/niri/waybar.nix
+++ /dev/null
@@ -1,288 +0,0 @@
1{ lib, pkgs, ... }:
2{
3 config = {
4 programs.waybar = {
5 enable = true;
6 systemd = {
7 enable = true;
8 target = "graphical-session.target";
9 };
10 settings = let
11 windowRewrites = {
12 "(.*) — Mozilla Firefox" = "$1";
13 "(.*) - Mozilla Thunderbird" = "$1";
14 "(.*) - mpv" = "$1";
15 };
16 iconSize = 11;
17 in [
18 {
19 layer = "top";
20 position = "top";
21 height = 14;
22 output = [ "eDP-1" "DP-2" "DP-3" ];
23 modules-left = [ "niri/workspaces" ];
24 modules-center = [ "niri/window" ];
25 modules-right = [ # "custom/worktime" "custom/worktime-today"
26 "custom/weather"
27 # "custom/keymap"
28 "privacy" "tray" "wireplumber" "backlight" "battery" "idle_inhibitor" "clock" ];
29
30 "custom/weather" = {
31 format = "{}";
32 tooltip = true;
33 interval = 3600;
34 exec = "${lib.getExe pkgs.wttrbar} --hide-conditions --nerd --custom-indicator \"<span font=\\\"Symbols Nerd Font Mono\\\" size=\\\"120%\\\">{ICON}</span> {FeelsLikeC}°\"";
35 return-type = "json";
36 };
37 "custom/keymap" = {
38 format = "{}";
39 tooltip = true;
40 return-type = "json";
41 exec = pkgs.writers.writePython3 "keymap" {} ''
42 import os
43 import socket
44 import re
45 import subprocess
46 import json
47
48
49 def output(keymap):
50 short = keymap
51 if keymap == "English (programmer Dvorak)":
52 short = "dvp"
53 elif keymap == "English (US)":
54 short = "<span color=\"#ffffff\">us</span>"
55 print(json.dumps({'text': short, 'tooltip': keymap}, separators=(',', ':')), flush=True) # noqa: E501
56
57
58 r = subprocess.run(["hyprctl", "devices", "-j"], check=True, stdout=subprocess.PIPE, text=True) # noqa: E501
59 for keyboard in json.loads(r.stdout)['keyboards']:
60 if keyboard['name'] != "at-translated-set-2-keyboard":
61 continue
62 output(keyboard['active_keymap'])
63
64 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
65 sock.connect(os.environ["XDG_RUNTIME_DIR"] + "/hypr/" + os.environ["HYPRLAND_INSTANCE_SIGNATURE"] + "/.socket2.sock") # noqa: E501
66 expected = re.compile(r'^activelayout>>at-translated-set-2-keyboard,(?P<keymap>.+)$') # noqa: E501
67 for line in sock.makefile(buffering=1, encoding='utf-8'):
68 if match := expected.match(line):
69 output(match.group("keymap"))
70 '';
71 on-click = "hyprctl switchxkblayout at-translated-set-2-keyboard next";
72 };
73 "custom/worktime" = {
74 interval = 60;
75 exec = lib.getExe pkgs.worktime;
76 tooltip = false;
77 };
78 "custom/worktime-today" = {
79 interval = 60;
80 exec = "${lib.getExe pkgs.worktime} today";
81 tooltip = false;
82 };
83 "niri/workspaces" = {
84 all-outputs = true;
85 };
86 "niri/window" = {
87 separate-outputs = true;
88 icon = true;
89 icon-size = 14;
90 rewrite = windowRewrites;
91 };
92 clock = {
93 interval = 1;
94 # timezone = "Europe/Berlin";
95 format = "W{:%V-%u %F %H:%M:%S%Ez}";
96 tooltip-format = "<tt><small>{calendar}</small></tt>";
97 calendar = {
98 mode = "year";
99 mode-mon-col = 3;
100 weeks-pos = "left";
101 on-scroll = 1;
102 format = {
103 months = "<span color='#ffead3'><b>{}</b></span>";
104 days = "{}";
105 weeks = "<span color='#99ffdd'><b>{}</b></span>";
106 weekdays = "<span color='#ffcc66'><b>{}</b></span>";
107 today = "<span color='#ff6699'><b>{}</b></span>";
108 };
109 };
110 };
111 battery = {
112 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
113 icon-size = iconSize - 2;
114 states = { warning = 30; critical = 15; };
115 format-icons = ["&#xf008e;" "&#xf007a;" "&#xf007b;" "&#xf007c;" "&#xf007d;" "&#xf007e;" "&#xf007f;" "&#xf0080;" "&#xf0081;" "&#xf0082;" "&#xf0079;" ];
116 format-charging = "&#xf0084;";
117 format-plugged = "&#xf06a5;";
118 tooltip-format = "{capacity}% {timeTo}";
119 interval = 20;
120 };
121 tray = {
122 icon-size = 16;
123 # show-passive-items = true;
124 spacing = 1;
125 };
126 privacy = {
127 icon-spacing = 7;
128 icon-size = iconSize;
129 modules = [
130 { type = "screenshare"; }
131 { type = "audio-in"; }
132 ];
133 };
134 idle_inhibitor = {
135 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
136 icon-size = iconSize;
137 format-icons = { activated = "&#xf0208;"; deactivated = "&#xf0209;"; };
138 timeout = 120;
139 };
140 backlight = {
141 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
142 icon-size = iconSize;
143 tooltip-format = "{percent}%";
144 format-icons = ["&#xf00da;" "&#xf00db;" "&#xf00dc;" "&#xf00dd;" "&#xf00de;" "&#xf00df;" "&#xf00e0;"];
145 on-scroll-up = "lightctl -d -e4 -n1 up";
146 on-scroll-down = "lightctl -d -e4 -n1 down";
147 };
148 wireplumber = {
149 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
150 icon-size = iconSize;
151 tooltip-format = "{volume}% {node_name}";
152 format-icons = ["&#xf057f;" "&#xf0580;" "&#xf057e;"];
153 format-muted = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">&#xf075f;</span>";
154 # ignored-sinks = ["Easy Effects Sink"];
155 on-scroll-up = "volumectl -d -u up";
156 on-scroll-down = "volumectl -d -u down";
157 on-click = "volumectl -d toggle-mute";
158 };
159 }
160 {
161 layer = "top";
162 position = "top";
163 height = 14;
164 output = [ "!eDP-1" "!DP-2" "!DP-3" ];
165 modules-left = [ "niri/workspaces" ];
166 modules-center = [ "niri/window" ];
167 modules-right = [ "clock" ];
168
169 "niri/workspaces" = {
170 all-outputs = false;
171 };
172 "niri/window" = {
173 separate-outputs = true;
174 icon = true;
175 icon-size = 14;
176 rewrite = windowRewrites;
177 };
178 clock = {
179 interval = 1;
180 # timezone = "Europe/Berlin";
181 format = "{:%H:%M}";
182 tooltip-format = "W{:%V-%u %F %H:%M:%S%Ez}";
183 };
184 }
185 ];
186 style = ''
187 @define-color white #ffffff;
188 @define-color grey #555555;
189 @define-color blue #1a8fff;
190 @define-color green #23fd00;
191 @define-color orange #f28a21;
192 @define-color red #f2201f;
193
194 * {
195 border: none;
196 font-family: "Fira Sans Nerd Font";
197 font-size: 10pt;
198 min-height: 0;
199 }
200
201 window#waybar {
202 background-color: rgba(0, 0, 0, 0.66);
203 color: @white;
204 }
205
206 .modules-left {
207 margin-left: 9px;
208 }
209 .modules-right {
210 margin-right: 9px;
211 }
212
213 .module {
214 margin: 0 5px;
215 }
216
217 #workspaces button {
218 color: @grey;
219 }
220 #workspaces button.hosting-monitor {
221 color: @white;
222 }
223 #workspaces button.visible {
224 color: @blue;
225 }
226 #workspaces button.active {
227 color: @green;
228 }
229 #workspaces button.urgent {
230 color: @red;
231 }
232
233 #custom-weather, #custom-keymap, #custom-worktime, #custom-worktime-today {
234 color: @grey;
235 margin: 0 5px;
236 }
237 #custom-weather, #custom-worktime-today {
238 margin-right: 3px;
239 }
240 #custom-keymap, #custom-weather {
241 margin-left: 3px;
242 }
243
244 #tray {
245 margin: 0;
246 }
247 #battery, #idle_inhibitor, #backlight, #wireplumber {
248 color: @grey;
249 margin: 0 5px 0 2px;
250 }
251 #idle_inhibitor {
252 margin-right: 2px;
253 margin-left: 3px;
254 }
255 #battery {
256 margin-right: 3px;
257 }
258 #battery.discharging {
259 color: @white;
260 }
261 #battery.warning {
262 color: @orange;
263 }
264 #battery.critical {
265 color: @red;
266 }
267 #battery.charging {
268 color: @white;
269 }
270 #idle_inhibitor.activated {
271 color: @white;
272 }
273
274 #idle_inhibitor {
275 padding-top: 1px;
276 }
277
278 #privacy {
279 color: @red;
280 margin: -1px 2px 0px 5px;
281 }
282 #clock {
283 /* margin-right: 5px; */
284 }
285 '';
286 };
287 };
288}