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.nix1009
-rw-r--r--accounts/gkleen@sif/niri/mako.nix113
-rw-r--r--accounts/gkleen@sif/niri/swayosd.nix66
-rw-r--r--accounts/gkleen@sif/niri/waybar.nix358
4 files changed, 1546 insertions, 0 deletions
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
new file mode 100644
index 00000000..35a3d799
--- /dev/null
+++ b/accounts/gkleen@sif/niri/default.nix
@@ -0,0 +1,1009 @@
1{ config, hostConfig, pkgs, lib, flakeInputs, ... }:
2let
3 cfg = config.programs.niri;
4
5 kdl = flakeInputs.niri-flake.lib.kdl;
6 sleaf = name: arg: kdl.node name [arg] [];
7
8 niri = cfg.package;
9 terminal = lib.getExe config.programs.kitty.package;
10 makoctl = lib.getExe' config.services.mako.package "makoctl";
11 loginctl = lib.getExe' hostConfig.systemd.package "loginctl";
12 systemctl = lib.getExe' hostConfig.systemd.package "systemctl";
13 swayosd-client = lib.getExe' config.services.swayosd.package "swayosd-client";
14
15 focus_or_spawn = pkgs.writeShellApplication {
16 name = "focus-or-spawn";
17 runtimeInputs = [ niri pkgs.gojq pkgs.gnugrep pkgs.socat ];
18 text = ''
19 window_select="$1"
20 shift
21 workspace_name="$1"
22 shift
23
24 workspaces_json="$(niri msg -j workspaces)"
25 workspace_output="$(jq -r --arg workspace_name "$workspace_name" '.[] | select(.name == $workspace_name) | .output' <<<"$workspaces_json")"
26 # active_workspace="$(jq -r --arg workspace_output "$workspace_output" '.[] | select(.output == $workspace_output and .is_active) | .id' <<<"$workspaces_json")"
27 active_output="$(jq -r '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
28 if [[ $workspace_output != "$active_output" ]]; then
29 niri msg action move-workspace-to-monitor --reference "$workspace_name" "$active_output"
30 # socat STDIO "$NIRI_SOCKET" <<<'{"Action":{"FocusWorkspace":{"reference":{"Id":'"''${active_workspace}"'}}}}'
31 # niri msg action move-workspace-to-index --reference "$workspace_name" 1
32 fi
33
34 while IFS=$'\n' read -r window_json; do
35 if [[ -n $(jq -c "$window_select" <<<"$window_json") ]]; then
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
45 exit 0
46 fi
47 done < <(niri msg -j windows | jq -c '.[]')
48
49 exec "$@"
50 '';
51 };
52 focus-or-spawn-action = config.lib.niri.actions.spawn (lib.getExe focus_or_spawn);
53
54 with_adjacent_workspace = pkgs.writeShellApplication {
55 name = "with-adjacent-workspace";
56 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
57 text = ''
58 blacklist="$1"
59 shift
60 direction="$1"
61 shift
62 action="$1"
63 shift
64
65 workspaces_json="$(niri msg -j workspaces)"
66 active_workspace="$(jq -r '.[] | select(.is_focused) | .id' <<<"$workspaces_json")"
67 workspace_output="$(jq -r --arg active_workspace "$active_workspace" '.[] | select(.id == ($active_workspace | tonumber)) | .output' <<<"$workspaces_json")"
68 workspace_idx="$(jq -r '.[] | select(.is_focused) | .idx' <<<"$workspaces_json")"
69
70 jq_script='map(select('
71 case "$direction" in
72 down)
73 # shellcheck disable=SC2016
74 jq_script=''${jq_script}'.idx > ($workspace_idx | tonumber)';;
75 up)
76 # shellcheck disable=SC2016
77 jq_script=''${jq_script}'.idx < ($workspace_idx | tonumber)';;
78 esac
79 # shellcheck disable=SC2016
80 jq_script=''${jq_script}' and .output == $workspace_output and ((.name == null) or (.name | test($blacklist) | not)))) | sort_by(.idx)'
81 [[ $direction == "up" ]] && jq_script=''${jq_script}' | reverse'
82 jq_script=''${jq_script}' | .[0]'
83
84 workspace_json=$(jq -c --arg blacklist "$blacklist" --arg workspace_output "$workspace_output" --arg workspace_idx "$workspace_idx" "$jq_script" <<<"$workspaces_json")
85 [[ -n $workspace_json && $workspace_json != null ]] || exit 0
86 jq --arg active_workspace "$active_workspace" -c "$action" <<<"$workspace_json" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
87 '';
88 };
89 with-adjacent-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_adjacent_workspace) "^${lib.concatMapStringsSep "|" ({ name, ...}: name) cfg.scratchspaces}$";
90 focus-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
91 move-column-to-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}, "focus": true}}}'';
92
93 with_unnamed_workspace = pkgs.writeShellApplication {
94 name = "with-unnamed-workspace";
95 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
96 text = ''
97 action="$1"
98 shift
99
100 workspaces_json="$(niri msg -j workspaces)"
101 active_output="$(jq -r '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
102 active_workspace="$(jq -r '.[] | select(.is_focused) | .id' <<<"$workspaces_json")"
103
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")"
106 [[ -n $workspace_json && $workspace_json != null ]] || exit 0
107 jq --arg active_workspace "$active_workspace" -c "$action" <<<"$workspace_json" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
108 '';
109 };
110 with-unnamed-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_unnamed_workspace);
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
127 with_select_window = pkgs.writeShellApplication {
128 name = "with-select-window";
129 runtimeInputs = [ niri pkgs.gojq pkgs.socat config.programs.fuzzel.package pkgs.gawk ];
130 text = ''
131 window_select="$1"
132 shift
133 action="$1"
134 shift
135
136 windows_json="$(niri msg -j windows)"
137 active_workspace="$(jq -r '.[] | select(.is_focused) | .workspace_id' <<<"$windows_json")"
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)"
139 # shellcheck disable=SC2016
140 window_json="$(gojq -rc --arg active_workspace "$active_workspace" --arg window_ix "$window_ix" 'map(select('"$window_select"')) | .[($window_ix | tonumber)]' <<<"$windows_json")"
141
142 [[ -z "$window_json" ]] && exit 1
143
144 jq -c "$action" <<<"$window_json" | socat STDIO "$NIRI_SOCKET"
145 '';
146 };
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"));
167in {
168 imports = [
169 ./waybar.nix
170 ./mako.nix
171 ./swayosd.nix
172 ];
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
233 config = {
234 systemd.user.services.xwayland-satellite = {
235 Unit = {
236 BindsTo = [ "graphical-session.target" ];
237 PartOf = [ "graphical-session.target" ];
238 After = [ "graphical-session.target" ];
239 Requisite = [ "graphical-session.target" ];
240 };
241 Service = {
242 Type = "notify";
243 NotifyAccess = "all";
244 Environment = [ "DISPLAY=:0" ];
245 ExecStart = ''${lib.getExe pkgs.xwayland-satellite-unstable} ''${DISPLAY}'';
246 ExecStartPre = "${systemctl} --user import-environment DISPLAY";
247 StandardOutput = "journal";
248 };
249 Install = {
250 WantedBy = [ "graphical-session.target" ];
251 };
252 };
253
254 services.swayidle = {
255 events = [
256 { event = "after-resume"; command = "${lib.getExe niri} msg action power-on-monitors"; }
257 ];
258 timeouts = [
259 { timeout = 540;
260 command = "${lib.getExe niri} msg action power-off-monitors";
261 }
262 ];
263 };
264
265 systemd.user.sockets.niri-workspace-history = {
266 Socket = {
267 ListenStream = "%t/niri-workspace-history.sock";
268 SocketMode = "0600";
269 };
270 };
271 systemd.user.services.niri-workspace-history = {
272 Unit = {
273 BindsTo = [ "niri.service" ];
274 After = [ "niri.service" ];
275 };
276 Install = {
277 WantedBy = [ "niri.service" ];
278 };
279 Service = {
280 Type = "simple";
281 Sockets = [ "niri-workspace-history.socket" ];
282 ExecStart = pkgs.writers.writePython3 "niri-workspace-history" { flakeIgnore = ["E501"]; } ''
283 import os
284 import socket
285 import json
286 # import sys
287 from collections import defaultdict
288 from threading import Thread, Lock
289 from socketserver import StreamRequestHandler, ThreadingTCPServer
290 from contextlib import contextmanager
291 from io import TextIOWrapper
292
293
294 @contextmanager
295 def detaching(thing):
296 try:
297 yield thing
298 finally:
299 thing.detach()
300
301
302 workspace_history = defaultdict(list)
303 history_lock = Lock()
304
305
306 def monitor_niri():
307 workspaces = list()
308
309 def focus_workspace(output, workspace):
310 with history_lock:
311 workspace_history[output] = [workspace] + [ws for ws in workspace_history[output] if ws != workspace]
312 # print(json.dumps(workspace_history), file=sys.stderr)
313
314 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
315 sock.connect(os.environ["NIRI_SOCKET"])
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 '';
365 };
366 };
367 systemd.user.services.niri-workspace-sort = {
368 Unit = {
369 BindsTo = [ "niri.service" ];
370 After = [ "niri.service" ];
371 };
372 Install = {
373 WantedBy = [ "niri.service" ];
374 };
375 Service = {
376 Type = "simple";
377 ExecStart = pkgs.writers.writePython3 "niri-workspace-sort" { flakeIgnore = ["E501"]; } ''
378 import os
379 import sys
380 import socket
381 import json
382
383 outputs = None
384 only = {'HDMI-A-1': {'bmr'}, 'eDP-1': {'vid'}}
385
386
387 class Niri(socket.socket):
388 def __init__(self):
389 super().__init__(socket.AF_UNIX, socket.SOCK_STREAM)
390 super().connect(os.environ["NIRI_SOCKET"])
391 self.fh = super().makefile(mode='rw', buffering=1, encoding='utf-8')
392
393 def cmd(self, obj):
394 print(json.dumps(obj, separators=(',', ':')), flush=True, file=self.fh)
395
396 def event_stream(self):
397 self.cmd("EventStream")
398 return self.fh
399
400
401 with Niri() as niri, Niri().event_stream() as niri_stream:
402 for line in niri_stream:
403 workspaces = None
404 if line_json := json.loads(line):
405 if "WorkspacesChanged" in line_json:
406 workspaces = line_json["WorkspacesChanged"]["workspaces"]
407
408 if workspaces is None:
409 continue
410
411 old_outputs = outputs
412 outputs = {ws["output"] for ws in workspaces}
413 if old_outputs is None:
414 print("Initial outputs: {}".format(outputs), file=sys.stderr)
415 continue
416
417 new_outputs = outputs - old_outputs
418 if not new_outputs:
419 continue
420 print("New outputs: {}".format(new_outputs), file=sys.stderr)
421
422 relevant_workspaces = list(filter(lambda ws: (ws["name"] is not None) or (ws["active_window_id"] is not None), workspaces))
423 target_output = next(iter(outputs - set(only.keys())))
424 if not target_output:
425 continue
426 for ws in relevant_workspaces:
427 ws_ident = ws["name"] if ws["name"] is not None else (ws["output"], ws["idx"])
428 if ws["output"] not in set(only.keys()):
429 continue
430 if ws_ident in only[ws["output"]]:
431 continue
432
433 print("{} -> {}".format(ws_ident, target_output), file=sys.stderr)
434 niri.cmd({"Action": {"MoveWorkspaceToMonitor": {"reference": {"Id": ws["id"]}, "output": target_output}}})
435 '';
436 Restart = "on-failure";
437 RestartSec = 10;
438 };
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 ];
1008 };
1009}
diff --git a/accounts/gkleen@sif/niri/mako.nix b/accounts/gkleen@sif/niri/mako.nix
new file mode 100644
index 00000000..eba26caa
--- /dev/null
+++ b/accounts/gkleen@sif/niri/mako.nix
@@ -0,0 +1,113 @@
1{ config, lib, pkgs, ... }:
2{
3 config = {
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 criteria = {
19 grouped.format = "<b>(%g)</b> <i>%s</i>\\n%b";
20 "urgency=low".text-color = "#999999ff";
21 "urgency=critical".background-color = "#900000dd";
22 "app-name=Element".group-by = "summary";
23 "app-name=poweralertd" = {
24 history = false;
25 ignore-timeout = true;
26 default-timeout = 2000;
27 };
28 "app-name=worktime".history = false;
29 "mode=silent".invisible = true;
30 };
31 package = pkgs.symlinkJoin {
32 name = "${pkgs.mako.name}-wrapped";
33 paths = with pkgs; [ mako ];
34 inherit (pkgs.mako) meta;
35 postBuild = ''
36 rm -r $out/share/dbus-1
37 '';
38 };
39 };
40 systemd.user.services.mako = {
41 Unit = {
42 Description = "Mako notification daemon";
43 PartOf = [ "graphical-session.target" ];
44 };
45 Install = {
46 WantedBy = [ "graphical-session.target" ];
47 };
48 Service = {
49 Type = "dbus";
50 BusName = "org.freedesktop.Notifications";
51 ExecStart = lib.getExe config.services.mako.package;
52 RestartSec = 5;
53 Restart = "always";
54 };
55 };
56
57 systemd.user.services.mako-follows-focus = {
58 Unit = {
59 BindsTo = [ "niri.service" "mako.service" ];
60 After = [ "niri.service" "mako.service" ];
61 };
62 Service = {
63 Type = "simple";
64 Restart = "always";
65 ExecStart = pkgs.writers.writePython3 "mako-follows-focus" {
66 libraries = with pkgs.python3Packages; [];
67 } ''
68 import os
69 import socket
70 import json
71 import subprocess
72
73
74 current_output = None
75 workspaces = []
76
77
78 def output_changed(new_output):
79 global current_output
80
81 if current_output == new_output:
82 return
83
84 current_output = new_output
85 subprocess.run(["makoctl", "reload"])
86
87
88 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
89 sock.connect(os.environ["NIRI_SOCKET"])
90 sock.send(b"\"EventStream\"\n")
91 for line in sock.makefile(buffering=1, encoding='utf-8'):
92 if line_json := json.loads(line):
93 if "WorkspacesChanged" in line_json:
94 workspaces = line_json["WorkspacesChanged"]["workspaces"]
95 for workspace in workspaces:
96 if not workspace["is_focused"]:
97 continue
98 output_changed(workspace["output"])
99 break
100 if "WorkspaceActivated" in line_json and line_json["WorkspaceActivated"]["focused"]: # noqa: E501
101 for workspace in workspaces:
102 if not workspace["id"] == line_json["WorkspaceActivated"]["id"]: # noqa: E501
103 continue
104 output_changed(workspace["output"])
105 break
106 '';
107 };
108 Install = {
109 WantedBy = [ "mako.service" ];
110 };
111 };
112 };
113}
diff --git a/accounts/gkleen@sif/niri/swayosd.nix b/accounts/gkleen@sif/niri/swayosd.nix
new file mode 100644
index 00000000..54ebb302
--- /dev/null
+++ b/accounts/gkleen@sif/niri/swayosd.nix
@@ -0,0 +1,66 @@
1{ pkgs, ... }:
2{
3 config = {
4 services.swayosd = {
5 enable = true;
6 topMargin = 0.4769706078;
7 stylePath = pkgs.runCommand "style.css" {
8 passAsFile = [ "src" ];
9 src = ''
10 window#osd {
11 padding: 12px 20px;
12 border-radius: 999px;
13 border: none;
14 background: rgba(0, 0, 0, 0.87);
15
16 #container {
17 margin: 16px;
18 }
19
20 image,
21 label {
22 color: rgb(255, 255, 255);
23
24 &:disabled {
25 opacity: 1;
26 color: rgb(84, 84, 84);
27 }
28 }
29
30 progressbar {
31 min-height: 6px;
32 border-radius: 999px;
33 background: transparent;
34 border: none;
35
36 trough, progress {
37 min-height: inherit;
38 border-radius: inherit;
39 border: none;
40 }
41
42 trough {
43 background: rgb(127, 127, 127);
44 }
45 progress {
46 background: rgb(255, 255, 255);
47 }
48
49 &:disabled {
50 opacity: 1;
51
52 trough {
53 background: rgb(19, 19, 19);
54 }
55 progress {
56 background: rgb(38, 38, 38);
57 }
58 }
59 }
60 }
61 '';
62 buildInputs = with pkgs; [sass];
63 } "scss -C --sourcemap=none --style=compact $srcPath $out";
64 };
65 };
66}
diff --git a/accounts/gkleen@sif/niri/waybar.nix b/accounts/gkleen@sif/niri/waybar.nix
new file mode 100644
index 00000000..c02a9a76
--- /dev/null
+++ b/accounts/gkleen@sif/niri/waybar.nix
@@ -0,0 +1,358 @@
1{ lib, config, pkgs, ... }:
2let
3 swayosd-client = lib.getExe' config.services.swayosd.package "swayosd-client";
4in {
5 config = {
6 programs.waybar = {
7 enable = true;
8 systemd = {
9 enable = true;
10 target = "graphical-session.target";
11 };
12 settings = let
13 windowRewrites = {
14 "(.*) — Mozilla Firefox" = "$1";
15 "(.*) - Mozilla Thunderbird" = "$1";
16 "(.*) - mpv" = "$1";
17 };
18 iconSize = 11;
19 in [
20 {
21 layer = "top";
22 position = "top";
23 height = 21;
24 output = [ "eDP-1" "DP-2" "DP-3" ];
25 modules-left = [ "niri/workspaces" ];
26 modules-center = [ "niri/window" ];
27 modules-right = [ "custom/worktime" "custom/worktime-today"
28 "custom/weather"
29 "custom/keymap"
30 "privacy" "tray" "wireplumber" "backlight" "battery" "idle_inhibitor" "custom/mako" "custom/lid_inhibitor" "clock" ];
31
32 "custom/lid_inhibitor" = {
33 format = "{}";
34 return-type = "json";
35 exec = lib.getExe pkgs.waybar-systemd-inhibit;
36 on-click = lib.getExe' pkgs.waybar-systemd-inhibit "waybar-systemd-inhibit-toggle";
37 };
38 "custom/mako" = {
39 format = "{}";
40 return-type = "json";
41 exec = pkgs.writers.writePython3 "mako-silent" { libraries = [ pkgs.python3Packages.dbus-next ]; } ''
42 from dbus_next.aio import MessageBus
43
44 import asyncio
45
46 import json
47
48
49 loop = asyncio.new_event_loop()
50 asyncio.set_event_loop(loop)
51
52
53 async def main():
54 bus = await MessageBus().connect()
55 # the introspection xml would normally be included in your project, but
56 # this is convenient for development
57 introspection = await bus.introspect('org.freedesktop.Notifications', '/fr/emersion/Mako') # noqa: E501
58
59 obj = bus.get_proxy_object('org.freedesktop.Notifications', '/fr/emersion/Mako', introspection) # noqa: E501
60 mako = obj.get_interface('fr.emersion.Mako')
61 properties = obj.get_interface('org.freedesktop.DBus.Properties')
62
63 async def print_mode():
64 modes = await mako.get_modes()
65 is_silent = "silent" in modes
66 icon = "&#xf009b;" if is_silent else "&#xf009a;"
67 text = f"<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>" # noqa: E501
68 if is_silent:
69 text = f"<span color=\"#ffffff\">{text}</span>"
70 print(json.dumps({'text': text, 'tooltip': ', '.join(modes)}, separators=(',', ':')), flush=True) # noqa: E501
71
72 async def on_properties_changed(interface_name, changed_properties, invalidated_properties): # noqa: E501
73 if "Modes" not in invalidated_properties:
74 return
75
76 await print_mode()
77
78 properties.on_properties_changed(on_properties_changed)
79 await print_mode()
80
81 await loop.create_future()
82
83
84 loop.run_until_complete(main())
85 '';
86 on-click = "makoctl mode -t silent";
87 };
88 "custom/weather" = {
89 format = "{}";
90 tooltip = true;
91 interval = 3600;
92 exec = "${lib.getExe pkgs.wttrbar} --hide-conditions --nerd --custom-indicator \"<span font=\\\"Symbols Nerd Font Mono\\\" size=\\\"100%\\\">{ICON}</span> {FeelsLikeC}°\"";
93 return-type = "json";
94 };
95 "custom/keymap" = {
96 format = "{}";
97 tooltip = true;
98 return-type = "json";
99 exec = pkgs.writers.writePython3 "keymap" {} ''
100 import os
101 import socket
102 import json
103
104
105 def output(keymap):
106 short = keymap
107 if keymap == "English (programmer Dvorak)":
108 short = "dvp"
109 elif keymap == "English (US)":
110 short = "<span color=\"#ffffff\">us</span>"
111 print(json.dumps({'text': short, 'tooltip': keymap}, separators=(',', ':')), flush=True) # noqa: E501
112
113
114 keyboard_layouts = []
115
116 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
117 sock.connect(os.environ["NIRI_SOCKET"])
118 sock.send(b"\"EventStream\"\n")
119 for line in sock.makefile(buffering=1, encoding='utf-8'):
120 if line_json := json.loads(line):
121 if "KeyboardLayoutsChanged" in line_json:
122 keyboard_layouts = line_json["KeyboardLayoutsChanged"]["keyboard_layouts"]["names"] # noqa: E501
123 output(keyboard_layouts[line_json["KeyboardLayoutsChanged"]["keyboard_layouts"]["current_idx"]]) # noqa: E501
124 if "KeyboardLayoutSwitched" in line_json:
125 output(keyboard_layouts[line_json["KeyboardLayoutSwitched"]["idx"]]) # noqa: E501
126 '';
127 on-click = "niri msg action switch-layout next";
128 };
129 "custom/worktime" = {
130 interval = 60;
131 exec = "${lib.getExe pkgs.worktime} time --waybar";
132 return-type = "json";
133 };
134 "custom/worktime-today" = {
135 interval = 60;
136 exec = "${lib.getExe pkgs.worktime} today --waybar";
137 return-type = "json";
138 };
139 "niri/workspaces" = {
140 ignore = map ({ name, ... }: name) config.programs.niri.scratchspaces;
141 };
142 "niri/window" = {
143 separate-outputs = true;
144 icon = true;
145 icon-size = 14;
146 rewrite = windowRewrites;
147 };
148 clock = {
149 interval = 1;
150 # timezone = "Europe/Berlin";
151 format = "W{:%V-%u %F %H:%M:%S%Ez}";
152 tooltip-format = "<tt><small>{calendar}</small></tt>";
153 calendar = {
154 mode = "year";
155 mode-mon-col = 3;
156 weeks-pos = "left";
157 on-scroll = 1;
158 format = {
159 months = "<span color='#ffead3'><b>{}</b></span>";
160 days = "{}";
161 weeks = "<span color='#99ffdd'><b>{}</b></span>";
162 weekdays = "<span color='#ffcc66'><b>{}</b></span>";
163 today = "<span color='#ff6699'><b>{}</b></span>";
164 };
165 };
166 };
167 battery = {
168 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
169 icon-size = iconSize - 2;
170 states = { warning = 30; critical = 15; };
171 format-icons = ["&#xf008e;" "&#xf007a;" "&#xf007b;" "&#xf007c;" "&#xf007d;" "&#xf007e;" "&#xf007f;" "&#xf0080;" "&#xf0081;" "&#xf0082;" "&#xf0079;" ];
172 format-charging = "&#xf0084;";
173 format-plugged = "&#xf06a5;";
174 tooltip-format = "{capacity}% {timeTo}";
175 interval = 20;
176 };
177 tray = {
178 icon-size = 16;
179 # show-passive-items = true;
180 spacing = 1;
181 };
182 privacy = {
183 icon-spacing = 7;
184 icon-size = iconSize;
185 modules = [
186 { type = "screenshare"; }
187 { type = "audio-in"; }
188 ];
189 };
190 idle_inhibitor = {
191 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
192 icon-size = iconSize;
193 format-icons = { activated = "&#xf0208;"; deactivated = "&#xf0209;"; };
194 timeout = 120;
195 };
196 backlight = {
197 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
198 icon-size = iconSize;
199 tooltip-format = "{percent}%";
200 format-icons = ["&#xf00da;" "&#xf00db;" "&#xf00dc;" "&#xf00dd;" "&#xf00de;" "&#xf00df;" "&#xf00e0;"];
201 on-scroll-up = "${swayosd-client} --brightness raise";
202 on-scroll-down = "${swayosd-client} --brightness lower";
203 };
204 wireplumber = {
205 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
206 icon-size = iconSize;
207 tooltip-format = "{volume}% {node_name}";
208 format-icons = ["&#xf057f;" "&#xf0580;" "&#xf057e;"];
209 format-muted = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">&#xf075f;</span>";
210 # ignored-sinks = ["Easy Effects Sink"];
211 on-scroll-up = "${swayosd-client} --output-volume raise";
212 on-scroll-down = "${swayosd-client} --output-volume lower";
213 on-click = "${swayosd-client} --output-volume mute-toggle";
214 };
215 }
216 {
217 layer = "top";
218 position = "top";
219 height = 14;
220 output = [ "!eDP-1" "!DP-2" "!DP-3" "*" ];
221 modules-left = [ "niri/workspaces" ];
222 modules-center = [ "niri/window" ];
223 modules-right = [ "clock" ];
224
225 "niri/workspaces" = {
226 ignore = map ({ name, ... }: name) config.programs.niri.scratchspaces;
227 };
228 "niri/window" = {
229 separate-outputs = true;
230 icon = true;
231 icon-size = 14;
232 rewrite = windowRewrites;
233 };
234 clock = {
235 interval = 1;
236 # timezone = "Europe/Berlin";
237 format = "{:%H:%M}";
238 tooltip-format = "W{:%V-%u %F %H:%M:%S%Ez}";
239 };
240 }
241 ];
242 style = ''
243 @define-color white #ffffff;
244 @define-color grey #555555;
245 @define-color blue #1a8fff;
246 @define-color green #23fd00;
247 @define-color orange #f28a21;
248 @define-color red #f2201f;
249
250 * {
251 border: none;
252 font-family: "Fira Sans";
253 font-size: 10pt;
254 min-height: 0;
255 }
256
257 window#waybar {
258 background-color: rgba(0, 0, 0, 0.66);
259 color: @white;
260 }
261
262 .modules-left {
263 margin-left: 38px;
264 }
265 .modules-right {
266 margin-right: 38px;
267 }
268
269 .module {
270 margin: 0 5px;
271 }
272
273 #workspaces button {
274 color: @white;
275 padding: 2px 5px;
276 }
277 #workspaces button.empty {
278 color: @grey;
279 }
280 #workspaces button.active {
281 color: @green;
282 }
283 #workspaces button.urgent {
284 color: @red;
285 }
286
287 #custom-weather, #custom-keymap, #custom-worktime, #custom-worktime-today {
288 color: @grey;
289 margin: 0 5px;
290 }
291 #custom-weather {
292 margin-right: 3px;
293 }
294 #custom-keymap {
295 margin-left: 3px;
296 margin-right: 3px;
297 }
298
299 #tray {
300 margin: 0;
301 }
302 #battery, #idle_inhibitor, #backlight, #wireplumber, #custom-mako, #custom-lid_inhibitor {
303 color: @grey;
304 margin: 0 5px 0 2px;
305 }
306 #idle_inhibitor {
307 margin-right: 4px;
308 margin-left: 6px;
309 }
310 #custom-mako {
311 margin-right: 4px;
312 margin-left: 3px;
313 }
314 #custom-lid_inhibitor {
315 margin-right: 3px;
316 margin-left: 3px;
317 }
318 #battery {
319 margin-right: 3px;
320 }
321 #battery.discharging {
322 color: @white;
323 }
324 #battery.warning {
325 color: @orange;
326 }
327 #battery.critical {
328 color: @red;
329 }
330 #battery.charging {
331 color: @white;
332 }
333 #idle_inhibitor.activated {
334 color: @white;
335 }
336 #custom-worktime.running, #custom-worktime-today.running {
337 color: @white;
338 }
339 #custom-worktime.over, #custom-worktime-today.over {
340 color: @orange;
341 }
342
343 #idle_inhibitor, #custom-lid_inhibitor {
344 padding-top: 1px;
345 }
346
347 #privacy {
348 color: @red;
349 margin: -1px 4px 0px 3px;
350 }
351 #clock {
352 /* margin-right: 5px; */
353 font-feature-settings: "tnum";
354 }
355 '';
356 };
357 };
358}