summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--_sources/generated.json8
-rw-r--r--_sources/generated.nix8
-rw-r--r--accounts/gkleen@sif/default.nix2
-rw-r--r--accounts/gkleen@sif/niri/default.nix1051
-rw-r--r--accounts/gkleen@sif/niri/waybar.nix4
-rw-r--r--accounts/gkleen@sif/ssh-hosts.nix5
-rw-r--r--flake.lock70
-rw-r--r--flake.nix2
-rw-r--r--hosts/sif/default.nix8
-rw-r--r--hosts/sif/greetd/default.nix5
-rw-r--r--hosts/surtr/default.nix1
-rw-r--r--hosts/surtr/dns/Gupfile2
-rw-r--r--hosts/surtr/dns/default.nix2
-rw-r--r--hosts/surtr/dns/key.gup2
-rw-r--r--hosts/surtr/dns/keys/hledger.yggdrasil.li_acme24
-rw-r--r--hosts/surtr/dns/keys/paperless.yggdrasil.li_acme24
-rw-r--r--hosts/surtr/dns/zones/li.141.soa4
-rw-r--r--hosts/surtr/dns/zones/li.yggdrasil.soa21
-rw-r--r--hosts/surtr/hledger.nix66
-rw-r--r--hosts/surtr/paperless.nix66
-rw-r--r--hosts/surtr/tls/tsig_key.gup2
-rw-r--r--hosts/surtr/tls/tsig_keys/hledger.yggdrasil.li24
-rw-r--r--hosts/surtr/tls/tsig_keys/paperless.yggdrasil.li24
-rw-r--r--hosts/vidhar/default.nix2
-rw-r--r--hosts/vidhar/hledger/default.nix83
-rw-r--r--hosts/vidhar/hledger/htpasswd24
-rw-r--r--hosts/vidhar/network/ruleset.nft8
-rw-r--r--hosts/vidhar/paperless/default.nix25
-rw-r--r--hosts/vidhar/paperless/rootpw24
-rw-r--r--installer/default.nix4
-rw-r--r--modules/backup-utils.nix3
-rw-r--r--modules/pgbackrest.nix1
-rw-r--r--overlays/keepassxc/database-open-dialog.patch47
-rw-r--r--overlays/keepassxc/default.nix2
-rw-r--r--overlays/swayosd/default.nix (renamed from overlays/swayosd.nix)3
-rw-r--r--overlays/swayosd/exponential.patch57
-rwxr-xr-xoverlays/worktime/worktime/__main__.py104
37 files changed, 1207 insertions, 605 deletions
diff --git a/_sources/generated.json b/_sources/generated.json
index 72f913ec..b3d09fc4 100644
--- a/_sources/generated.json
+++ b/_sources/generated.json
@@ -407,7 +407,7 @@
407 }, 407 },
408 "v4l2loopback": { 408 "v4l2loopback": {
409 "cargoLocks": null, 409 "cargoLocks": null,
410 "date": "2025-01-18", 410 "date": "2025-02-03",
411 "extract": null, 411 "extract": null,
412 "name": "v4l2loopback", 412 "name": "v4l2loopback",
413 "passthru": null, 413 "passthru": null,
@@ -419,12 +419,12 @@
419 "name": null, 419 "name": null,
420 "owner": "umlaeute", 420 "owner": "umlaeute",
421 "repo": "v4l2loopback", 421 "repo": "v4l2loopback",
422 "rev": "39ad8a43522c18b5e4f4363ce053f604312fc413", 422 "rev": "7164d6e6b9aad52a27652c8bb8bd3c3d7a5b336b",
423 "sha256": "sha256-A1p5ZfoMlw6/J3vBdQcXMvERdyBnqs9Ca+0LcLnu7b8=", 423 "sha256": "sha256-1f4+pIbPM/TOJOc7Ns2VDXlBCGyrXiNpmKfThl5kZfk=",
424 "sparseCheckout": [], 424 "sparseCheckout": [],
425 "type": "github" 425 "type": "github"
426 }, 426 },
427 "version": "39ad8a43522c18b5e4f4363ce053f604312fc413" 427 "version": "7164d6e6b9aad52a27652c8bb8bd3c3d7a5b336b"
428 }, 428 },
429 "xcompose": { 429 "xcompose": {
430 "cargoLocks": null, 430 "cargoLocks": null,
diff --git a/_sources/generated.nix b/_sources/generated.nix
index e25f1bda..63c464bb 100644
--- a/_sources/generated.nix
+++ b/_sources/generated.nix
@@ -254,15 +254,15 @@
254 }; 254 };
255 v4l2loopback = { 255 v4l2loopback = {
256 pname = "v4l2loopback"; 256 pname = "v4l2loopback";
257 version = "39ad8a43522c18b5e4f4363ce053f604312fc413"; 257 version = "7164d6e6b9aad52a27652c8bb8bd3c3d7a5b336b";
258 src = fetchFromGitHub { 258 src = fetchFromGitHub {
259 owner = "umlaeute"; 259 owner = "umlaeute";
260 repo = "v4l2loopback"; 260 repo = "v4l2loopback";
261 rev = "39ad8a43522c18b5e4f4363ce053f604312fc413"; 261 rev = "7164d6e6b9aad52a27652c8bb8bd3c3d7a5b336b";
262 fetchSubmodules = true; 262 fetchSubmodules = true;
263 sha256 = "sha256-A1p5ZfoMlw6/J3vBdQcXMvERdyBnqs9Ca+0LcLnu7b8="; 263 sha256 = "sha256-1f4+pIbPM/TOJOc7Ns2VDXlBCGyrXiNpmKfThl5kZfk=";
264 }; 264 };
265 date = "2025-01-18"; 265 date = "2025-02-03";
266 }; 266 };
267 xcompose = { 267 xcompose = {
268 pname = "xcompose"; 268 pname = "xcompose";
diff --git a/accounts/gkleen@sif/default.nix b/accounts/gkleen@sif/default.nix
index 58cfb425..f4f7daed 100644
--- a/accounts/gkleen@sif/default.nix
+++ b/accounts/gkleen@sif/default.nix
@@ -260,7 +260,7 @@ in {
260 enable = true; 260 enable = true;
261 settings.default = { 261 settings.default = {
262 path = "~/.wallpapers"; 262 path = "~/.wallpapers";
263 duration = "8h"; 263 duration = "15m";
264 mode = "center"; 264 mode = "center";
265 }; 265 };
266 }; 266 };
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
index 7e187c84..e4a93a49 100644
--- a/accounts/gkleen@sif/niri/default.nix
+++ b/accounts/gkleen@sif/niri/default.nix
@@ -1,6 +1,10 @@
1{ config, hostConfig, pkgs, lib, ... }: 1{ config, hostConfig, pkgs, lib, flakeInputs, ... }:
2let 2let
3 niri = config.programs.niri.package; 3 cfg = config.programs.niri;
4
5 kdl = flakeInputs.niri-flake.lib.kdl;
6
7 niri = cfg.package;
4 terminal = lib.getExe config.programs.kitty.package; 8 terminal = lib.getExe config.programs.kitty.package;
5 makoctl = lib.getExe' config.services.mako.package "makoctl"; 9 makoctl = lib.getExe' config.services.mako.package "makoctl";
6 loginctl = lib.getExe' hostConfig.systemd.package "loginctl"; 10 loginctl = lib.getExe' hostConfig.systemd.package "loginctl";
@@ -78,7 +82,7 @@ let
78 jq --arg active_workspace "$active_workspace" -c "$action" <<<"$workspace_json" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET" 82 jq --arg active_workspace "$active_workspace" -c "$action" <<<"$workspace_json" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
79 ''; 83 '';
80 }; 84 };
81 with-adjacent-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_adjacent_workspace) "^pwctl|eff|kpxc|bmgr|edit|term$"; 85 with-adjacent-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_adjacent_workspace) "^${lib.concatMapStringsSep "|" ({ name, ...}: name) cfg.scratchspaces}$";
82 focus-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}''; 86 focus-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
83 move-column-to-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}}}}''; 87 move-column-to-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}}}}'';
84 88
@@ -101,6 +105,21 @@ let
101 }; 105 };
102 with-unnamed-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_unnamed_workspace); 106 with-unnamed-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_unnamed_workspace);
103 107
108 with_empty_unnamed_workspace = pkgs.writeShellApplication {
109 name = "with-empty-unnamed-workspace";
110 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
111 text = ''
112 action="$1"
113 shift
114
115 workspaces_json="$(niri msg -j workspaces)"
116 active_output="$(jq '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
117 target_workspace_id="$(jq --argjson active_output "$active_output" 'map(select(.active_window_id == null and .name == null and .output == $active_output)) | sort_by(.idx) | .[0].id' <<<"$workspaces_json")"
118 jq --argjson workspace_id "$target_workspace_id" -nc "$action" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
119 '';
120 };
121 with-empty-unnamed-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_empty_unnamed_workspace);
122
104 with_select_window = pkgs.writeShellApplication { 123 with_select_window = pkgs.writeShellApplication {
105 name = "with-select-window"; 124 name = "with-select-window";
106 runtimeInputs = [ niri pkgs.gojq pkgs.socat config.programs.fuzzel.package pkgs.gawk ]; 125 runtimeInputs = [ niri pkgs.gojq pkgs.socat config.programs.fuzzel.package pkgs.gawk ];
@@ -129,6 +148,54 @@ in {
129 ./swayosd.nix 148 ./swayosd.nix
130 ]; 149 ];
131 150
151 options = {
152 programs.niri.scratchspaces = lib.mkOption {
153 type = lib.types.listOf (lib.types.submodule ({ config, ... }: {
154 options = {
155 name = lib.mkOption {
156 type = lib.types.str;
157 };
158 match = lib.mkOption {
159 type = lib.types.listOf (lib.types.attrsOf kdl.types.kdl-args);
160 default = [];
161 };
162 exclude = lib.mkOption {
163 type = lib.types.listOf (lib.types.attrsOf kdl.types.kdl-args);
164 default = [];
165 };
166 windowRuleExtra = lib.mkOption {
167 type = kdl.types.kdl-nodes;
168 default = [];
169 };
170 key = lib.mkOption {
171 type = lib.types.nullOr lib.types.str;
172 default = null;
173 };
174 spawn = lib.mkOption {
175 type = lib.types.nullOr (lib.types.listOf lib.types.str);
176 default = null;
177 };
178 app-id = lib.mkOption {
179 type = lib.types.nullOr lib.types.str;
180 default = null;
181 };
182 selector = lib.mkOption {
183 type = lib.types.nullOr lib.types.str;
184 default = null;
185 };
186 };
187
188 config = lib.mkMerge [
189 (lib.mkIf (config.app-id != null) {
190 match = lib.mkDefault [ { app-id = "^${lib.escapeRegex config.app-id}$"; } ];
191 selector = lib.mkDefault "select(.app_id == \"${config.app-id}\")";
192 })
193 ];
194 }));
195 default = [];
196 };
197 };
198
132 config = { 199 config = {
133 systemd.user.services.xwayland-satellite = { 200 systemd.user.services.xwayland-satellite = {
134 Unit = { 201 Unit = {
@@ -268,476 +335,516 @@ in {
268 }; 335 };
269 }; 336 };
270 337
271 programs.niri.settings = { 338 programs.niri.scratchspaces = [
272 prefer-no-csd = true; 339 { name = "pwctl";
273 screenshot-path = "${config.home.homeDirectory}/screenshots"; 340 key = "Mod+Control+A";
274 341 spawn = ["pwvucontrol"];
275 hotkey-overlay.skip-at-startup = true; 342 app-id = "com.saivert.pwvucontrol";
276 343 }
277 input = { 344 { name = "kpxc";
278 keyboard = { 345 exclude = [
279 repeat-delay = 300; 346 { title = "^Unlock Database.*"; }
280 repeat-rate = 50; 347 { title = "^Access Request.*"; }
281 348 { title = ".*Passkey credentials$"; }
282 xkb = {
283 layout = "us,us";
284 variant = "dvp,";
285 options = "compose:caps,grp:win_space_toggle";
286 };
287 };
288
289 workspace-auto-back-and-forth = true;
290 # focus-follows-mouse.enable = true;
291 warp-mouse-to-focus = true;
292 };
293
294 outputs = {
295 "eDP-1" = {
296 scale = 1.5;
297 position = { x = 0; y = 0; };
298 };
299 "Ancor Communications Inc ASUS PB287Q 0x0000DD9B" = {
300 scale = 1.5;
301 position = { x = 2560; y = 0; };
302 };
303 "HP Inc. HP 727pu CN4417143K" = {
304 mode = { width = 2560; height = 1440; refresh = 119.998; };
305 scale = 1;
306 position = { x = 2560; y = 0; };
307 variable-refresh-rate = "on-demand";
308 };
309 };
310
311 environment = {
312 NIXOS_OZONE_WL = "1";
313 QT_QPA_PLATFORM = "wayland";
314 QT_WAYLAND_DISABLE_WINDOWDECORATION = "1";
315 GDK_BACKEND = "wayland";
316 SDL_VIDEODRIVER = "wayland";
317 DISPLAY = ":0";
318 };
319
320 debug.render-drm-device = "/dev/dri/by-path/pci-0000:00:02.0-render";
321
322 animations = {
323 slowdown = 0.5;
324 workspace-switch = null;
325 };
326
327 layout = {
328 gaps = 8;
329 struts = { left = 0; right = 0; top = 0; bottom = 0; };
330 focus-ring = {
331 width = 2;
332 active.gradient = {
333 from = "hsla(195 100% 60% 0.75)";
334 to = "hsla(155 100% 50% 0.75)";
335 angle = 29;
336 relative-to = "workspace-view";
337 };
338 inactive.gradient = {
339 from = "hsla(0 0% 42% 0.66)";
340 to = "hsla(0 0% 35% 0.66)";
341 angle = 29;
342 relative-to = "workspace-view";
343 };
344 };
345
346 preset-column-widths = [
347 { proportion = 1. / 4.; }
348 { proportion = 1. / 3.; }
349 { proportion = 1. / 2.; }
350 { proportion = 2. / 3.; }
351 { proportion = 3. / 4.; }
352 ]; 349 ];
353 default-column-width.proportion = 1. / 2.; 350 windowRuleExtra = [
354 preset-window-heights = [ 351 (kdl.leaf "open-focused" false)
355 { proportion = 1. / 3.; } 352 ];
356 { proportion = 1. / 2.; } 353 key = "Mod+Control+P";
357 { proportion = 2. / 3.; } 354 app-id = "org.keepassxc.KeePassXC";
358 { proportion = 1.; } 355 spawn = [ "keepassxc" ];
356 }
357 { name = "bmgr";
358 key = "Mod+Control+B";
359 app-id = ".blueman-manager-wrapped";
360 spawn = [ "blueman-manager" ];
361 }
362 { name = "term";
363 key = "Mod+Control+Return";
364 app-id = "kitty-scratch";
365 spawn = [ "kitty" "--app-id" "kitty-scratch" ];
366 }
367 { name = "edit";
368 match = [ { title = "^scratch$"; app-id = "^emacs$"; } ];
369 key = "Mod+Control+E";
370 selector = "select(.app_id == \"emacs\" and .title == \"scratch\")";
371 spawn = [ "emacsclient" "-c" "--frame-parameters=(quote (name . \"scratch\"))" ];
372 }
373 { name = "eff";
374 key = "Mod+Control+O";
375 app-id = "com.github.wwmm.easyeffects";
376 spawn = [ "easyeffects" ];
377 }
378 ];
379 programs.niri.config =
380 let
381 inherit (kdl) node plain leaf flag;
382 optional-node = cond: v:
383 if cond
384 then v
385 else null;
386 opt-props = lib.filterAttrs (lib.const (value: value != null));
387 in
388 [ (flag "prefer-no-csd")
389
390 (plain "hotkey-overlay" [
391 (flag "skip-at-startup")
392 ])
393
394 (plain "input" [
395 (plain "keyboard" [
396 (leaf "repeat-delay" 300)
397 (leaf "repeat-rate" 50)
398
399 (plain "xkb" [
400 (leaf "layout" "us,us")
401 (leaf "variant" "dvp,")
402 (leaf "options" "compose:caps,grp:win_space_toggle")
403 ])
404 ])
405
406 (flag "workspace-auto-back-and-forth")
407 # (leaf "focus-follows-mouse" {})
408 # (flag "warp-mouse-to-focus")
409
410 (plain "touchpad" [ (flag "off") ])
411 (plain "trackball" [
412 (leaf "scroll-method" "on-button-down")
413 (leaf "scroll-button" 278)
414 ])
415 (plain "touch" [
416 (leaf "map-to-output" "eDP-1")
417 ])
418 ])
419
420 (plain "environment" (lib.mapAttrsToList leaf {
421 NIXOS_OZONE_WL = "1";
422 QT_QPA_PLATFORM = "wayland";
423 QT_WAYLAND_DISABLE_WINDOWDECORATION = "1";
424 GDK_BACKEND = "wayland";
425 SDL_VIDEODRIVER = "wayland";
426 DISPLAY = ":0";
427 }))
428
429 (node "output" "eDP-1" [
430 (leaf "scale" 1.5)
431 (leaf "position" { x = 0; y = 0; })
432 ])
433 (node "output" "Ancor Communications Inc ASUS PB287Q 0x0000DD9B" [
434 (leaf "scale" 1.5)
435 (leaf "position" { x = 2560; y = 0; })
436 ])
437 (node "output" "HP Inc. HP 727pu CN4417143K" [
438 (leaf "mode" "2560x1440@119.998")
439 (leaf "scale" 1)
440 (leaf "position" { x = 2560; y = 0; })
441 (flag "variable-refresh-rate")
442 ])
443
444 (plain "debug" [
445 (leaf "render-drm-device" "/dev/dri/by-path/pci-0000:00:02.0-render")
446 ])
447
448 (plain "animations" [
449 (leaf "slowdown" 0.5)
450 (plain "workspace-switch" [(flag "off")])
451 ])
452
453 (plain "layout" [
454 (leaf "gaps" 8)
455 (plain "struts" [
456 (leaf "left" 0)
457 (leaf "right" 0)
458 (leaf "top" 0)
459 (leaf "bottom" 0)
460 ])
461 (plain "border" [
462 (leaf "width" 2)
463 (leaf "active-gradient" {
464 from = "hsla(195 100% 45% 1)";
465 to = "hsla(155 100% 37.5% 1)";
466 angle = 29;
467 relative-to = "workspace-view";
468 })
469 (leaf "inactive-gradient" {
470 from = "hsla(0 0% 27.7% 1)";
471 to = "hsla(0 0% 23% 1)";
472 angle = 29;
473 relative-to = "workspace-view";
474 })
475 ])
476 (plain "focus-ring" [
477 (flag "off")
478 ])
479
480 (plain "preset-column-widths" (map (prop: leaf "proportion" prop) [
481 (1. / 4.) (1. / 3.) (1. / 2.) (2. / 3.) (3. / 4.) (1.)
482 ]))
483 (plain "default-column-width" [ (leaf "proportion" (1. / 2.)) ])
484 (plain "preset-window-heights" (map (prop: leaf "proportion" prop) [
485 (1. / 3.) (1. / 2.) (2. / 3.) (1.)
486 ]))
487
488 (flag "always-center-single-column")
489
490 (plain "tab-indicator" [
491 (leaf "gap" 4)
492 (leaf "width" 8)
493 (leaf "gaps-between-tabs" 4)
494 (flag "place-within-column")
495 (leaf "length" { total-proportion = 1.; })
496 (leaf "active-gradient" {
497 from = "hsla(195 100% 60% 0.75)";
498 to = "hsla(155 100% 50% 0.75)";
499 angle = 29;
500 relative-to = "workspace-view";
501 })
502 (leaf "inactive-gradient" {
503 from = "hsla(0 0% 42% 0.66)";
504 to = "hsla(0 0% 35% 0.66)";
505 angle = 29;
506 relative-to = "workspace-view";
507 })
508 ])
509 ])
510
511 (plain "cursor" [
512 (flag "hide-when-typing")
513 ])
514
515 (map (name:
516 (node "workspace" name [
517 (leaf "open-on-output" "eDP-1")
518 ])
519 ) (map ({name, ...}: name) cfg.scratchspaces))
520 (map (name:
521 (leaf "workspace" name)
522 ) ["comm" "web" "vid" "bmr"])
523
524 (plain "window-rule" [
525 (leaf "clip-to-geometry" true)
526 ])
527
528 (plain "window-rule" [
529 (leaf "match" { is-floating = true; })
530 (leaf "geometry-corner-radius" 8)
531 (plain "shadow" [ (flag "on") ])
532 ])
533
534 (plain "window-rule" [
535 (leaf "match" { app-id = "^org\\.keepassxc\\.KeePassXC$"; })
536 (leaf "block-out-from" "screencast")
537 ])
538 (plain "window-rule" [
539 (map (title:
540 (leaf "match" { app-id = "^org\\.keepassxc\\.KeePassXC$"; inherit title; })
541 ) ["^Unlock Database.*" "^Access Request.*" ".*Passkey credentials$"])
542 (leaf "open-focused" true)
543 (leaf "open-floating" true)
544 ])
545
546 (map ({ name, match, exclude, windowRuleExtra, ... }:
547 (optional-node (match != []) (plain "window-rule" [
548 (map (leaf "match") match)
549 (map (leaf "exclude") exclude)
550 (leaf "open-on-workspace" name)
551 (leaf "open-maximized" true)
552 windowRuleExtra
553 ]))
554 ) cfg.scratchspaces)
555
556 (plain "window-rule" [
557 (leaf "match" { app-id = "^emacs$"; })
558 (leaf "match" { app-id = "^firefox$"; })
559 (plain "default-column-width" [(leaf "proportion" (2. / 3.))])
560 ])
561 (plain "window-rule" [
562 (leaf "match" { app-id = "^kitty$"; })
563 (leaf "match" { app-id = "^kitty-play$"; })
564 (plain "default-column-width" [(leaf "proportion" (1. / 3.))])
565 ])
566
567 (plain "window-rule" [
568 (leaf "match" { app-id = "^thunderbird$"; })
569 (leaf "match" { app-id = "^Element$"; })
570 (leaf "match" { app-id = "^Rainbow$"; })
571 (leaf "open-on-workspace" "comm")
572 ])
573 (plain "window-rule" [
574 (leaf "match" { app-id = "^firefox$"; })
575 (leaf "open-on-workspace" "web")
576 (leaf "open-maximized" true)
577 ])
578 (plain "window-rule" [
579 (leaf "match" { app-id = "^mpv$"; })
580 (leaf "open-on-workspace" "vid")
581 (plain "default-column-width" [(leaf "proportion" 1.)])
582 ])
583 (plain "window-rule" [
584 (leaf "match" { app-id = "^kitty-play$"; })
585 (leaf "open-on-workspace" "vid")
586 (leaf "open-focused" false)
587 ])
588 (plain "window-rule" [
589 (leaf "match" { app-id = "^pdfpc$"; })
590 (plain "default-column-width" [(leaf "proportion" 1.)])
591 ])
592 (plain "window-rule" [
593 (leaf "match" { app-id = "^pdfpc$"; title = "^pdfpc - presentation$"; })
594 (plain "default-column-width" [(leaf "proportion" 1.)])
595 (leaf "open-fullscreen" true)
596 (leaf "open-on-workspace" "bmr")
597 (leaf "open-focused" false)
598 ])
599 (plain "window-rule" [
600 (map (leaf "match") [
601 { app-id = "^Gimp-"; title = "^Quit GIMP$"; }
602 { app-id = "^org\\.kde\\.polkit-kde-authentication-agent-1$"; }
603 { app-id = "^xdg-desktop-portal-gtk$"; }
604 ])
605 (leaf "open-floating" true)
606 ])
607 (plain "window-rule" [
608 (leaf "match" { app-id = "^org\\.pwmt\\.zathura$"; })
609 (leaf "match" { app-id = "^evince$"; })
610 (leaf "default-column-display" "tabbed")
611 ])
612
613 (plain "layer-rule" [
614 (leaf "match" { namespace = "^notifications$"; })
615 (leaf "match" { namespace = "^waybar$"; })
616 (leaf "match" { namespace = "^launcher$"; })
617 (leaf "block-out-from" "screencast")
618 ])
619
620 (plain "binds"
621 (let
622 bind = name: cfg: node name (opt-props {
623 cooldown-ms = cfg.cooldown-ms or null;
624 }
625 // (lib.optionalAttrs (!(cfg.repeat or true)) {
626 repeat = false;
627 })
628 // (lib.optionalAttrs (cfg.allow-when-locked or false) {
629 allow-when-locked = true;
630 })) (lib.mapAttrsToList leaf (lib.removeAttrs cfg.action ["__functor"]));
631 in
632 [
633 (lib.mapAttrsToList bind (with config.lib.niri.actions; {
634 "Mod+Slash".action = show-hotkey-overlay;
635
636 "Mod+Return".action = spawn terminal;
637 "Mod+Q".action = close-window;
638 "Mod+O".action = spawn (lib.getExe config.programs.fuzzel.package);
639 "Mod+Shift+O".action = spawn (lib.getExe config.programs.fuzzel.package) "--list-executables-in-path";
640
641 "Mod+Alt+E".action = spawn (lib.getExe' config.services.emacs.package "emacsclient") "-c";
642 "Mod+Alt+Y".action = spawn (lib.getExe (pkgs.writeShellApplication {
643 name = "queue-yt-dlp";
644 runtimeInputs = with pkgs; [ wl-clipboard-rs socat ];
645 text = ''
646 socat STDIO UNIX-CONNECT:"$XDG_RUNTIME_DIR"/yt-dlp.sock <<<$'{ "urls": ["'"$(wl-paste)"$'"] }'
647 '';
648 }));
649 "Mod+Alt+L".action = spawn (lib.getExe (pkgs.writeShellApplication {
650 name = "queue-yt-dlp";
651 runtimeInputs = with pkgs; [ wl-clipboard-rs config.programs.kitty.package ];
652 text = ''
653 exec -- kitty --app-id kitty-play --directory "$HOME"/media mpv "$(wl-paste)"
654 '';
655 }));
656 "Mod+Alt+M".action = spawn (lib.getExe' pkgs.screen-message "sm") "-n" "Fira Mono" "-a" "1" "-f" "#fff" "-b" "#000";
657
658 "Mod+U".action = spawn (lib.getExe (pkgs.writeShellApplication {
659 name = "qalc-fuzzel";
660 runtimeInputs = with pkgs; [ wl-clipboard-rs libqalculate config.programs.fuzzel.package coreutils findutils libnotify gnugrep ];
661 text = ''
662 RESULTS_DIR="$HOME/.cache/qalc-fuzzel"
663 prev() {
664 FOUND=false
665 while IFS= read -r line; do
666 [[ -n "$line" ]] || continue
667 FOUND=true
668 echo "$line"
669 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)
670 $FOUND || echo
671 }
672 FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> ") || exit $?
673 if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then
674 QALC_RES="$FUZZEL_RES"
675 QALC_RET=0
676 else
677 QALC_RES=$(qalc "$FUZZEL_RES" 2>&1)
678 QALC_RET=$?
679 fi
680 [[ -n "$QALC_RES" ]] || exit 1
681 EXISTING=false
682 set +o pipefail
683 grep -Fxrl "$QALC_RES" "$RESULTS_DIR" | xargs -r touch
684 [[ ''${PIPESTATUS[0]} -eq 0 ]] && EXISTING=true
685 set -o pipefail
686 if [[ $QALC_RET -eq 0 ]] && ! $EXISTING; then
687 set +o pipefail
688 RES_FILE="$RESULTS_DIR"/$(date -uIs).$(tr -Cd 'a-zA-Z0-9' </dev/random | head -c 10)
689 set -o pipefail
690 cat >"$RES_FILE" <<<"$QALC_RES"
691 fi
692 [[ "$QALC_RES" =~ .*\ =\ (.*) ]] && QALC_RES="''${BASH_REMATCH[1]}"
693 [[ $QALC_RET -eq 0 ]] && wl-copy "$QALC_RES"
694 notify-send "$QALC_RES"
695 '';
696 }));
697 "Mod+E".action = spawn (lib.getExe (pkgs.writeShellApplication {
698 name = "emoji-fuzzel";
699 runtimeInputs = with pkgs; [ config.programs.fuzzel.package wtype wl-clipboard-rs ];
700 text = ''
701 FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " <"$HOME"/.local/share/emoji-data/list.txt) || exit $?
702 [[ -n "$FUZZEL_RES" ]] || exit 1
703 wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste
704 '';
705 }));
706 "Print".action = spawn (lib.getExe (pkgs.writeShellApplication {
707 name = "screenshot";
708 runtimeInputs = with pkgs; [ grim slurp wl-clipboard-rs coreutils ];
709 text = ''
710 grim -g "$(slurp -b 00000080 -c FFFFFFFF -s 00000000 -w 1)" - \
711 | tee "$HOME/screenshots/$(date +"%Y-%m-%dT%H:%M:%S").png" \
712 | wl-copy --type image/png
713 '';
714 }));
715 "Shift+Print".action = spawn (lib.getExe (pkgs.writeShellApplication {
716 name = "screenshot";
717 runtimeInputs = with pkgs; [ grim niri gojq wl-clipboard-rs coreutils ];
718 text = ''
719 grim -o "$(niri msg -j workspaces | jq -r '.[] | select(.is_focused) | .output')" - \
720 | tee "$HOME/screenshots/$(date +"%Y-%m-%dT%H:%M:%S").png" \
721 | wl-copy --type image/png
722 '';
723 }));
724 "Mod+B".action = with-select-window-action ".workspace_id == ($active_workspace | tonumber)" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
725 "Mod+Shift+B".action = with-select-window-action "true" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
726
727 "Mod+H".action = focus-column-left;
728 "Mod+T".action = focus-window-down;
729 "Mod+N".action = focus-window-up;
730 "Mod+S".action = focus-column-right;
731
732 "Mod+Shift+H".action = move-column-left;
733 "Mod+Shift+T".action = move-window-down;
734 "Mod+Shift+N".action = move-window-up;
735 "Mod+Shift+S".action = move-column-right;
736
737 "Mod+Control+H".action = focus-monitor-left;
738 "Mod+Control+T".action = focus-monitor-down;
739 "Mod+Control+N".action = focus-monitor-up;
740 "Mod+Control+S".action = focus-monitor-right;
741
742 "Mod+Shift+Control+H".action = move-workspace-to-monitor-left;
743 "Mod+Shift+Control+T".action = move-workspace-to-monitor-down;
744 "Mod+Shift+Control+N".action = move-workspace-to-monitor-up;
745 "Mod+Shift+Control+S".action = move-workspace-to-monitor-right;
746
747 "Mod+G".action = focus-adjacent-workspace "down";
748 "Mod+C".action = focus-adjacent-workspace "up";
749
750 "Mod+Shift+G".action = move-column-to-adjacent-workspace "down";
751 "Mod+Shift+C".action = move-column-to-adjacent-workspace "up";
752
753 "Mod+Shift+Control+G".action = move-workspace-down;
754 "Mod+Shift+Control+C".action = move-workspace-up;
755
756 "Mod+ParenLeft".action = focus-workspace "comm";
757 "Mod+Shift+ParenLeft".action = move-column-to-workspace "comm";
758
759 "Mod+ParenRight".action = focus-workspace "web";
760 "Mod+Shift+ParenRight".action = move-column-to-workspace "web";
761
762 "Mod+BraceRight".action = focus-workspace "read";
763 "Mod+Shift+BraceRight".action = move-column-to-workspace "read";
764
765 "Mod+BraceLeft".action = focus-workspace "mon";
766 "Mod+Shift+BraceLeft".action = move-column-to-workspace "mon";
767
768 "Mod+Asterisk".action = focus-workspace "vid";
769 "Mod+Shift+Asterisk".action = move-column-to-workspace "vid";
770
771 "Mod+Plus".action = with-unnamed-workspace-action ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
772 "Mod+Shift+Plus".action = with-unnamed-workspace-action ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}}}}'';
773
774 "Mod+M".action = consume-or-expel-window-left;
775 "Mod+W".action = consume-or-expel-window-right;
776
777 "Mod+Shift+M".action = toggle-column-tabbed-display;
778
779 "Mod+R".action = switch-preset-column-width;
780 "Mod+Shift+R".action = switch-preset-window-height;
781 "Mod+F".action = center-column;
782 "Mod+Shift+F".action = maximize-column;
783 "Mod+Shift+Ctrl+F".action = fullscreen-window;
784
785 "Mod+V".action = switch-focus-between-floating-and-tiling;
786 "Mod+Shift+V".action = toggle-window-floating;
787
788 "Mod+Left".action = set-column-width "-10%";
789 "Mod+Down".action = set-window-height "-10%";
790 "Mod+Up".action = set-window-height "+10%";
791 "Mod+Right".action = set-column-width "+10%";
792
793 "Mod+Shift+Z" = {
794 action = spawn (lib.getExe niri) "msg" "action" "power-off-monitors";
795 allow-when-locked = true;
796 };
797 "Mod+Shift+L".action = spawn loginctl "lock-session";
798 "Mod+Shift+E".action = quit;
799 "Mod+Shift+Minus" = {
800 action = spawn systemctl "suspend";
801 allow-when-locked = true;
802 };
803 "Mod+Shift+Control+Minus" = {
804 action = spawn systemctl "hibernate";
805 allow-when-locked = true;
806 };
807 "Mod+Shift+P" = {
808 action = spawn (lib.getExe pkgs.playerctl) "-a" "pause";
809 allow-when-locked = true;
810 };
811
812 "XF86MonBrightnessUp" = {
813 action = spawn swayosd-client "--brightness" "raise";
814 allow-when-locked = true;
815 };
816 "XF86MonBrightnessDown" = {
817 action = spawn swayosd-client "--brightness" "lower";
818 allow-when-locked = true;
819 };
820 "XF86AudioRaiseVolume" = {
821 action = spawn swayosd-client "--output-volume" "raise";
822 allow-when-locked = true;
823 };
824 "XF86AudioLowerVolume" = {
825 action = spawn swayosd-client "--output-volume" "lower";
826 allow-when-locked = true;
827 };
828 "XF86AudioMute" = {
829 action = spawn swayosd-client "--output-volume" "mute-toggle";
830 allow-when-locked = true;
831 };
832 "XF86AudioMicMute" = {
833 action = spawn swayosd-client "--input-volume" "mute-toggle";
834 allow-when-locked = true;
835 };
836
837 "Mod+Semicolon".action = spawn makoctl "dismiss" "--group";
838 "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all";
839 "Mod+Period".action = spawn makoctl "menu" (lib.getExe config.programs.fuzzel.package) "--dmenu";
840 "Mod+Comma".action = spawn makoctl "restore";
841
842 "Mod+Control+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}";
843 "Mod+Control+Shift+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"MoveColumnToWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}";
844 }))
845 (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)
846 ]
847 ))
359 ]; 848 ];
360
361 always-center-single-column = true;
362 };
363
364 cursor.hide-when-typing = true;
365
366 input = {
367 touchpad.enable = false;
368 trackball = {
369 scroll-method = "on-button-down";
370 scroll-button = 278;
371 };
372 };
373
374 workspaces = {
375 "001" = { name = "pwctl"; open-on-output = "eDP-1"; };
376 "002" = { name = "kpxc"; open-on-output = "eDP-1"; };
377 "003" = { name = "bmgr"; open-on-output = "eDP-1"; };
378 "004" = { name = "term"; open-on-output = "eDP-1"; };
379 "005" = { name = "edit"; open-on-output = "eDP-1"; };
380 "006" = { name = "eff"; open-on-output = "eDP-1"; };
381 "101".name = "comm";
382 "102".name = "web";
383 # "104".name = "read";
384 # "105".name = "mon";
385 "110".name = "vid";
386 "120".name = "bmr";
387 };
388
389 window-rules = [
390 {
391 matches = [ { is-floating = true; } ];
392 geometry-corner-radius =
393 let
394 allCorners = r: { bottom-left = r; bottom-right = r; top-left = r; top-right = r; };
395 in allCorners 8.;
396 clip-to-geometry = true;
397 }
398 {
399 matches = [ { app-id = "^com\.saivert\.pwvucontrol$"; } ];
400 open-on-workspace = "pwctl";
401 open-maximized = true;
402 }
403 {
404 matches = [ { app-id = "^com\.github\.wwmm\.easyeffects$"; } ];
405 open-on-workspace = "eff";
406 open-maximized = true;
407 }
408 {
409 matches = [ { app-id = "^\.blueman-manager-wrapped$"; } ];
410 open-on-workspace = "bmgr";
411 open-maximized = true;
412 }
413 {
414 matches = [ { app-id = "^org\.keepassxc\.KeePassXC$"; } ];
415 block-out-from = "screencast";
416 }
417 {
418 matches = [ { app-id = "^org\.keepassxc\.KeePassXC$"; } ];
419 excludes = [
420 { title = "^Unlock Database.*"; }
421 { title = "^Access Request.*"; }
422 { title = ".*Passkey credentials$"; }
423 ];
424 open-on-workspace = "kpxc";
425 open-maximized = true;
426 open-focused = false;
427 }
428 {
429 matches = [
430 { app-id = "^org\.keepassxc\.KeePassXC$"; title = "^Unlock Database.*"; }
431 { app-id = "^org\.keepassxc\.KeePassXC$"; title = "^Access Request.*"; }
432 { app-id = "^org\.keepassxc\.KeePassXC$"; title = ".*Passkey credentials$"; }
433 ];
434 open-focused = true;
435 open-floating = true;
436 }
437 {
438 matches = [ { app-id = "^kitty-scratch$"; } ];
439 open-on-workspace = "term";
440 open-maximized = true;
441 }
442 {
443 matches = [ { title = "^scratch$"; app-id = "^emacs$"; } ];
444 open-on-workspace = "edit";
445 open-maximized = true;
446 }
447 {
448 matches = [
449 { app-id = "^emacs$"; }
450 { app-id = "^firefox$"; }
451 ];
452 default-column-width.proportion = 2. / 3.;
453 }
454 {
455 matches = [
456 { app-id = "^kitty$"; }
457 { app-id = "^kitty-play$"; }
458 ];
459 default-column-width.proportion = 1. / 3.;
460 }
461 {
462 matches = [
463 { app-id = "^thunderbird$"; }
464 { app-id = "^Element$"; }
465 { app-id = "^Rainbow$"; }
466 ];
467 open-on-workspace = "comm";
468 }
469 {
470 matches = [ { app-id = "^firefox$"; } ];
471 open-on-workspace = "web";
472 open-maximized = true;
473 variable-refresh-rate = true;
474 }
475 # {
476 # matches = [
477 # { app-id = "^evince$"; }
478 # { app-id = "^imv$"; }
479 # { app-id = "^org\.pwmt\.zathura$"; }
480 # ];
481 # open-on-workspace = "read";
482 # }
483 {
484 matches = [ { app-id = "^mpv$"; } ];
485 open-on-workspace = "vid";
486 default-column-width.proportion = 1.;
487 variable-refresh-rate = true;
488 }
489 {
490 matches = [ { app-id = "^kitty-play$"; } ];
491 open-on-workspace = "vid";
492 open-focused = false;
493 }
494 # {
495 # matches = [
496 # { app-id = "^qemu$"; }
497 # { app-id = "^virt-manager$"; }
498 # ];
499 # open-on-workspace = "mon";
500 # }
501 {
502 matches = [ { app-id = "^pdfpc$"; } ];
503 default-column-width.proportion = 1.;
504 }
505 {
506 matches = [ { app-id = "^pdfpc$"; title = "^pdfpc - presentation"; } ];
507 open-on-workspace = "bmr";
508 open-fullscreen = true;
509 }
510 {
511 matches = [
512 { app-id = "^Gimp-"; title = "^Quit GIMP$"; }
513 { app-id = "^org\.kde\.polkit-kde-authentication-agent-1$"; }
514 { app-id = "^xdg-desktop-portal-gtk$"; }
515 ];
516 open-floating = true;
517 }
518 ];
519 layer-rules = [
520 { matches = [
521 { namespace = "^notifications$"; }
522 { namespace = "^waybar$"; }
523 ];
524 block-out-from = "screencast";
525 }
526 ];
527
528 binds = with config.lib.niri.actions; {
529 "Mod+Slash".action = show-hotkey-overlay;
530
531 "Mod+Return".action = spawn terminal;
532 "Mod+Q".action = close-window;
533 "Mod+O".action = spawn (lib.getExe config.programs.fuzzel.package);
534 "Mod+Shift+O".action = spawn (lib.getExe config.programs.fuzzel.package) "--list-executables-in-path";
535
536 "Mod+Alt+E".action = spawn (lib.getExe' config.services.emacs.package "emacsclient") "-c";
537 "Mod+Alt+Y".action = spawn (lib.getExe (pkgs.writeShellApplication {
538 name = "queue-yt-dlp";
539 runtimeInputs = with pkgs; [ wl-clipboard-rs socat ];
540 text = ''
541 socat STDIO UNIX-CONNECT:"$XDG_RUNTIME_DIR"/yt-dlp.sock <<<$'{ "urls": ["'"$(wl-paste)"$'"] }'
542 '';
543 }));
544 "Mod+Alt+L".action = spawn (lib.getExe (pkgs.writeShellApplication {
545 name = "queue-yt-dlp";
546 runtimeInputs = with pkgs; [ wl-clipboard-rs config.programs.kitty.package ];
547 text = ''
548 exec -- kitty --app-id kitty-play --directory "$HOME"/media mpv "$(wl-paste)"
549 '';
550 }));
551
552 "Mod+U".action = spawn (lib.getExe (pkgs.writeShellApplication {
553 name = "qalc-fuzzel";
554 runtimeInputs = with pkgs; [ wl-clipboard-rs libqalculate config.programs.fuzzel.package coreutils findutils libnotify gnugrep ];
555 text = ''
556 RESULTS_DIR="$HOME/.cache/qalc-fuzzel"
557 prev() {
558 FOUND=false
559 while IFS= read -r line; do
560 [[ -n "$line" ]] || continue
561 FOUND=true
562 echo "$line"
563 done < <(export LC_ALL=C.UTF-8; echo; find "$RESULTS_DIR" -type f -printf $'%T@ %p\n' | sort -n | cut -d' ' -f2- | xargs -r cat)
564 $FOUND || echo
565 }
566 FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> ") || exit $?
567 if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then
568 QALC_RES="$FUZZEL_RES"
569 QALC_RET=0
570 else
571 QALC_RES=$(qalc "$FUZZEL_RES" 2>&1)
572 QALC_RET=$?
573 fi
574 [[ -n "$QALC_RES" ]] || exit 1
575 EXISTING=false
576 set +o pipefail
577 grep -Fxrl "$QALC_RES" "$RESULTS_DIR" | xargs -r touch
578 [[ ''${PIPESTATUS[0]} -eq 0 ]] && EXISTING=true
579 set -o pipefail
580 if [[ $QALC_RET -eq 0 ]] && ! $EXISTING; then
581 set +o pipefail
582 RES_FILE="$RESULTS_DIR"/$(date -uIs).$(tr -Cd 'a-zA-Z0-9' </dev/random | head -c 10)
583 set -o pipefail
584 cat >"$RES_FILE" <<<"$QALC_RES"
585 fi
586 [[ "$QALC_RES" =~ .*\ =\ (.*) ]] && QALC_RES="''${BASH_REMATCH[1]}"
587 [[ $QALC_RET -eq 0 ]] && wl-copy "$QALC_RES"
588 notify-send "$QALC_RES"
589 '';
590 }));
591 "Mod+E".action = spawn (lib.getExe (pkgs.writeShellApplication {
592 name = "emoji-fuzzel";
593 runtimeInputs = with pkgs; [ config.programs.fuzzel.package wtype wl-clipboard-rs ];
594 text = ''
595 FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " <"$HOME"/.local/share/emoji-data/list.txt) || exit $?
596 [[ -n "$FUZZEL_RES" ]] || exit 1
597 wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste
598 '';
599 }));
600 "Print".action = spawn (lib.getExe (pkgs.writeShellApplication {
601 name = "screenshot";
602 runtimeInputs = with pkgs; [ grim slurp wl-clipboard-rs coreutils ];
603 text = ''
604 grim -g "$(slurp -b 00000080 -c FFFFFFFF -s 00000000 -w 1)" - \
605 | tee "$HOME/screenshots/$(date +"%Y-%m-%dT%H:%M:%S").png" \
606 | wl-copy --type image/png
607 '';
608 }));
609 "Shift+Print".action = spawn (lib.getExe (pkgs.writeShellApplication {
610 name = "screenshot";
611 runtimeInputs = with pkgs; [ grim niri gojq wl-clipboard-rs coreutils ];
612 text = ''
613 grim -o "$(niri msg -j workspaces | jq -r '.[] | select(.is_focused) | .output')" - \
614 | tee "$HOME/screenshots/$(date +"%Y-%m-%dT%H:%M:%S").png" \
615 | wl-copy --type image/png
616 '';
617 }));
618 "Mod+B".action = with-select-window-action ".workspace_id == ($active_workspace | tonumber)" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
619 "Mod+Shift+B".action = with-select-window-action "true" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
620
621 "Mod+H".action = focus-column-left;
622 "Mod+T".action = focus-window-down;
623 "Mod+N".action = focus-window-up;
624 "Mod+S".action = focus-column-right;
625
626 "Mod+Shift+H".action = move-column-left;
627 "Mod+Shift+T".action = move-window-down;
628 "Mod+Shift+N".action = move-window-up;
629 "Mod+Shift+S".action = move-column-right;
630
631 "Mod+Control+H".action = focus-monitor-left;
632 "Mod+Control+T".action = focus-monitor-down;
633 "Mod+Control+N".action = focus-monitor-up;
634 "Mod+Control+S".action = focus-monitor-right;
635
636 "Mod+Shift+Control+H".action = move-workspace-to-monitor-left;
637 "Mod+Shift+Control+T".action = move-workspace-to-monitor-down;
638 "Mod+Shift+Control+N".action = move-workspace-to-monitor-up;
639 "Mod+Shift+Control+S".action = move-workspace-to-monitor-right;
640
641 "Mod+G".action = focus-adjacent-workspace "down";
642 "Mod+C".action = focus-adjacent-workspace "up";
643
644 "Mod+Shift+G".action = move-column-to-adjacent-workspace "down";
645 "Mod+Shift+C".action = move-column-to-adjacent-workspace "up";
646
647 "Mod+Shift+Control+G".action = move-workspace-down;
648 "Mod+Shift+Control+C".action = move-workspace-up;
649
650 "Mod+ParenLeft".action = focus-workspace "comm";
651 "Mod+Shift+ParenLeft".action = move-column-to-workspace "comm";
652
653 "Mod+ParenRight".action = focus-workspace "web";
654 "Mod+Shift+ParenRight".action = move-column-to-workspace "web";
655
656 "Mod+BraceRight".action = focus-workspace "read";
657 "Mod+Shift+BraceRight".action = move-column-to-workspace "read";
658
659 "Mod+BraceLeft".action = focus-workspace "mon";
660 "Mod+Shift+BraceLeft".action = move-column-to-workspace "mon";
661
662 "Mod+Asterisk".action = focus-workspace "vid";
663 "Mod+Shift+Asterisk".action = move-column-to-workspace "vid";
664
665 "Mod+Plus".action = with-unnamed-workspace-action ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
666 "Mod+Shift+Plus".action = with-unnamed-workspace-action ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}}}}'';
667
668 "Mod+M".action = consume-or-expel-window-left;
669 "Mod+W".action = consume-or-expel-window-right;
670
671 "Mod+R".action = switch-preset-column-width;
672 "Mod+Shift+R".action = switch-preset-window-height;
673 "Mod+F".action = center-column;
674 "Mod+Shift+F".action = maximize-column;
675 "Mod+Shift+Ctrl+F".action = fullscreen-window;
676
677 "Mod+V".action = switch-focus-between-floating-and-tiling;
678 "Mod+Shift+V".action = toggle-window-floating;
679
680 "Mod+Left".action = set-column-width "-10%";
681 "Mod+Down".action = set-window-height "-10%";
682 "Mod+Up".action = set-window-height "+10%";
683 "Mod+Right".action = set-column-width "+10%";
684
685 "Mod+Shift+Z" = {
686 action = spawn (lib.getExe niri) "msg" "action" "power-off-monitors";
687 allow-when-locked = true;
688 };
689 "Mod+Shift+L".action = spawn loginctl "lock-session";
690 "Mod+Shift+E".action = quit;
691 "Mod+Shift+Minus" = {
692 action = spawn systemctl "suspend";
693 allow-when-locked = true;
694 };
695 "Mod+Shift+Control+Minus" = {
696 action = spawn systemctl "hibernate";
697 allow-when-locked = true;
698 };
699 "Mod+Shift+P" = {
700 action = spawn (lib.getExe pkgs.playerctl) "-a" "pause";
701 allow-when-locked = true;
702 };
703
704 "XF86MonBrightnessUp" = {
705 action = spawn swayosd-client "--brightness" "raise";
706 allow-when-locked = true;
707 };
708 "XF86MonBrightnessDown" = {
709 action = spawn swayosd-client "--brightness" "lower";
710 allow-when-locked = true;
711 };
712 "XF86AudioRaiseVolume" = {
713 action = spawn swayosd-client "--output-volume" "raise";
714 allow-when-locked = true;
715 };
716 "XF86AudioLowerVolume" = {
717 action = spawn swayosd-client "--output-volume" "lower";
718 allow-when-locked = true;
719 };
720 "XF86AudioMute" = {
721 action = spawn swayosd-client "--output-volume" "mute-toggle";
722 allow-when-locked = true;
723 };
724 "XF86AudioMicMute" = {
725 action = spawn swayosd-client "--input-volume" "mute-toggle";
726 allow-when-locked = true;
727 };
728
729 "Mod+Semicolon".action = spawn makoctl "dismiss" "--group";
730 "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all";
731 "Mod+Period".action = spawn makoctl "menu" (lib.getExe config.programs.fuzzel.package) "--dmenu";
732 "Mod+Comma".action = spawn makoctl "restore";
733
734 "Mod+Control+A".action = focus-or-spawn-action-app_id "com.saivert.pwvucontrol" "pwctl" "pwvucontrol";
735 "Mod+Control+O".action = focus-or-spawn-action-app_id "com.github.wwmm.easyeffects" "eff" "easyeffects";
736 "Mod+Control+P".action = focus-or-spawn-action-app_id "org.keepassxc.KeePassXC" "kpxc" "keepassxc";
737 "Mod+Control+B".action = focus-or-spawn-action-app_id ".blueman-manager-wrapped" "bmgr" "blueman-manager";
738 "Mod+Control+Return".action = focus-or-spawn-action-app_id "kitty-scratch" "term" "kitty" "--app-id" "kitty-scratch";
739 "Mod+Control+E".action = focus-or-spawn-action "select(.app_id == \"emacs\" and .title == \"scratch\")" "edit" "emacsclient" "-c" "--frame-parameters=(quote (name . \"scratch\"))";
740 };
741 };
742 }; 849 };
743} 850}
diff --git a/accounts/gkleen@sif/niri/waybar.nix b/accounts/gkleen@sif/niri/waybar.nix
index 3f1f8119..bae818f6 100644
--- a/accounts/gkleen@sif/niri/waybar.nix
+++ b/accounts/gkleen@sif/niri/waybar.nix
@@ -131,7 +131,7 @@ in {
131 return-type = "json"; 131 return-type = "json";
132 }; 132 };
133 "niri/workspaces" = { 133 "niri/workspaces" = {
134 ignore = ["eff" "pwctl" "kpxc" "bmgr" "edit" "term"]; 134 ignore = map ({ name, ... }: name) config.programs.niri.scratchspaces;
135 }; 135 };
136 "niri/window" = { 136 "niri/window" = {
137 separate-outputs = true; 137 separate-outputs = true;
@@ -217,7 +217,7 @@ in {
217 modules-right = [ "clock" ]; 217 modules-right = [ "clock" ];
218 218
219 "niri/workspaces" = { 219 "niri/workspaces" = {
220 ignore = ["pwctl" "kpxc" "bmgr" "edit" "term"]; 220 ignore = map ({ name, ... }: name) config.programs.niri.scratchspaces;
221 }; 221 };
222 "niri/window" = { 222 "niri/window" = {
223 separate-outputs = true; 223 separate-outputs = true;
diff --git a/accounts/gkleen@sif/ssh-hosts.nix b/accounts/gkleen@sif/ssh-hosts.nix
index 107f1e76..ac930614 100644
--- a/accounts/gkleen@sif/ssh-hosts.nix
+++ b/accounts/gkleen@sif/ssh-hosts.nix
@@ -554,9 +554,4 @@
554 HostKeyAlgorithms = "+ecdsa-sha2-nistp256"; 554 HostKeyAlgorithms = "+ecdsa-sha2-nistp256";
555 }; 555 };
556 }; 556 };
557 "game01" =
558 { hostname = "game01.yggdrasil.li";
559 user = "factorio";
560 identityFile = "~/.ssh/gkleen@sif.midgard.yggdrasil";
561 };
562} 557}
diff --git a/flake.lock b/flake.lock
index 876ebecf..99ea7468 100644
--- a/flake.lock
+++ b/flake.lock
@@ -322,11 +322,11 @@
322 ] 322 ]
323 }, 323 },
324 "locked": { 324 "locked": {
325 "lastModified": 1737831749, 325 "lastModified": 1738691953,
326 "narHash": "sha256-La1xZYZ1yHvT4h5MNl5WC2wxBi2P4vozce2n7V/9+2w=", 326 "narHash": "sha256-JY/w2Xyrj3mhUhLJkSgk8t7MSf3LGZjewPTQ7QtCbHE=",
327 "owner": "gkleen", 327 "owner": "gkleen",
328 "repo": "home-manager", 328 "repo": "home-manager",
329 "rev": "8b16ee252e38acc29ba634ab60672a051ebc9f59", 329 "rev": "c077fc8684289ab1b1c2231bab1566879e099c97",
330 "type": "github" 330 "type": "github"
331 }, 331 },
332 "original": { 332 "original": {
@@ -397,11 +397,11 @@
397 "xwayland-satellite-unstable": "xwayland-satellite-unstable" 397 "xwayland-satellite-unstable": "xwayland-satellite-unstable"
398 }, 398 },
399 "locked": { 399 "locked": {
400 "lastModified": 1737961005, 400 "lastModified": 1739339370,
401 "narHash": "sha256-b4hqJNgyx8lnngz7NFcJ1W+59xQnMQYF0EK5g0IOy7c=", 401 "narHash": "sha256-kvuVhsaVa8j0P9Genf96CLX2cNjForojX5aB1BN+Bwk=",
402 "owner": "sodiboo", 402 "owner": "sodiboo",
403 "repo": "niri-flake", 403 "repo": "niri-flake",
404 "rev": "e98ae62893568dd31e7a7e4e75e1dbbf23f759a0", 404 "rev": "498e8bbc149b38fd14d4ff7fbf31c49fdaa23282",
405 "type": "github" 405 "type": "github"
406 }, 406 },
407 "original": { 407 "original": {
@@ -431,15 +431,15 @@
431 "niri-unstable": { 431 "niri-unstable": {
432 "flake": false, 432 "flake": false,
433 "locked": { 433 "locked": {
434 "lastModified": 1737795105, 434 "lastModified": 1739336386,
435 "narHash": "sha256-OsrjQ8O9t9NjDCwfG/EY8MT+K3lb+A5U6SZZ+4PyKzk=", 435 "narHash": "sha256-H9E3lfJibzWwqV9C1pI81uhav1RLWRA8JbH3ADv3X/4=",
436 "owner": "gkleen", 436 "owner": "YaLTeR",
437 "repo": "niri", 437 "repo": "niri",
438 "rev": "78697d1cea20e6b53013e820999b0403c45d9f00", 438 "rev": "7e552333a993e83a2dba52392109617e486f5f60",
439 "type": "github" 439 "type": "github"
440 }, 440 },
441 "original": { 441 "original": {
442 "owner": "gkleen", 442 "owner": "YaLTeR",
443 "repo": "niri", 443 "repo": "niri",
444 "type": "github" 444 "type": "github"
445 } 445 }
@@ -472,11 +472,11 @@
472 ] 472 ]
473 }, 473 },
474 "locked": { 474 "locked": {
475 "lastModified": 1737861961, 475 "lastModified": 1739071773,
476 "narHash": "sha256-LIRtMvAwLGb8pBoamzgEF67oKlNPz4LuXiRPVZf+TpE=", 476 "narHash": "sha256-/Ak+Quinhmdxa9m3shjm4lwwwqmzG8zzGhhhhgR1k9I=",
477 "owner": "Mic92", 477 "owner": "Mic92",
478 "repo": "nix-index-database", 478 "repo": "nix-index-database",
479 "rev": "79b7b8eae3243fc5aa9aad34ba6b9bbb2266f523", 479 "rev": "895d81b6228bbd50a6ef22f5a58a504ca99763ea",
480 "type": "github" 480 "type": "github"
481 }, 481 },
482 "original": { 482 "original": {
@@ -493,11 +493,11 @@
493 ] 493 ]
494 }, 494 },
495 "locked": { 495 "locked": {
496 "lastModified": 1736736253, 496 "lastModified": 1739078428,
497 "narHash": "sha256-GrktftEfXmmdKOU0yz3QXckDz1ncZ+f4KLU8XnYKYuA=", 497 "narHash": "sha256-9Q8lxL99vaTtK/myj+I6vQvzt3uJiCpazq0jovQswGs=",
498 "owner": "AshleyYakeley", 498 "owner": "AshleyYakeley",
499 "repo": "NixVirt", 499 "repo": "NixVirt",
500 "rev": "9063243af5e6674359a0ff7cec57f02eeacf0cea", 500 "rev": "f2e4e9ad0b02bbd80c509b63d27a2f11359c16a8",
501 "type": "github" 501 "type": "github"
502 }, 502 },
503 "original": { 503 "original": {
@@ -508,11 +508,11 @@
508 }, 508 },
509 "nixos-hardware": { 509 "nixos-hardware": {
510 "locked": { 510 "locked": {
511 "lastModified": 1737751639, 511 "lastModified": 1738816619,
512 "narHash": "sha256-ZEbOJ9iT72iwqXsiEMbEa8wWjyFvRA9Ugx8utmYbpz4=", 512 "narHash": "sha256-5yRlg48XmpcX5b5HesdGMOte+YuCy9rzQkJz+imcu6I=",
513 "owner": "NixOS", 513 "owner": "NixOS",
514 "repo": "nixos-hardware", 514 "repo": "nixos-hardware",
515 "rev": "dfad538f751a5aa5d4436d9781ab27a6128ec9d4", 515 "rev": "2eccff41bab80839b1d25b303b53d339fbb07087",
516 "type": "github" 516 "type": "github"
517 }, 517 },
518 "original": { 518 "original": {
@@ -630,11 +630,11 @@
630 }, 630 },
631 "nixpkgs-stable_2": { 631 "nixpkgs-stable_2": {
632 "locked": { 632 "locked": {
633 "lastModified": 1737885640, 633 "lastModified": 1739206421,
634 "narHash": "sha256-GFzPxJzTd1rPIVD4IW+GwJlyGwBDV1Tj5FLYwDQQ9sM=", 634 "narHash": "sha256-PwQASeL2cGVmrtQYlrBur0U20Xy07uSWVnFup2PHnDs=",
635 "owner": "NixOS", 635 "owner": "NixOS",
636 "repo": "nixpkgs", 636 "repo": "nixpkgs",
637 "rev": "4e96537f163fad24ed9eb317798a79afc85b51b7", 637 "rev": "44534bc021b85c8d78e465021e21f33b856e2540",
638 "type": "github" 638 "type": "github"
639 }, 639 },
640 "original": { 640 "original": {
@@ -678,11 +678,11 @@
678 }, 678 },
679 "nixpkgs_2": { 679 "nixpkgs_2": {
680 "locked": { 680 "locked": {
681 "lastModified": 1737885589, 681 "lastModified": 1739214665,
682 "narHash": "sha256-Zf0hSrtzaM1DEz8//+Xs51k/wdSajticVrATqDrfQjg=", 682 "narHash": "sha256-26L8VAu3/1YRxS8MHgBOyOM8xALdo6N0I04PgorE7UM=",
683 "owner": "NixOS", 683 "owner": "NixOS",
684 "repo": "nixpkgs", 684 "repo": "nixpkgs",
685 "rev": "852ff1d9e153d8875a83602e03fdef8a63f0ecf8", 685 "rev": "64e75cd44acf21c7933d61d7721e812eac1b5a0a",
686 "type": "github" 686 "type": "github"
687 }, 687 },
688 "original": { 688 "original": {
@@ -748,11 +748,11 @@
748 "treefmt-nix": "treefmt-nix" 748 "treefmt-nix": "treefmt-nix"
749 }, 749 },
750 "locked": { 750 "locked": {
751 "lastModified": 1736884309, 751 "lastModified": 1738741221,
752 "narHash": "sha256-eiCqmKl0BIRiYk5/ZhZozwn4/7Km9CWTbc15Cv+VX5k=", 752 "narHash": "sha256-UiTOA89yQV5YNlO1ZAp4IqJUGWOnTyBC83netvt8rQE=",
753 "owner": "nix-community", 753 "owner": "nix-community",
754 "repo": "poetry2nix", 754 "repo": "poetry2nix",
755 "rev": "75d0515332b7ca269f6d7abfd2c44c47a7cbca7b", 755 "rev": "be1fe795035d3d36359ca9135b26dcc5321b31fb",
756 "type": "github" 756 "type": "github"
757 }, 757 },
758 "original": { 758 "original": {
@@ -891,11 +891,11 @@
891 ] 891 ]
892 }, 892 },
893 "locked": { 893 "locked": {
894 "lastModified": 1737411508, 894 "lastModified": 1739262228,
895 "narHash": "sha256-j9IdflJwRtqo9WpM0OfAZml47eBblUHGNQTe62OUqTw=", 895 "narHash": "sha256-7JAGezJ0Dn5qIyA2+T4Dt/xQgAbhCglh6lzCekTVMeU=",
896 "owner": "Mic92", 896 "owner": "Mic92",
897 "repo": "sops-nix", 897 "repo": "sops-nix",
898 "rev": "015d461c16678fc02a2f405eb453abb509d4e1d4", 898 "rev": "07af005bb7d60c7f118d9d9f5530485da5d1e975",
899 "type": "github" 899 "type": "github"
900 }, 900 },
901 "original": { 901 "original": {
@@ -1000,11 +1000,11 @@
1000 "xwayland-satellite-unstable": { 1000 "xwayland-satellite-unstable": {
1001 "flake": false, 1001 "flake": false,
1002 "locked": { 1002 "locked": {
1003 "lastModified": 1737837494, 1003 "lastModified": 1739246919,
1004 "narHash": "sha256-wIMowP8Juas4ZwMRcpc+58sZ0kKTDu8fm13THPmv/F8=", 1004 "narHash": "sha256-/hBM43/Gd0/tW+egrhlWgOIISeJxEs2uAOIYVpfDKeU=",
1005 "owner": "Supreeeme", 1005 "owner": "Supreeeme",
1006 "repo": "xwayland-satellite", 1006 "repo": "xwayland-satellite",
1007 "rev": "3944c9a0e40e5629f16ad023bbc90dac80d35a0f", 1007 "rev": "44590a416d4a3e8220e19e29e0b6efe64a80315d",
1008 "type": "github" 1008 "type": "github"
1009 }, 1009 },
1010 "original": { 1010 "original": {
diff --git a/flake.nix b/flake.nix
index 5cc1e298..4e119e98 100644
--- a/flake.nix
+++ b/flake.nix
@@ -191,7 +191,7 @@
191 ref = "main"; 191 ref = "main";
192 inputs = { 192 inputs = {
193 nixpkgs.follows = "nixpkgs"; 193 nixpkgs.follows = "nixpkgs";
194 niri-unstable.url = "github:gkleen/niri"; 194 # niri-unstable.url = "github:gkleen/niri";
195 }; 195 };
196 }; 196 };
197 }; 197 };
diff --git a/hosts/sif/default.nix b/hosts/sif/default.nix
index 32651e14..2a3a6be9 100644
--- a/hosts/sif/default.nix
+++ b/hosts/sif/default.nix
@@ -477,10 +477,10 @@ in {
477 systemd.tmpfiles.settings = { 477 systemd.tmpfiles.settings = {
478 "10-localtime"."/etc/localtime".L.argument = "/.bcachefs/etc/localtime"; 478 "10-localtime"."/etc/localtime".L.argument = "/.bcachefs/etc/localtime";
479 479
480 "10-regreet"."/var/cache/regreet/cache.toml".C.argument = toString ((pkgs.formats.toml {}).generate "cache.toml" { 480 # "10-regreet"."/var/cache/regreet/cache.toml".C.argument = toString ((pkgs.formats.toml {}).generate "cache.toml" {
481 last_user = "gkleen"; 481 # last_user = "gkleen";
482 user_to_last_sess.gkleen = "niri"; 482 # user_to_last_sess.gkleen = "Niri";
483 }); 483 # });
484 }; 484 };
485 485
486 users = { 486 users = {
diff --git a/hosts/sif/greetd/default.nix b/hosts/sif/greetd/default.nix
index f609fc05..37ca13c5 100644
--- a/hosts/sif/greetd/default.nix
+++ b/hosts/sif/greetd/default.nix
@@ -11,6 +11,11 @@
11 # exec ${pkgs.dbus}/bin/dbus-run-session ${lib.getExe pkgs.cage} ${lib.escapeShellArgs cfg.cageArgs} -- ${lib.getExe cfg.package} 11 # exec ${pkgs.dbus}/bin/dbus-run-session ${lib.getExe pkgs.cage} ${lib.escapeShellArgs cfg.cageArgs} -- ${lib.getExe cfg.package}
12 # ''; 12 # '';
13 }; 13 };
14 systemd.services.greetd.environment = {
15 XKB_DEFAULT_LAYOUT = "us,us";
16 XKB_DEFAULT_VARIANT = "dvp,";
17 XKB_DEFAULT_OPTIONS = "compose:caps,grp:win_space_toggle";
18 };
14 programs.regreet = { 19 programs.regreet = {
15 enable = true; 20 enable = true;
16 theme = { 21 theme = {
diff --git a/hosts/surtr/default.nix b/hosts/surtr/default.nix
index b8a639d5..3bbd51a4 100644
--- a/hosts/surtr/default.nix
+++ b/hosts/surtr/default.nix
@@ -7,6 +7,7 @@ with lib;
7 tmpfs-root qemu-guest openssh rebuild-machines zfs 7 tmpfs-root qemu-guest openssh rebuild-machines zfs
8 ./zfs.nix ./dns ./tls ./http ./bifrost ./matrix ./postgresql 8 ./zfs.nix ./dns ./tls ./http ./bifrost ./matrix ./postgresql
9 ./prometheus ./email ./vpn ./borg.nix ./etebase ./immich.nix 9 ./prometheus ./email ./vpn ./borg.nix ./etebase ./immich.nix
10 ./paperless.nix ./hledger.nix
10 ]; 11 ];
11 12
12 config = { 13 config = {
diff --git a/hosts/surtr/dns/Gupfile b/hosts/surtr/dns/Gupfile
index ac96f620..70674cce 100644
--- a/hosts/surtr/dns/Gupfile
+++ b/hosts/surtr/dns/Gupfile
@@ -1,2 +1,2 @@
1key.gup: 1key.gup:
2 keys/*.yaml \ No newline at end of file 2 keys/* \ No newline at end of file
diff --git a/hosts/surtr/dns/default.nix b/hosts/surtr/dns/default.nix
index ee1d089d..5dd60190 100644
--- a/hosts/surtr/dns/default.nix
+++ b/hosts/surtr/dns/default.nix
@@ -157,7 +157,7 @@ in {
157 ${concatMapStringsSep "\n" mkZone [ 157 ${concatMapStringsSep "\n" mkZone [
158 { domain = "yggdrasil.li"; 158 { domain = "yggdrasil.li";
159 addACLs = { "yggdrasil.li" = ["ymir_acme_acl"]; }; 159 addACLs = { "yggdrasil.li" = ["ymir_acme_acl"]; };
160 acmeDomains = ["surtr.yggdrasil.li" "yggdrasil.li" "etesync.yggdrasil.li" "immich.yggdrasil.li" "app.etesync.yggdrasil.li"]; 160 acmeDomains = ["surtr.yggdrasil.li" "yggdrasil.li" "etesync.yggdrasil.li" "immich.yggdrasil.li" "app.etesync.yggdrasil.li" "paperless.yggdrasil.li" "hledger.yggdrasil.li"];
161 } 161 }
162 { domain = "nights.email"; 162 { domain = "nights.email";
163 addACLs = { "nights.email" = ["ymir_acme_acl"]; }; 163 addACLs = { "nights.email" = ["ymir_acme_acl"]; };
diff --git a/hosts/surtr/dns/key.gup b/hosts/surtr/dns/key.gup
index 32d4f7d6..5b5058b3 100644
--- a/hosts/surtr/dns/key.gup
+++ b/hosts/surtr/dns/key.gup
@@ -3,4 +3,4 @@
3keyName=${${2:t}%.yaml}_key 3keyName=${${2:t}%.yaml}_key
4 4
5keymgr -t ${keyName} > $1 5keymgr -t ${keyName} > $1
6sops -p '7ED22F4AA7BB55728B643DC5471B7D88E4EF66F8,30D3453B8CD02FE2A3E7C78C0FB536FB87AE8F51' --input-type=binary --output-type=binary -e -i $1 \ No newline at end of file 6sops --input-type=binary --output-type=binary -e -i $1
diff --git a/hosts/surtr/dns/keys/hledger.yggdrasil.li_acme b/hosts/surtr/dns/keys/hledger.yggdrasil.li_acme
new file mode 100644
index 00000000..b3f4cfb6
--- /dev/null
+++ b/hosts/surtr/dns/keys/hledger.yggdrasil.li_acme
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:lCj8VYJL9z29FJ154XQtxKQLwwitCRGy4krJ6u8yw2FMzoHprEpFgm33+mFspxSKk/It2G8cfTGMZSeVkYJEHb66HNKHl0A2Fz3hwjpRjh1MZAw0wiZJlnS/LNqoGstQ2PJmTQTW3aJRMoT1GS7q/gSp/3rqySA5EOm0GgUiA3Vi7nGpkBenKDEbQbcIBXRdMOk66BCdiz5XGm/1VLQQLO9oVwY2KBnLaZSISohyGVhbIy7GT2ygoWHHxHn0c5CRVNvGNwesM1gO1NnTFrISLMWSrsDPaAtQ,iv:fa8LFjzqsf2ccfbEe5MOmerb7FzXb4xr24y1GWIMT1Q=,tag:7oQ54DKBb76Pbw1lmEHt+Q==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzcHJLRHh0VkpDNDU3cGZS\nMWFlVWpFemZ2RnpnSllDWU1EWGoyWUxyejNzClBGandzaFI5NXY1bG51Y3VxRk9r\nc29NbXBOaEZDblBuaVowemQydkxBdjgKLS0tICtVb2xkMmh4T0Q0cU4xQnBzZExI\namFRUnRYTWIyQ3RHNUVHWTFrUzhhK1UKqmATNmxlhkxM5PP1U6w7fSYVA8AgIRAt\nJ9WZrTffQfXMdw4RmjWcoVHFH39Fe4SteedxliCCcqjkjgSEB4Rgow==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWlhpQWI4c0x0ZXRGYVE2\nR1dHZzZud0ZxQWsrREhJWUowWE1zem5FVFI0CkJBUnIwY1FGS3N2VnpuSkZjRllZ\ncVgyeVg4cTVjRitzL0RKb0ZQb3BsOEkKLS0tIGs3SDBkamVBNDhQUlh5dmVVZXJs\nM3VKdlFKc21GcFY0UUtiaHFvYWI4V0kKKuWYEncxe9NT2ZS3X3+l/gT4BQOrdCg8\nj2jGL+Yzy/356GO3PFTn2HHLam6KWDKaYB5TlK/zSohfUt5giQH2Lw==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-19T17:13:51Z",
19 "mac": "ENC[AES256_GCM,data:XsBdMCBjB+YuBMZQrjJ5uZtaYKSqsdWVvm+IEoJflCKPIhPk2rBZ3nY8KngXFbq2fWgsYyTM83kb2trEGIEHUPuERt+mgfCI3bSlylriwgsDWihCjyBecNE+BbdXE0+YcNl8pIwBU4M+3f2StQMH22YamToLJ9i9kfKcBrirDuU=,iv:VTIdBVY3kVBMYWhYUmrP2vZ9rpH90DzF68y1aDf2EAs=,tag:YkL+nw6LNXAceZtx9vgf6A==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/surtr/dns/keys/paperless.yggdrasil.li_acme b/hosts/surtr/dns/keys/paperless.yggdrasil.li_acme
new file mode 100644
index 00000000..bc4640db
--- /dev/null
+++ b/hosts/surtr/dns/keys/paperless.yggdrasil.li_acme
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:wOl8KLHD0H+btq0A3UreyVF9bOXZsiTwWJkVH8GubyIQDyiDC8vQm+dfv0rz8TwcBWYpC4aMIPPflG2HsdYO4rKGQ/nBmWmxhNXjpnyRo8iKM1BGb5bxNe4eVcUVhI60NuRJDRLmtDp+0rYGT/MVYp0/mHBINsQCXWBPDoaN2PI2GSnRag/x0wcL27xgH6NDd8glcdCN5nCAPDvazA3LialkXXv7/cceA5Q/Ee6HGzPP0w212/UvBm07Z5tXnHiy5cTbAGTUBfIqC8n501jtaQhpMh/yzA1R8KwUrw==,iv:bLzsthCaanNikNS2Es4J1++E5lijEbjyW5hU4zzNBcg=,tag:eWfZ3AtcSAGv8jWXzqlAwQ==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNYWZKOHd2UmxWYVlIamk4\nZ04zQnAwcDdsSDBDWUZnekNva3BzRERyWXljCmNvUU1Fczc2aUN6VGl6NEJ6MGIx\nWHVpeVluWnRnbjNadGxkSmYyNE1rZzQKLS0tIGpkYVZQRDJGS21ZZHdlRk1MMm02\nQ09aanNXSWltNi9QeUNtUVQ2UEZybmMK6/qcNYLMcyKTmtROX+ZsRqDxMXwkXiAV\ndsdsWJ5+zSJuK5SEIh0fqEZ/t4pxnMcr1WieETgLSd+w0sNQS7EKPQ==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByRjdJOHdjamRHVjlZOTNV\nbTBuam5vNERIWTB6T0JkR2pLUnlQN1BGbUJNClhrZ2hPRWZtT3BERFNwdmNEMmVu\nT0dxcjNkNGIvMVJQWENoUmRhTGd6SXcKLS0tIDV6WDd1bks4K1VuVkgybjdMd0w0\nMHBsT3FmOWU0WnJsM2diQm1sTU1ON2MKtf5HZ0S1cLMx98vDKRKamS7aHIJZ0OnA\nzH4VoeVm+PKsOeqVfY+gMHLdaMEWLKYsz3B8bxIoL5pvnCdT1QAN2A==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-13T19:23:42Z",
19 "mac": "ENC[AES256_GCM,data:o7zNTjkohzAouYpJUGqf8DUfYf4/g3GZgc+4cf+PjI0OF8uc1WDCPvliBFe6pf/8QMhV5DFWd2SfszWnpnQhtiIVG/2BEk5sw3P6r/SUbSErakFYHueVQKp+9rdxK6uKcHUYhO46E332AwIxTuvNeHtSBMxx0kAwQPuuD/u3L4A=,iv:aiM0sGyGMk5lfBOpB2bDFCY+UfWwyUNixieww6eOSLs=,tag:MI7xJ7RsyZgQfF1SBVVmcQ==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/surtr/dns/zones/li.141.soa b/hosts/surtr/dns/zones/li.141.soa
index d42b4719..ab117f09 100644
--- a/hosts/surtr/dns/zones/li.141.soa
+++ b/hosts/surtr/dns/zones/li.141.soa
@@ -1,7 +1,7 @@
1$ORIGIN 141.li. 1$ORIGIN 141.li.
2$TTL 3600 2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li ( 3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2024102100 ; serial 4 2025020900 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -59,5 +59,3 @@ _infinoted._tcp IN SRV 5 0 6523 ymir.yggdrasil.li.
59_submission._tcp IN SRV 5 0 25 ymir.yggdrasil.li. 59_submission._tcp IN SRV 5 0 25 ymir.yggdrasil.li.
60_imap._tcp IN SRV 5 0 143 ymir.yggdrasil.li. 60_imap._tcp IN SRV 5 0 143 ymir.yggdrasil.li.
61_imaps._tcp IN SRV 5 0 993 ymir.yggdrasil.li. 61_imaps._tcp IN SRV 5 0 993 ymir.yggdrasil.li.
62
63_factorio._udp IN SRV 5 0 34197 game01.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/li.yggdrasil.soa b/hosts/surtr/dns/zones/li.yggdrasil.soa
index 9af6232f..2ef120e6 100644
--- a/hosts/surtr/dns/zones/li.yggdrasil.soa
+++ b/hosts/surtr/dns/zones/li.yggdrasil.soa
@@ -1,7 +1,7 @@
1$ORIGIN yggdrasil.li. 1$ORIGIN yggdrasil.li.
2$TTL 3600 2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li ( 3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2025010300 ; serial 4 2025021901 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -77,6 +77,22 @@ _acme-challenge.immich IN NS ns.yggdrasil.li.
77 77
78immich IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::" 78immich IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
79 79
80paperless IN A 202.61.241.61
81paperless IN AAAA 2a03:4000:52:ada::
82paperless IN MX 0 surtr.yggdrasil.li
83paperless IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
84_acme-challenge.paperless IN NS ns.yggdrasil.li.
85
86paperless IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
87
88hledger IN A 202.61.241.61
89hledger IN AAAA 2a03:4000:52:ada::
90hledger IN MX 0 surtr.yggdrasil.li
91hledger IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
92_acme-challenge.hledger IN NS ns.yggdrasil.li.
93
94hledger IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
95
80vidhar IN AAAA 2a03:4000:52:ada:4:1:: 96vidhar IN AAAA 2a03:4000:52:ada:4:1::
81vidhar IN MX 0 ymir.yggdrasil.li 97vidhar IN MX 0 ymir.yggdrasil.li
82vidhar IN TXT "v=spf1 redirect=yggdrasil.li" 98vidhar IN TXT "v=spf1 redirect=yggdrasil.li"
@@ -104,6 +120,3 @@ _infinoted._tcp IN SRV 5 0 6523 ymir.yggdrasil.li.
104_submission._tcp IN SRV 5 0 25 ymir.yggdrasil.li. 120_submission._tcp IN SRV 5 0 25 ymir.yggdrasil.li.
105_imap._tcp IN SRV 5 0 143 ymir.yggdrasil.li. 121_imap._tcp IN SRV 5 0 143 ymir.yggdrasil.li.
106_imaps._tcp IN SRV 5 0 993 ymir.yggdrasil.li. 122_imaps._tcp IN SRV 5 0 993 ymir.yggdrasil.li.
107
108game01 IN A 94.16.107.151
109game01 IN AAAA 2a03:4000:50:13d:34ee:a2ff:fed0:328f
diff --git a/hosts/surtr/hledger.nix b/hosts/surtr/hledger.nix
new file mode 100644
index 00000000..e44933c3
--- /dev/null
+++ b/hosts/surtr/hledger.nix
@@ -0,0 +1,66 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "hledger.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."hledger" = {
13 servers = {
14 "[2a03:4000:52:ada:4:1::]:5000" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "hledger.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/hledger.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/hledger.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/hledger.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://hledger;
35
36 proxy_http_version 1.1;
37 proxy_set_header Upgrade $http_upgrade;
38 proxy_set_header Connection "upgrade";
39
40 proxy_redirect off;
41 proxy_set_header Host $host;
42 proxy_set_header X-Real-IP $remote_addr;
43 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44 proxy_set_header X-Forwarded-Host $server_name;
45 proxy_set_header X-Forwarded-Proto $scheme;
46
47 client_max_body_size 0;
48 proxy_request_buffering off;
49 proxy_buffering off;
50 '';
51 };
52 };
53 };
54 };
55
56 systemd.services.nginx = {
57 serviceConfig = {
58 LoadCredential = [
59 "hledger.yggdrasil.li.key.pem:${config.security.acme.certs."hledger.yggdrasil.li".directory}/key.pem"
60 "hledger.yggdrasil.li.pem:${config.security.acme.certs."hledger.yggdrasil.li".directory}/fullchain.pem"
61 "hledger.yggdrasil.li.chain.pem:${config.security.acme.certs."hledger.yggdrasil.li".directory}/chain.pem"
62 ];
63 };
64 };
65 };
66}
diff --git a/hosts/surtr/paperless.nix b/hosts/surtr/paperless.nix
new file mode 100644
index 00000000..7bc4397c
--- /dev/null
+++ b/hosts/surtr/paperless.nix
@@ -0,0 +1,66 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "paperless.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."paperless" = {
13 servers = {
14 "[2a03:4000:52:ada:4:1::]:28981" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "paperless.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/paperless.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/paperless.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/paperless.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://paperless;
35
36 proxy_http_version 1.1;
37 proxy_set_header Upgrade $http_upgrade;
38 proxy_set_header Connection "upgrade";
39
40 proxy_redirect off;
41 proxy_set_header Host $host;
42 proxy_set_header X-Real-IP $remote_addr;
43 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44 proxy_set_header X-Forwarded-Host $server_name;
45 proxy_set_header X-Forwarded-Proto $scheme;
46
47 client_max_body_size 0;
48 proxy_request_buffering off;
49 proxy_buffering off;
50 '';
51 };
52 };
53 };
54 };
55
56 systemd.services.nginx = {
57 serviceConfig = {
58 LoadCredential = [
59 "paperless.yggdrasil.li.key.pem:${config.security.acme.certs."paperless.yggdrasil.li".directory}/key.pem"
60 "paperless.yggdrasil.li.pem:${config.security.acme.certs."paperless.yggdrasil.li".directory}/fullchain.pem"
61 "paperless.yggdrasil.li.chain.pem:${config.security.acme.certs."paperless.yggdrasil.li".directory}/chain.pem"
62 ];
63 };
64 };
65 };
66}
diff --git a/hosts/surtr/tls/tsig_key.gup b/hosts/surtr/tls/tsig_key.gup
index 825479e5..46a3789e 100644
--- a/hosts/surtr/tls/tsig_key.gup
+++ b/hosts/surtr/tls/tsig_key.gup
@@ -3,4 +3,4 @@
3keyFile=../dns/keys/${2:t}_acme 3keyFile=../dns/keys/${2:t}_acme
4gup -u $keyFile 4gup -u $keyFile
5sops -d --input-type=binary --output-type=binary ${keyFile} | yq -r '.key[0].secret' > $1 5sops -d --input-type=binary --output-type=binary ${keyFile} | yq -r '.key[0].secret' > $1
6sops -p '7ED22F4AA7BB55728B643DC5471B7D88E4EF66F8,30D3453B8CD02FE2A3E7C78C0FB536FB87AE8F51' --input-type=binary -e -i $1 6sops --input-type=binary -e -i $1
diff --git a/hosts/surtr/tls/tsig_keys/hledger.yggdrasil.li b/hosts/surtr/tls/tsig_keys/hledger.yggdrasil.li
new file mode 100644
index 00000000..ab6cdd68
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/hledger.yggdrasil.li
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:Yd70QIj9DE6a5IN+Mf2M5p95vkRMHRg9BXaM686W7BRtthOw9m54/5FK6JWr,iv:cIOIKinkqFFPgTZdewWVY0h6kM5hGfVzuA4iYNhwK5c=,tag:Ds/oI+TOERbIdcGbI4WoEg==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1NnJ4SmsvSlh3ZlliSm9C\nNG45clh3NEZWZ05jaHhVK0xqTVRFL2wxN1ZrCk5hL2p2ZjhtcDBjTEZscXFTNkY5\nemVZSUUwV2V5cFBTdWo4RWxsM2xROVEKLS0tIDBFSFlkUVJ0ajJEUENlelVFKzVk\ndnJhMURMU2o3WVBKZGNVRXBiNytqUFEKHivcSTYy5D770C0h7RsmLBmkIG9+MDoV\ngJHvfkGzXPKwmDTMKdHbIk+ctI+u0/1jMn/K2Q9OFnOIYxP3gHiFag==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArYUNCQ1U2MXpQdmM4SHR0\nTnJWZlFGTUQ1NjVqZ3Z2MGt5NFpBVFp0b0YwCkdseitkbzI1RkZyL3V5Z1pNMG9R\nVnEzRUxrTDQrS3BiMXB1QUFPeUcxZUkKLS0tIGJFODkwNFY3c0tBLzFBNjFiYjJk\nZW82bTdia2F1NHpNSG1IYmhWb1ZCTHMK9ovFx3+x5PrV4y6+RH5XA5DK2wRPXlAt\ncxxpRZIlmnvhZXIeCYE9yhHFmz3uAn0Oib1RUDblca9FlnF9tyYD6g==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-19T17:13:51Z",
19 "mac": "ENC[AES256_GCM,data:ReRuK9qdZV8AbMzA9Yur0AZW+1RF3aRnfBvsKJkQtXsFdkmJQ4QkRGtL27RmjFdvQ3kXBIyhib7hYA60AJ0amduYrSScY0dtz8AurjyE4f2BGQ9/QeKRBfKXHxLvj4/xWNvS4+PVdGKkKbqIs8isz9n77WQQ3lTHop2K/TjaTuQ=,iv:gUhDK9oeUHdpQ2Fp8mFDIgPFo2JjHE0jjooL7FmvmrE=,tag:2lkwSl3j3oqamdLbM9wbow==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/surtr/tls/tsig_keys/paperless.yggdrasil.li b/hosts/surtr/tls/tsig_keys/paperless.yggdrasil.li
new file mode 100644
index 00000000..b1029931
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/paperless.yggdrasil.li
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:D9l0pklD2KDZ4/TXHtXg00MmCnjCVVBG0AK9j5OxxBCyYseCTckp2P/iPOng,iv:DjvuKWPr/jldfk0eZ5+jWHN0RurdruR4Md7AMAPzRQg=,tag:h0c4m3hpATzzb6a7DVmi9w==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3S05aS0ZEcVU2T3BIOG13\nSDJLTUp4OG9ZYVluK2gxbUJDSFRaQ0xnS0YwClFkKzByanJGOWFwbTlISndyU0Rx\nN1FJb3FVaUZOVDBsWEdHTVNGaGNtMVkKLS0tIDI1RmxaMlhVd1FPL1dNdlRGK0Nq\nMFpJZTNnWncrbkV5YS82ZnhGRld0UG8KIuf7bC7GVxaGeR7gwC7kGu/wtBppjq4H\nyDT05CYJf9/EE3K5aJpIOlxyqowRs2SINvIVkyd5ggYkkxCctmGXjQ==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUV3ZqTVJkNWdHeTlKK3Vl\nVTdrdzE2Z3YzVmZxelNFMDNMTUJneHNtNzFRCmQxTU84ekV4bFVLajU0ajB6ZzZK\neEpuQTcyS1o4MW9xTU5nMXVUR0gxTmcKLS0tIGVzZC8xYTc1VkU2RzA2NFQ1K2xz\nLzhPVjBUcytWNGRsdnFob3A4aWljelkKFMlmigcEVzelcEiv6WGya1dsIOJYr7YT\naBHgMttV7zzYHLqvIVJSCz+uw2FqDyqN46twmzFC0HSHeiKbvRrHVw==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-13T19:23:42Z",
19 "mac": "ENC[AES256_GCM,data:0Fcgq0pOZtBBSiK8pUr/jadXMdtbZYFhUbSe+7DQpB8Fo2r8cEoT+Cpcy7tu+l9eXUiDk/tXTBJyMXaW4XWwS/Fe6Zcb95UYaYR1Y6OM9JVPYmwd6QSeC13MwzhYaCDlBiWWq69Zn8grEg7npWo/LS9LK7IEbN7EI8o7QYDI6cw=,iv:C+8ZmVTNWySQ+/6j+YirSwZzoMqXRlgstk47Efxmqps=,tag:Be/6Ve6M/Dcm/6QrbF+JTw==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/vidhar/default.nix b/hosts/vidhar/default.nix
index b0797d8a..90ab40dd 100644
--- a/hosts/vidhar/default.nix
+++ b/hosts/vidhar/default.nix
@@ -4,7 +4,7 @@ with lib;
4 4
5{ 5{
6 imports = with flake.nixosModules.systemProfiles; [ 6 imports = with flake.nixosModules.systemProfiles; [
7 ./zfs.nix ./network ./samba.nix ./dns ./prometheus ./borg ./pgbackrest ./postgresql.nix ./immich.nix 7 ./zfs.nix ./network ./samba.nix ./dns ./prometheus ./borg ./pgbackrest ./postgresql.nix ./immich.nix ./paperless ./hledger
8 tmpfs-root zfs 8 tmpfs-root zfs
9 initrd-all-crypto-modules default-locale openssh rebuild-machines 9 initrd-all-crypto-modules default-locale openssh rebuild-machines
10 build-server 10 build-server
diff --git a/hosts/vidhar/hledger/default.nix b/hosts/vidhar/hledger/default.nix
new file mode 100644
index 00000000..ae080f66
--- /dev/null
+++ b/hosts/vidhar/hledger/default.nix
@@ -0,0 +1,83 @@
1{ config, lib, pkgs, ... }:
2{
3 config = {
4 services.hledger-web = {
5 enable = true;
6 allow = "view";
7 stateDir = "/var/lib/hledger";
8 journalFiles = lib.mkForce ["web.journal"];
9 baseUrl = "https://hledger.yggdrasil.li";
10 extraOptions = [
11 "--socket=/run/hledger-web/http.sock"
12 ];
13 };
14 users = {
15 users.hledger.uid = 982;
16 groups.hledger.gid = 979;
17 };
18 systemd.services.hledger-web = {
19 serviceConfig = {
20 UMask = "0002";
21 ReadOnlyPaths = [ config.services.hledger-web.stateDir ];
22 RuntimeDirectory = [ "hledger-web" ];
23 PrivateDevices = true;
24 StateDirectory = "hledger";
25 CapabilityBoundingSet = "";
26 AmbientCapabilities = "";
27 ProtectSystem = "strict";
28 ProtectKernelTunables = true;
29 ProtectKernelModules = true;
30 ProtectControlGroups = true;
31 ProtectClock = true;
32 ProtectHostname = true;
33 ProtectHome = "tmpfs";
34 ProtectKernelLogs = true;
35 ProtectProc = "invisible";
36 ProcSubset = "pid";
37 PrivateNetwork = false;
38 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
39 SystemCallArchitectures = "native";
40 SystemCallFilter = [
41 "@system-service @resources"
42 "~@obsolete @privileged"
43 ];
44 RestrictSUIDSGID = true;
45 RemoveIPC = true;
46 NoNewPrivileges = true;
47 RestrictRealtime = true;
48 RestrictNamespaces = true;
49 LockPersonality = true;
50 PrivateUsers = true;
51 TemporaryFileSystem = [ "/var/lib/hledger/.cache:mode=0750,uid=${toString (config.users.users.hledger.uid)},gid=${toString (config.users.groups.hledger.gid)}" ];
52 };
53 };
54 services.nginx = {
55 upstreams.hledger = {
56 servers = { "unix:/run/hledger-web/http.sock" = {}; };
57 };
58 virtualHosts."hledger.yggdrasil.li" = {
59 listen = [
60 { addr = "[2a03:4000:52:ada:4:1::]"; port = 5000; }
61 ];
62 extraConfig = ''
63 set_real_ip_from 2a03:4000:52:ada:4::;
64 auth_basic "hledger";
65 auth_basic_user_file "/run/credentials/nginx.service/hledger_users";
66 '';
67 locations."/" = {
68 proxyPass = "http://hledger/";
69 proxyWebsockets = true;
70 };
71 };
72 };
73 systemd.services.nginx.serviceConfig = {
74 SupplementaryGroups = [ "hledger" ];
75 LoadCredential = [ "hledger_users:${config.sops.secrets."hledger_users".path}" ];
76 };
77 sops.secrets."hledger_users" = {
78 format = "binary";
79 sopsFile = ./htpasswd;
80 reloadUnits = [ "nginx.service" ];
81 };
82 };
83}
diff --git a/hosts/vidhar/hledger/htpasswd b/hosts/vidhar/hledger/htpasswd
new file mode 100644
index 00000000..016cb525
--- /dev/null
+++ b/hosts/vidhar/hledger/htpasswd
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:9MNDIAc7ePYk3xQDorX2pU8ybJkJb33RKiJxc2DYauXFNQYxtGwCYhZwod7p7fPh3KqZxBNMRoZXr+/RnV+trsqjAcOOjnXTWLbX6nubq/xm+q0BxEjOPn7FvJF9XOblBeupldo+byGh2CMH9qQv5Fov,iv:3Tym+Mfr48OJet3qDFZPg0XjYr4sNQdNdiu0vUxmzbY=,tag:E0sxRY/jeMVlqH6uAYvD/Q==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3eFBsOEM2ZUNVT2V3LytC\nTUJvUDdKc0VzMyt2cDFKYU03djBjZVFpeVY4CjByMXhPVXRJVjhKQWZvQ2xuOTE3\ncXdJV1lZaHR3cVl0Z0hQaG00M2dGbjQKLS0tIEIzenVxb3cwM3pXTUl1YUZlSlk2\nbDc3VmE5NkEyZ2tRd01OUGZibmhtUlEKxdesIdvzm8s0SmXU5R+tSbmS5Dj24jrb\nEiMERYy1g8GyHR3d2/mU5iOIdsBegSZReUVzomaMT9L7/TmubgOP3g==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age1qffdqvy9arld9zd5a5cylt0n98xhcns5shxhrhwjq5g4qa844ejselaa4l",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPa2RDZzR6cEFYTFA1QkND\nbndVeHVrMVJ0MWZvRmw5VXRhOHlRYllIRWxRCjU4dks4R25LS1RZMHFnbmpQRVZz\nNXhubkJvZFc2amRwMDVtQlE0NnBKNzQKLS0tIHRyeDUxTEFPMEMzWUVkZURzODdm\nSHdqbUpvNmFTS1QveFRpRHdnWHpHb28KnvdUkMkKGiBVHQD7Yv7n6WZjihCGJAR2\nMKl2WAn4g4jzgcXPwwIAIjUrMGSIdGpwCTUDcDnlKWAbRYO2B6P17A==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-19T17:11:17Z",
19 "mac": "ENC[AES256_GCM,data:yBIEqHhr4igoMlRcgg2SigKfejqeuNmuleYolsLJo+QOaW4BHITJTvLxRV1JHPpcMVQkF//zx4ZfUUrb8tTN0znGu3Jnpd0JVagbfCVyEuT6d1SB/GzyUVvoQ2GlcA9us+5gjI4oEJTQCfVqnLDBWsw+jXdr3nEIWo6Mvbqo3lI=,iv:I6Swk4wyd+96+tJKRY/FHlS7ZShMDROcbl+l+ZLRxhM=,tag:P1uQvB4NLdkPEKRMI6lLxw==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/vidhar/network/ruleset.nft b/hosts/vidhar/network/ruleset.nft
index 10fd4c51..1edae167 100644
--- a/hosts/vidhar/network/ruleset.nft
+++ b/hosts/vidhar/network/ruleset.nft
@@ -92,6 +92,8 @@ table inet filter {
92 counter tftp-rx {} 92 counter tftp-rx {}
93 counter pgbackrest-rx {} 93 counter pgbackrest-rx {}
94 counter immich-rx {} 94 counter immich-rx {}
95 counter paperless-rx {}
96 counter hledger-rx {}
95 97
96 counter established-rx {} 98 counter established-rx {}
97 99
@@ -121,6 +123,8 @@ table inet filter {
121 counter tftp-tx {} 123 counter tftp-tx {}
122 counter pgbackrest-tx {} 124 counter pgbackrest-tx {}
123 counter immich-tx {} 125 counter immich-tx {}
126 counter paperless-tx {}
127 counter hledger-tx {}
124 128
125 counter tx {} 129 counter tx {}
126 130
@@ -197,6 +201,8 @@ table inet filter {
197 tcp dport 8432 counter name pgbackrest-rx accept 201 tcp dport 8432 counter name pgbackrest-rx accept
198 202
199 iifname bifrost tcp dport 2283 ip6 saddr $bifrost_surtr counter name immich-rx accept 203 iifname bifrost tcp dport 2283 ip6 saddr $bifrost_surtr counter name immich-rx accept
204 iifname bifrost tcp dport 28981 ip6 saddr $bifrost_surtr counter name paperless-rx accept
205 iifname bifrost tcp dport 5000 ip6 saddr $bifrost_surtr counter name hledger-rx accept
200 206
201 ct state { established, related } counter name established-rx accept 207 ct state { established, related } counter name established-rx accept
202 208
@@ -246,6 +252,8 @@ table inet filter {
246 tcp sport 8432 counter name pgbackrest-tx accept 252 tcp sport 8432 counter name pgbackrest-tx accept
247 253
248 iifname bifrost tcp sport 2283 ip6 daddr $bifrost_surtr counter name immich-tx accept 254 iifname bifrost tcp sport 2283 ip6 daddr $bifrost_surtr counter name immich-tx accept
255 iifname bifrost tcp sport 28981 ip6 daddr $bifrost_surtr counter name paperless-tx accept
256 iifname bifrost tcp sport 5000 ip6 daddr $bifrost_surtr counter name hledger-tx accept
249 257
250 258
251 counter name tx 259 counter name tx
diff --git a/hosts/vidhar/paperless/default.nix b/hosts/vidhar/paperless/default.nix
new file mode 100644
index 00000000..34cd18c4
--- /dev/null
+++ b/hosts/vidhar/paperless/default.nix
@@ -0,0 +1,25 @@
1{ config, ... }:
2
3{
4 config = {
5 services.paperless = {
6 enable = true;
7 address = "[2a03:4000:52:ada:4:1::]";
8 passwordFile = config.sops.secrets."paperless-rootpw".path;
9 settings = {
10 PAPERLESS_OCR_LANGUAGE = "deu+eng";
11 PAPERLESS_URL = "https://paperless.yggdrasil.li";
12 PAPERLESS_FILENAME_FORMAT = "{{ created_year }}/{{ document_type }}/{{ correspondent }}/{{ created }}_{{ doc_pk }}_{{ title }}";
13 PAPERLESS_FILENAME_FORMAT_REMOVE_NONE = "true";
14 PAPERLESS_TASK_WORKERS = "3";
15 PAPERLESS_THREADS_PER_WORKER = "4";
16 };
17 database.createLocally = true;
18 };
19
20 sops.secrets."paperless-rootpw" = {
21 format = "binary";
22 sopsFile = ./rootpw;
23 };
24 };
25}
diff --git a/hosts/vidhar/paperless/rootpw b/hosts/vidhar/paperless/rootpw
new file mode 100644
index 00000000..11f48fcb
--- /dev/null
+++ b/hosts/vidhar/paperless/rootpw
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:Bsns3bLs7aA++eTf2Vh4g2iAXhmrMRTF,iv:zQ6hgXEvgHAloN6UMW54f2nYCvEhHPXQSBVSihHFiC0=,tag:uiGTEs07dpx12PcAjmbr9Q==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlVUJjdEdIZGd6UDJBRXlL\nODFyWDhHOU9oTEVCVlFiUXVXNm9XZmVuampVCkJ0YkFXTlZXVnRldmtlVkJaR3R2\nMFhpaHB5M3pLeDFkUkkzMUFydGNnOFEKLS0tIEJtNWc0V2JaaWYvQlp6TGxVdVZO\neVpzQzB5Um82TUZOeHBHeE50MGlqNWsKj1P54Fc+c5n35+Og9DwBWkvW947hgFsp\ni/G2QcaLHHJMTexTCZYsr1naSVa/cMBAbrZmtjz0HV4Q1kCJtvlrIg==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age1qffdqvy9arld9zd5a5cylt0n98xhcns5shxhrhwjq5g4qa844ejselaa4l",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1UG1QSWtXcFZoQVRBOC9D\nT2VnTW9pcTRCMForcHdZVld0c1NmNFZpWUNBCkRkMERKUVliYXRqb25saWxyb2JN\nbC9YL2ZQbytRM0ZjNmlQOTlTZTQrV2sKLS0tIFZyUWtRcXNqZUZxMGN5d0tHUng2\nVXNSdFEwMmtIVEdVRVlWeVU1YmJVSkUKRJa42k551QtiC6S0tmMv7eVN7GRqpXWz\nvzNh+BM9TOJNaTMmVesr4vXNDLOSFS3PxYv95xuOBzVg3zOHuai72g==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-13T19:20:33Z",
19 "mac": "ENC[AES256_GCM,data:mG6AC3L8MMeZ0Ajr7zV1mzPcHviQw2adtGjSbrbPRw1xqN7siu6svoybv8xkahP2Grq/xKAiyfXFOFo7Uyc3ub5fSovAEolNazqybZYsyam5vHpeC23dXcEkZUJSPJ9/CSB5uI9nX3NPC64QUjCxHZ7qfH5gcXT9D12H8LSqKlQ=,iv:4Skdj8l9jlTX9Unc2xE2hCKVawHBnHR8L4kZA6H8xNw=,tag:zJsJ3S//faAn7AGwLefNoA==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/installer/default.nix b/installer/default.nix
index cd1ee064..ec47832a 100644
--- a/installer/default.nix
+++ b/installer/default.nix
@@ -57,6 +57,10 @@ with lib;
57 57
58 system.disableInstallerTools = false; 58 system.disableInstallerTools = false;
59 59
60 xdg.autostart.enable = lib.mkForce false;
61 xdg.icons.enable = lib.mkForce false;
62 xdg.mime.enable = lib.mkForce false;
63
60 systemd.sysusers.enable = false; 64 systemd.sysusers.enable = false;
61 system.machine-id.generate.enable = false; 65 system.machine-id.generate.enable = false;
62 system.stateVersion = config.system.nixos.release; # No state in installer 66 system.stateVersion = config.system.nixos.release; # No state in installer
diff --git a/modules/backup-utils.nix b/modules/backup-utils.nix
index 82a42ecd..698140da 100644
--- a/modules/backup-utils.nix
+++ b/modules/backup-utils.nix
@@ -9,5 +9,8 @@ with lib;
9 9
10 config = { 10 config = {
11 services.borgsnap.archive-prefix = mkDefault "yggdrasil.${hostName}."; 11 services.borgsnap.archive-prefix = mkDefault "yggdrasil.${hostName}.";
12
13 systemd.services."zfssnap-prune".restartIfChanged = false;
14 systemd.services."zfssnap".restartIfChanged = false;
12 }; 15 };
13} 16}
diff --git a/modules/pgbackrest.nix b/modules/pgbackrest.nix
index 886840b9..81c74a8e 100644
--- a/modules/pgbackrest.nix
+++ b/modules/pgbackrest.nix
@@ -216,6 +216,7 @@ in {
216 }; 216 };
217 }; 217 };
218 } // mapAttrs' (name: backupCfg: nameValuePair "pgbackrest-backup@${escapeSystemdPath name}" { 218 } // mapAttrs' (name: backupCfg: nameValuePair "pgbackrest-backup@${escapeSystemdPath name}" {
219 restartIfChanged = false;
219 description = "Perform pgBackRest Backup (${name}${optionalString (!(isNull backupCfg.repo)) " repo${backupCfg.repo}"})"; 220 description = "Perform pgBackRest Backup (${name}${optionalString (!(isNull backupCfg.repo)) " repo${backupCfg.repo}"})";
220 serviceConfig = { 221 serviceConfig = {
221 Type = "oneshot"; 222 Type = "oneshot";
diff --git a/overlays/keepassxc/database-open-dialog.patch b/overlays/keepassxc/database-open-dialog.patch
index 4916dc1b..dff84846 100644
--- a/overlays/keepassxc/database-open-dialog.patch
+++ b/overlays/keepassxc/database-open-dialog.patch
@@ -1,7 +1,8 @@
1diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/BrowserService.cpp 1diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp
2--- source.orig/src/browser/BrowserService.cpp 2025-01-27 20:55:04.128198171 +0100 2index 60412b5a..c0497d91 100644
3+++ source/src/browser/BrowserService.cpp 2025-01-27 21:16:07.068959077 +0100 3--- a/src/browser/BrowserService.cpp
4@@ -249,7 +249,7 @@ 4+++ b/src/browser/BrowserService.cpp
5@@ -249,7 +249,7 @@ QJsonObject BrowserService::createNewGroup(const QString& groupName)
5 return result; 6 return result;
6 } 7 }
7 8
@@ -10,7 +11,7 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
10 tr("KeePassXC - Create a new group"), 11 tr("KeePassXC - Create a new group"),
11 tr("A request for creating a new group \"%1\" has been received.\n" 12 tr("A request for creating a new group \"%1\" has been received.\n"
12 "Do you want to create this group?\n") 13 "Do you want to create this group?\n")
13@@ -422,7 +422,7 @@ 14@@ -422,7 +422,7 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& entriesToConfirm,
14 15
15 m_dialogActive = true; 16 m_dialogActive = true;
16 updateWindowState(); 17 updateWindowState();
@@ -19,7 +20,7 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
19 20
20 connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &accessControlDialog, SLOT(reject())); 21 connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &accessControlDialog, SLOT(reject()));
21 22
22@@ -512,7 +512,7 @@ 23@@ -512,7 +512,7 @@ QString BrowserService::storeKey(const QString& key)
23 QString id; 24 QString id;
24 25
25 do { 26 do {
@@ -28,7 +29,7 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
28 connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &keyDialog, SLOT(reject())); 29 connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &keyDialog, SLOT(reject()));
29 keyDialog.setWindowTitle(tr("KeePassXC - New key association request")); 30 keyDialog.setWindowTitle(tr("KeePassXC - New key association request"));
30 keyDialog.setLabelText(tr("You have received an association request for the following database:\n%1\n\n" 31 keyDialog.setLabelText(tr("You have received an association request for the following database:\n%1\n\n"
31@@ -535,7 +535,7 @@ 32@@ -535,7 +535,7 @@ QString BrowserService::storeKey(const QString& key)
32 33
33 contains = db->metadata()->customData()->contains(CustomData::BrowserKeyPrefix + id); 34 contains = db->metadata()->customData()->contains(CustomData::BrowserKeyPrefix + id);
34 if (contains) { 35 if (contains) {
@@ -37,7 +38,7 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
37 tr("KeePassXC - Overwrite existing key?"), 38 tr("KeePassXC - Overwrite existing key?"),
38 tr("A shared encryption key with the name \"%1\" " 39 tr("A shared encryption key with the name \"%1\" "
39 "already exists.\nDo you want to overwrite it?") 40 "already exists.\nDo you want to overwrite it?")
40@@ -595,7 +595,7 @@ 41@@ -595,7 +595,7 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
41 const auto existingEntries = getPasskeyEntriesWithUserHandle(rpId, userId, keyList); 42 const auto existingEntries = getPasskeyEntriesWithUserHandle(rpId, userId, keyList);
42 43
43 raiseWindow(); 44 raiseWindow();
@@ -46,7 +47,7 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
46 confirmDialog.registerCredential(username, rpId, existingEntries, timeout); 47 confirmDialog.registerCredential(username, rpId, existingEntries, timeout);
47 48
48 auto dialogResult = confirmDialog.exec(); 49 auto dialogResult = confirmDialog.exec();
49@@ -612,7 +612,7 @@ 50@@ -612,7 +612,7 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
50 // If no entry is selected, show the import dialog for manual entry selection 51 // If no entry is selected, show the import dialog for manual entry selection
51 auto selectedEntry = confirmDialog.getSelectedEntry(); 52 auto selectedEntry = confirmDialog.getSelectedEntry();
52 if (!selectedEntry) { 53 if (!selectedEntry) {
@@ -55,7 +56,7 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
55 const auto result = passkeyImporter.showImportDialog(db, 56 const auto result = passkeyImporter.showImportDialog(db,
56 nullptr, 57 nullptr,
57 origin, 58 origin,
58@@ -683,7 +683,7 @@ 59@@ -683,7 +683,7 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject&
59 const auto timeout = publicKeyOptions["timeout"].toInt(); 60 const auto timeout = publicKeyOptions["timeout"].toInt();
60 61
61 raiseWindow(); 62 raiseWindow();
@@ -64,7 +65,7 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
64 confirmDialog.authenticateCredential(entries, rpId, timeout); 65 confirmDialog.authenticateCredential(entries, rpId, timeout);
65 auto dialogResult = confirmDialog.exec(); 66 auto dialogResult = confirmDialog.exec();
66 if (dialogResult == QDialog::Accepted) { 67 if (dialogResult == QDialog::Accepted) {
67@@ -760,7 +760,7 @@ 68@@ -760,7 +760,7 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
68 69
69 // Ask confirmation if entry already contains a Passkey 70 // Ask confirmation if entry already contains a Passkey
70 if (entry->hasPasskey()) { 71 if (entry->hasPasskey()) {
@@ -73,7 +74,7 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
73 tr("KeePassXC - Update passkey"), 74 tr("KeePassXC - Update passkey"),
74 tr("Entry already has a passkey.\nDo you want to overwrite the passkey in %1 - %2?") 75 tr("Entry already has a passkey.\nDo you want to overwrite the passkey in %1 - %2?")
75 .arg(entry->title(), passkeyUtils()->getUsernameFromEntry(entry)), 76 .arg(entry->title(), passkeyUtils()->getUsernameFromEntry(entry)),
76@@ -873,7 +873,7 @@ 77@@ -873,7 +873,7 @@ bool BrowserService::updateEntry(const EntryParameters& entryParameters, const Q
77 MessageBox::Button dialogResult = MessageBox::No; 78 MessageBox::Button dialogResult = MessageBox::No;
78 if (!browserSettings()->alwaysAllowUpdate()) { 79 if (!browserSettings()->alwaysAllowUpdate()) {
79 raiseWindow(); 80 raiseWindow();
@@ -82,7 +83,7 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
82 tr("KeePassXC - Update Entry"), 83 tr("KeePassXC - Update Entry"),
83 tr("Do you want to update the information in %1 - %2?") 84 tr("Do you want to update the information in %1 - %2?")
84 .arg(QUrl(entryParameters.siteUrl).host(), username), 85 .arg(QUrl(entryParameters.siteUrl).host(), username),
85@@ -909,7 +909,7 @@ 86@@ -909,7 +909,7 @@ bool BrowserService::deleteEntry(const QString& uuid)
86 return false; 87 return false;
87 } 88 }
88 89
@@ -91,7 +92,7 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
91 tr("KeePassXC - Delete entry"), 92 tr("KeePassXC - Delete entry"),
92 tr("A request for deleting entry \"%1\" has been received.\n" 93 tr("A request for deleting entry \"%1\" has been received.\n"
93 "Do you want to delete the entry?\n") 94 "Do you want to delete the entry?\n")
94@@ -1536,7 +1536,7 @@ 95@@ -1536,7 +1536,7 @@ QSharedPointer<Database> BrowserService::selectedDatabase()
95 } 96 }
96 } 97 }
97 98
@@ -100,10 +101,11 @@ diff -u3 -r source.orig/src/browser/BrowserService.cpp source/src/browser/Browse
100 int openDatabaseCount = browserEntrySaveDialog.setItems(databaseWidgets, m_currentDatabaseWidget); 101 int openDatabaseCount = browserEntrySaveDialog.setItems(databaseWidgets, m_currentDatabaseWidget);
101 if (openDatabaseCount > 1) { 102 if (openDatabaseCount > 1) {
102 int res = browserEntrySaveDialog.exec(); 103 int res = browserEntrySaveDialog.exec();
103diff -u3 -r source.orig/src/fdosecrets/objects/Prompt.cpp source/src/fdosecrets/objects/Prompt.cpp 104diff --git a/src/fdosecrets/objects/Prompt.cpp b/src/fdosecrets/objects/Prompt.cpp
104--- source.orig/src/fdosecrets/objects/Prompt.cpp 2025-01-27 20:55:04.135942791 +0100 105index e89cd499..347c98b8 100644
105+++ source/src/fdosecrets/objects/Prompt.cpp 2025-01-27 21:01:37.166710935 +0100 106--- a/src/fdosecrets/objects/Prompt.cpp
106@@ -313,7 +313,7 @@ 107+++ b/src/fdosecrets/objects/Prompt.cpp
108@@ -313,7 +313,7 @@ namespace FdoSecrets
107 if (!entries.isEmpty()) { 109 if (!entries.isEmpty()) {
108 QString app = tr("%1 (PID: %2)").arg(client->name()).arg(client->pid()); 110 QString app = tr("%1 (PID: %2)").arg(client->name()).arg(client->pid());
109 auto ac = new AccessControlDialog( 111 auto ac = new AccessControlDialog(
@@ -112,10 +114,11 @@ diff -u3 -r source.orig/src/fdosecrets/objects/Prompt.cpp source/src/fdosecrets/
112 connect(ac, &AccessControlDialog::finished, this, &UnlockPrompt::itemUnlockFinished); 114 connect(ac, &AccessControlDialog::finished, this, &UnlockPrompt::itemUnlockFinished);
113 connect(ac, &AccessControlDialog::finished, ac, &AccessControlDialog::deleteLater); 115 connect(ac, &AccessControlDialog::finished, ac, &AccessControlDialog::deleteLater);
114 ac->open(); 116 ac->open();
115diff -u3 -r source.orig/src/gui/DatabaseTabWidget.cpp source/src/gui/DatabaseTabWidget.cpp 117diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp
116--- source.orig/src/gui/DatabaseTabWidget.cpp 2025-01-27 20:55:04.134589500 +0100 118index 805d4eab..4836199e 100644
117+++ source/src/gui/DatabaseTabWidget.cpp 2025-01-27 21:07:09.785284837 +0100 119--- a/src/gui/DatabaseTabWidget.cpp
118@@ -41,7 +41,7 @@ 120+++ b/src/gui/DatabaseTabWidget.cpp
121@@ -41,7 +41,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
119 : QTabWidget(parent) 122 : QTabWidget(parent)
120 , m_dbWidgetStateSync(new DatabaseWidgetStateSync(this)) 123 , m_dbWidgetStateSync(new DatabaseWidgetStateSync(this))
121 , m_dbWidgetPendingLock(nullptr) 124 , m_dbWidgetPendingLock(nullptr)
diff --git a/overlays/keepassxc/default.nix b/overlays/keepassxc/default.nix
index 25429a66..46b3a459 100644
--- a/overlays/keepassxc/default.nix
+++ b/overlays/keepassxc/default.nix
@@ -1,7 +1,7 @@
1{ final, prev, ... }: 1{ final, prev, ... }:
2{ 2{
3 keepassxc = prev.keepassxc.overrideAttrs (oldAttrs: { 3 keepassxc = prev.keepassxc.overrideAttrs (oldAttrs: {
4 patches = (oldAttrs.patches or []) ++ [ 4 patches = (oldAttrs.patches or []) ++ prev.lib.optional (prev.lib.versionAtLeast oldAttrs.version "2.7.9") [
5 ./database-open-dialog.patch 5 ./database-open-dialog.patch
6 ]; 6 ];
7 }); 7 });
diff --git a/overlays/swayosd.nix b/overlays/swayosd/default.nix
index 61c865e7..28c8f1b9 100644
--- a/overlays/swayosd.nix
+++ b/overlays/swayosd/default.nix
@@ -23,5 +23,8 @@
23 udev 23 udev
24 sassc 24 sassc
25 ]; 25 ];
26 patches = (oldAttrs.patches or []) ++ [
27 ./exponential.patch
28 ];
26 }); 29 });
27} 30}
diff --git a/overlays/swayosd/exponential.patch b/overlays/swayosd/exponential.patch
new file mode 100644
index 00000000..eb90d739
--- /dev/null
+++ b/overlays/swayosd/exponential.patch
@@ -0,0 +1,57 @@
1diff --git a/src/brightness_backend/brightnessctl.rs b/src/brightness_backend/brightnessctl.rs
2index ccb0e11..740fdb6 100644
3--- a/src/brightness_backend/brightnessctl.rs
4+++ b/src/brightness_backend/brightnessctl.rs
5@@ -107,10 +107,21 @@ impl VirtualDevice {
6 }
7 }
8
9- fn set_percent(&mut self, mut val: u32) -> anyhow::Result<()> {
10- val = val.clamp(0, 100);
11- self.current = self.max.map(|max| val * max / 100);
12- let _: String = self.run(("set", &*format!("{val}%")))?;
13+ fn val_to_percent(&mut self, val: u32) -> u32 {
14+ return ((val as f64 / self.get_max() as f64).powf(0.25) * 100_f64).round() as u32;
15+ }
16+ fn percent_to_val(&mut self, perc: u32) -> u32 {
17+ return ((perc as f64 / 100_f64).powf(4_f64) * self.get_max() as f64).round() as u32;
18+ }
19+
20+ fn set_percent(&mut self, val: u32) -> anyhow::Result<()> {
21+ let new = self.percent_to_val(val);
22+ self.set_val(new)
23+ }
24+ fn set_val(&mut self, val: u32) -> anyhow::Result<()> {
25+ let curr = val.clamp(0, self.get_max());
26+ self.current = Some(curr);
27+ let _: String = self.run(("set", &*format!("{curr}")))?;
28 Ok(())
29 }
30 }
31@@ -134,20 +145,18 @@ impl BrightnessBackend for BrightnessCtl {
32
33 fn lower(&mut self, by: u32) -> anyhow::Result<()> {
34 let curr = self.get_current();
35- let max = self.get_max();
36-
37- let curr = curr * 100 / max;
38+ let mut new = self.device.val_to_percent(curr).saturating_sub(by);
39+ new = self.device.percent_to_val(new).min(curr.saturating_sub(1));
40
41- self.device.set_percent(curr.saturating_sub(by))
42+ self.device.set_val(new)
43 }
44
45 fn raise(&mut self, by: u32) -> anyhow::Result<()> {
46 let curr = self.get_current();
47- let max = self.get_max();
48-
49- let curr = curr * 100 / max;
50+ let mut new = self.device.val_to_percent(curr) + by;
51+ new = self.device.percent_to_val(new).max(curr + 1);
52
53- self.device.set_percent(curr + by)
54+ self.device.set_val(new)
55 }
56
57 fn set(&mut self, val: u32) -> anyhow::Result<()> {
diff --git a/overlays/worktime/worktime/__main__.py b/overlays/worktime/worktime/__main__.py
index 9335afdc..16769953 100755
--- a/overlays/worktime/worktime/__main__.py
+++ b/overlays/worktime/worktime/__main__.py
@@ -224,6 +224,7 @@ class Worktime(object):
224 leave_budget = dict() 224 leave_budget = dict()
225 time_per_day = None 225 time_per_day = None
226 workdays = None 226 workdays = None
227 pull_forward = dict()
227 228
228 @staticmethod 229 @staticmethod
229 @cache 230 @cache
@@ -391,8 +392,6 @@ class Worktime(object):
391 if e.errno != 2: 392 if e.errno != 2:
392 raise e 393 raise e
393 394
394 pull_forward = dict()
395
396 start_day = self.start_date.date() 395 start_day = self.start_date.date()
397 end_day = self.end_date.date() 396 end_day = self.end_date.date()
398 397
@@ -419,15 +418,15 @@ class Worktime(object):
419 if not d == datetime.strptime(c, date_format).replace(tzinfo=tzlocal()).date(): break 418 if not d == datetime.strptime(c, date_format).replace(tzinfo=tzlocal()).date(): break
420 else: 419 else:
421 if d >= self.end_date.date(): 420 if d >= self.end_date.date():
422 pull_forward[d] = min(timedelta(hours = float(hours)), self.time_per_day(d) - (holidays[d] if d in holidays else timedelta())) 421 self.pull_forward[d] = min(timedelta(hours = float(hours)), self.time_per_day(d) - (holidays[d] if d in holidays else timedelta()))
423 except IOError as e: 422 except IOError as e:
424 if e.errno != 2: 423 if e.errno != 2:
425 raise e 424 raise e
426 425
427 self.days_to_work = dict() 426 self.days_to_work = dict()
428 427
429 if pull_forward: 428 if self.pull_forward:
430 end_day = max(end_day, max(list(pull_forward))) 429 end_day = max(end_day, max(list(self.pull_forward)))
431 430
432 for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]: 431 for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]:
433 if day.isoweekday() in self.workdays: 432 if day.isoweekday() in self.workdays:
@@ -471,17 +470,17 @@ class Worktime(object):
471 self.extra_days_to_work[self.now.date()] = timedelta() 470 self.extra_days_to_work[self.now.date()] = timedelta()
472 471
473 self.time_to_work = sum([self.days_to_work[day] for day in self.days_to_work.keys() if day <= self.end_date.date()], timedelta()) 472 self.time_to_work = sum([self.days_to_work[day] for day in self.days_to_work.keys() if day <= self.end_date.date()], timedelta())
474 for day in [d for d in list(pull_forward) if d > self.end_date.date()]: 473 for day in [d for d in list(self.pull_forward) if d > self.end_date.date()]:
475 days_forward = set([d for d in self.days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in pull_forward or d == self.end_date.date())]) 474 days_forward = set([d for d in self.days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in self.pull_forward or d == self.end_date.date())])
476 extra_days_forward = set([d for d in self.extra_days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in pull_forward or d == self.end_date.date())]) 475 extra_days_forward = set([d for d in self.extra_days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in self.pull_forward or d == self.end_date.date())])
477 days_forward = days_forward.union(extra_days_forward) 476 days_forward = days_forward.union(extra_days_forward)
478 477
479 extra_day_time_left = timedelta() 478 extra_day_time_left = timedelta()
480 for extra_day in extra_days_forward: 479 for extra_day in extra_days_forward:
481 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day]) 480 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day])
482 extra_day_time_left += day_time 481 extra_day_time_left += day_time
483 extra_day_time = min(extra_day_time_left, pull_forward[day]) 482 extra_day_time = min(extra_day_time_left, self.pull_forward[day])
484 time_forward = pull_forward[day] - extra_day_time 483 time_forward = self.pull_forward[day] - extra_day_time
485 if extra_day_time_left > timedelta(): 484 if extra_day_time_left > timedelta():
486 for extra_day in extra_days_forward: 485 for extra_day in extra_days_forward:
487 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day]) 486 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day])
@@ -543,7 +542,7 @@ def worktime(pull_forward_cutoff, waybar, **args):
543 542
544 return difference_string 543 return difference_string
545 544
546 difference = worktime.time_to_work - worktime.time_worked 545 difference = worktime.time_to_work - worktime.time_worked + sum(worktime.pull_forward.values(), start=timedelta(milliseconds=0))
547 total_minutes_difference = 5 * ceil(difference / timedelta(minutes = 5)) 546 total_minutes_difference = 5 * ceil(difference / timedelta(minutes = 5))
548 547
549 if worktime.running_entry and abs(difference) < timedelta(days = 1) and (total_minutes_difference > 0 or abs(worktime.running_entry) >= abs(difference)) : 548 if worktime.running_entry and abs(difference) < timedelta(days = 1) and (total_minutes_difference > 0 or abs(worktime.running_entry) >= abs(difference)) :
@@ -571,11 +570,15 @@ def worktime(pull_forward_cutoff, waybar, **args):
571 return f"({difference_string})" 570 return f"({difference_string})"
572 571
573 out_class = "running" if worktime.running_entry else "stopped" 572 out_class = "running" if worktime.running_entry else "stopped"
574 tooltip = tooltip_timedelta(worktime.time_to_work - worktime.time_worked) 573 difference = worktime.time_to_work - worktime.time_worked
574 if worktime.running_entry and -min(timedelta(milliseconds=0), difference) > sum(worktime.pull_forward.values(), start=timedelta(milliseconds=0)) or not worktime.running_entry and max(timedelta(milliseconds=0), difference) > worktime.time_per_day(worktime.now.date()) and worktime.now_is_workday:
575 out_class = "over"
576 tooltip = tooltip_timedelta(difference)
575 if worktime.time_pulled_forward >= min(pull_forward_cutoff, timedelta(seconds = 1)): 577 if worktime.time_pulled_forward >= min(pull_forward_cutoff, timedelta(seconds = 1)):
576 worktime_no_pulled_forward = deepcopy(worktime) 578 worktime_no_pulled_forward = deepcopy(worktime)
577 worktime_no_pulled_forward.time_to_work -= worktime_no_pulled_forward.time_pulled_forward 579 worktime_no_pulled_forward.time_to_work -= worktime_no_pulled_forward.time_pulled_forward
578 worktime_no_pulled_forward.time_pulled_forward = timedelta() 580 worktime_no_pulled_forward.time_pulled_forward = timedelta()
581 worktime_no_pulled_forward.pull_forward = dict()
579 582
580 difference_string = format_worktime(worktime) 583 difference_string = format_worktime(worktime)
581 difference_string_no_pulled_forward = format_worktime(worktime_no_pulled_forward) 584 difference_string_no_pulled_forward = format_worktime(worktime_no_pulled_forward)
@@ -593,6 +596,10 @@ def worktime(pull_forward_cutoff, waybar, **args):
593 else: 596 else:
594 print(out_text) 597 print(out_text)
595 598
599def pull_forward(**args):
600 worktime = Worktime(**args)
601 print(tooltip_timedelta(sum(worktime.pull_forward.values(), start=timedelta(milliseconds=0))))
602
596def time_worked(now, waybar, **args): 603def time_worked(now, waybar, **args):
597 then = now.replace(hour = 0, minute = 0, second = 0, microsecond = 0) 604 then = now.replace(hour = 0, minute = 0, second = 0, microsecond = 0)
598 if now.time() == time(): 605 if now.time() == time():
@@ -604,40 +611,51 @@ def time_worked(now, waybar, **args):
604 worked = now.time_worked - then.time_worked 611 worked = now.time_worked - then.time_worked
605 612
606 out_text = None 613 out_text = None
607 out_class = "stopped" 614 out_class = "running" if now.running_entry else "stopped"
608 tooltip = tooltip_timedelta(worked) 615 tooltip = tooltip_timedelta(worked)
616 target_time = max(then.time_per_day(then.now.date()), now.time_per_day(now.now.date())) if then.time_per_day(then.now.date()) and now.time_per_day(now.now.date()) else (then.time_per_day(then.now.date()) if then.time_per_day(then.now.date()) else now.time_per_day(now.now.date()));
617 difference = target_time - worked
618 difference_pull_forward = difference + now.time_pulled_forward
619 if now.running_entry and difference_pull_forward < timedelta(seconds=0):
620 out_class = "over"
609 if args['do_round']: 621 if args['do_round']:
610 total_minutes_difference = 5 * ceil(worked / timedelta(minutes = 5)) 622 total_minutes_difference = 5 * ceil(worked / timedelta(minutes = 5))
611 (hours_difference, minutes_difference) = divmod(abs(total_minutes_difference), 60) 623 (hours_difference, minutes_difference) = divmod(abs(total_minutes_difference), 60)
612 sign = '' if total_minutes_difference >= 0 else '-' 624 sign = '' if total_minutes_difference >= 0 else '-'
613
614 difference_string = f"{sign}"
615 if hours_difference != 0:
616 difference_string += f"{hours_difference}h"
617 if hours_difference == 0 or minutes_difference != 0:
618 difference_string += f"{minutes_difference}m"
619
620 clockout_time = None
621 clockout_difference = None
622 if then.now_is_workday or now.now_is_workday:
623 target_time = max(then.time_per_day(then.now.date()), now.time_per_day(now.now.date())) if then.time_per_day(then.now.date()) and now.time_per_day(now.now.date()) else (then.time_per_day(then.now.date()) if then.time_per_day(then.now.date()) else now.time_per_day(now.now.date()));
624 difference = target_time - worked
625 clockout_difference = 5 * ceil(difference / timedelta(minutes = 5))
626 clockout_time = now.now + difference
627 exact_clockout_time = clockout_time
628 clockout_time += (5 - clockout_time.minute % 5) * timedelta(minutes = 1)
629 clockout_time = clockout_time.replace(second = 0, microsecond = 0)
630 625
631 if now.running_entry and clockout_time and clockout_difference >= 0: 626 difference_string = f"{sign}"
632 out_class = "running" 627 if hours_difference != 0:
633 out_text = f"{difference_string}/{clockout_time:%H:%M}" 628 difference_string += f"{hours_difference}h"
634 tooltip = f"{tooltip_timedelta(worked)}/{exact_clockout_time:%H:%M}" 629 if hours_difference == 0 or minutes_difference != 0:
635 else: 630 difference_string += f"{minutes_difference}m"
636 if now.running_entry: 631
637 out_class = "over" 632 def round_clockout_time(difference):
638 out_text = difference_string 633 clockout_time = None
634 clockout_difference = None
635 exact_clockout_time = None
636 if then.now_is_workday or now.now_is_workday:
637 clockout_difference = 5 * ceil(difference / timedelta(minutes = 5))
638 clockout_time = now.now + difference
639 exact_clockout_time = clockout_time
640 clockout_time += (5 - clockout_time.minute % 5) * timedelta(minutes = 1)
641 clockout_time = clockout_time.replace(second = 0, microsecond = 0)
642
643 return clockout_time, exact_clockout_time, clockout_difference
644
645 clockout_time, exact_clockout_time, clockout_difference = round_clockout_time(difference)
646 clockout_time_pull_forward, exact_clockout_time_pull_forward, clockout_difference_pull_forward = round_clockout_time(difference_pull_forward)
647 if now.running_entry and clockout_time and (clockout_difference >= 0 or clockout_difference_pull_forward >= 0):
648 out_text = f"{difference_string}/{clockout_time:%H:%M}"
649 tooltip = f"{tooltip_timedelta(worked)}/{exact_clockout_time:%H:%M:%S}"
650
651 if clockout_time_pull_forward != clockout_time:
652 out_text += f"…{clockout_time_pull_forward:%H:%M}"
653 if exact_clockout_time_pull_forward != exact_clockout_time:
654 tooltip += f"…{exact_clockout_time_pull_forward:%H:%M:%S}"
655 else:
656 out_text = difference_string
639 else: 657 else:
640 out_text = str(worked) 658 out_text = str(worked)
641 659
642 if waybar: 660 if waybar:
643 json.dump({"text": out_text, "class": out_class, "tooltip": tooltip}, stdout) 661 json.dump({"text": out_text, "class": out_class, "tooltip": tooltip}, stdout)
@@ -895,6 +913,8 @@ def main():
895 classification_parser.add_argument('--table', action = 'store_true') 913 classification_parser.add_argument('--table', action = 'store_true')
896 classification_parser.add_argument('--table-format', dest='table_format', type=str, default='fancy_grid') 914 classification_parser.add_argument('--table-format', dest='table_format', type=str, default='fancy_grid')
897 classification_parser.set_defaults(cmd = partial(classification, classification_name=classification_name)) 915 classification_parser.set_defaults(cmd = partial(classification, classification_name=classification_name))
916 pull_forward_parser = subparsers.add_parser('pull-forward')
917 pull_forward_parser.set_defaults(cmd = pull_forward)
898 parser.set_default_subparser('time_worked') 918 parser.set_default_subparser('time_worked')
899 args = parser.parse_args() 919 args = parser.parse_args()
900 920