summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.sops.yaml6
-rw-r--r--_sources/generated.json74
-rw-r--r--_sources/generated.nix60
-rw-r--r--accounts/gkleen@eostre.nix8
-rw-r--r--accounts/gkleen@installer.nix8
-rw-r--r--accounts/gkleen@sif/default.nix233
-rw-r--r--accounts/gkleen@sif/emacs.el4
-rw-r--r--accounts/gkleen@sif/niri/default.nix242
-rw-r--r--accounts/gkleen@sif/niri/mako.nix56
-rw-r--r--accounts/gkleen@sif/niri/swayosd.nix7
-rw-r--r--accounts/gkleen@sif/niri/waybar.nix27
-rw-r--r--accounts/gkleen@sif/synadm/default.nix9
-rw-r--r--accounts/gkleen@sif/synadm/synadm_yaml15
-rw-r--r--accounts/gkleen@sif/systemd.nix32
-rw-r--r--accounts/gkleen@sif/utils/async-yt-dlp.nix57
-rw-r--r--accounts/gkleen@sif/utils/sieve-edit.nix24
-rw-r--r--accounts/gkleen@sif/zshrc132
-rw-r--r--accounts/gkleen@surtr.nix8
-rw-r--r--accounts/gkleen@vidhar.nix4
-rw-r--r--accounts/mherold@eostre.nix6
-rw-r--r--accounts/root@installer.nix8
-rw-r--r--accounts/root@sif.nix8
-rw-r--r--accounts/root@surtr.nix8
-rw-r--r--accounts/root@vidhar.nix9
-rw-r--r--flake.lock186
-rw-r--r--flake.nix34
-rw-r--r--home-modules/nixpkgs-release-check.nix4
-rw-r--r--hosts/sif/default.nix83
-rw-r--r--hosts/sif/email/default.nix110
-rw-r--r--hosts/sif/email/relay.crt11
-rw-r--r--hosts/sif/email/relay.key19
-rw-r--r--hosts/sif/email/secrets.yaml (renamed from hosts/sif/mail/secrets.yaml)0
-rw-r--r--hosts/sif/mail/default.nix70
-rw-r--r--hosts/sif/ruleset.nft8
-rw-r--r--hosts/surtr/audiobookshelf.nix66
-rw-r--r--hosts/surtr/bifrost/default.nix4
-rw-r--r--hosts/surtr/default.nix2
-rw-r--r--hosts/surtr/dns/default.nix8
-rw-r--r--hosts/surtr/dns/keys/audiobookshelf.yggdrasil.li_acme19
-rw-r--r--hosts/surtr/dns/keys/kimai.yggdrasil.li_acme19
-rw-r--r--hosts/surtr/dns/zones/email.nights.soa8
-rw-r--r--hosts/surtr/dns/zones/li.141.soa9
-rw-r--r--hosts/surtr/dns/zones/li.kleen.soa9
-rw-r--r--hosts/surtr/dns/zones/li.synapse.soa2
-rw-r--r--hosts/surtr/dns/zones/li.xmpp.soa43
-rw-r--r--hosts/surtr/dns/zones/li.yggdrasil.soa20
-rw-r--r--hosts/surtr/dns/zones/org.dirty-haskell.soa34
-rw-r--r--hosts/surtr/dns/zones/org.praseodym.soa9
-rw-r--r--hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py16
-rw-r--r--hosts/surtr/email/default.nix143
-rw-r--r--hosts/surtr/email/internal-policy-server/.envrc4
-rw-r--r--hosts/surtr/email/internal-policy-server/.gitignore2
-rw-r--r--hosts/surtr/email/internal-policy-server/internal_policy_server/__init__.py0
-rw-r--r--hosts/surtr/email/internal-policy-server/internal_policy_server/__main__.py106
-rw-r--r--hosts/surtr/email/internal-policy-server/pyproject.toml18
-rw-r--r--hosts/surtr/email/internal-policy-server/uv.lock119
-rw-r--r--hosts/surtr/kimai.nix68
-rw-r--r--hosts/surtr/postgresql/default.nix58
-rw-r--r--hosts/surtr/tls/tsig_keys/audiobookshelf.yggdrasil.li19
-rw-r--r--hosts/surtr/tls/tsig_keys/kimai.yggdrasil.li19
-rw-r--r--hosts/surtr/vpn/default.nix8
-rw-r--r--hosts/surtr/vpn/geri.pub2
-rw-r--r--hosts/vidhar/audiobookshelf/abs-podcast-autoplaylist-gkleen.toml19
-rw-r--r--hosts/vidhar/audiobookshelf/default.nix21
-rw-r--r--hosts/vidhar/default.nix2
-rw-r--r--hosts/vidhar/kimai/default.nix89
-rw-r--r--hosts/vidhar/kimai/ruleset.nft149
-rw-r--r--hosts/vidhar/network/default.nix9
-rw-r--r--hosts/vidhar/network/dhcp/default.nix221
-rw-r--r--hosts/vidhar/network/ruleset.nft16
-rw-r--r--hosts/vidhar/paperless/default.nix2
-rw-r--r--hosts/vidhar/prometheus/default.nix3
-rw-r--r--installer-profiles/cd-dvd.nix8
-rw-r--r--installer-profiles/netboot.nix5
-rw-r--r--installer-profiles/nfsroot.nix2
-rw-r--r--installer/default.nix4
-rw-r--r--lib/pythonSet.nix28
-rw-r--r--modules/abs-podcast-autoplaylist.nix55
-rw-r--r--modules/borgcopy/default.nix1
-rw-r--r--modules/i18n.nix156
-rw-r--r--modules/installer.nix56
-rw-r--r--modules/pgbackrest.nix2
-rw-r--r--modules/postsrsd.nix157
-rw-r--r--modules/systemd-run0.nix4
-rw-r--r--modules/tzupdate.nix81
-rw-r--r--modules/uucp.nix373
-rw-r--r--nvfetcher.toml8
-rw-r--r--overlays/abs-podcast-autoplaylist/.envrc4
-rw-r--r--overlays/abs-podcast-autoplaylist/.gitignore2
-rw-r--r--overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__init__.py0
-rw-r--r--overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__main__.py107
-rw-r--r--overlays/abs-podcast-autoplaylist/default.nix19
-rw-r--r--overlays/abs-podcast-autoplaylist/pyproject.toml16
-rw-r--r--overlays/abs-podcast-autoplaylist/uv.lock129
-rw-r--r--overlays/cake-prometheus-exporter/default.nix11
-rw-r--r--overlays/deploy-rs.nix16
-rw-r--r--overlays/inwx-cdnskey/default.nix11
-rw-r--r--overlays/lesspipe.nix2
-rw-r--r--overlays/nftables-prometheus-exporter/default.nix11
-rw-r--r--overlays/nix-direnv/default.nix53
-rw-r--r--overlays/nix-direnv/static-nix.patch62
-rw-r--r--overlays/nix-monitored.nix8
-rw-r--r--overlays/persistent-nix-shell/default.nix5
-rw-r--r--overlays/postsrsd.nix11
-rw-r--r--overlays/scutiger.nix2
-rw-r--r--overlays/swayosd/default.nix4
-rw-r--r--overlays/thunderbird.nix11
-rw-r--r--overlays/uucp/default.nix9
-rw-r--r--overlays/uucp/mailprogram.patch16
-rw-r--r--overlays/waybar-systemd-inhibit/.envrc4
-rw-r--r--overlays/waybar-systemd-inhibit/.gitignore2
-rw-r--r--overlays/waybar-systemd-inhibit/default.nix20
-rw-r--r--overlays/waybar-systemd-inhibit/pyproject.toml17
-rw-r--r--overlays/waybar-systemd-inhibit/uv.lock102
-rw-r--r--overlays/waybar-systemd-inhibit/waybar_systemd_inhibit/__init__.py0
-rw-r--r--overlays/waybar-systemd-inhibit/waybar_systemd_inhibit/__main__.py117
-rw-r--r--overlays/worktime/.envrc4
-rw-r--r--overlays/worktime/.gitignore2
-rw-r--r--overlays/worktime/default.nix26
-rw-r--r--overlays/worktime/poetry.lock284
-rw-r--r--overlays/worktime/pyproject.toml41
-rw-r--r--overlays/worktime/uv.lock248
-rwxr-xr-xoverlays/worktime/worktime/__main__.py398
-rw-r--r--overlays/zte-prometheus-exporter/default.nix11
-rw-r--r--shell.nix16
-rw-r--r--system-profiles/core/default.nix26
-rw-r--r--system-profiles/default-locale.nix27
-rw-r--r--system-profiles/nfsroot.nix2
-rw-r--r--system-profiles/rebuild-machines/default.nix20
-rw-r--r--user-profiles/core.nix6
-rw-r--r--user-profiles/feeds/alot.config50
-rw-r--r--user-profiles/feeds/default.nix11
-rw-r--r--user-profiles/feeds/imm-notmuch-insert.py52
-rw-r--r--user-profiles/feeds/module.nix236
-rw-r--r--user-profiles/tmux/default.nix10
-rw-r--r--user-profiles/tmux/tmux.conf9
-rw-r--r--user-profiles/utils.nix23
-rw-r--r--user-profiles/yt-dlp.nix2
-rw-r--r--user-profiles/zsh/default.nix98
-rw-r--r--user-profiles/zsh/zshrc6
-rw-r--r--users/gkleen/default.nix2
-rw-r--r--users/root.nix2
142 files changed, 4234 insertions, 2024 deletions
diff --git a/.sops.yaml b/.sops.yaml
index 948383b2..a65dca8e 100644
--- a/.sops.yaml
+++ b/.sops.yaml
@@ -8,6 +8,12 @@ creation_rules:
8 - path_regex: ^hosts/surtr/email/ca 8 - path_regex: ^hosts/surtr/email/ca
9 key_groups: 9 key_groups:
10 - age: [ *admin_gkleen ] 10 - age: [ *admin_gkleen ]
11 - path_regex: ^home-modules/lmu-hausschrift/
12 key_groups:
13 - age: [ *admin_gkleen ]
14 - path_regex: ^accounts/gkleen@sif/
15 key_groups:
16 - age: [ *admin_gkleen ]
11 - path_regex: surtr\/?[^\/]*$ 17 - path_regex: surtr\/?[^\/]*$
12 key_groups: 18 key_groups:
13 - age: [ *admin_gkleen, *machine_surtr ] 19 - age: [ *admin_gkleen, *machine_surtr ]
diff --git a/_sources/generated.json b/_sources/generated.json
index b85cb274..d98f141f 100644
--- a/_sources/generated.json
+++ b/_sources/generated.json
@@ -76,12 +76,12 @@
76 "name": null, 76 "name": null,
77 "owner": "Mange", 77 "owner": "Mange",
78 "repo": "emoji-data", 78 "repo": "emoji-data",
79 "rev": "v2.6", 79 "rev": "v2.7",
80 "sha256": "sha256-6nBiT9q139P1pXLqkV1JejE0s2rZn1gUbNsejXJR6RU=", 80 "sha256": "sha256-bUFh0Q7xcnKTBgVBUJU8BH6zzq1Y3krLfJJAgx5TqKs=",
81 "sparseCheckout": [], 81 "sparseCheckout": [],
82 "type": "github" 82 "type": "github"
83 }, 83 },
84 "version": "v2.6" 84 "version": "v2.7"
85 }, 85 },
86 "lesspipe": { 86 "lesspipe": {
87 "cargoLocks": null, 87 "cargoLocks": null,
@@ -99,7 +99,7 @@
99 }, 99 },
100 "mako": { 100 "mako": {
101 "cargoLocks": null, 101 "cargoLocks": null,
102 "date": "2025-03-21", 102 "date": "2025-04-17",
103 "extract": null, 103 "extract": null,
104 "name": "mako", 104 "name": "mako",
105 "passthru": null, 105 "passthru": null,
@@ -109,13 +109,13 @@
109 "fetchSubmodules": false, 109 "fetchSubmodules": false,
110 "leaveDotGit": false, 110 "leaveDotGit": false,
111 "name": null, 111 "name": null,
112 "rev": "2a06a341efae601431a6944f94cfe4965a46cb44", 112 "rev": "84637d1cb42def0ef74d6ec961e05c3900b4c23f",
113 "sha256": "sha256-QH2rxJcNiurHFEtkmq6Ki15k11b6ft6WYiZKbGs7SS0=", 113 "sha256": "sha256-5Mv0P/SoUiJIiYvGm5wvNZhtz2ytwsqqRc3Pk3ju0WA=",
114 "sparseCheckout": [], 114 "sparseCheckout": [],
115 "type": "git", 115 "type": "git",
116 "url": "https://github.com/emersion/mako" 116 "url": "https://github.com/emersion/mako"
117 }, 117 },
118 "version": "2a06a341efae601431a6944f94cfe4965a46cb44" 118 "version": "84637d1cb42def0ef74d6ec961e05c3900b4c23f"
119 }, 119 },
120 "mpv-autosave": { 120 "mpv-autosave": {
121 "cargoLocks": null, 121 "cargoLocks": null,
@@ -223,7 +223,7 @@
223 }, 223 },
224 "mpv-subselect": { 224 "mpv-subselect": {
225 "cargoLocks": null, 225 "cargoLocks": null,
226 "date": "2024-12-22", 226 "date": "2025-04-04",
227 "extract": null, 227 "extract": null,
228 "name": "mpv-subselect", 228 "name": "mpv-subselect",
229 "passthru": null, 229 "passthru": null,
@@ -233,13 +233,13 @@
233 "fetchSubmodules": false, 233 "fetchSubmodules": false,
234 "leaveDotGit": false, 234 "leaveDotGit": false,
235 "name": null, 235 "name": null,
236 "rev": "77d0148aa6aa952f07f06212cabe32d54dfdf49e", 236 "rev": "26d24a0fd1d69988eaedda6056a2c87d0a55b6cb",
237 "sha256": "sha256-VxwwTxE8c8rRQt/m2NA7cRC7+7O1ItYFFGv81nxqIxg=", 237 "sha256": "sha256-+eVga4b7KIBnfrtmlgq/0HNjQVS3SK6YWVXCPvOeOOc=",
238 "sparseCheckout": [], 238 "sparseCheckout": [],
239 "type": "git", 239 "type": "git",
240 "url": "https://github.com/CogentRedTester/mpv-sub-select" 240 "url": "https://github.com/CogentRedTester/mpv-sub-select"
241 }, 241 },
242 "version": "77d0148aa6aa952f07f06212cabe32d54dfdf49e" 242 "version": "26d24a0fd1d69988eaedda6056a2c87d0a55b6cb"
243 }, 243 },
244 "mpv-youtube-quality": { 244 "mpv-youtube-quality": {
245 "cargoLocks": null, 245 "cargoLocks": null,
@@ -261,6 +261,36 @@
261 }, 261 },
262 "version": "1f8c31457459ffc28cd1c3f3c2235a53efad7148" 262 "version": "1f8c31457459ffc28cd1c3f3c2235a53efad7148"
263 }, 263 },
264 "netbootxyz-efi": {
265 "cargoLocks": null,
266 "date": null,
267 "extract": null,
268 "name": "netbootxyz-efi",
269 "passthru": null,
270 "pinned": false,
271 "src": {
272 "name": null,
273 "sha256": "sha256-8kd17ChqLuVH5/OdPc2rVDKEDWHl9ZWLh8k+EBrCGH8=",
274 "type": "url",
275 "url": "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.87/netboot.xyz.efi"
276 },
277 "version": "2.0.87"
278 },
279 "netbootxyz-lkrn": {
280 "cargoLocks": null,
281 "date": null,
282 "extract": null,
283 "name": "netbootxyz-lkrn",
284 "passthru": null,
285 "pinned": false,
286 "src": {
287 "name": null,
288 "sha256": "sha256-/qY3NdRC0SghQ4kamrkm9EFumrKlirqDCJ+XY+jHWLA=",
289 "type": "url",
290 "url": "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.87/netboot.xyz.lkrn"
291 },
292 "version": "2.0.87"
293 },
264 "postfix-mta-sts-resolver": { 294 "postfix-mta-sts-resolver": {
265 "cargoLocks": null, 295 "cargoLocks": null,
266 "date": null, 296 "date": null,
@@ -367,7 +397,7 @@
367 }, 397 },
368 "swayosd": { 398 "swayosd": {
369 "cargoLocks": null, 399 "cargoLocks": null,
370 "date": "2025-03-03", 400 "date": "2025-04-20",
371 "extract": null, 401 "extract": null,
372 "name": "swayosd", 402 "name": "swayosd",
373 "passthru": null, 403 "passthru": null,
@@ -377,13 +407,13 @@
377 "fetchSubmodules": false, 407 "fetchSubmodules": false,
378 "leaveDotGit": false, 408 "leaveDotGit": false,
379 "name": null, 409 "name": null,
380 "rev": "b3c78fce3d90be2ce6a6ffee0e22a50379952e2b", 410 "rev": "ce1f34d80a7f8b4393a5551ea0535bd8beabb28c",
381 "sha256": "sha256-V3V18BoBRJU8mtvwWXvdYPbKBDIHdu5LzVSkDkGJjFU=", 411 "sha256": "sha256-Z9c/5jKxs5ctUuVu7g+BXA1Wy4lyZLpGATtj2jd84jI=",
382 "sparseCheckout": [], 412 "sparseCheckout": [],
383 "type": "git", 413 "type": "git",
384 "url": "https://github.com/ErikReider/SwayOSD" 414 "url": "https://github.com/ErikReider/SwayOSD"
385 }, 415 },
386 "version": "b3c78fce3d90be2ce6a6ffee0e22a50379952e2b" 416 "version": "ce1f34d80a7f8b4393a5551ea0535bd8beabb28c"
387 }, 417 },
388 "tomorrow-night-paradise-theme": { 418 "tomorrow-night-paradise-theme": {
389 "cargoLocks": null, 419 "cargoLocks": null,
@@ -407,7 +437,7 @@
407 }, 437 },
408 "v4l2loopback": { 438 "v4l2loopback": {
409 "cargoLocks": null, 439 "cargoLocks": null,
410 "date": "2025-03-24", 440 "date": "2025-04-29",
411 "extract": null, 441 "extract": null,
412 "name": "v4l2loopback", 442 "name": "v4l2loopback",
413 "passthru": null, 443 "passthru": null,
@@ -419,12 +449,12 @@
419 "name": null, 449 "name": null,
420 "owner": "umlaeute", 450 "owner": "umlaeute",
421 "repo": "v4l2loopback", 451 "repo": "v4l2loopback",
422 "rev": "84cccedd9d3979f0a8ec5478b100ea57adf29696", 452 "rev": "8d806ad688961d8840081a609c39d1a82d296b24",
423 "sha256": "sha256-o31+j5OdMV0e5GvyoGSR1RK6GjTwz9sfhfUK+vbW+b4=", 453 "sha256": "sha256-zuE/qFI8QCWCePmHWjTIPTh2KzmDkwQ2uj5C1dAwo1c=",
424 "sparseCheckout": [], 454 "sparseCheckout": [],
425 "type": "github" 455 "type": "github"
426 }, 456 },
427 "version": "84cccedd9d3979f0a8ec5478b100ea57adf29696" 457 "version": "8d806ad688961d8840081a609c39d1a82d296b24"
428 }, 458 },
429 "xcompose": { 459 "xcompose": {
430 "cargoLocks": null, 460 "cargoLocks": null,
@@ -456,10 +486,10 @@
456 "pinned": false, 486 "pinned": false,
457 "src": { 487 "src": {
458 "name": null, 488 "name": null,
459 "sha256": "sha256-G/4OZg0acKCeJ7LVj5LjCx4uNizEh4KfL4JDRq5J+5E=", 489 "sha256": "sha256-6nOFTF2rwSTymjWo+um8XUIu8yMb6+6ivfqCrBkanCk=",
460 "type": "url", 490 "type": "url",
461 "url": "https://pypi.org/packages/source/y/yt_dlp/yt_dlp-2025.3.31.tar.gz" 491 "url": "https://pypi.org/packages/source/y/yt_dlp/yt_dlp-2025.5.22.tar.gz"
462 }, 492 },
463 "version": "2025.3.31" 493 "version": "2025.5.22"
464 } 494 }
465} \ No newline at end of file 495} \ No newline at end of file
diff --git a/_sources/generated.nix b/_sources/generated.nix
index ddcbbf10..3bf73fed 100644
--- a/_sources/generated.nix
+++ b/_sources/generated.nix
@@ -42,13 +42,13 @@
42 }; 42 };
43 emoji-data = { 43 emoji-data = {
44 pname = "emoji-data"; 44 pname = "emoji-data";
45 version = "v2.6"; 45 version = "v2.7";
46 src = fetchFromGitHub { 46 src = fetchFromGitHub {
47 owner = "Mange"; 47 owner = "Mange";
48 repo = "emoji-data"; 48 repo = "emoji-data";
49 rev = "v2.6"; 49 rev = "v2.7";
50 fetchSubmodules = true; 50 fetchSubmodules = true;
51 sha256 = "sha256-6nBiT9q139P1pXLqkV1JejE0s2rZn1gUbNsejXJR6RU="; 51 sha256 = "sha256-bUFh0Q7xcnKTBgVBUJU8BH6zzq1Y3krLfJJAgx5TqKs=";
52 }; 52 };
53 }; 53 };
54 lesspipe = { 54 lesspipe = {
@@ -61,17 +61,17 @@
61 }; 61 };
62 mako = { 62 mako = {
63 pname = "mako"; 63 pname = "mako";
64 version = "2a06a341efae601431a6944f94cfe4965a46cb44"; 64 version = "84637d1cb42def0ef74d6ec961e05c3900b4c23f";
65 src = fetchgit { 65 src = fetchgit {
66 url = "https://github.com/emersion/mako"; 66 url = "https://github.com/emersion/mako";
67 rev = "2a06a341efae601431a6944f94cfe4965a46cb44"; 67 rev = "84637d1cb42def0ef74d6ec961e05c3900b4c23f";
68 fetchSubmodules = false; 68 fetchSubmodules = false;
69 deepClone = false; 69 deepClone = false;
70 leaveDotGit = false; 70 leaveDotGit = false;
71 sparseCheckout = [ ]; 71 sparseCheckout = [ ];
72 sha256 = "sha256-QH2rxJcNiurHFEtkmq6Ki15k11b6ft6WYiZKbGs7SS0="; 72 sha256 = "sha256-5Mv0P/SoUiJIiYvGm5wvNZhtz2ytwsqqRc3Pk3ju0WA=";
73 }; 73 };
74 date = "2025-03-21"; 74 date = "2025-04-17";
75 }; 75 };
76 mpv-autosave = { 76 mpv-autosave = {
77 pname = "mpv-autosave"; 77 pname = "mpv-autosave";
@@ -136,17 +136,17 @@
136 }; 136 };
137 mpv-subselect = { 137 mpv-subselect = {
138 pname = "mpv-subselect"; 138 pname = "mpv-subselect";
139 version = "77d0148aa6aa952f07f06212cabe32d54dfdf49e"; 139 version = "26d24a0fd1d69988eaedda6056a2c87d0a55b6cb";
140 src = fetchgit { 140 src = fetchgit {
141 url = "https://github.com/CogentRedTester/mpv-sub-select"; 141 url = "https://github.com/CogentRedTester/mpv-sub-select";
142 rev = "77d0148aa6aa952f07f06212cabe32d54dfdf49e"; 142 rev = "26d24a0fd1d69988eaedda6056a2c87d0a55b6cb";
143 fetchSubmodules = false; 143 fetchSubmodules = false;
144 deepClone = false; 144 deepClone = false;
145 leaveDotGit = false; 145 leaveDotGit = false;
146 sparseCheckout = [ ]; 146 sparseCheckout = [ ];
147 sha256 = "sha256-VxwwTxE8c8rRQt/m2NA7cRC7+7O1ItYFFGv81nxqIxg="; 147 sha256 = "sha256-+eVga4b7KIBnfrtmlgq/0HNjQVS3SK6YWVXCPvOeOOc=";
148 }; 148 };
149 date = "2024-12-22"; 149 date = "2025-04-04";
150 }; 150 };
151 mpv-youtube-quality = { 151 mpv-youtube-quality = {
152 pname = "mpv-youtube-quality"; 152 pname = "mpv-youtube-quality";
@@ -162,6 +162,22 @@
162 }; 162 };
163 date = "2020-02-10"; 163 date = "2020-02-10";
164 }; 164 };
165 netbootxyz-efi = {
166 pname = "netbootxyz-efi";
167 version = "2.0.87";
168 src = fetchurl {
169 url = "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.87/netboot.xyz.efi";
170 sha256 = "sha256-8kd17ChqLuVH5/OdPc2rVDKEDWHl9ZWLh8k+EBrCGH8=";
171 };
172 };
173 netbootxyz-lkrn = {
174 pname = "netbootxyz-lkrn";
175 version = "2.0.87";
176 src = fetchurl {
177 url = "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.87/netboot.xyz.lkrn";
178 sha256 = "sha256-/qY3NdRC0SghQ4kamrkm9EFumrKlirqDCJ+XY+jHWLA=";
179 };
180 };
165 postfix-mta-sts-resolver = { 181 postfix-mta-sts-resolver = {
166 pname = "postfix-mta-sts-resolver"; 182 pname = "postfix-mta-sts-resolver";
167 version = "1.5.0"; 183 version = "1.5.0";
@@ -226,17 +242,17 @@
226 }; 242 };
227 swayosd = { 243 swayosd = {
228 pname = "swayosd"; 244 pname = "swayosd";
229 version = "b3c78fce3d90be2ce6a6ffee0e22a50379952e2b"; 245 version = "ce1f34d80a7f8b4393a5551ea0535bd8beabb28c";
230 src = fetchgit { 246 src = fetchgit {
231 url = "https://github.com/ErikReider/SwayOSD"; 247 url = "https://github.com/ErikReider/SwayOSD";
232 rev = "b3c78fce3d90be2ce6a6ffee0e22a50379952e2b"; 248 rev = "ce1f34d80a7f8b4393a5551ea0535bd8beabb28c";
233 fetchSubmodules = false; 249 fetchSubmodules = false;
234 deepClone = false; 250 deepClone = false;
235 leaveDotGit = false; 251 leaveDotGit = false;
236 sparseCheckout = [ ]; 252 sparseCheckout = [ ];
237 sha256 = "sha256-V3V18BoBRJU8mtvwWXvdYPbKBDIHdu5LzVSkDkGJjFU="; 253 sha256 = "sha256-Z9c/5jKxs5ctUuVu7g+BXA1Wy4lyZLpGATtj2jd84jI=";
238 }; 254 };
239 date = "2025-03-03"; 255 date = "2025-04-20";
240 }; 256 };
241 tomorrow-night-paradise-theme = { 257 tomorrow-night-paradise-theme = {
242 pname = "tomorrow-night-paradise-theme"; 258 pname = "tomorrow-night-paradise-theme";
@@ -254,15 +270,15 @@
254 }; 270 };
255 v4l2loopback = { 271 v4l2loopback = {
256 pname = "v4l2loopback"; 272 pname = "v4l2loopback";
257 version = "84cccedd9d3979f0a8ec5478b100ea57adf29696"; 273 version = "8d806ad688961d8840081a609c39d1a82d296b24";
258 src = fetchFromGitHub { 274 src = fetchFromGitHub {
259 owner = "umlaeute"; 275 owner = "umlaeute";
260 repo = "v4l2loopback"; 276 repo = "v4l2loopback";
261 rev = "84cccedd9d3979f0a8ec5478b100ea57adf29696"; 277 rev = "8d806ad688961d8840081a609c39d1a82d296b24";
262 fetchSubmodules = true; 278 fetchSubmodules = true;
263 sha256 = "sha256-o31+j5OdMV0e5GvyoGSR1RK6GjTwz9sfhfUK+vbW+b4="; 279 sha256 = "sha256-zuE/qFI8QCWCePmHWjTIPTh2KzmDkwQ2uj5C1dAwo1c=";
264 }; 280 };
265 date = "2025-03-24"; 281 date = "2025-04-29";
266 }; 282 };
267 xcompose = { 283 xcompose = {
268 pname = "xcompose"; 284 pname = "xcompose";
@@ -278,10 +294,10 @@
278 }; 294 };
279 yt-dlp = { 295 yt-dlp = {
280 pname = "yt-dlp"; 296 pname = "yt-dlp";
281 version = "2025.3.31"; 297 version = "2025.5.22";
282 src = fetchurl { 298 src = fetchurl {
283 url = "https://pypi.org/packages/source/y/yt_dlp/yt_dlp-2025.3.31.tar.gz"; 299 url = "https://pypi.org/packages/source/y/yt_dlp/yt_dlp-2025.5.22.tar.gz";
284 sha256 = "sha256-G/4OZg0acKCeJ7LVj5LjCx4uNizEh4KfL4JDRq5J+5E="; 300 sha256 = "sha256-6nOFTF2rwSTymjWo+um8XUIu8yMb6+6ivfqCrBkanCk=";
285 }; 301 };
286 }; 302 };
287} 303}
diff --git a/accounts/gkleen@eostre.nix b/accounts/gkleen@eostre.nix
index 72818d44..28daf3fd 100644
--- a/accounts/gkleen@eostre.nix
+++ b/accounts/gkleen@eostre.nix
@@ -1,16 +1,16 @@
1{ flake, userName, pkgs, ... }: 1{ flake, userName, pkgs, ... }:
2{ 2{
3 imports = with flake.nixosModules.userProfiles.${userName}; [ 3 imports = with flake.nixosModules.userProfiles.${userName}; [
4 zsh utils tmux 4 utils
5 ]; 5 ];
6 6
7 config = { 7 config = {
8 home-manager.users.${userName} = { 8 home-manager.users.${userName} = {
9 home.stateVersion = "20.09"; 9 home.stateVersion = "20.09";
10 10
11 nixpkgs.config = { 11 # nixpkgs.config = {
12 allowUnfree = true; 12 # allowUnfree = true;
13 }; 13 # };
14 14
15 home.packages = with pkgs; [ 15 home.packages = with pkgs; [
16 thunderbird libreoffice element-desktop keepassxc vlc 16 thunderbird libreoffice element-desktop keepassxc vlc
diff --git a/accounts/gkleen@installer.nix b/accounts/gkleen@installer.nix
index c7a418f8..5fe1db38 100644
--- a/accounts/gkleen@installer.nix
+++ b/accounts/gkleen@installer.nix
@@ -1,7 +1,11 @@
1{ userName, ... }: 1{ flake, userName, ... }:
2 2
3{ 3{
4 home-manager.users.${userName} = { config, ... } : { 4 imports = with flake.nixosModules.userProfiles.${userName}; [
5 zsh tmux
6 ];
7
8 config.home-manager.users.${userName} = { config, ... } : {
5 home.stateVersion = config.home.version.release; 9 home.stateVersion = config.home.version.release;
6 }; 10 };
7} 11}
diff --git a/accounts/gkleen@sif/default.nix b/accounts/gkleen@sif/default.nix
index 11f24f31..706eb241 100644
--- a/accounts/gkleen@sif/default.nix
+++ b/accounts/gkleen@sif/default.nix
@@ -50,9 +50,20 @@ let
50 }; 50 };
51 51
52 lockCommand = "${lib.getExe' config.systemd.package "systemctl"} --user start gtklock.service"; 52 lockCommand = "${lib.getExe' config.systemd.package "systemctl"} --user start gtklock.service";
53
54 editor = pkgs.symlinkJoin {
55 inherit (cfg.services.emacs.package) name;
56 buildInputs = with pkgs; [ makeWrapper ];
57 paths = [ cfg.services.emacs.package ];
58 postBuild = ''
59 wrapProgram $out/bin/emacsclient \
60 --inherit-argv0 \
61 --add-flags ${lib.escapeShellArg (lib.escapeShellArgs cfg.services.emacs.client.arguments)}
62 '';
63 };
53in { 64in {
54 imports = with flake.nixosModules.userProfiles.${userName}; [ 65 imports = with flake.nixosModules.userProfiles.${userName}; [
55 mpv yt-dlp (args: import ./xcompose.nix (inputs // args)) 66 zsh tmux mpv yt-dlp (args: import ./xcompose.nix (inputs // args))
56 ]; 67 ];
57 68
58 config = { 69 config = {
@@ -60,16 +71,17 @@ in {
60 imports = [ 71 imports = [
61 ./libvirt 72 ./libvirt
62 ./niri 73 ./niri
74 ./synadm
63 flakeInputs.nix-index-database.hmModules.nix-index 75 flakeInputs.nix-index-database.hmModules.nix-index
64 flakeInputs.impermanence.nixosModules.home-manager.impermanence 76 flakeInputs.impermanence.nixosModules.home-manager.impermanence
65 ]; 77 ];
66 78
67 home.stateVersion = "20.09"; 79 home.stateVersion = "20.09";
68 80
69 nixpkgs.config = { 81 # nixpkgs.config = {
70 allowUnfree = true; 82 # allowUnfree = true;
71 zathura.useMupdf = false; 83 # zathura.useMupdf = false;
72 }; 84 # };
73 85
74 nix.registry = { 86 nix.registry = {
75 "flk" = { 87 "flk" = {
@@ -175,13 +187,91 @@ in {
175 gpu-api = "vulkan"; 187 gpu-api = "vulkan";
176 }; 188 };
177 189
178 zsh.initExtra = '' 190 zsh.initContent = let
179 source ${./zshrc} 191 zshrc = pkgs.resholve.mkDerivation {
192 pname = "zshrc";
193 version = "0.0.0";
194
195 src = ./zshrc;
196
197 dontUnpack = true;
198 dontConfigure = true;
199 dontBuild = true;
200
201 installPhase = ''
202 mkdir -p $out/share
203 install "$src" $out/share/zshrc
204 '';
205
206 solutions = {
207 default = {
208 scripts = [ "share/zshrc" ];
209 interpreter = "none";
210 inputs = with pkgs; [
211 coreutils
212 rpm
213 binutils
214 squashfsTools
215 unzip
216 cfg.programs.git.package
217 magickWrapped
218 curl
219 file
220 gnutar
221 cpio
222 magic-wormhole
223 cfg.programs.zsh.package
224 fuse
225 util-linux
226 findutils
227 qrencode
228 tty-clock
229 cfg.programs.jq.package
230 eza
231 less
232 config.systemd.package
233 config.programs.ssh.package
234 gnused
235 miniserve
236 ];
237 execer = with pkgs; [
238 "cannot:${lib.getExe' rpm "rpm2cpio"}"
239 "cannot:${lib.getExe' squashfsTools "unsquashfs"}"
240 "cannot:${lib.getExe' unzip "unzip"}"
241 "cannot:${lib.getExe cfg.programs.git.package}"
242 "cannot:${lib.getExe cpio}"
243 "cannot:${lib.getExe' magic-wormhole "wormhole"}"
244 "cannot:${lib.getExe' fuse "fusermount"}"
245 "cannot:${lib.getExe less}"
246 "cannot:${lib.getExe' config.systemd.package "systemctl"}"
247 "cannot:${lib.getExe config.programs.ssh.package}"
248 ];
249 wrapper = with pkgs; [
250 "${lib.getExe' magickWrapped "magick"}:${lib.getExe' imagemagick "magick"}"
251 ];
252 fake = {
253 builtin = ["print"];
254 external = ["sudo" "umount"];
255 };
256 };
257 };
258 };
259 magickWrapped = pkgs.symlinkJoin {
260 inherit (pkgs.imagemagick) name;
261 paths = [ pkgs.imagemagick ];
262
263 buildInputs = with pkgs; [ makeWrapper ];
264 postBuild = ''
265 wrapProgram $out/bin/magick \
266 --prefix PATH : ${lib.makeBinPath (with pkgs; [ ghostscript ])}
267 '';
268 };
269 in ''
270 source ${zshrc}/share/zshrc
180 ''; 271 '';
181 zsh.dirHashes = let 272 zsh.dirHashes = let
182 flakeHashes = mapAttrs' (n: v: nameValuePair (inputNames.${n} or n) (toString v)) flakeInputs; 273 flakeHashes = mapAttrs' (n: v: nameValuePair (inputNames.${n} or n) (toString v)) flakeInputs;
183 inputNames = { 274 inputNames = {
184 "nixpkgs" = "nixos";
185 }; 275 };
186 in flakeHashes // { 276 in flakeHashes // {
187 u2w = "$HOME/projects/uni2work"; 277 u2w = "$HOME/projects/uni2work";
@@ -193,6 +283,16 @@ in {
193 pro = "$HOME/projects/pro"; 283 pro = "$HOME/projects/pro";
194 media = "$HOME/media"; 284 media = "$HOME/media";
195 }; 285 };
286 jq.colors = {
287 arrays = "1;37";
288 "false" = "0;37";
289 "null" = "2;37";
290 numbers = "0;37";
291 objectKeys = "1;34";
292 objects = "1;37";
293 strings = "0;32";
294 "true" = "0;37";
295 };
196 296
197 obs-studio = { 297 obs-studio = {
198 enable = true; 298 enable = true;
@@ -202,7 +302,7 @@ in {
202 gh = { 302 gh = {
203 enable = true; 303 enable = true;
204 settings = { 304 settings = {
205 editor = lib.getExe' config.home-manager.users.${userName}.programs.emacs.package "emacsclient"; 305 editor = lib.getExe' editor "emacsclient";
206 gitProtocol = "ssh"; 306 gitProtocol = "ssh";
207 }; 307 };
208 }; 308 };
@@ -228,16 +328,10 @@ in {
228 # notify_on_cmd_finish = "invisible 120"; 328 # notify_on_cmd_finish = "invisible 120";
229 }; 329 };
230 keybindings = { 330 keybindings = {
231 "kitty_mod+n" = "detach_window"; 331 "kitty_mod+n" = "new_os_window_with_cwd";
232 "kitty_mod+m" = "detach_window ask"; 332 "kitty_mod+m" = "detach_window ask";
233 }; 333 "kitty_mod+enter" = "new_window_with_cwd";
234 }; 334 "kitty_mod+t" = "new_tab_with_cwd";
235 wpaperd = {
236 enable = true;
237 settings.default = {
238 path = "~/.wallpapers";
239 duration = "15m";
240 mode = "center";
241 }; 335 };
242 }; 336 };
243 fuzzel = { 337 fuzzel = {
@@ -250,7 +344,7 @@ in {
250 font = "Fira Sans"; 344 font = "Fira Sans";
251 }; 345 };
252 colors = { 346 colors = {
253 background = "000000aa"; 347 background = "000000cc";
254 text = "cdd6f4ff"; 348 text = "cdd6f4ff";
255 match = "94e2d5ff"; 349 match = "94e2d5ff";
256 selection = "585b70ff"; 350 selection = "585b70ff";
@@ -267,15 +361,28 @@ in {
267 enable = true; 361 enable = true;
268 extraAbbreviations = ["i.A." "d.h." "D.h." "gdw."]; 362 extraAbbreviations = ["i.A." "d.h." "D.h." "gdw."];
269 }; 363 };
364 nushell = {
365 enable = true;
366 settings.show_banner = false;
367 };
368 fd.enable = true;
270 }; 369 };
271 370
272 services = { 371 services = {
372 wpaperd = {
373 enable = true;
374 settings.default = {
375 path = "~/.wallpapers";
376 duration = "15m";
377 mode = "center";
378 };
379 };
273 emacs = { 380 emacs = {
274 enable = true; 381 enable = true;
275 socketActivation.enable = true; 382 socketActivation.enable = true;
276 client = { 383 client = {
277 enable = true; 384 enable = true;
278 arguments = mkForce ["--reuse-frame" "--alternate-editor" "\"\""]; 385 arguments = mkForce ["--create-frame" "--alternate-editor" (lib.getExe cfg.services.emacs.package)];
279 }; 386 };
280 }; 387 };
281 gpg-agent = { 388 gpg-agent = {
@@ -384,6 +491,13 @@ in {
384 }; 491 };
385 }; 492 };
386 493
494 qt.kde.settings = {
495 kwalletrc = {
496 KSecretD.Enabled = false;
497 Wallet."Default Wallet" = "store";
498 };
499 };
500
387 xsession.preferStatusNotifierItems = true; 501 xsession.preferStatusNotifierItems = true;
388 502
389 xresources.properties = import ./xresources.nix; 503 xresources.properties = import ./xresources.nix;
@@ -392,20 +506,19 @@ in {
392 packages = with pkgs; [ 506 packages = with pkgs; [
393 fira fira-code pwvucontrol wrappedKeepassxc wl-clipboard-rs 507 fira fira-code pwvucontrol wrappedKeepassxc wl-clipboard-rs
394 mumble pulseaudio-ctl pamixer libnotify screen-message 508 mumble pulseaudio-ctl pamixer libnotify screen-message
395 wrappedYTMDesktop libsForQt5.qt5ct playerctl evince 509 wrappedYTMDesktop libsForQt5.qt5ct playerctl evince papers
396 thunderbird zoom-us xdg-desktop-portal steam steam-run 510 thunderbird zoom-us xdg-desktop-portal steam steam-run
397 wireshark virt-manager rclone cached-nix-shell worktime 511 wireshark virt-manager rclone cached-nix-shell worktime
398 fira-code-symbols libreoffice xournalpp google-chrome 512 fira-code-symbols libreoffice xournalpp google-chrome
399 nixos-shell virt-viewer freerdp gnome-icon-theme 513 nixos-shell virt-viewer freerdp gnome-icon-theme
400 paper-icon-theme sshpassSecret weechat element-desktop 514 paper-icon-theme sshpassSecret weechat element-desktop
401 flakeInputs.deploy-rs.packages.${config.nixpkgs.system}.deploy-rs 515 sieve-connect gimp3 inkscape udiskie glab nitrokey-app
402 sieve-connect gimp inkscape udiskie glab nitrokey-app
403 pynitrokey gtklock wlrctl remmina openscad spice-record 516 pynitrokey gtklock wlrctl remmina openscad spice-record
404 libguestfs-with-appliance nerd-fonts.fira-mono 517 libguestfs-with-appliance nerd-fonts.fira-mono
405 nerd-fonts.symbols-only nerd-fonts.fira-code powerline-fonts 518 nerd-fonts.symbols-only nerd-fonts.fira-code powerline-fonts
406 swtpm (hunspellWithDicts (with hunspellDicts; [en_GB-large de_DE])) 519 swtpm (hunspellWithDicts (with hunspellDicts; [en_GB-large de_DE]))
407 # synadm 520 libation libqalculate
408 ]; 521 ] ++ mapAttrsToList (_name: pkg: pkgs.callPackage pkg {}) (customUtils.nixImport { dir = ./utils; });
409 522
410 file = { 523 file = {
411 ".backup-munin".source = ./backup-patterns; 524 ".backup-munin".source = ./backup-patterns;
@@ -425,12 +538,9 @@ in {
425 QT_QPA_PLATFORMTHEME = "qt5ct"; 538 QT_QPA_PLATFORMTHEME = "qt5ct";
426 LIBVIRT_DEFAULT_URI = "qemu:///system"; 539 LIBVIRT_DEFAULT_URI = "qemu:///system";
427 STACK_XDG = 1; 540 STACK_XDG = 1;
428 EDITOR = pkgs.writeShellScript "editor" '' 541 EDITOR = lib.getExe' editor "emacsclient";
429 args=("--reuse-frame" "--alternate-editor" "")
430 args+=("$@")
431 exec -a emacsclient ${lib.getExe' cfg.services.emacs.package "emacsclient"} "''${args[@]}"
432 '';
433 RCLONE_PASSWORD_COMMAND = "${lib.getExe' pkgs.libsecret "secret-tool"} lookup service rclone"; 542 RCLONE_PASSWORD_COMMAND = "${lib.getExe' pkgs.libsecret "secret-tool"} lookup service rclone";
543 SYSTEMD_TINT_BACKGROUND = "false";
434 }; 544 };
435 545
436 extraProfileCommands = '' 546 extraProfileCommands = ''
@@ -467,9 +577,17 @@ in {
467 General = { 577 General = {
468 dot_as_separator = 0; 578 dot_as_separator = 0;
469 }; 579 };
580 Mode = {
581 calculate_as_you_type = 1;
582 };
470 }; 583 };
471 }; 584 };
472 "emacs/init.el".source = ./emacs.el; 585 "emacs/init.el".source = pkgs.substitute {
586 src = ./emacs.el;
587 substitutions = [
588 "--subst-var-by" "ksshaskpass" (lib.getExe pkgs.kdePackages.ksshaskpass)
589 ];
590 };
473 "systemd/user/xdg-desktop-portal.service.d/after-graphical-session.conf".text = '' 591 "systemd/user/xdg-desktop-portal.service.d/after-graphical-session.conf".text = ''
474 [Unit] 592 [Unit]
475 After=graphical-session.target 593 After=graphical-session.target
@@ -487,6 +605,8 @@ in {
487 xdg.dataFile = { 605 xdg.dataFile = {
488 "dbus-1/services/org.keepassxc.KeePassXC.service".source = "${wrappedKeepassxc}/share/dbus-1/services/org.keepassxc.KeePassXC.service"; 606 "dbus-1/services/org.keepassxc.KeePassXC.service".source = "${wrappedKeepassxc}/share/dbus-1/services/org.keepassxc.KeePassXC.service";
489 "dbus-1/services/org.freedesktop.secrets.service.service".source = "${wrappedKeepassxc}/share/dbus-1/services/org.freedesktop.secrets.service.service"; 607 "dbus-1/services/org.freedesktop.secrets.service.service".source = "${wrappedKeepassxc}/share/dbus-1/services/org.freedesktop.secrets.service.service";
608 "dbus-1/services/org.kde.kwalletd6.service".source = "${pkgs.kdePackages.kwallet}/share/dbus-1/services/org.kde.kwalletd6.service";
609 "dbus-1/services/org.kde.kwalletd5.service".source = "${pkgs.kdePackages.kwallet}/share/dbus-1/services/org.kde.kwalletd5.service";
490 "emoji-data/list.txt".source = pkgs.stdenv.mkDerivation { 610 "emoji-data/list.txt".source = pkgs.stdenv.mkDerivation {
491 inherit (sources.emoji-data) pname src; 611 inherit (sources.emoji-data) pname src;
492 version = lib.removePrefix "v" sources.emoji-data.version; 612 version = lib.removePrefix "v" sources.emoji-data.version;
@@ -574,10 +694,10 @@ in {
574 exec -- \ 694 exec -- \
575 ${lib.getExe' config.systemd.package "systemd-run"} --wait --user --slice-inherit \ 695 ${lib.getExe' config.systemd.package "systemd-run"} --wait --user --slice-inherit \
576 --property 'CPUAccounting=yes' --property 'CPUQuotaPeriodSec=50ms' \ 696 --property 'CPUAccounting=yes' --property 'CPUQuotaPeriodSec=50ms' \
577 --property 'Environment=DSCP=46' \ 697 -E DSCP=46 -E NIXOS_OZONE_WL \
578 -- ${lib.getExe pkgs.dscp} ${lib.getExe' pkgs.google-chrome "google-chrome-stable"} \ 698 -- ${lib.getExe pkgs.dscp} ${lib.getExe' pkgs.google-chrome "google-chrome-stable"} \
579 --class=Rainbow \ 699 --class=Rainbow \
580 --kiosk "https://web.openrainbow.com" \ 700 --app="https://web.openrainbow.com" \
581 --user-data-dir=''${HOME}/.config/google-chrome-rainbow 701 --user-data-dir=''${HOME}/.config/google-chrome-rainbow
582 ''); 702 '');
583 icon = pkgs.fetchurl { 703 icon = pkgs.fetchurl {
@@ -588,6 +708,53 @@ in {
588 StartupWMClass = "Rainbow"; 708 StartupWMClass = "Rainbow";
589 }; 709 };
590 }; 710 };
711 kimai = {
712 name = "Kimai";
713 exec = toString (pkgs.writeShellScript "kimai" ''
714 exec -- \
715 ${lib.getExe' pkgs.google-chrome "google-chrome-stable"} \
716 --class=Kimai \
717 --app="https://kimai.yggdrasil.li" \
718 --user-data-dir=''${HOME}/.config/google-chrome-kimai
719 '');
720 icon = pkgs.fetchurl {
721 url = "https://www.kimai.org/images/kimai_logo.png";
722 hash = "sha256-lnlOttzR2SwXA70R+egJUkeKr4U5V0avqTk8uX4bqfs=";
723 };
724 settings = {
725 StartupWMClass = "Kimai";
726 StartupNotify = "true";
727 };
728 };
729 audiobookshelf = {
730 name = "Audiobookshelf";
731 exec = toString (pkgs.writeShellScript "audiobookshelf" ''
732 exec -- \
733 ${lib.getExe' pkgs.google-chrome "google-chrome-stable"} \
734 --class=Audiobookshelf \
735 --app="https://audiobookshelf.yggdrasil.li" \
736 --user-data-dir=''${HOME}/.config/google-chrome-audiobookshelf
737 '');
738 icon = pkgs.fetchurl {
739 url = "https://www.audiobookshelf.org/Logo.png";
740 hash = "sha256-JGPk+WNT1C4DC4lSMb0K0YmAMT5LvmSOeO0QRzkc7Lk=";
741 };
742 settings = {
743 StartupWMClass = "Audiobookshelf";
744 StartupNotify = "true";
745 };
746 };
747 thunderbird-lmu = {
748 name = "Thunderbird (LMU)";
749 exec = "thunderbird --name thunderbird -P lmu %U";
750 icon = "thunderbird";
751 genericName = "Email Client";
752 categories = [ "Network" "Chat" "Email" "Feed" "GTK" "News" ];
753 settings = {
754 StartupWMClass = "thunderbird";
755 StartupNotify = "true";
756 };
757 };
591 }; 758 };
592 759
593 fonts = { 760 fonts = {
diff --git a/accounts/gkleen@sif/emacs.el b/accounts/gkleen@sif/emacs.el
index 5cee16b0..3beefba6 100644
--- a/accounts/gkleen@sif/emacs.el
+++ b/accounts/gkleen@sif/emacs.el
@@ -51,7 +51,7 @@
51 51
52;; (require 'scratch) 52;; (require 'scratch)
53(global-set-key (kbd "C-x B") 'scratch-create) 53(global-set-key (kbd "C-x B") 'scratch-create)
54(setq initial-major-mode 'scratch-mode) 54;; (setq initial-major-mode 'scratch-mode)
55(setq initial-scratch-message "") 55(setq initial-scratch-message "")
56 56
57(global-set-key (kbd "C-x K") 'kill-current-buffer) 57(global-set-key (kbd "C-x K") 'kill-current-buffer)
@@ -254,3 +254,5 @@ necessarily running."
254(bind-key "C-x C-m" #'move-file) 254(bind-key "C-x C-m" #'move-file)
255 255
256(let ((ssh_auth_sock (string-chop-newline (shell-command-to-string "gpgconf --list-dirs agent-ssh-socket")))) (setenv "SSH_AUTH_SOCK" ssh_auth_sock)) 256(let ((ssh_auth_sock (string-chop-newline (shell-command-to-string "gpgconf --list-dirs agent-ssh-socket")))) (setenv "SSH_AUTH_SOCK" ssh_auth_sock))
257(setenv "SSH_ASKPASS_REQUIRE" "prefer")
258(setenv "SSH_ASKPASS" "@ksshaskpass@")
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
index 216a98ea..8752f3e3 100644
--- a/accounts/gkleen@sif/niri/default.nix
+++ b/accounts/gkleen@sif/niri/default.nix
@@ -35,7 +35,11 @@ let
35 if jq -e '.is_focused' <<<"$window_json" >/dev/null; then 35 if jq -e '.is_focused' <<<"$window_json" >/dev/null; then
36 niri msg action focus-workspace-previous 36 niri msg action focus-workspace-previous
37 else 37 else
38 niri msg action focus-window --id "$(jq -r '.id' <<<"$window_json")" 38 if [[ $(jq -r --arg workspace_name "$workspace_name" 'map(select(.name == $workspace_name)) | .[0].is_focused' <<<"$workspaces_json") != "true" ]] && [[ $(jq -r --arg workspace_name "$workspace_name" 'map(select(.name == $workspace_name)) | .[0].id' <<<"$workspaces_json") = $(jq -r '.workspace_id' <<<"$window_json") ]]; then
39 niri msg action focus-workspace "$workspace_name"
40 else
41 niri msg action focus-window --id "$(jq -r '.id' <<<"$window_json")"
42 fi
39 fi 43 fi
40 exit 0 44 exit 0
41 fi 45 fi
@@ -45,7 +49,6 @@ let
45 ''; 49 '';
46 }; 50 };
47 focus-or-spawn-action = config.lib.niri.actions.spawn (lib.getExe focus_or_spawn); 51 focus-or-spawn-action = config.lib.niri.actions.spawn (lib.getExe focus_or_spawn);
48 focus-or-spawn-action-app_id = app_id: focus-or-spawn-action ''select(.app_id == "${app_id}")'';
49 52
50 with_adjacent_workspace = pkgs.writeShellApplication { 53 with_adjacent_workspace = pkgs.writeShellApplication {
51 name = "with-adjacent-workspace"; 54 name = "with-adjacent-workspace";
@@ -84,7 +87,7 @@ let
84 }; 87 };
85 with-adjacent-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_adjacent_workspace) "^${lib.concatMapStringsSep "|" ({ name, ...}: name) cfg.scratchspaces}$"; 88 with-adjacent-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_adjacent_workspace) "^${lib.concatMapStringsSep "|" ({ name, ...}: name) cfg.scratchspaces}$";
86 focus-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}''; 89 focus-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
87 move-column-to-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}}}}''; 90 move-column-to-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}, "focus": true}}}'';
88 91
89 with_unnamed_workspace = pkgs.writeShellApplication { 92 with_unnamed_workspace = pkgs.writeShellApplication {
90 name = "with-unnamed-workspace"; 93 name = "with-unnamed-workspace";
@@ -131,7 +134,7 @@ let
131 134
132 windows_json="$(niri msg -j windows)" 135 windows_json="$(niri msg -j windows)"
133 active_workspace="$(jq -r '.[] | select(.is_focused) | .workspace_id' <<<"$windows_json")" 136 active_workspace="$(jq -r '.[] | select(.is_focused) | .workspace_id' <<<"$windows_json")"
134 window_ix="$(gojq -r --arg active_workspace "$active_workspace" '.[] | select('"$window_select"') | "\(.title)\u0000icon\u001f\(.app_id)"' <<<"$windows_json" | fuzzel --log-level=warning --dmenu --index)" 137 window_ix="$(gojq -r --arg active_workspace "$active_workspace" '.[] | select('"$window_select"') | "\(.title)\u0000icon\u001f\(.app_id)"' <<<"$windows_json" | fuzzel --width=60 --log-level=warning --dmenu --index)"
135 # shellcheck disable=SC2016 138 # shellcheck disable=SC2016
136 window_json="$(gojq -rc --arg active_workspace "$active_workspace" --arg window_ix "$window_ix" 'map(select('"$window_select"')) | .[($window_ix | tonumber)]' <<<"$windows_json")" 139 window_json="$(gojq -rc --arg active_workspace "$active_workspace" --arg window_ix "$window_ix" 'map(select('"$window_select"')) | .[($window_ix | tonumber)]' <<<"$windows_json")"
137 140
@@ -141,6 +144,25 @@ let
141 ''; 144 '';
142 }; 145 };
143 with-select-window-action = config.lib.niri.actions.spawn (lib.getExe with_select_window); 146 with-select-window-action = config.lib.niri.actions.spawn (lib.getExe with_select_window);
147
148 with_predicate_window = pred: pkgs.writeShellApplication {
149 name = "with-predicate-window";
150 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
151 text = ''
152 action="$1"
153 shift
154
155 windows_json="$(niri msg -j windows)"
156 window_json="$(gojq -rc 'map(select(${pred})) | .[0]' <<<"$windows_json")"
157
158 [[ -z "$window_json" || $window_json = "null" ]] && exit 1
159
160 jq -c "$action" <<<"$window_json" | socat STDIO "$NIRI_SOCKET"
161 '';
162 };
163
164 with-urgent-window-action = config.lib.niri.actions.spawn (lib.getExe (with_predicate_window ".is_urgent"));
165 with-focused-window-action = config.lib.niri.actions.spawn (lib.getExe (with_predicate_window ".is_focused"));
144in { 166in {
145 imports = [ 167 imports = [
146 ./waybar.nix 168 ./waybar.nix
@@ -171,6 +193,17 @@ in {
171 type = lib.types.nullOr lib.types.str; 193 type = lib.types.nullOr lib.types.str;
172 default = null; 194 default = null;
173 }; 195 };
196 moveKey = lib.mkOption {
197 type = lib.types.nullOr lib.types.str;
198 default = let
199 keys = lib.splitString "+" config.key;
200 defMoveKey = lib.concatStringsSep "+" (lib.flatten [
201 (lib.take (lib.length keys - 1) keys)
202 ["Shift"]
203 (lib.takeEnd 1 keys)
204 ]);
205 in if config.key == null then null else defMoveKey;
206 };
174 spawn = lib.mkOption { 207 spawn = lib.mkOption {
175 type = lib.types.nullOr (lib.types.listOf lib.types.str); 208 type = lib.types.nullOr (lib.types.listOf lib.types.str);
176 default = null; 209 default = null;
@@ -245,11 +278,11 @@ in {
245 Service = { 278 Service = {
246 Type = "simple"; 279 Type = "simple";
247 Sockets = [ "niri-workspace-history.socket" ]; 280 Sockets = [ "niri-workspace-history.socket" ];
248 ExecStart = pkgs.writers.writePython3 "niri-workspace-history" {} '' 281 ExecStart = pkgs.writers.writePython3 "niri-workspace-history" { flakeIgnore = ["E501"]; } ''
249 import os 282 import os
250 import socket 283 import socket
251 import json 284 import json
252 import sys 285 # import sys
253 from collections import defaultdict 286 from collections import defaultdict
254 from threading import Thread, Lock 287 from threading import Thread, Lock
255 from socketserver import StreamRequestHandler, ThreadingTCPServer 288 from socketserver import StreamRequestHandler, ThreadingTCPServer
@@ -273,11 +306,9 @@ in {
273 workspaces = list() 306 workspaces = list()
274 307
275 def focus_workspace(output, workspace): 308 def focus_workspace(output, workspace):
276 global workspace_history, history_lock
277
278 with history_lock: 309 with history_lock:
279 workspace_history[output] = [workspace] + [ws for ws in workspace_history[output] if ws != workspace] # noqa: E501 310 workspace_history[output] = [workspace] + [ws for ws in workspace_history[output] if ws != workspace]
280 print(json.dumps(workspace_history), file=sys.stderr) 311 # print(json.dumps(workspace_history), file=sys.stderr)
281 312
282 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 313 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
283 sock.connect(os.environ["NIRI_SOCKET"]) 314 sock.connect(os.environ["NIRI_SOCKET"])
@@ -299,16 +330,14 @@ in {
299 330
300 class RequestHandler(StreamRequestHandler): 331 class RequestHandler(StreamRequestHandler):
301 def handle(self): 332 def handle(self):
302 global workspace_history, history_lock 333 with detaching(TextIOWrapper(self.wfile, encoding='utf-8', write_through=True)) as out:
303
304 with detaching(TextIOWrapper(self.wfile, encoding='utf-8', write_through=True)) as out: # noqa: E501
305 with history_lock: 334 with history_lock:
306 json.dump(workspace_history, out) 335 json.dump(workspace_history, out)
307 336
308 337
309 class Server(ThreadingTCPServer): 338 class Server(ThreadingTCPServer):
310 def __init__(self): 339 def __init__(self):
311 ThreadingTCPServer.__init__(self, ("", 8000), RequestHandler, bind_and_activate=False) # noqa: E501 340 ThreadingTCPServer.__init__(self, ("", 8000), RequestHandler, bind_and_activate=False)
312 self.socket = socket.fromfd(3, self.address_family, self.socket_type) 341 self.socket = socket.fromfd(3, self.address_family, self.socket_type)
313 342
314 343
@@ -334,6 +363,79 @@ in {
334 ''; 363 '';
335 }; 364 };
336 }; 365 };
366 systemd.user.services.niri-workspace-sort = {
367 Unit = {
368 BindsTo = [ "niri.service" ];
369 After = [ "niri.service" ];
370 };
371 Install = {
372 WantedBy = [ "niri.service" ];
373 };
374 Service = {
375 Type = "simple";
376 ExecStart = pkgs.writers.writePython3 "niri-workspace-sort" { flakeIgnore = ["E501"]; } ''
377 import os
378 import sys
379 import socket
380 import json
381
382 outputs = None
383 only = {'HDMI-A-1': {'bmr'}, 'eDP-1': {'vid'}}
384
385
386 class Niri(socket.socket):
387 def __init__(self):
388 super().__init__(socket.AF_UNIX, socket.SOCK_STREAM)
389 super().connect(os.environ["NIRI_SOCKET"])
390 self.fh = super().makefile(mode='rw', buffering=1, encoding='utf-8')
391
392 def cmd(self, obj):
393 print(json.dumps(obj, separators=(',', ':')), flush=True, file=self.fh)
394
395 def event_stream(self):
396 self.cmd("EventStream")
397 return self.fh
398
399
400 with Niri() as niri, Niri().event_stream() as niri_stream:
401 for line in niri_stream:
402 workspaces = None
403 if line_json := json.loads(line):
404 if "WorkspacesChanged" in line_json:
405 workspaces = line_json["WorkspacesChanged"]["workspaces"]
406
407 if workspaces is None:
408 continue
409
410 old_outputs = outputs
411 outputs = {ws["output"] for ws in workspaces}
412 if old_outputs is None:
413 print("Initial outputs: {}".format(outputs), file=sys.stderr)
414 continue
415
416 new_outputs = outputs - old_outputs
417 if not new_outputs:
418 continue
419 print("New outputs: {}".format(new_outputs), file=sys.stderr)
420
421 relevant_workspaces = list(filter(lambda ws: (ws["name"] is not None) or (ws["active_window_id"] is not None), workspaces))
422 target_output = next(iter(outputs - set(only.keys())))
423 if not target_output:
424 continue
425 for ws in relevant_workspaces:
426 ws_ident = ws["name"] if ws["name"] is not None else (ws["output"], ws["idx"])
427 if ws["output"] not in set(only.keys()):
428 continue
429 if ws_ident in only[ws["output"]]:
430 continue
431
432 print("{} -> {}".format(ws_ident, target_output), file=sys.stderr)
433 niri.cmd({"Action": {"MoveWorkspaceToMonitor": {"reference": {"Id": ws["id"]}, "output": target_output}}})
434 '';
435 Restart = "on-failure";
436 RestartSec = 10;
437 };
438 };
337 439
338 programs.niri.scratchspaces = [ 440 programs.niri.scratchspaces = [
339 { name = "pwctl"; 441 { name = "pwctl";
@@ -347,7 +449,7 @@ in {
347 { title = "^Access Request.*"; } 449 { title = "^Access Request.*"; }
348 { title = ".*Passkey credentials$"; } 450 { title = ".*Passkey credentials$"; }
349 ]; 451 ];
350 windowRuleExtra = [ 452 windowRuleExtra = with kdl; [
351 (kdl.leaf "open-focused" false) 453 (kdl.leaf "open-focused" false)
352 ]; 454 ];
353 key = "Mod+Control+P"; 455 key = "Mod+Control+P";
@@ -375,6 +477,20 @@ in {
375 app-id = "com.github.wwmm.easyeffects"; 477 app-id = "com.github.wwmm.easyeffects";
376 spawn = [ "easyeffects" ]; 478 spawn = [ "easyeffects" ];
377 } 479 }
480 { name = "time";
481 key = "Mod+Control+K";
482 app-id = "chrome-kimai.yggdrasil.li__-Default";
483 spawn = [ (toString (pkgs.resholve.writeScript "kimai" {
484 interpreter = pkgs.runtimeShell;
485 inputs = [ pkgs.dex ];
486 execer = [ "cannot:${lib.getExe pkgs.dex}" ];
487 } ''
488 exec dex $HOME/.local/state/nix/profile/share/applications/kimai.desktop
489 '')) ];
490 windowRuleExtra = with kdl; [
491 (leaf "block-out-from" "screencast")
492 ];
493 }
378 ]; 494 ];
379 programs.niri.config = 495 programs.niri.config =
380 let 496 let
@@ -419,6 +535,10 @@ in {
419 ]) 535 ])
420 ]) 536 ])
421 537
538 (plain "gestures" [
539 (plain "hot-corners" [(flag "off")])
540 ])
541
422 (plain "environment" (lib.mapAttrsToList leaf { 542 (plain "environment" (lib.mapAttrsToList leaf {
423 NIXOS_OZONE_WL = "1"; 543 NIXOS_OZONE_WL = "1";
424 QT_QPA_PLATFORM = "wayland"; 544 QT_QPA_PLATFORM = "wayland";
@@ -426,6 +546,10 @@ in {
426 GDK_BACKEND = "wayland"; 546 GDK_BACKEND = "wayland";
427 SDL_VIDEODRIVER = "wayland"; 547 SDL_VIDEODRIVER = "wayland";
428 DISPLAY = ":0"; 548 DISPLAY = ":0";
549 ELECTRON_OZONE_PLATFORM_HINT = "auto";
550 SSH_ASKPASS_REQUIRE = "prefer";
551 SSH_ASKPASS = lib.getExe pkgs.kdePackages.ksshaskpass;
552 SUDO_ASKPASS = lib.getExe pkgs.kdePackages.ksshaskpass;
429 })) 553 }))
430 554
431 (node "output" "eDP-1" [ 555 (node "output" "eDP-1" [
@@ -455,8 +579,8 @@ in {
455 (plain "layout" [ 579 (plain "layout" [
456 (leaf "gaps" 8) 580 (leaf "gaps" 8)
457 (plain "struts" [ 581 (plain "struts" [
458 (leaf "left" 0) 582 (leaf "left" 26)
459 (leaf "right" 0) 583 (leaf "right" 26)
460 (leaf "top" 0) 584 (leaf "top" 0)
461 (leaf "bottom" 0) 585 (leaf "bottom" 0)
462 ]) 586 ])
@@ -540,7 +664,7 @@ in {
540 (plain "window-rule" [ 664 (plain "window-rule" [
541 (map (title: 665 (map (title:
542 (leaf "match" { app-id = "^org\\.keepassxc\\.KeePassXC$"; inherit title; }) 666 (leaf "match" { app-id = "^org\\.keepassxc\\.KeePassXC$"; inherit title; })
543 ) ["^Unlock Database.*" "^Access Request.*" ".*Passkey credentials$"]) 667 ) ["^Unlock Database.*" "^Access Request.*" ".*Passkey credentials$" "Browser Access Request$"])
544 (leaf "open-focused" true) 668 (leaf "open-focused" true)
545 (leaf "open-floating" true) 669 (leaf "open-floating" true)
546 ]) 670 ])
@@ -569,7 +693,7 @@ in {
569 (plain "window-rule" [ 693 (plain "window-rule" [
570 (leaf "match" { app-id = "^thunderbird$"; }) 694 (leaf "match" { app-id = "^thunderbird$"; })
571 (leaf "match" { app-id = "^Element$"; }) 695 (leaf "match" { app-id = "^Element$"; })
572 (leaf "match" { app-id = "^Rainbow$"; }) 696 (leaf "match" { app-id = "^chrome-web\.openrainbow\.com__-Default$"; })
573 (leaf "open-on-workspace" "comm") 697 (leaf "open-on-workspace" "comm")
574 ]) 698 ])
575 (plain "window-rule" [ 699 (plain "window-rule" [
@@ -588,11 +712,16 @@ in {
588 (leaf "open-focused" false) 712 (leaf "open-focused" false)
589 ]) 713 ])
590 (plain "window-rule" [ 714 (plain "window-rule" [
715 (leaf "match" { app-id = "^chrome-audiobookshelf\.yggdrasil\.li__-Default$"; })
716 (leaf "match" { app-id = "^YouTube Music Desktop App$"; })
717 (leaf "open-on-workspace" "vid")
718 ])
719 (plain "window-rule" [
591 (leaf "match" { app-id = "^pdfpc$"; }) 720 (leaf "match" { app-id = "^pdfpc$"; })
592 (plain "default-column-width" [(leaf "proportion" 1.)]) 721 (plain "default-column-width" [(leaf "proportion" 1.)])
593 ]) 722 ])
594 (plain "window-rule" [ 723 (plain "window-rule" [
595 (leaf "match" { app-id = "^pdfpc$"; title = "^pdfpc - presentation$"; }) 724 (leaf "match" { app-id = "^pdfpc$"; title = "^.*presentation.*$"; })
596 (plain "default-column-width" [(leaf "proportion" 1.)]) 725 (plain "default-column-width" [(leaf "proportion" 1.)])
597 (leaf "open-fullscreen" true) 726 (leaf "open-fullscreen" true)
598 (leaf "open-on-workspace" "bmr") 727 (leaf "open-on-workspace" "bmr")
@@ -609,6 +738,7 @@ in {
609 (plain "window-rule" [ 738 (plain "window-rule" [
610 (leaf "match" { app-id = "^org\\.pwmt\\.zathura$"; }) 739 (leaf "match" { app-id = "^org\\.pwmt\\.zathura$"; })
611 (leaf "match" { app-id = "^evince$"; }) 740 (leaf "match" { app-id = "^evince$"; })
741 (leaf "match" { app-id = "^org\\.gnome\\.Papers$"; })
612 (leaf "default-column-display" "tabbed") 742 (leaf "default-column-display" "tabbed")
613 ]) 743 ])
614 744
@@ -636,6 +766,21 @@ in {
636 "Mod+Slash".action = show-hotkey-overlay; 766 "Mod+Slash".action = show-hotkey-overlay;
637 767
638 "Mod+Return".action = spawn terminal; 768 "Mod+Return".action = spawn terminal;
769 "Mod+Shift+Return".action =
770 let
771 nushellKitty = pkgs.symlinkJoin {
772 name = "nushell-kitty";
773 paths = [ config.programs.kitty.package ];
774 buildInputs = [ pkgs.makeWrapper ];
775 postBuild = ''
776 wrapProgram $out/bin/kitty \
777 --add-flags "--config ${pkgs.writeText "kitty.conf" ''
778 include $HOME/${config.xdg.configFile."kitty/kitty.conf".target}
779 shell ${lib.getExe config.programs.nushell.package}
780 ''}"
781 '';
782 };
783 in spawn (lib.getExe' nushellKitty "kitty");
639 "Mod+Q".action = close-window; 784 "Mod+Q".action = close-window;
640 "Mod+O".action = spawn (lib.getExe config.programs.fuzzel.package); 785 "Mod+O".action = spawn (lib.getExe config.programs.fuzzel.package);
641 "Mod+Shift+O".action = spawn (lib.getExe config.programs.fuzzel.package) "--list-executables-in-path"; 786 "Mod+Shift+O".action = spawn (lib.getExe config.programs.fuzzel.package) "--list-executables-in-path";
@@ -671,12 +816,12 @@ in {
671 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) 816 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)
672 $FOUND || echo 817 $FOUND || echo
673 } 818 }
674 FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> ") || exit $? 819 FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> " --width=60) || exit $?
675 if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then 820 if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then
676 QALC_RES="$FUZZEL_RES" 821 QALC_RES="$FUZZEL_RES"
677 QALC_RET=0 822 QALC_RET=0
678 else 823 else
679 QALC_RES=$(qalc "$FUZZEL_RES" 2>&1) 824 QALC_RES=$(qalc -set "autocalc off" "$FUZZEL_RES" 2>&1)
680 QALC_RET=$? 825 QALC_RET=$?
681 fi 826 fi
682 [[ -n "$QALC_RES" ]] || exit 1 827 [[ -n "$QALC_RES" ]] || exit 1
@@ -696,18 +841,33 @@ in {
696 notify-send "$QALC_RES" 841 notify-send "$QALC_RES"
697 ''; 842 '';
698 })); 843 }));
844 "Mod+Shift+U".action =
845 let
846 qalcKitty = pkgs.symlinkJoin {
847 name = "qalc-kitty";
848 paths = [ config.programs.kitty.package ];
849 buildInputs = [ pkgs.makeWrapper ];
850 postBuild = ''
851 wrapProgram $out/bin/kitty \
852 --add-flags "--config ${pkgs.writeText "kitty.conf" ''
853 include $HOME/${config.xdg.configFile."kitty/kitty.conf".target}
854 shell ${lib.getExe pkgs.libqalculate}
855 ''}"
856 '';
857 };
858 in spawn (lib.getExe' qalcKitty "kitty");
699 "Mod+E".action = spawn (lib.getExe (pkgs.writeShellApplication { 859 "Mod+E".action = spawn (lib.getExe (pkgs.writeShellApplication {
700 name = "emoji-fuzzel"; 860 name = "emoji-fuzzel";
701 runtimeInputs = with pkgs; [ config.programs.fuzzel.package wtype wl-clipboard-rs ]; 861 runtimeInputs = with pkgs; [ config.programs.fuzzel.package wtype wl-clipboard-rs ];
702 text = '' 862 text = ''
703 FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " <"$HOME"/.local/share/emoji-data/list.txt) || exit $? 863 FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " --cache "$HOME"/.cache/fuzzel-emoji --width=60 <"$HOME"/.local/share/emoji-data/list.txt) || exit $?
704 [[ -n "$FUZZEL_RES" ]] || exit 1 864 [[ -n "$FUZZEL_RES" ]] || exit 1
705 wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste 865 wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste
706 ''; 866 '';
707 })); 867 }));
708 "Print".action = screenshot; 868 "Print".action = screenshot;
709 "Control+Print".action = screenshot-window; 869 "Control+Print".action = screenshot-window;
710 # "Shift+Print".action = screenshot-screen; 870 "Shift+Print".action = kdl.magic-leaf "screenshot-screen";
711 "Mod+B".action = with-select-window-action ".workspace_id == ($active_workspace | tonumber)" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}"; 871 "Mod+B".action = with-select-window-action ".workspace_id == ($active_workspace | tonumber)" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
712 "Mod+Shift+B".action = with-select-window-action "true" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}"; 872 "Mod+Shift+B".action = with-select-window-action "true" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
713 873
@@ -746,22 +906,22 @@ in {
746 "Mod+Shift+Control+C".action = move-workspace-up; 906 "Mod+Shift+Control+C".action = move-workspace-up;
747 907
748 "Mod+ParenLeft".action = focus-workspace "comm"; 908 "Mod+ParenLeft".action = focus-workspace "comm";
749 "Mod+Shift+ParenLeft".action = move-column-to-workspace "comm"; 909 "Mod+Shift+ParenLeft".action = kdl.magic-leaf "move-column-to-workspace" "comm";
750 910
751 "Mod+ParenRight".action = focus-workspace "web"; 911 "Mod+ParenRight".action = focus-workspace "web";
752 "Mod+Shift+ParenRight".action = move-column-to-workspace "web"; 912 "Mod+Shift+ParenRight".action = kdl.magic-leaf "move-column-to-workspace" "web";
753 913
754 "Mod+BraceRight".action = focus-workspace "read"; 914 "Mod+BraceRight".action = focus-workspace "read";
755 "Mod+Shift+BraceRight".action = move-column-to-workspace "read"; 915 "Mod+Shift+BraceRight".action = kdl.magic-leaf "move-column-to-workspace" "read";
756 916
757 "Mod+BraceLeft".action = focus-workspace "mon"; 917 "Mod+BraceLeft".action = focus-workspace "mon";
758 "Mod+Shift+BraceLeft".action = move-column-to-workspace "mon"; 918 "Mod+Shift+BraceLeft".action = kdl.magic-leaf "move-column-to-workspace" "mon";
759 919
760 "Mod+Asterisk".action = focus-workspace "vid"; 920 "Mod+Asterisk".action = focus-workspace "vid";
761 "Mod+Shift+Asterisk".action = move-column-to-workspace "vid"; 921 "Mod+Shift+Asterisk".action = kdl.magic-leaf "move-column-to-workspace" "vid";
762 922
763 "Mod+Plus".action = with-unnamed-workspace-action ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}''; 923 "Mod+Plus".action = with-unnamed-workspace-action ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
764 "Mod+Shift+Plus".action = with-unnamed-workspace-action ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}}}}''; 924 "Mod+Shift+Plus".action = with-unnamed-workspace-action ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}, "focus": true}}}'';
765 925
766 "Mod+M".action = consume-or-expel-window-left; 926 "Mod+M".action = consume-or-expel-window-left;
767 "Mod+W".action = consume-or-expel-window-right; 927 "Mod+W".action = consume-or-expel-window-right;
@@ -769,10 +929,11 @@ in {
769 "Mod+Shift+M".action = toggle-column-tabbed-display; 929 "Mod+Shift+M".action = toggle-column-tabbed-display;
770 930
771 "Mod+R".action = switch-preset-column-width; 931 "Mod+R".action = switch-preset-column-width;
772 "Mod+Shift+R".action = switch-preset-window-height; 932 "Mod+Shift+R".action = maximize-column;
933 "Mod+Shift+Ctrl+R".action = switch-preset-window-height;
773 "Mod+F".action = center-column; 934 "Mod+F".action = center-column;
774 "Mod+Shift+F".action = maximize-column; 935 "Mod+Shift+F".action = toggle-windowed-fullscreen;
775 "Mod+Shift+Ctrl+F".action = fullscreen-window; 936 "Mod+Ctrl+Shift+F".action = fullscreen-window;
776 937
777 "Mod+V".action = switch-focus-between-floating-and-tiling; 938 "Mod+V".action = switch-focus-between-floating-and-tiling;
778 "Mod+Shift+V".action = toggle-window-floating; 939 "Mod+Shift+V".action = toggle-window-floating;
@@ -828,13 +989,24 @@ in {
828 989
829 "Mod+Semicolon".action = spawn makoctl "dismiss" "--group"; 990 "Mod+Semicolon".action = spawn makoctl "dismiss" "--group";
830 "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all"; 991 "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all";
831 "Mod+Period".action = spawn makoctl "menu" (lib.getExe config.programs.fuzzel.package) "--dmenu"; 992 "Mod+Period".action = spawn makoctl "menu" "--" (lib.getExe config.programs.fuzzel.package) "--dmenu";
832 "Mod+Comma".action = spawn makoctl "restore"; 993 "Mod+Comma".action = spawn makoctl "restore";
833 994
834 "Mod+Control+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}"; 995 "Mod+Control+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}";
835 "Mod+Control+Shift+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"MoveColumnToWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}"; 996 "Mod+Control+Shift+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"MoveColumnToWorkspace\":{\"reference\":{\"Id\": $workspace_id}, \"focus\": true}}}";
997
998 "Mod+X".action = set-dynamic-cast-window;
999 "Mod+Shift+X".action = set-dynamic-cast-monitor;
1000 "Mod+Control+Shift+X".action = clear-dynamic-cast-target;
1001
1002 "Mod+D".action = with-urgent-window-action "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
1003 "Mod+Shift+D".action = with-focused-window-action "{\"Action\":{\"UnsetUrgent\":{\"id\": .id}}}";
1004
1005 "Mod+K".action = spawn (lib.getExe' pkgs.worktime "worktime-ui");
1006 "Mod+Shift+K".action = spawn (lib.getExe' pkgs.worktime "worktime-stop");
836 })) 1007 }))
837 (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) 1008 (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)
1009 (map ({ name, moveKey, ...}: if moveKey != null then bind moveKey { action = kdl.magic-leaf "move-column-to-workspace" name; } else null) cfg.scratchspaces)
838 ] 1010 ]
839 )) 1011 ))
840 ]; 1012 ];
diff --git a/accounts/gkleen@sif/niri/mako.nix b/accounts/gkleen@sif/niri/mako.nix
index 2788fb82..eba26caa 100644
--- a/accounts/gkleen@sif/niri/mako.nix
+++ b/accounts/gkleen@sif/niri/mako.nix
@@ -3,37 +3,31 @@
3 config = { 3 config = {
4 services.mako = { 4 services.mako = {
5 enable = true; 5 enable = true;
6 font = "Fira Sans 10"; 6 settings = {
7 format = "<i>%s</i>\\n%b"; 7 font = "Fira Sans 10";
8 margin = "2"; 8 format = "<i>%s</i>\\n%b";
9 maxVisible = -1; 9 margin = "2";
10 backgroundColor = "#000000dd"; 10 max-visible = -1;
11 progressColor = "source #223544ff"; 11 background-color = "#000000dd";
12 width = 384; 12 progress-color = "source #223544ff";
13 extraConfig = '' 13 width = 384;
14 outer-margin=1 14 outer-margin = 1;
15 max-history=100 15 max-history = 100;
16 max-icon-size=48 16 max-icon-size = 48;
17 17 };
18 [grouped] 18 criteria = {
19 format=<b>(%g)</b> <i>%s</i>\n%b 19 grouped.format = "<b>(%g)</b> <i>%s</i>\\n%b";
20 20 "urgency=low".text-color = "#999999ff";
21 [urgency=low] 21 "urgency=critical".background-color = "#900000dd";
22 text-color=#999999ff 22 "app-name=Element".group-by = "summary";
23 23 "app-name=poweralertd" = {
24 [urgency=critical] 24 history = false;
25 background-color=#900000dd 25 ignore-timeout = true;
26 26 default-timeout = 2000;
27 [app-name=Element] 27 };
28 group-by=summary 28 "app-name=worktime".history = false;
29 29 "mode=silent".invisible = true;
30 [app-name=poweralertd] 30 };
31 ignore-timeout=1
32 default-timeout=2000
33
34 [mode=silent]
35 invisible=1
36 '';
37 package = pkgs.symlinkJoin { 31 package = pkgs.symlinkJoin {
38 name = "${pkgs.mako.name}-wrapped"; 32 name = "${pkgs.mako.name}-wrapped";
39 paths = with pkgs; [ mako ]; 33 paths = with pkgs; [ mako ];
diff --git a/accounts/gkleen@sif/niri/swayosd.nix b/accounts/gkleen@sif/niri/swayosd.nix
index 984927c2..54ebb302 100644
--- a/accounts/gkleen@sif/niri/swayosd.nix
+++ b/accounts/gkleen@sif/niri/swayosd.nix
@@ -3,9 +3,10 @@
3 config = { 3 config = {
4 services.swayosd = { 4 services.swayosd = {
5 enable = true; 5 enable = true;
6 topMargin = 0.946154; 6 topMargin = 0.4769706078;
7 stylePath = pkgs.runCommand "style.css" { 7 stylePath = pkgs.runCommand "style.css" {
8 src = pkgs.writeText "style.scss" '' 8 passAsFile = [ "src" ];
9 src = ''
9 window#osd { 10 window#osd {
10 padding: 12px 20px; 11 padding: 12px 20px;
11 border-radius: 999px; 12 border-radius: 999px;
@@ -59,7 +60,7 @@
59 } 60 }
60 ''; 61 '';
61 buildInputs = with pkgs; [sass]; 62 buildInputs = with pkgs; [sass];
62 } "scss -C --sourcemap=none --style=compact $src $out"; 63 } "scss -C --sourcemap=none --style=compact $srcPath $out";
63 }; 64 };
64 }; 65 };
65} 66}
diff --git a/accounts/gkleen@sif/niri/waybar.nix b/accounts/gkleen@sif/niri/waybar.nix
index bae818f6..c02a9a76 100644
--- a/accounts/gkleen@sif/niri/waybar.nix
+++ b/accounts/gkleen@sif/niri/waybar.nix
@@ -20,15 +20,21 @@ in {
20 { 20 {
21 layer = "top"; 21 layer = "top";
22 position = "top"; 22 position = "top";
23 height = 14; 23 height = 21;
24 output = [ "eDP-1" "DP-2" "DP-3" ]; 24 output = [ "eDP-1" "DP-2" "DP-3" ];
25 modules-left = [ "niri/workspaces" ]; 25 modules-left = [ "niri/workspaces" ];
26 modules-center = [ "niri/window" ]; 26 modules-center = [ "niri/window" ];
27 modules-right = [ "custom/worktime" "custom/worktime-today" 27 modules-right = [ "custom/worktime" "custom/worktime-today"
28 "custom/weather" 28 "custom/weather"
29 "custom/keymap" 29 "custom/keymap"
30 "privacy" "tray" "wireplumber" "backlight" "battery" "idle_inhibitor" "custom/mako" "clock" ]; 30 "privacy" "tray" "wireplumber" "backlight" "battery" "idle_inhibitor" "custom/mako" "custom/lid_inhibitor" "clock" ];
31 31
32 "custom/lid_inhibitor" = {
33 format = "{}";
34 return-type = "json";
35 exec = lib.getExe pkgs.waybar-systemd-inhibit;
36 on-click = lib.getExe' pkgs.waybar-systemd-inhibit "waybar-systemd-inhibit-toggle";
37 };
32 "custom/mako" = { 38 "custom/mako" = {
33 format = "{}"; 39 format = "{}";
34 return-type = "json"; 40 return-type = "json";
@@ -211,7 +217,7 @@ in {
211 layer = "top"; 217 layer = "top";
212 position = "top"; 218 position = "top";
213 height = 14; 219 height = 14;
214 output = [ "!eDP-1" "!DP-2" "!DP-3" ]; 220 output = [ "!eDP-1" "!DP-2" "!DP-3" "*" ];
215 modules-left = [ "niri/workspaces" ]; 221 modules-left = [ "niri/workspaces" ];
216 modules-center = [ "niri/window" ]; 222 modules-center = [ "niri/window" ];
217 modules-right = [ "clock" ]; 223 modules-right = [ "clock" ];
@@ -254,10 +260,10 @@ in {
254 } 260 }
255 261
256 .modules-left { 262 .modules-left {
257 margin-left: 12px; 263 margin-left: 38px;
258 } 264 }
259 .modules-right { 265 .modules-right {
260 margin-right: 12px; 266 margin-right: 38px;
261 } 267 }
262 268
263 .module { 269 .module {
@@ -293,7 +299,7 @@ in {
293 #tray { 299 #tray {
294 margin: 0; 300 margin: 0;
295 } 301 }
296 #battery, #idle_inhibitor, #backlight, #wireplumber, #custom-mako { 302 #battery, #idle_inhibitor, #backlight, #wireplumber, #custom-mako, #custom-lid_inhibitor {
297 color: @grey; 303 color: @grey;
298 margin: 0 5px 0 2px; 304 margin: 0 5px 0 2px;
299 } 305 }
@@ -302,7 +308,11 @@ in {
302 margin-left: 6px; 308 margin-left: 6px;
303 } 309 }
304 #custom-mako { 310 #custom-mako {
305 margin-right: 2px; 311 margin-right: 4px;
312 margin-left: 3px;
313 }
314 #custom-lid_inhibitor {
315 margin-right: 3px;
306 margin-left: 3px; 316 margin-left: 3px;
307 } 317 }
308 #battery { 318 #battery {
@@ -330,7 +340,7 @@ in {
330 color: @orange; 340 color: @orange;
331 } 341 }
332 342
333 #idle_inhibitor { 343 #idle_inhibitor, #custom-lid_inhibitor {
334 padding-top: 1px; 344 padding-top: 1px;
335 } 345 }
336 346
@@ -340,6 +350,7 @@ in {
340 } 350 }
341 #clock { 351 #clock {
342 /* margin-right: 5px; */ 352 /* margin-right: 5px; */
353 font-feature-settings: "tnum";
343 } 354 }
344 ''; 355 '';
345 }; 356 };
diff --git a/accounts/gkleen@sif/synadm/default.nix b/accounts/gkleen@sif/synadm/default.nix
new file mode 100644
index 00000000..0a8e0d4c
--- /dev/null
+++ b/accounts/gkleen@sif/synadm/default.nix
@@ -0,0 +1,9 @@
1{ config, pkgs, ... }:
2{
3 home.packages = with pkgs; [ synadm ];
4 sops.secrets."synadm.yaml" = {
5 format = "binary";
6 sopsFile = ./synadm_yaml;
7 path = config.xdg.configHome + "/synadm.yaml";
8 };
9}
diff --git a/accounts/gkleen@sif/synadm/synadm_yaml b/accounts/gkleen@sif/synadm/synadm_yaml
new file mode 100644
index 00000000..8d951ccc
--- /dev/null
+++ b/accounts/gkleen@sif/synadm/synadm_yaml
@@ -0,0 +1,15 @@
1{
2 "data": "ENC[AES256_GCM,data:qJy4Pmbbxja4jmW7OaHsD0mQZ7anZwLhiVmAgkavb+CqwWGDnUBXdz22/MHCbxng5NshcFSpBoCBhgY6B9V2bUiES6bH9AtMlDcs9ebKGMArBTUTnQ2MjWQGfQTqraWdNgy+n327uj9swwCH8EZXdYH/Hlv0t/re470W+VOHeXhGghQ3Y9IGz2sgfvMGr8QxaJNydZz85rgs5QUP/PglCwWIOw2mY1EX2vYwnmiAo49LmIEaxWvRi++KHaeBveDt0nlkJwzUlipL2VOKWxkgpK3yGucQn2mz+FRe1btp+4KGm8H17eUI9FO9sBwq,iv:kgM921ovwCgDYHQj3c5Rupy/8JxHehxUD2jb1k9Ik2Y=,tag:3TLQkJbv679VWy8V2TMugw==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6bzVHUGNxZTF2WC9MYmZr\neGdVVzJXN3lGdEk3cTBER3J6UTFtcUJna2d3CjdNQmRXd2haZW1MYlJzNkk1dWVD\nVTFQc2gvS0JrejJ6SFh2MXpPWDZpRE0KLS0tIE0wTC85bEpvSnlGdGFkZVFhNjFZ\nbzRiZkxMWUg2ODNVUlBmNFlPNGRrZlkK1VXLJWcssv3ETyZSSM/Hhn5VIaI9iov9\nzShZA9Zx/FX6PYTuUMC29pJ57gKourcIxa/7HwSv/xYn1A6WcYfgSg==\n-----END AGE ENCRYPTED FILE-----\n"
8 }
9 ],
10 "lastmodified": "2025-05-18T11:03:42Z",
11 "mac": "ENC[AES256_GCM,data:yonJC68PhilAgEHNNJQ8nO53Qo3rx/LnfiOWfuMm24bOUIH9QM3WZZxpigd7bHI4eC4TqRb4LvcSi0nEURTRAhwiTqGNrWbpw2Iv3n5dhLEN9aTcetG5ZuhaXqfVUoML45/ovdBZG/0l8+XIHqxN2M/g/h4JwKoR/6lqzcrVhgo=,iv:xvxBJwy+E5zUdjhGPdZPdy7tnBIEj50hfiDJFsS3wNg=,tag:L4Fas36ZOg4h0QQwC4gjNA==,type:str]",
12 "unencrypted_suffix": "_unencrypted",
13 "version": "3.10.2"
14 }
15}
diff --git a/accounts/gkleen@sif/systemd.nix b/accounts/gkleen@sif/systemd.nix
index 2237b708..90cccc58 100644
--- a/accounts/gkleen@sif/systemd.nix
+++ b/accounts/gkleen@sif/systemd.nix
@@ -242,7 +242,7 @@ in {
242 "-${lib.getExe pkgs.playerctl} -a pause" 242 "-${lib.getExe pkgs.playerctl} -a pause"
243 "-${lib.getExe (pkgs.writeShellApplication { 243 "-${lib.getExe (pkgs.writeShellApplication {
244 name = "generate-css"; 244 name = "generate-css";
245 runtimeInputs = with pkgs; [cfg.programs.wpaperd.package jq coreutils imagemagick findutils]; 245 runtimeInputs = with pkgs; [cfg.services.wpaperd.package jq coreutils imagemagick findutils];
246 text = '' 246 text = ''
247 declare -A monitors 247 declare -A monitors
248 monitors=() 248 monitors=()
@@ -333,21 +333,21 @@ in {
333 ExecStopPost = "${pkgs.coreutils}/bin/rm -rfv \"$CACHE_DIRECTORY\""; 333 ExecStopPost = "${pkgs.coreutils}/bin/rm -rfv \"$CACHE_DIRECTORY\"";
334 }; 334 };
335 }; 335 };
336 wpaperd = { 336 # wpaperd = {
337 Install = { 337 # Install = {
338 WantedBy = ["graphical-session.target"]; 338 # WantedBy = ["graphical-session.target"];
339 }; 339 # };
340 Unit = { 340 # Unit = {
341 After = [ "graphical-session.target" ]; 341 # After = [ "graphical-session.target" ];
342 PartOf = [ "graphical-session.target" ]; 342 # PartOf = [ "graphical-session.target" ];
343 }; 343 # };
344 Service = { 344 # Service = {
345 ExecStart = lib.getExe cfg.programs.wpaperd.package; 345 # ExecStart = lib.getExe cfg.services.wpaperd.package;
346 Type = "simple"; 346 # Type = "simple";
347 Restart = "always"; 347 # Restart = "always";
348 RestartSec = "2s"; 348 # RestartSec = "2s";
349 }; 349 # };
350 }; 350 # };
351 xembed-sni-proxy = { 351 xembed-sni-proxy = {
352 Unit = { 352 Unit = {
353 PartOf = lib.mkForce ["tray.target"]; 353 PartOf = lib.mkForce ["tray.target"];
diff --git a/accounts/gkleen@sif/utils/async-yt-dlp.nix b/accounts/gkleen@sif/utils/async-yt-dlp.nix
new file mode 100644
index 00000000..c3b82ec5
--- /dev/null
+++ b/accounts/gkleen@sif/utils/async-yt-dlp.nix
@@ -0,0 +1,57 @@
1{ writers, python3Packages, ... }:
2
3writers.writePython3Bin "async-yt-dlp" {
4 libraries = with python3Packages; [ yt-dlp ];
5 flakeIgnore = ["E501"];
6} ''
7import sys
8import os
9
10import yt_dlp
11import yt_dlp.options
12from collections import namedtuple
13import socket
14from pathlib import Path
15import json
16
17create_parser = yt_dlp.options.create_parser
18
19
20def parse_patched_options(opts):
21 patched_parser = create_parser()
22 patched_parser.defaults.update({
23 'ignoreerrors': False,
24 'retries': 0,
25 'fragment_retries': 0,
26 'extract_flat': False,
27 'concat_playlist': 'never',
28 })
29 yt_dlp.options.create_parser = lambda: patched_parser
30 try:
31 return yt_dlp.parse_options(opts)
32 finally:
33 yt_dlp.options.create_parser = create_parser
34
35
36default_opts = parse_patched_options([]).ydl_opts
37
38
39def cli_to_api(opts):
40 opts = parse_patched_options(opts)
41 urls = opts.urls
42 opts = opts.ydl_opts
43
44 diff = {k: v for k, v in opts.items() if default_opts[k] != v}
45 if 'postprocessors' in diff:
46 diff['postprocessors'] = [pp for pp in diff['postprocessors']
47 if pp not in default_opts['postprocessors']]
48 return namedtuple('Options', ('params', 'urls'))(diff, urls)
49
50
51if __name__ == '__main__':
52 opts = cli_to_api(sys.argv[1:])
53 with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
54 sock.connect(str(Path(os.environ["XDG_RUNTIME_DIR"]) / "yt-dlp.sock").encode('utf-8'))
55 with sock.makefile(mode='w', buffering=1, encoding='utf-8') as fh:
56 json.dump(opts._asdict(), fh)
57''
diff --git a/accounts/gkleen@sif/utils/sieve-edit.nix b/accounts/gkleen@sif/utils/sieve-edit.nix
new file mode 100644
index 00000000..f985a3f6
--- /dev/null
+++ b/accounts/gkleen@sif/utils/sieve-edit.nix
@@ -0,0 +1,24 @@
1pkgs@{ lib, resholve, zsh, sieve-connect, sops, ... }:
2
3resholve.writeScriptBin "sieve-edit" {
4 inputs = with pkgs; [sieve-connect sops];
5 interpreter = lib.getExe zsh;
6 execer = with pkgs; [
7 "cannot:${lib.getExe sieve-connect}"
8 "cannot:${lib.getExe sops}"
9 ];
10} ''
11 host=$1; shift
12 case "$host" in
13 surtr)
14 sieve-connect -s surtr.yggdrasil.li -m EXTERNAL --clientkey <(sops decrypt $HOME/projects/machines/hosts/surtr/email/ca/gkleen@sif.key) --clientcert $HOME/projects/machines/hosts/surtr/email/ca/gkleen@sif.crt --edit --remotesieve sieve
15 ;;
16 ymir)
17 sieve-connect -s ymir.yggdrasil.li -u gkleen --edit --remotesieve sieve
18 ;;
19 *)
20 echo "Unknown host: ‘$host’" >&2
21 return 2
22 ;;
23 esac
24''
diff --git a/accounts/gkleen@sif/zshrc b/accounts/gkleen@sif/zshrc
index c628e2e9..abc200c6 100644
--- a/accounts/gkleen@sif/zshrc
+++ b/accounts/gkleen@sif/zshrc
@@ -2,17 +2,14 @@ dir() {
2 curlArchive=false 2 curlArchive=false
3 templateArchive="" 3 templateArchive=""
4 repoUrl="" 4 repoUrl=""
5 nixShell=""
6 findNix=false
7 dir="" 5 dir=""
8 forceShell=false 6 forceShell=false
9 wormhole=false 7 wormhole=false
10 gitWorktree="" 8 gitWorktree=""
11 # notmuchMsg=""
12 quickserve=false
13 modifyPDF="" 9 modifyPDF=""
10 miniserve=false
14 11
15 while getopts ':t:a:s:Sd:ir:wqg:p:' arg; do 12 while getopts ':t:a:d:ir:wg:p:m' arg; do
16 case $arg in 13 case $arg in
17 "t") ;; 14 "t") ;;
18 "a") 15 "a")
@@ -23,16 +20,13 @@ dir() {
23 templateArchive=${OPTARG:a} 20 templateArchive=${OPTARG:a}
24 fi 21 fi
25 ;; 22 ;;
26 "s") nixShell=${OPTARG:a} ;;
27 "S") findNix=true ;;
28 "d") dir=${OPTARG} ;; 23 "d") dir=${OPTARG} ;;
29 "i") forceShell=true ;; 24 "i") forceShell=true ;;
30 "r") repoUrl=${OPTARG} ;; 25 "r") repoUrl=${OPTARG} ;;
31 "w") wormhole=true ;; 26 "w") wormhole=true ;;
32 "g") gitWorktree=${OPTARG} ;; 27 "g") gitWorktree=${OPTARG} ;;
33 # "n") notmuchMsg=${OPTARG} ;;
34 "q") quickserve=true ;;
35 "p") modifyPDF=${OPTARG:a} ;; 28 "p") modifyPDF=${OPTARG:a} ;;
29 "m") miniserve=true ;;
36 *) printf "Invalid option: %s\n" $arg >&2; exit 2 ;; 30 *) printf "Invalid option: %s\n" $arg >&2; exit 2 ;;
37 esac 31 esac
38 done 32 done
@@ -56,20 +50,34 @@ dir() {
56 gitWorktree="" 50 gitWorktree=""
57 fi 51 fi
58 52
53 miniservePIDFile=""
54 if [[ ${miniserve} = "true" ]]; then
55 miniservePIDFile=$(mktemp --tmpdir --suffix=.pid)
56 fi
57
59 cleanup() 58 cleanup()
60 { 59 {
61 cd ${modifyPDF:h} 60 if [[ -n ${modifyPDF} ]]; then
62 [[ -n ${modifyPDF} ]] && nix shell 'nixos#imagemagick' -c convert -verbose ${dir}/${modifyPDF:t:r}_*.png(on) ${modifyPDF} 61 cd ${modifyPDF:h}
62 typeset -a pages
63 eval 'pages=(${dir}/${modifyPDF:t:r}_*.png(on))'
64 magick -verbose "$pages" ${modifyPDF}
65 modifyPDF=""
66 fi
67 if [[ -n ${miniservePIDFile} ]]; then
68 command kill --verbose -- $(cat ${miniservePIDFile}) && wait $(cat ${miniservePIDFile})
69 miniservePIDFile=""
70 fi
63 } 71 }
64 72
65 ( 73 (
74 set -o localoptions -o localtraps
75 trap 'return 1' INT TERM
66 trap cleanup EXIT 76 trap cleanup EXIT
67 77
68 cd ${dir} 78 cd ${dir}
69 export dir; 79 export dir;
70 80
71 ${findNix} && { nixShell=$(findNix) || return $? }
72
73 [[ -n ${repoUrl} ]] && git clone -- ${repoUrl} . 81 [[ -n ${repoUrl} ]] && git clone -- ${repoUrl} .
74 82
75 [[ -n ${modifyPDF} ]] && templateArchive=${modifyPDF} 83 [[ -n ${modifyPDF} ]] && templateArchive=${modifyPDF}
@@ -82,7 +90,7 @@ dir() {
82 } 90 }
83 trap cleanup EXIT 91 trap cleanup EXIT
84 92
85 if ${curlArchive}; then 93 if [[ $curlArchive = "true" ]]; then
86 archiveFile=$(mktemp -t "archive.XXXXXXXXXX.${templateArchive:t}") 94 archiveFile=$(mktemp -t "archive.XXXXXXXXXX.${templateArchive:t}")
87 95
88 curl -L -o ${archiveFile} ${templateArchive} 96 curl -L -o ${archiveFile} ${templateArchive}
@@ -91,14 +99,14 @@ dir() {
91 fi 99 fi
92 100
93 unpack=true 101 unpack=true
94 while ${unpack}; do 102 while [[ $unpack = "true" ]]; do
95 case $(file --brief --mime-type --dereference ${templateArchive}) in 103 case $(file --brief --mime-type --dereference ${templateArchive}) in
96 application/zip) 104 application/zip)
97 unzip ${templateArchive} 105 unzip ${templateArchive}
98 unpack=false 106 unpack=false
99 ;; 107 ;;
100 application/vnd.debian.binary-package) 108 application/vnd.debian.binary-package)
101 nix shell 'nixos#binutils' --command ar x ${templateArchive} 109 ar x ${templateArchive}
102 mkdir control data 110 mkdir control data
103 tar -C control -xvaf control.* 111 tar -C control -xvaf control.*
104 tar -C data -xvaf data.* 112 tar -C data -xvaf data.*
@@ -106,7 +114,7 @@ dir() {
106 ;; 114 ;;
107 application/x-rpm) 115 application/x-rpm)
108 cpioArchive=$(mktemp -t "archive.XXXXXXXXXX.${templateArchive:t:r}.cpio") 116 cpioArchive=$(mktemp -t "archive.XXXXXXXXXX.${templateArchive:t:r}.cpio")
109 nix shell 'nixos#busybox' --command rpm2cpio ${templateArchive} > ${cpioArchive} 117 rpm2cpio ${templateArchive} > ${cpioArchive}
110 templateArchive=${cpioArchive} 118 templateArchive=${cpioArchive}
111 unpack=true 119 unpack=true
112 ;; 120 ;;
@@ -115,12 +123,12 @@ dir() {
115 unpack=false 123 unpack=false
116 ;; 124 ;;
117 application/pdf) 125 application/pdf)
118 nix shell 'nixos#ghostscript' 'nixos#imagemagick' -c convert -verbose -density 400 ${templateArchive} ${modifyPDF:t:r}_%0d.png 126 magick -verbose -density 400 ${templateArchive} ${modifyPDF:t:r}_%0d.png
119 unpack=false 127 unpack=false
120 ;; 128 ;;
121 application/octet-stream) 129 application/octet-stream)
122 if [[ $(file --brief --dereferenc ${templateArchive}) =~ Squashfs ]]; then 130 if [[ $(file --brief --dereference ${templateArchive}) =~ Squashfs ]]; then
123 nix shell 'nixos#squashfsTools' -c unsquashfs -d . ${templateArchive} 131 unsquashfs -d . ${templateArchive}
124 unpack=false 132 unpack=false
125 fi 133 fi
126 ;; 134 ;;
@@ -134,25 +142,21 @@ dir() {
134 fi 142 fi
135 143
136 144
137 ${wormhole} && wormhole receive --accept-file 145 [[ $wormhole = "true" ]] && wormhole receive --accept-file
138 146
139 147
140 if ${quickserve}; then 148 if [[ ${#@} -gt 0 ]]; then
141 quickserve --root . --upload . --show-hidden --tar gz 149 ${@}
142 fi 150 fi
143 151
152 cd $(pwd) # Needed for mounting to work
144 153
145 if [[ ${#@} -eq 0 ]] || ${forceShell}; then 154 if [[ ${miniserve} = "true" ]]; then
146 if [[ ${#@} -gt 0 ]]; then 155 miniserve --random-route --hidden --enable-tar-gz --enable-zip . &
147 if [[ -z ${nixShell} ]]; then 156 echo $! > "${miniservePIDFile}"
148 ${@} 157 fi
149 else
150 nix-shell ${nixShell} --run "${@}"
151 fi
152 fi
153
154 cd $(pwd) # Needed for mounting to work
155 158
159 if [[ ${#@} -eq 0 ]] && [[ ${miniserve} != "true" ]] || [[ $forceShell = "true" ]]; then
156 isSingleDir() { 160 isSingleDir() {
157 typeset -a contents 161 typeset -a contents
158 contents=(*(N) .*(N)) 162 contents=(*(N) .*(N))
@@ -166,18 +170,9 @@ dir() {
166 } 170 }
167 while d=$(isSingleDir); do cd ${d}; done 171 while d=$(isSingleDir); do cd ${d}; done
168 172
169 173 zsh
170 if [[ -z ${nixShell} ]]; then 174 elif [[ ${miniserve} == "true" ]]; then
171 zsh 175 wait $(cat "${miniservePIDFile}")
172 else
173 nix-shell ${nixShell} --run zsh
174 fi
175 else
176 if [[ -z ${nixShell} ]]; then
177 ${@}
178 else
179 nix-shell ${nixShell} --run "${@}"
180 fi
181 fi 176 fi
182 ) 177 )
183} 178}
@@ -185,27 +180,30 @@ dir() {
185tmpdir() { 180tmpdir() {
186 cleanup() 181 cleanup()
187 { 182 {
188 cd / 183 cd /
189 unmount() { 184 unmount() {
190 printf "Unmounting %s\n" ${1} >&2 185 printf "Unmounting %s\n" ${1} >&2
191 fusermount -u ${1} || umount ${1} || sudo umount ${1} 186 fusermount -u ${1} || umount ${1} || sudo umount ${1}
192 } 187 }
193 188
194 if mountpoint -q -- ${dir}; then 189 if [[ -n ${dir} ]]; then
195 unmount ${dir} || return $? 190 if mountpoint -q -- ${dir}; then
196 else 191 unmount ${dir} || return $?
197 while read -d $'\0' subDir; do 192 else
198 mountpoint -q -- ${subDir} || continue 193 while read -d $'\0' subDir; do
199 unmount ${subDir} || return $? 194 mountpoint -q -- ${subDir} || continue
200 done <<<$(find ${dir} -xdev -type d -print0 | sort -zr) 195 unmount ${subDir} || return $?
201 fi 196 done <<<$(find ${dir} -xdev -type d -print0 | sort -zr)
202 197 fi
203 rm -rfv --one-file-system -- ${dir} 198
199 rm -rfv --one-file-system -- ${dir}
200 dir=""
201 fi
204 } 202 }
205 203
206 local tmpdir="" 204 local tmpdir=""
207 205
208 while getopts ':t:a:s:Sd:ir:wqg:p:' arg; do 206 while getopts ':t:a:d:ir:wg:p:m' arg; do
209 case $arg in 207 case $arg in
210 "t") tmpdir="=${OPTARG}" ;; 208 "t") tmpdir="=${OPTARG}" ;;
211 "?"|":") printf "Invalid option: %s\n" $arg >&2; exit 2 ;; 209 "?"|":") printf "Invalid option: %s\n" $arg >&2; exit 2 ;;
@@ -213,6 +211,8 @@ tmpdir() {
213 done 211 done
214 212
215 ( 213 (
214 set -o localoptions -o localtraps
215 trap 'return 1' INT TERM
216 trap cleanup EXIT 216 trap cleanup EXIT
217 217
218 218
@@ -234,16 +234,6 @@ public-ip() {
234 curl -s -H 'Accept: application/json' $@ ifconfig.co | jq -r '.ip' 234 curl -s -H 'Accept: application/json' $@ ifconfig.co | jq -r '.ip'
235} 235}
236 236
237nix-ghci() {
238 pkgExpr=""
239 if [[ ${#@} -gt 0 ]]; then
240 pkgExpr="${1}"
241 shift
242 fi
243
244 nix-shell -p "with (import <nixpkgs> {}); pkgs.haskellPackages.ghcWithPackages (p: with p; [${pkgExpr}])" --run "ghci ${@}"
245}
246
247swap() { 237swap() {
248 f1=${1} 238 f1=${1}
249 f2=${2} 239 f2=${2}
@@ -271,14 +261,6 @@ l() {
271 ls --long --binary --git --time-style=iso --header $@ 261 ls --long --binary --git --time-style=iso --header $@
272} 262}
273 263
274re() {
275 systemctl restart $@
276}
277
278ure() {
279 systemctl --user restart $@
280}
281
282ssh-installer() { 264ssh-installer() {
283 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/gkleen@sif.midgard.yggdrasil $@ 265 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/gkleen@sif.midgard.yggdrasil $@
284} 266}
diff --git a/accounts/gkleen@surtr.nix b/accounts/gkleen@surtr.nix
index 58c4f21d..8f678ac9 100644
--- a/accounts/gkleen@surtr.nix
+++ b/accounts/gkleen@surtr.nix
@@ -1,3 +1,7 @@
1{ userName, ... }: { 1{ flake, userName, ... }: {
2 home-manager.users.${userName}.home.stateVersion = "20.09"; 2 imports = with flake.nixosModules.userProfiles.${userName}; [
3 zsh tmux
4 ];
5
6 config.home-manager.users.${userName}.home.stateVersion = "20.09";
3} 7}
diff --git a/accounts/gkleen@vidhar.nix b/accounts/gkleen@vidhar.nix
index 8509c2f4..3a37c4bd 100644
--- a/accounts/gkleen@vidhar.nix
+++ b/accounts/gkleen@vidhar.nix
@@ -1,4 +1,8 @@
1{ flake, pkgs, userName, config, ... }: { 1{ flake, pkgs, userName, config, ... }: {
2 imports = with flake.nixosModules.userProfiles.${userName}; [
3 zsh tmux
4 ];
5
2 config = { 6 config = {
3 users.users.${userName} = { 7 users.users.${userName} = {
4 uid = 1000; 8 uid = 1000;
diff --git a/accounts/mherold@eostre.nix b/accounts/mherold@eostre.nix
index 51e4529a..0e2f37aa 100644
--- a/accounts/mherold@eostre.nix
+++ b/accounts/mherold@eostre.nix
@@ -7,9 +7,9 @@
7 home-manager.users.${userName} = { 7 home-manager.users.${userName} = {
8 home.stateVersion = "20.09"; 8 home.stateVersion = "20.09";
9 9
10 nixpkgs.config = { 10 # nixpkgs.config = {
11 allowUnfree = true; 11 # allowUnfree = true;
12 }; 12 # };
13 13
14 home.packages = with pkgs; [ 14 home.packages = with pkgs; [
15 thunderbird libreoffice element-desktop keepassxc vlc 15 thunderbird libreoffice element-desktop keepassxc vlc
diff --git a/accounts/root@installer.nix b/accounts/root@installer.nix
index c7a418f8..5fe1db38 100644
--- a/accounts/root@installer.nix
+++ b/accounts/root@installer.nix
@@ -1,7 +1,11 @@
1{ userName, ... }: 1{ flake, userName, ... }:
2 2
3{ 3{
4 home-manager.users.${userName} = { config, ... } : { 4 imports = with flake.nixosModules.userProfiles.${userName}; [
5 zsh tmux
6 ];
7
8 config.home-manager.users.${userName} = { config, ... } : {
5 home.stateVersion = config.home.version.release; 9 home.stateVersion = config.home.version.release;
6 }; 10 };
7} 11}
diff --git a/accounts/root@sif.nix b/accounts/root@sif.nix
index c9e129a0..bb816230 100644
--- a/accounts/root@sif.nix
+++ b/accounts/root@sif.nix
@@ -1,6 +1,10 @@
1{ userName, ... }: 1{ flake, userName, ... }:
2{ 2{
3 home-manager.users.${userName} = { 3 imports = with flake.nixosModules.userProfiles.${userName}; [
4 zsh tmux
5 ];
6
7 config.home-manager.users.${userName} = {
4 home.stateVersion = "20.09"; 8 home.stateVersion = "20.09";
5 9
6 programs.ssh.matchBlocks = { 10 programs.ssh.matchBlocks = {
diff --git a/accounts/root@surtr.nix b/accounts/root@surtr.nix
index 58c4f21d..8f678ac9 100644
--- a/accounts/root@surtr.nix
+++ b/accounts/root@surtr.nix
@@ -1,3 +1,7 @@
1{ userName, ... }: { 1{ flake, userName, ... }: {
2 home-manager.users.${userName}.home.stateVersion = "20.09"; 2 imports = with flake.nixosModules.userProfiles.${userName}; [
3 zsh tmux
4 ];
5
6 config.home-manager.users.${userName}.home.stateVersion = "20.09";
3} 7}
diff --git a/accounts/root@vidhar.nix b/accounts/root@vidhar.nix
index e82414a8..0fc56633 100644
--- a/accounts/root@vidhar.nix
+++ b/accounts/root@vidhar.nix
@@ -1,6 +1,11 @@
1{ config, userName, ... }: 1{ flake, config, userName, ... }:
2
2{ 3{
3 home-manager.users.${userName} = { 4 imports = with flake.nixosModules.userProfiles.${userName}; [
5 zsh tmux
6 ];
7
8 config.home-manager.users.${userName} = {
4 home.stateVersion = "20.09"; 9 home.stateVersion = "20.09";
5 10
6 programs.ssh.matchBlocks = { 11 programs.ssh.matchBlocks = {
diff --git a/flake.lock b/flake.lock
index 55213878..cab6ae5f 100644
--- a/flake.lock
+++ b/flake.lock
@@ -115,11 +115,11 @@
115 "flake-compat_3": { 115 "flake-compat_3": {
116 "flake": false, 116 "flake": false,
117 "locked": { 117 "locked": {
118 "lastModified": 1733328505, 118 "lastModified": 1747046372,
119 "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 119 "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
120 "owner": "edolstra", 120 "owner": "edolstra",
121 "repo": "flake-compat", 121 "repo": "flake-compat",
122 "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 122 "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
123 "type": "github" 123 "type": "github"
124 }, 124 },
125 "original": { 125 "original": {
@@ -202,11 +202,11 @@
202 "flake-registry": { 202 "flake-registry": {
203 "flake": false, 203 "flake": false,
204 "locked": { 204 "locked": {
205 "lastModified": 1734450202, 205 "lastModified": 1744623129,
206 "narHash": "sha256-/3gigrEBFORQs6a8LL5twoHs7biu08y/8Xc5aQmk3b0=", 206 "narHash": "sha256-nlQTQrHqM+ywXN0evDXnYEV6z6WWZB5BFQ2TkXsduKw=",
207 "owner": "NixOS", 207 "owner": "NixOS",
208 "repo": "flake-registry", 208 "repo": "flake-registry",
209 "rev": "02fe640c9e117dd9d6a34efc7bcb8bd09c08111d", 209 "rev": "1322f33d5836ae757d2e6190239252cf8402acf6",
210 "type": "github" 210 "type": "github"
211 }, 211 },
212 "original": { 212 "original": {
@@ -322,11 +322,11 @@
322 ] 322 ]
323 }, 323 },
324 "locked": { 324 "locked": {
325 "lastModified": 1738691953, 325 "lastModified": 1746904907,
326 "narHash": "sha256-JY/w2Xyrj3mhUhLJkSgk8t7MSf3LGZjewPTQ7QtCbHE=", 326 "narHash": "sha256-XYo6bwc7xwo4lO6a/D2ttYRN4yDmsAjyt5O1E0vOLDg=",
327 "owner": "gkleen", 327 "owner": "gkleen",
328 "repo": "home-manager", 328 "repo": "home-manager",
329 "rev": "c077fc8684289ab1b1c2231bab1566879e099c97", 329 "rev": "696495266c65b76f08d8196b87aa7bd835906570",
330 "type": "github" 330 "type": "github"
331 }, 331 },
332 "original": { 332 "original": {
@@ -343,11 +343,11 @@
343 ] 343 ]
344 }, 344 },
345 "locked": { 345 "locked": {
346 "lastModified": 1710245356, 346 "lastModified": 1747139300,
347 "narHash": "sha256-8cQGUn+N1dTgklMWMejSLN2q8Oz+7Rnqsfaw2rt3bU4=", 347 "narHash": "sha256-V+YnIIM2wMprHGgzOU0HzyeWQEjP6EhG8kc4IffWFeg=",
348 "owner": "gkleen", 348 "owner": "gkleen",
349 "repo": "home-manager", 349 "repo": "home-manager",
350 "rev": "a14fe0c27d04dfa3d80abe2db743e9a7f4f2a33d", 350 "rev": "50182497604587a24bdbe97d6400b1696eac57b1",
351 "type": "github" 351 "type": "github"
352 }, 352 },
353 "original": { 353 "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": 1743186084, 400 "lastModified": 1747638609,
401 "narHash": "sha256-zEPpazywKQLIRWxeTBFnSnacSnGIqM2Qr1iJ5qB94IQ=", 401 "narHash": "sha256-rPTN667tMqC1IQYgsnotVfXbVNbOzScxn0ontMkkSPk=",
402 "owner": "sodiboo", 402 "owner": "sodiboo",
403 "repo": "niri-flake", 403 "repo": "niri-flake",
404 "rev": "111a3afc23fdf64fd76115df48c2e6571fee51b7", 404 "rev": "af697f3a8665c8d0770485a2e659ddde88430e3b",
405 "type": "github" 405 "type": "github"
406 }, 406 },
407 "original": { 407 "original": {
@@ -431,11 +431,11 @@
431 "niri-unstable": { 431 "niri-unstable": {
432 "flake": false, 432 "flake": false,
433 "locked": { 433 "locked": {
434 "lastModified": 1743017820, 434 "lastModified": 1747635487,
435 "narHash": "sha256-EYq2NfWSSO87O7hLrJLPPt0VbgvSjgPzru+58LQ29WI=", 435 "narHash": "sha256-za7ctGh4MaW1h5Drm1WtwNZxiXvQK9yXZAeeIyY9b2Q=",
436 "owner": "YaLTeR", 436 "owner": "YaLTeR",
437 "repo": "niri", 437 "repo": "niri",
438 "rev": "7cfecf4b1b9b8c11c80061fb31926f888228499d", 438 "rev": "3f2b7e63ba15cf33475116d32e8b7d22208a8438",
439 "type": "github" 439 "type": "github"
440 }, 440 },
441 "original": { 441 "original": {
@@ -472,11 +472,11 @@
472 ] 472 ]
473 }, 473 },
474 "locked": { 474 "locked": {
475 "lastModified": 1743306489, 475 "lastModified": 1747540584,
476 "narHash": "sha256-LROaIjSLo347cwcHRfSpqzEOa2FoLSeJwU4dOrGm55E=", 476 "narHash": "sha256-cxCQ413JTUuRv9Ygd8DABJ1D6kuB/nTfQqC0Lu9C0ls=",
477 "owner": "Mic92", 477 "owner": "Mic92",
478 "repo": "nix-index-database", 478 "repo": "nix-index-database",
479 "rev": "b3696bfb6c24aa61428839a99e8b40c53ac3a82d", 479 "rev": "ec179dd13fb7b4c6844f55be91436f7857226dce",
480 "type": "github" 480 "type": "github"
481 }, 481 },
482 "original": { 482 "original": {
@@ -486,6 +486,27 @@
486 "type": "github" 486 "type": "github"
487 } 487 }
488 }, 488 },
489 "nix-monitored": {
490 "inputs": {
491 "nixpkgs": [
492 "nixpkgs"
493 ]
494 },
495 "locked": {
496 "lastModified": 1745680380,
497 "narHash": "sha256-Z8PknjkmIr/8ZCH+dmc2Pc+UltiOr7/oKg37PXuVvuU=",
498 "owner": "ners",
499 "repo": "nix-monitored",
500 "rev": "60f3baa4701d58eab86c2d1d9c3d7e820074d461",
501 "type": "github"
502 },
503 "original": {
504 "owner": "ners",
505 "ref": "master",
506 "repo": "nix-monitored",
507 "type": "github"
508 }
509 },
489 "nixVirt": { 510 "nixVirt": {
490 "inputs": { 511 "inputs": {
491 "nixpkgs": [ 512 "nixpkgs": [
@@ -493,11 +514,11 @@
493 ] 514 ]
494 }, 515 },
495 "locked": { 516 "locked": {
496 "lastModified": 1741549407, 517 "lastModified": 1747637556,
497 "narHash": "sha256-f9SXK+/rvlryDNlc++Eva/hYjbkf7OCalWwmwifRhtI=", 518 "narHash": "sha256-AYd1nE+BLWTZS8J0eFQ7kuNiuE+XjhhndoXinj7en/M=",
498 "owner": "AshleyYakeley", 519 "owner": "AshleyYakeley",
499 "repo": "NixVirt", 520 "repo": "NixVirt",
500 "rev": "9950b932dce4ae6b9bda7c83d41705c1a14e10f0", 521 "rev": "a7d3d3ae8b9a0cf3ec3cf504bb593df0618a6dbc",
501 "type": "github" 522 "type": "github"
502 }, 523 },
503 "original": { 524 "original": {
@@ -508,11 +529,11 @@
508 }, 529 },
509 "nixos-hardware": { 530 "nixos-hardware": {
510 "locked": { 531 "locked": {
511 "lastModified": 1743167577, 532 "lastModified": 1747129300,
512 "narHash": "sha256-I09SrXIO0UdyBFfh0fxDq5WnCDg8XKmZ1HQbaXzMA1k=", 533 "narHash": "sha256-L3clA5YGeYCF47ghsI7Tcex+DnaaN/BbQ4dR2wzoiKg=",
513 "owner": "NixOS", 534 "owner": "NixOS",
514 "repo": "nixos-hardware", 535 "repo": "nixos-hardware",
515 "rev": "0ed819e708af17bfc4bbc63ee080ef308a24aa42", 536 "rev": "e81fd167b33121269149c57806599045fd33eeed",
516 "type": "github" 537 "type": "github"
517 }, 538 },
518 "original": { 539 "original": {
@@ -630,11 +651,11 @@
630 }, 651 },
631 "nixpkgs-stable_2": { 652 "nixpkgs-stable_2": {
632 "locked": { 653 "locked": {
633 "lastModified": 1742937945, 654 "lastModified": 1747485343,
634 "narHash": "sha256-lWc+79eZRyvHp/SqMhHTMzZVhpxkRvthsP1Qx6UCq0E=", 655 "narHash": "sha256-YbsZyuRE1tobO9sv0PUwg81QryYo3L1F3R3rF9bcG38=",
635 "owner": "NixOS", 656 "owner": "NixOS",
636 "repo": "nixpkgs", 657 "repo": "nixpkgs",
637 "rev": "d02d88f8de5b882ccdde0465d8fa2db3aa1169f7", 658 "rev": "9b5ac7ad45298d58640540d0323ca217f32a6762",
638 "type": "github" 659 "type": "github"
639 }, 660 },
640 "original": { 661 "original": {
@@ -678,11 +699,11 @@
678 }, 699 },
679 "nixpkgs_2": { 700 "nixpkgs_2": {
680 "locked": { 701 "locked": {
681 "lastModified": 1743095683, 702 "lastModified": 1747542820,
682 "narHash": "sha256-gWd4urRoLRe8GLVC/3rYRae1h+xfQzt09xOfb0PaHSk=", 703 "narHash": "sha256-GaOZntlJ6gPPbbkTLjbd8BMWaDYafhuuYRNrxCGnPJw=",
683 "owner": "NixOS", 704 "owner": "NixOS",
684 "repo": "nixpkgs", 705 "repo": "nixpkgs",
685 "rev": "5e5402ecbcb27af32284d4a62553c019a3a49ea6", 706 "rev": "292fa7d4f6519c074f0a50394dbbe69859bb6043",
686 "type": "github" 707 "type": "github"
687 }, 708 },
688 "original": { 709 "original": {
@@ -748,11 +769,11 @@
748 "treefmt-nix": "treefmt-nix" 769 "treefmt-nix": "treefmt-nix"
749 }, 770 },
750 "locked": { 771 "locked": {
751 "lastModified": 1742397518, 772 "lastModified": 1743690424,
752 "narHash": "sha256-nzgO/ZCSBzWjbMkYDxG+yl9Z2eGbCgQu06Oku3ir5D4=", 773 "narHash": "sha256-cX98bUuKuihOaRp8dNV1Mq7u6/CQZWTPth2IJPATBXc=",
753 "owner": "nix-community", 774 "owner": "nix-community",
754 "repo": "poetry2nix", 775 "repo": "poetry2nix",
755 "rev": "b9a98080beff0903a5e5fe431f42cde1e3e50d6b", 776 "rev": "ce2369db77f45688172384bbeb962bc6c2ea6f94",
756 "type": "github" 777 "type": "github"
757 }, 778 },
758 "original": { 779 "original": {
@@ -858,6 +879,52 @@
858 "type": "gitlab" 879 "type": "gitlab"
859 } 880 }
860 }, 881 },
882 "pyproject-build-systems": {
883 "inputs": {
884 "nixpkgs": [
885 "nixpkgs"
886 ],
887 "pyproject-nix": [
888 "pyproject-nix"
889 ],
890 "uv2nix": [
891 "uv2nix"
892 ]
893 },
894 "locked": {
895 "lastModified": 1744599653,
896 "narHash": "sha256-nysSwVVjG4hKoOjhjvE6U5lIKA8sEr1d1QzEfZsannU=",
897 "owner": "pyproject-nix",
898 "repo": "build-system-pkgs",
899 "rev": "7dba6dbc73120e15b558754c26024f6c93015dd7",
900 "type": "github"
901 },
902 "original": {
903 "owner": "pyproject-nix",
904 "repo": "build-system-pkgs",
905 "type": "github"
906 }
907 },
908 "pyproject-nix": {
909 "inputs": {
910 "nixpkgs": [
911 "nixpkgs"
912 ]
913 },
914 "locked": {
915 "lastModified": 1746540146,
916 "narHash": "sha256-QxdHGNpbicIrw5t6U3x+ZxeY/7IEJ6lYbvsjXmcxFIM=",
917 "owner": "pyproject-nix",
918 "repo": "pyproject.nix",
919 "rev": "e09c10c24ebb955125fda449939bfba664c467fd",
920 "type": "github"
921 },
922 "original": {
923 "owner": "pyproject-nix",
924 "repo": "pyproject.nix",
925 "type": "github"
926 }
927 },
861 "root": { 928 "root": {
862 "inputs": { 929 "inputs": {
863 "backup-utils": "backup-utils", 930 "backup-utils": "backup-utils",
@@ -871,6 +938,7 @@
871 "impermanence": "impermanence", 938 "impermanence": "impermanence",
872 "niri-flake": "niri-flake", 939 "niri-flake": "niri-flake",
873 "nix-index-database": "nix-index-database", 940 "nix-index-database": "nix-index-database",
941 "nix-monitored": "nix-monitored",
874 "nixVirt": "nixVirt", 942 "nixVirt": "nixVirt",
875 "nixos-hardware": "nixos-hardware", 943 "nixos-hardware": "nixos-hardware",
876 "nixpkgs": "nixpkgs_2", 944 "nixpkgs": "nixpkgs_2",
@@ -880,7 +948,10 @@
880 "nvfetcher": "nvfetcher", 948 "nvfetcher": "nvfetcher",
881 "poetry2nix": "poetry2nix", 949 "poetry2nix": "poetry2nix",
882 "prometheus-borg-exporter": "prometheus-borg-exporter", 950 "prometheus-borg-exporter": "prometheus-borg-exporter",
951 "pyproject-build-systems": "pyproject-build-systems",
952 "pyproject-nix": "pyproject-nix",
883 "sops-nix": "sops-nix", 953 "sops-nix": "sops-nix",
954 "uv2nix": "uv2nix",
884 "waybar": "waybar" 955 "waybar": "waybar"
885 } 956 }
886 }, 957 },
@@ -891,11 +962,11 @@
891 ] 962 ]
892 }, 963 },
893 "locked": { 964 "locked": {
894 "lastModified": 1743305778, 965 "lastModified": 1747603214,
895 "narHash": "sha256-Ux/UohNtnM5mn9SFjaHp6IZe2aAnUCzklMluNtV6zFo=", 966 "narHash": "sha256-lAblXm0VwifYCJ/ILPXJwlz0qNY07DDYdLD+9H+Wc8o=",
896 "owner": "Mic92", 967 "owner": "Mic92",
897 "repo": "sops-nix", 968 "repo": "sops-nix",
898 "rev": "8e873886bbfc32163fe027b8676c75637b7da114", 969 "rev": "8d215e1c981be3aa37e47aeabd4e61bb069548fd",
899 "type": "github" 970 "type": "github"
900 }, 971 },
901 "original": { 972 "original": {
@@ -956,6 +1027,29 @@
956 "type": "github" 1027 "type": "github"
957 } 1028 }
958 }, 1029 },
1030 "uv2nix": {
1031 "inputs": {
1032 "nixpkgs": [
1033 "nixpkgs"
1034 ],
1035 "pyproject-nix": [
1036 "pyproject-nix"
1037 ]
1038 },
1039 "locked": {
1040 "lastModified": 1747441483,
1041 "narHash": "sha256-W8BFXk5R0TuJcjIhcGoMpSOaIufGXpizK0pm+uTqynA=",
1042 "owner": "pyproject-nix",
1043 "repo": "uv2nix",
1044 "rev": "582024dc64663e9f88d467c2f7f7b20d278349de",
1045 "type": "github"
1046 },
1047 "original": {
1048 "owner": "pyproject-nix",
1049 "repo": "uv2nix",
1050 "type": "github"
1051 }
1052 },
959 "waybar": { 1053 "waybar": {
960 "inputs": { 1054 "inputs": {
961 "flake-compat": [ 1055 "flake-compat": [
@@ -966,16 +1060,16 @@
966 ] 1060 ]
967 }, 1061 },
968 "locked": { 1062 "locked": {
969 "lastModified": 1742140394, 1063 "lastModified": 1747383113,
970 "narHash": "sha256-U1Lp5HZbpnWQRetOLzQl3dURplY2BRfAZYkjBawYrVM=", 1064 "narHash": "sha256-/YW7eOKU3gsNplxvUDpEj1LiXtcCENSFpS1c8kXSDWw=",
971 "owner": "gkleen", 1065 "owner": "gkleen",
972 "repo": "Waybar", 1066 "repo": "Waybar",
973 "rev": "f310667db199c570b599a08152d49b7f80db93f2", 1067 "rev": "919036587381595f15010ac95644992fe6d7343d",
974 "type": "github" 1068 "type": "github"
975 }, 1069 },
976 "original": { 1070 "original": {
977 "owner": "gkleen", 1071 "owner": "gkleen",
978 "ref": "feat/niri-workspaces-hide", 1072 "ref": "feat/niri-urgency",
979 "repo": "Waybar", 1073 "repo": "Waybar",
980 "type": "github" 1074 "type": "github"
981 } 1075 }
@@ -1000,11 +1094,11 @@
1000 "xwayland-satellite-unstable": { 1094 "xwayland-satellite-unstable": {
1001 "flake": false, 1095 "flake": false,
1002 "locked": { 1096 "locked": {
1003 "lastModified": 1742773235, 1097 "lastModified": 1747111562,
1004 "narHash": "sha256-YhJex62HHVF6EfdGLIC01uM6jH8XJu5ryZ+LlhG7wMs=", 1098 "narHash": "sha256-GAqhWoxaBIk0tgoecZPa8gTHDHxNc0JtlwWHZN2iOOo=",
1005 "owner": "Supreeeme", 1099 "owner": "Supreeeme",
1006 "repo": "xwayland-satellite", 1100 "repo": "xwayland-satellite",
1007 "rev": "b2613aec05f9e3f8488ef924203d62cafb712642", 1101 "rev": "ec9ff64c1e0cbec42710b580b7c0f759b1694e72",
1008 "type": "github" 1102 "type": "github"
1009 }, 1103 },
1010 "original": { 1104 "original": {
diff --git a/flake.nix b/flake.nix
index 1cc9782c..20ab9f7e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -125,6 +125,21 @@
125 nixpkgs.follows = "nixpkgs"; 125 nixpkgs.follows = "nixpkgs";
126 }; 126 };
127 }; 127 };
128 pyproject-nix = {
129 url = "github:pyproject-nix/pyproject.nix";
130 inputs.nixpkgs.follows = "nixpkgs";
131 };
132 uv2nix = {
133 url = "github:pyproject-nix/uv2nix";
134 inputs.pyproject-nix.follows = "pyproject-nix";
135 inputs.nixpkgs.follows = "nixpkgs";
136 };
137 pyproject-build-systems = {
138 url = "github:pyproject-nix/build-system-pkgs";
139 inputs.pyproject-nix.follows = "pyproject-nix";
140 inputs.uv2nix.follows = "uv2nix";
141 inputs.nixpkgs.follows = "nixpkgs";
142 };
128 143
129 ca-util = { 144 ca-util = {
130 type = "gitlab"; 145 type = "gitlab";
@@ -172,7 +187,7 @@
172 type = "github"; 187 type = "github";
173 owner = "gkleen"; 188 owner = "gkleen";
174 repo = "Waybar"; 189 repo = "Waybar";
175 ref = "feat/niri-workspaces-hide"; 190 ref = "feat/niri-urgency";
176 inputs = { 191 inputs = {
177 nixpkgs.follows = "nixpkgs"; 192 nixpkgs.follows = "nixpkgs";
178 flake-compat.follows = "flake-compat"; 193 flake-compat.follows = "flake-compat";
@@ -194,6 +209,15 @@
194 # niri-unstable.url = "github:gkleen/niri"; 209 # niri-unstable.url = "github:gkleen/niri";
195 }; 210 };
196 }; 211 };
212 nix-monitored = {
213 type = "github";
214 owner = "ners";
215 repo = "nix-monitored";
216 ref = "master";
217 inputs = {
218 nixpkgs.follows = "nixpkgs";
219 };
220 };
197 }; 221 };
198 222
199 outputs = { self, nixpkgs, home-manager, sops-nix, deploy-rs, nvfetcher, niri-flake, ... }@inputs: 223 outputs = { self, nixpkgs, home-manager, sops-nix, deploy-rs, nvfetcher, niri-flake, ... }@inputs:
@@ -339,7 +363,7 @@
339 363
340 overlays = mapAttrs (_name: path: mkOverlay path) overlayPaths; 364 overlays = mapAttrs (_name: path: mkOverlay path) overlayPaths;
341 365
342 packages = forAllSystems (system: systemPkgs: nixImport rec { dir = ./tools; _import = _path: name: import "${toString dir}/${name}" ({ inherit system; } // inputs); }); 366 packages = forAllSystems (system: systemPkgs: nixImport rec { dir = ./tools; _import = name: _base: import (dir + "/${name}") ({ inherit system; } // inputs); });
343 367
344 # packages = mapAttrs (_name: filterAttrs (_name: isDerivation)) packages; 368 # packages = mapAttrs (_name: filterAttrs (_name: isDerivation)) packages;
345 # packages' = mapAttrs (_name: filterAttrs (_name: value: !(isDerivation value))) packages; 369 # packages' = mapAttrs (_name: filterAttrs (_name: value: !(isDerivation value))) packages;
@@ -351,6 +375,8 @@
351 activateNixosConfigurations activateHomeManagerConfigurations 375 activateNixosConfigurations activateHomeManagerConfigurations
352 ]; 376 ];
353 377
378 lib = nixImport rec { dir = ./lib; _import = name: _base: import (dir + "/${name}") inputs; };
379
354 devShells = forAllSystems (system: systemPkgs: { default = import ./shell.nix ({ inherit system; } // inputs); } // installerShells system systemPkgs); 380 devShells = forAllSystems (system: systemPkgs: { default = import ./shell.nix ({ inherit system; } // inputs); } // installerShells system systemPkgs);
355 381
356 templates.default = { 382 templates.default = {
@@ -374,10 +400,10 @@
374 # path = activateHomeManager (self.nixosConfigurations.${hostname}.config.nixpkgs.system) usercfg.home; 400 # path = activateHomeManager (self.nixosConfigurations.${hostname}.config.nixpkgs.system) usercfg.home;
375 # }) self.nixosConfigurations.${hostname}.config.home-manager.users); 401 # }) self.nixosConfigurations.${hostname}.config.home-manager.users);
376 }) (nixImport { dir = ./hosts; _import = (_path: name: name); }); 402 }) (nixImport { dir = ./hosts; _import = (_path: name: name); });
377 overrides = if pathExists ./deploy then nixImport { dir = ./deploy; _import = path: _name: import (./deploy + "/${path}") inputs; } else {}; 403 overrides = if pathExists ./deploy then nixImport rec { dir = ./deploy; _import = path: _name: import (dir + "/${path}") inputs; } else {};
378 filterEnabled = attrs: mapAttrs (_n: v: filterAttrs (n: _v: n != "enabled") v) (filterAttrs (_n: v: v.enabled or true) attrs); 404 filterEnabled = attrs: mapAttrs (_n: v: filterAttrs (n: _v: n != "enabled") v) (filterAttrs (_n: v: v.enabled or true) attrs);
379 in mapAttrs (_n: v: if v ? "profiles" then v // { profiles = filterEnabled v.profiles; } else v) (filterEnabled (recursiveUpdate defaults overrides)); 405 in mapAttrs (_n: v: if v ? "profiles" then v // { profiles = filterEnabled v.profiles; } else v) (filterEnabled (recursiveUpdate defaults overrides));
380 406
381 checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib; 407 # checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
382 }; 408 };
383} 409}
diff --git a/home-modules/nixpkgs-release-check.nix b/home-modules/nixpkgs-release-check.nix
new file mode 100644
index 00000000..baf2713a
--- /dev/null
+++ b/home-modules/nixpkgs-release-check.nix
@@ -0,0 +1,4 @@
1{ ... }:
2{
3 config.home.enableNixpkgsReleaseCheck = false;
4}
diff --git a/hosts/sif/default.nix b/hosts/sif/default.nix
index 9208e391..6214569a 100644
--- a/hosts/sif/default.nix
+++ b/hosts/sif/default.nix
@@ -12,7 +12,7 @@ let
12in { 12in {
13 imports = with flake.nixosModules.systemProfiles; [ 13 imports = with flake.nixosModules.systemProfiles; [
14 ./hw.nix 14 ./hw.nix
15 ./mail ./libvirt ./greetd 15 ./email ./libvirt ./greetd
16 tmpfs-root bcachefs initrd-all-crypto-modules default-locale openssh rebuild-machines niri-unstable networkmanager 16 tmpfs-root bcachefs initrd-all-crypto-modules default-locale openssh rebuild-machines niri-unstable networkmanager
17 flakeInputs.nixos-hardware.nixosModules.lenovo-thinkpad-p1 17 flakeInputs.nixos-hardware.nixosModules.lenovo-thinkpad-p1
18 flakeInputs.impermanence.nixosModules.impermanence 18 flakeInputs.impermanence.nixosModules.impermanence
@@ -52,6 +52,7 @@ in {
52 systemd-boot = { 52 systemd-boot = {
53 enable = true; 53 enable = true;
54 configurationLimit = 15; 54 configurationLimit = 15;
55 netbootxyz.enable = true;
55 }; 56 };
56 efi.canTouchEfiVariables = true; 57 efi.canTouchEfiVariables = true;
57 timeout = null; 58 timeout = null;
@@ -125,40 +126,16 @@ in {
125 rulesetFile = ./ruleset.nft; 126 rulesetFile = ./ruleset.nft;
126 }; 127 };
127 128
128 # firewall = {
129 # enable = true;
130 # allowedTCPPorts = [ 22 # ssh
131 # 8000 # quickserve
132 # ];
133 # };
134
135 # wlanInterfaces = {
136 # wlan0 = {
137 # device = "wlp82s0";
138 # };
139 # };
140
141 # bonds = {
142 # "lan" = {
143 # interfaces = [ "wlan0" "enp0s31f6" "dock0" ];
144 # driverOptions = {
145 # miimon = "1000";
146 # mode = "active-backup";
147 # primary_reselect = "always";
148 # };
149 # };
150 # };
151
152 useDHCP = false; 129 useDHCP = false;
153 useNetworkd = true; 130 useNetworkd = true;
154
155 # interfaces."tinc.yggdrasil" = {
156 # virtual = true;
157 # virtualType = config.services.tinc.networks.yggdrasil.interfaceType;
158 # macAddress = "5c:93:21:c3:61:39";
159 # };
160 }; 131 };
161 132
133 environment.etc."NetworkManager/dnsmasq.d/dnssec.conf" = {
134 text = ''
135 conf-file=${pkgs.dnsmasq}/share/dnsmasq/trust-anchors.conf
136 dnssec
137 '';
138 };
162 environment.etc."NetworkManager/dnsmasq.d/libvirt_dnsmasq.conf" = { 139 environment.etc."NetworkManager/dnsmasq.d/libvirt_dnsmasq.conf" = {
163 text = '' 140 text = ''
164 except-interface=virbr0 141 except-interface=virbr0
@@ -401,19 +378,6 @@ in {
401 ]; 378 ];
402 379
403 services = { 380 services = {
404 uucp = {
405 enable = true;
406 nodeName = "sif";
407 remoteNodes = {
408 "ymir" = {
409 publicKeys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG6KNtsCOl5fsZ4rV7udTulGMphJweLBoKapzerWNoLY root@ymir"];
410 hostnames = ["ymir.yggdrasil.li" "ymir.niflheim.yggdrasil"];
411 };
412 };
413
414 defaultCommands = lib.mkForce [];
415 };
416
417 avahi.enable = true; 381 avahi.enable = true;
418 382
419 fwupd.enable = true; 383 fwupd.enable = true;
@@ -432,8 +396,8 @@ in {
432 396
433 logind = { 397 logind = {
434 lidSwitch = "suspend"; 398 lidSwitch = "suspend";
435 lidSwitchDocked = "lock"; 399 lidSwitchDocked = "ignore";
436 lidSwitchExternalPower = "lock"; 400 lidSwitchExternalPower = "ignore";
437 }; 401 };
438 402
439 atd = { 403 atd = {
@@ -639,25 +603,6 @@ in {
639 603
640 environment.etc."X11/xorg.conf.d/50-wacom.conf".source = lib.mkForce ./wacom.conf; 604 environment.etc."X11/xorg.conf.d/50-wacom.conf".source = lib.mkForce ./wacom.conf;
641 605
642 systemd.services."ac-plugged" = {
643 description = "Inhibit handling of lid-switch and sleep";
644
645 path = with pkgs; [ systemd coreutils ];
646
647 script = ''
648 exec systemd-inhibit --what=handle-lid-switch --why="AC is connected" --mode=block sleep infinity
649 '';
650
651 serviceConfig = {
652 Type = "simple";
653 };
654 };
655
656 services.udev.extraRules = with pkgs; lib.mkAfter ''
657 SUBSYSTEM=="power_supply", ENV{POWER_SUPPLY_ONLINE}=="0", RUN+="${systemd}/bin/systemctl --no-block stop ac-plugged.service"
658 SUBSYSTEM=="power_supply", ENV{POWER_SUPPLY_ONLINE}=="1", RUN+="${systemd}/bin/systemctl --no-block start ac-plugged.service"
659 '';
660
661 systemd.services."nix-daemon".serviceConfig = { 606 systemd.services."nix-daemon".serviceConfig = {
662 MemoryAccounting = true; 607 MemoryAccounting = true;
663 MemoryHigh = "50%"; 608 MemoryHigh = "50%";
@@ -717,7 +662,7 @@ in {
717 directories = [ 662 directories = [
718 "/nix" 663 "/nix"
719 "/root" 664 "/root"
720 "/home" 665 "/home"
721 "/var/log" 666 "/var/log"
722 "/var/lib/sops-nix" 667 "/var/lib/sops-nix"
723 "/var/lib/nixos" 668 "/var/lib/nixos"
@@ -728,8 +673,6 @@ in {
728 "/var/lib/upower" 673 "/var/lib/upower"
729 "/var/lib/postfix" 674 "/var/lib/postfix"
730 "/etc/NetworkManager/system-connections" 675 "/etc/NetworkManager/system-connections"
731 { directory = "/var/uucp"; user = "uucp"; group = "uucp"; mode = "0700"; }
732 { directory = "/var/spool/uucp"; user = "uucp"; group = "uucp"; mode = "0750"; }
733 ]; 676 ];
734 files = [ 677 files = [
735 ]; 678 ];
@@ -750,10 +693,6 @@ in {
750 693
751 home-manager.sharedModules = [ flakeInputs.nixVirt.homeModules.default ]; 694 home-manager.sharedModules = [ flakeInputs.nixVirt.homeModules.default ];
752 695
753 environment.pathsToLink = [
754 "share/zsh"
755 ];
756
757 system.stateVersion = "24.11"; 696 system.stateVersion = "24.11";
758 }; 697 };
759} 698}
diff --git a/hosts/sif/email/default.nix b/hosts/sif/email/default.nix
new file mode 100644
index 00000000..4eda236e
--- /dev/null
+++ b/hosts/sif/email/default.nix
@@ -0,0 +1,110 @@
1{ config, lib, pkgs, ... }:
2{
3 services.postfix = {
4 enable = true;
5 enableSmtp = false;
6 enableSubmission = false;
7 setSendmail = true;
8 networksStyle = "host";
9 hostname = "sif.midgard.yggdrasil";
10 destination = [];
11 recipientDelimiter = "+";
12 config = {
13 mydomain = "yggdrasil.li";
14
15 local_transport = "error:5.1.1 No local delivery";
16 alias_database = [];
17 alias_maps = [];
18 local_recipient_maps = [];
19
20 inet_interfaces = "loopback-only";
21
22 message_size_limit = "0";
23
24 authorized_submit_users = "inline:{ gkleen= }";
25 authorized_flush_users = "inline:{ gkleen= }";
26 authorized_mailq_users = "inline:{ gkleen= }";
27
28 smtp_generic_maps = "inline:{ root=root+sif }";
29
30 mynetworks = ["127.0.0.0/8" "[::1]/128"];
31 smtpd_client_restrictions = ["permit_mynetworks" "reject"];
32 smtpd_relay_restrictions = ["permit_mynetworks" "reject"];
33
34 sender_dependent_default_transport_maps = ''regexp:${pkgs.writeText "sender_relay" ''
35 /@(cip|stud)\.ifi\.(lmu|uni-muenchen)\.de$/ smtp:smtp.ifi.lmu.de
36 /@ifi\.(lmu|uni-muenchen)\.de$/ smtp:smtpin1.ifi.lmu.de:587
37 /@math(ematik)?\.(lmu|uni-muenchen)\.de$/ smtps:smtp.math.lmu.de:465
38 /@(campus\.)?lmu\.de$/ smtp:postout.lrz.de
39 ''}'';
40 sender_bcc_maps = ''regexp:${pkgs.writeText "sender_bcc" ''
41 /^uni2work(-[^@]*)?@ifi\.lmu\.de$/ uni2work@ifi.lmu.de
42 /@ifi\.lmu\.de$/ gregor.kleen@ifi.lmu.de
43 ''}'';
44 relayhost = "[surtr.yggdrasil.li]:465";
45 default_transport = "relay";
46
47 smtp_sasl_auth_enable = true;
48 smtp_sender_dependent_authentication = true;
49 smtp_sasl_tls_security_options = "noanonymous";
50 smtp_sasl_mechanism_filter = ["plain"];
51 smtp_sasl_password_maps = "regexp:/run/credentials/postfix.service/sasl_passwd";
52 smtp_cname_overrides_servername = false;
53 smtp_always_send_ehlo = true;
54 smtp_tls_security_level = "dane";
55
56 smtp_tls_loglevel = "1";
57 smtp_dns_support_level = "dnssec";
58 };
59 masterConfig = {
60 submission = {
61 type = "inet";
62 private = false;
63 command = "smtpd";
64 args = [
65 "-o" "syslog_name=postfix/$service_name"
66 ];
67 };
68 smtp = { };
69 smtps = {
70 type = "unix";
71 private = true;
72 privileged = true;
73 chroot = false;
74 command = "smtp";
75 args = [
76 "-o" "smtp_tls_wrappermode=yes"
77 "-o" "smtp_tls_security_level=encrypt"
78 ];
79 };
80 relay = {
81 command = "smtp";
82 args = [
83 "-o" "smtp_fallback_relay="
84 "-o" "smtp_tls_security_level=verify"
85 "-o" "smtp_tls_wrappermode=yes"
86 "-o" "smtp_tls_cert_file=${./relay.crt}"
87 "-o" "smtp_tls_key_file=/run/credentials/postfix.service/relay.key"
88 ];
89 };
90 };
91 };
92
93 systemd.services.postfix = {
94 serviceConfig.LoadCredential = [
95 "sasl_passwd:${config.sops.secrets."postfix-sasl-passwd".path}"
96 "relay.key:${config.sops.secrets."relay-key".path}"
97 ];
98 };
99
100 sops.secrets = {
101 postfix-sasl-passwd = {
102 key = "sasl-passwd";
103 sopsFile = ./secrets.yaml;
104 };
105 relay-key = {
106 format = "binary";
107 sopsFile = ./relay.key;
108 };
109 };
110}
diff --git a/hosts/sif/email/relay.crt b/hosts/sif/email/relay.crt
new file mode 100644
index 00000000..ac13e7cb
--- /dev/null
+++ b/hosts/sif/email/relay.crt
@@ -0,0 +1,11 @@
1-----BEGIN CERTIFICATE-----
2MIIBjDCCAQygAwIBAgIPQAAAAGgLfNoL/PSMAsutMAUGAytlcTAXMRUwEwYDVQQD
3DAx5Z2dkcmFzaWwubGkwHhcNMjUwNDI1MTIwOTQ1WhcNMzUwNDI2MTIxNDQ1WjAR
4MQ8wDQYDVQQDDAZna2xlZW4wKjAFBgMrZXADIQB3outi3/3F4YO7Q97WAAaMHW0a
5m+Blldrgee+EZnWnD6N1MHMwHwYDVR0jBBgwFoAUTtn+VjMw6Ge1f68KD8dT1CWn
6l3YwHQYDVR0OBBYEFFOa4rYZYMbXUVdKv98NB504GUhjMA4GA1UdDwEB/wQEAwID
76DAMBgNVHRMBAf8EAjAAMBMGA1UdJQQMMAoGCCsGAQUFBwMCMAUGAytlcQNzABC0
80UgIt7gLZrU1TmzGoqPBris8R1DbKOJacicF5CU0MIIjHcX7mPFW8KtB4qm6KcPq
9kF6IaEPmgKpX3Nubk8HJik9vhIy9ysfINcVTvzXx8pO1bxbvREJRyA/apj10nzav
10yauId0cXHvN6g5RLAMsMAA==
11-----END CERTIFICATE-----
diff --git a/hosts/sif/email/relay.key b/hosts/sif/email/relay.key
new file mode 100644
index 00000000..412a44e0
--- /dev/null
+++ b/hosts/sif/email/relay.key
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:lBlTuzOS75pvRmcTKT4KhHMH44RlE2SvCFAUP+GfsXws1Uai7DZ1MmbhvxxCa+pcLW19+sQYxrXLRNZWby1yOeKBJ2UQeYV5LOk9LSL/WIE3FZkCo5Dv0O0gSFKjjb61WN22a4JnHbLWADf/mLT3GZv91XfvFDo=,iv:ho8wQH3UNzX9JPW5gVcUGtxZzdVwsMFus0Z4KYe5t48=,tag:dAgZyHOva2xVVhE1nTl+lg==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6eTVRSUdFNUZGZmcxSUlT\nWmlsOGNyWXIzMGNTZjlKbXlhcEdZUXFRVkR3Cll0T0RMd0h2UW16QkR3SHlhYmNZ\nNDFrYXh3Rkp5NWsvcWc3UFJJaHVwT1UKLS0tIHhXVEI0VHBZVkpDQ1FzWENjMmJH\nb1FQWXVUUTBiZ1pKWG00MTNqVEo2SjAKK3VOU+QgRuxWYWEcrJiVMRFCprBICz4F\ngD+9zuPUzPezyJkYwTs+M+wX5GYkXppqm5W58yQLS2UDD38sr+SRjg==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age1fj65apkhfkrwyv5tx6zcs9nkjg8267fy733qph30sc7zfn7vapjqkd5kne",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzWmJmZDVFazN2bDY1TkNG\nNXpJN2twMFFjZUxMTVdSNzJwQTFiYktrcGdrCjk4eFVHTko0bFVMSlFFWm9tbjMr\nbWNHMEQ1Rm1qUVhodlB1RGw2aDc4TUEKLS0tIERBK0J5NkN4OXJEZ1ZOZXhNc1Jm\naWNnUmZGbTIxdmNkYi9TZ2h2bGs3MVEKPQGaEf7M/5/xvSOfawpIp50fB3QfFSuz\nPgkrPMneaBeUx+uBYMyEFX4rpzLIBR3pnYMjAfoc+bjWaOtGQuEqyQ==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-04-25T12:14:44Z",
15 "mac": "ENC[AES256_GCM,data:pObl2bJA93az9E3Ya+hA3ekI8TKKZ9NNTi0KzmWZBOiQwi9FuQYtpnmmT80L1KXWyOKJV6wGdAri3mNe/ue2S0TziSbQ/4+Dj4ubFKgkH7thb5q2dFyxw5FzhYzRQiXFqD/pxcNN9uL0lQI2Al0Eci0zX8Kcd1rAQ6RzLEoSmco=,iv:zo/3QFKTUEDxLy1k5yyU7Z1JMZ7cKdYUc6GHjaTTZKQ=,tag:f63Eja3lBfwJCYAOyEt56g==,type:str]",
16 "unencrypted_suffix": "_unencrypted",
17 "version": "3.10.2"
18 }
19}
diff --git a/hosts/sif/mail/secrets.yaml b/hosts/sif/email/secrets.yaml
index 3c74b710..3c74b710 100644
--- a/hosts/sif/mail/secrets.yaml
+++ b/hosts/sif/email/secrets.yaml
diff --git a/hosts/sif/mail/default.nix b/hosts/sif/mail/default.nix
deleted file mode 100644
index 8d6cd705..00000000
--- a/hosts/sif/mail/default.nix
+++ /dev/null
@@ -1,70 +0,0 @@
1{ config, lib, pkgs, ... }:
2{
3 services.postfix = {
4 enable = true;
5 enableSmtp = true;
6 enableSubmission = false;
7 setSendmail = true;
8 networksStyle = "host";
9 hostname = "sif.midgard.yggdrasil";
10 destination = [];
11 relayHost = "uucp:ymir";
12 recipientDelimiter = "+";
13 masterConfig = {
14 uucp = {
15 type = "unix";
16 private = true;
17 privileged = true;
18 chroot = false;
19 command = "pipe";
20 args = [ "flags=Fqhu" "user=uucp" ''argv=${config.security.wrapperDir}/uux -z -a $sender - $nexthop!rmail ($recipient)'' ];
21 };
22 smtps = {
23 type = "unix";
24 private = true;
25 privileged = true;
26 chroot = false;
27 command = "smtp";
28 args = [ "-o" "smtp_tls_wrappermode=yes" "-o" "smtp_tls_security_level=encrypt" ];
29 };
30 };
31 config = {
32 default_transport = "uucp:ymir";
33
34 inet_interfaces = "loopback-only";
35
36 authorized_submit_users = ["!uucp" "static:anyone"];
37 message_size_limit = "0";
38
39 sender_dependent_default_transport_maps = ''regexp:${pkgs.writeText "sender_relay" ''
40 /@(cip|stud)\.ifi\.(lmu|uni-muenchen)\.de$/ smtp:smtp.ifi.lmu.de
41 /@ifi\.(lmu|uni-muenchen)\.de$/ smtp:smtpin1.ifi.lmu.de:587
42 /@math(ematik)?\.(lmu|uni-muenchen)\.de$/ smtps:smtp.math.lmu.de:465
43 /@(campus\.)?lmu\.de$/ smtp:postout.lrz.de
44 ''}'';
45 sender_bcc_maps = ''regexp:${pkgs.writeText "sender_bcc" ''
46 /^uni2work(-[^@]*)?@ifi\.lmu\.de$/ uni2work@ifi.lmu.de
47 /@ifi\.lmu\.de$/ gregor.kleen@ifi.lmu.de
48 ''}'';
49
50 smtp_sasl_auth_enable = true;
51 smtp_sender_dependent_authentication = true;
52 smtp_sasl_tls_security_options = "noanonymous";
53 smtp_sasl_mechanism_filter = ["plain"];
54 smtp_sasl_password_maps = "regexp:/var/db/postfix/sasl_passwd";
55 smtp_cname_overrides_servername = false;
56 smtp_always_send_ehlo = true;
57 smtp_tls_security_level = "dane";
58
59 smtp_tls_loglevel = "1";
60 smtp_dns_support_level = "dnssec";
61 };
62 };
63
64 sops.secrets.postfix-sasl-passwd = {
65 key = "sasl-passwd";
66 path = "/var/db/postfix/sasl_passwd";
67 owner = "postfix";
68 sopsFile = ./secrets.yaml;
69 };
70}
diff --git a/hosts/sif/ruleset.nft b/hosts/sif/ruleset.nft
index 2af8b2ee..62339f69 100644
--- a/hosts/sif/ruleset.nft
+++ b/hosts/sif/ruleset.nft
@@ -61,7 +61,7 @@ table inet filter {
61 counter mosh-rx {} 61 counter mosh-rx {}
62 counter wg-rx {} 62 counter wg-rx {}
63 counter yggdrasil-gre-rx {} 63 counter yggdrasil-gre-rx {}
64 counter quickserve-rx {} 64 counter miniserve-rx {}
65 counter ausweisapp2-rx {} 65 counter ausweisapp2-rx {}
66 66
67 counter established-rx {} 67 counter established-rx {}
@@ -81,7 +81,7 @@ table inet filter {
81 counter mosh-tx {} 81 counter mosh-tx {}
82 counter wg-tx {} 82 counter wg-tx {}
83 counter yggdrasil-gre-tx {} 83 counter yggdrasil-gre-tx {}
84 counter quickserve-tx {} 84 counter miniserve-tx {}
85 85
86 counter tx {} 86 counter tx {}
87 87
@@ -134,7 +134,7 @@ table inet filter {
134 tcp dport 22 counter name ssh-rx accept 134 tcp dport 22 counter name ssh-rx accept
135 udp dport 60000-61000 counter name mosh-rx accept 135 udp dport 60000-61000 counter name mosh-rx accept
136 136
137 tcp dport 8000 counter name quickserve-rx accept 137 tcp dport 8080 counter name miniserve-rx accept
138 udp dport 24727 counter name ausweisapp2-rx accept 138 udp dport 24727 counter name ausweisapp2-rx accept
139 139
140 udp dport 51820-51822 counter name wg-rx accept 140 udp dport 51820-51822 counter name wg-rx accept
@@ -173,7 +173,7 @@ table inet filter {
173 udp sport 51820-51822 counter name wg-tx 173 udp sport 51820-51822 counter name wg-tx
174 iifname "yggdrasil-wg-*" meta l4proto gre counter name yggdrasil-gre-tx 174 iifname "yggdrasil-wg-*" meta l4proto gre counter name yggdrasil-gre-tx
175 175
176 tcp sport 8000 counter name quickserve-tx accept 176 tcp sport 8080 counter name miniserve-tx accept
177 177
178 oifname virbr0 udp sport 67 counter name libvirt-dhcp accept 178 oifname virbr0 udp sport 67 counter name libvirt-dhcp accept
179 oifname virbr0 udp sport 547 counter name libvirt-dhcp accept 179 oifname virbr0 udp sport 547 counter name libvirt-dhcp accept
diff --git a/hosts/surtr/audiobookshelf.nix b/hosts/surtr/audiobookshelf.nix
new file mode 100644
index 00000000..728851a4
--- /dev/null
+++ b/hosts/surtr/audiobookshelf.nix
@@ -0,0 +1,66 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "audiobookshelf.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."audiobookshelf" = {
13 servers = {
14 "[2a03:4000:52:ada:4:1::]:28982" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "audiobookshelf.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/audiobookshelf.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/audiobookshelf.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/audiobookshelf.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://audiobookshelf;
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 "audiobookshelf.yggdrasil.li.key.pem:${config.security.acme.certs."audiobookshelf.yggdrasil.li".directory}/key.pem"
60 "audiobookshelf.yggdrasil.li.pem:${config.security.acme.certs."audiobookshelf.yggdrasil.li".directory}/fullchain.pem"
61 "audiobookshelf.yggdrasil.li.chain.pem:${config.security.acme.certs."audiobookshelf.yggdrasil.li".directory}/chain.pem"
62 ];
63 };
64 };
65 };
66}
diff --git a/hosts/surtr/bifrost/default.nix b/hosts/surtr/bifrost/default.nix
index fbfde757..52ab43f5 100644
--- a/hosts/surtr/bifrost/default.nix
+++ b/hosts/surtr/bifrost/default.nix
@@ -18,7 +18,7 @@ in {
18 ListenPort = 51822; 18 ListenPort = 51822;
19 }; 19 };
20 wireguardPeers = [ 20 wireguardPeers = [
21 { AllowedIPs = [ "2a03:4000:52:ada:4:1::/96" ]; 21 { AllowedIPs = [ "2a03:4000:52:ada:4:1::/96" "2a03:4000:52:ada:6::/80" ];
22 PublicKey = trim (readFile ../../vidhar/network/bifrost/vidhar.pub); 22 PublicKey = trim (readFile ../../vidhar/network/bifrost/vidhar.pub);
23 } 23 }
24 ]; 24 ];
@@ -34,6 +34,8 @@ in {
34 routes = [ 34 routes = [
35 { Destination = "2a03:4000:52:ada:4::/80"; 35 { Destination = "2a03:4000:52:ada:4::/80";
36 } 36 }
37 { Destination = "2a03:4000:52:ada:6::/80";
38 }
37 ]; 39 ];
38 linkConfig = { 40 linkConfig = {
39 RequiredForOnline = false; 41 RequiredForOnline = false;
diff --git a/hosts/surtr/default.nix b/hosts/surtr/default.nix
index 0a79d047..9d3101c0 100644
--- a/hosts/surtr/default.nix
+++ b/hosts/surtr/default.nix
@@ -7,7 +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 ./paperless.nix ./hledger.nix ./audiobookshelf.nix ./kimai.nix
11 ]; 11 ];
12 12
13 config = { 13 config = {
diff --git a/hosts/surtr/dns/default.nix b/hosts/surtr/dns/default.nix
index 5dd60190..8aca2b97 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" "paperless.yggdrasil.li" "hledger.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" "audiobookshelf.yggdrasil.li" "kimai.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"]; };
@@ -169,15 +169,9 @@ in {
169 { domain = "kleen.li"; 169 { domain = "kleen.li";
170 addACLs = { "kleen.li" = ["ymir_acme_acl"]; }; 170 addACLs = { "kleen.li" = ["ymir_acme_acl"]; };
171 } 171 }
172 { domain = "xmpp.li";
173 addACLs = { "xmpp.li" = ["ymir_acme_acl"]; };
174 }
175 { domain = "synapse.li"; 172 { domain = "synapse.li";
176 acmeDomains = ["element.synapse.li" "turn.synapse.li" "synapse.li"]; 173 acmeDomains = ["element.synapse.li" "turn.synapse.li" "synapse.li"];
177 } 174 }
178 { domain = "dirty-haskell.org";
179 addACLs = { "dirty-haskell.org" = ["ymir_acme_acl"]; };
180 }
181 { domain = "praseodym.org"; 175 { domain = "praseodym.org";
182 addACLs = { "praseodym.org" = ["ymir_acme_acl"]; }; 176 addACLs = { "praseodym.org" = ["ymir_acme_acl"]; };
183 } 177 }
diff --git a/hosts/surtr/dns/keys/audiobookshelf.yggdrasil.li_acme b/hosts/surtr/dns/keys/audiobookshelf.yggdrasil.li_acme
new file mode 100644
index 00000000..e3af9966
--- /dev/null
+++ b/hosts/surtr/dns/keys/audiobookshelf.yggdrasil.li_acme
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:5BLk/MZtQsLjpdKGiZjtOH1soIXB05MkEoPWyr5m27ho7H5udDjRZE0/OjvSlMzQNjFNJc+OwbeHwaJ8sPLCnXqoFHJXikAmi0gpdFC0uN0JGYCBT1EaK7j6yVHyiqFXo6xwVSCO/zP/fbjItiuqU+B4llGx5N2I4HQqRLFoW/35PZazkR15xI1zo8LeC8T+jm16apFQw2Ih/RqsJK7XlHqnXq9SzeA2qhgkCbJf6aJ6zDS7eQVFt7qHh2mVtV7Al++bZiIkJiNJ5SJO1ck18w2t9HwXBhfMFKXvfFW0I6kKur1qSJk=,iv:rxm5PwOzXDaK+nj2k3bUqlYbIFFA49ispyfamtQqU/A=,tag:7dOua39f+0JsLldLRLr1NQ==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwV1hIcWNNRVJXOG1xYlFK\nb1VxWG03dGVmS3dmQTltUTR0VmEzYjRFanlBCnE1M1BTZjNCQnpsNkU5ZlR5T3BR\nOHliWUV2bXMrK0w1K3JXbUE0dEhKdzgKLS0tIDdkUEZ4QUE0NUN2TXFYcklybjBS\nRVpxV2J6U1BnUng1dytWTGRkd0FrRGcKVaMCzcrX+BQqbmh95JEk3lRdfnv9uO8n\nwotKxp/+xX7GEF42BOzJ/mWcgyVjABlnWeVyaRxfpbCUrmNuFYO6XA==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkZmRFR0gwc1pjVVorK0t6\nVHU4U0xob09Dd2J5QUJXbndZdWp3cytpYWhRCnFnMlZ0cjNGdXN1RWEvQlVHSE1M\nMEdzSjlEb2NkaE1zMEJJOTlCMC9FVFEKLS0tIGJiTEJSRVZZbVUzZVFGeXQwYm1w\ndWVYQVdoVlJnRENTYzhnQm1BcWVRdW8K0ECfLVQBQuZWFFFjdHLcJBz+CDzsOOwh\nOA38qxHD4EWfXR1c3G8RDbow7nB2MGc1Zohc7qhtuTj2wL0qhVKWqA==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-05-09T17:07:16Z",
15 "mac": "ENC[AES256_GCM,data:K5I6YXQXCUPHFBNVlXIdLLKqiNPVZh95KoHni2m16SdAvTyBab79SZ5xNvotrtKXp0iISCogEgdMm+OWbxYywEiZ+sUsxgx6RE5nAXruZOiAwzuyUr88qgCHTBZnKzgaDZlbYOgWB+LCzr8s5JbcTD4G6/RaXYyqmx7igygubHA=,iv:Zbij4eoDqoP5XYhAsDGBGqlcP5ACQAY/QngTmrJYRzs=,tag:WYxhUEZwmXkQmnpyOuP2Bg==,type:str]",
16 "unencrypted_suffix": "_unencrypted",
17 "version": "3.10.2"
18 }
19}
diff --git a/hosts/surtr/dns/keys/kimai.yggdrasil.li_acme b/hosts/surtr/dns/keys/kimai.yggdrasil.li_acme
new file mode 100644
index 00000000..bdfb135a
--- /dev/null
+++ b/hosts/surtr/dns/keys/kimai.yggdrasil.li_acme
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:sKFt4pH0Xn7Qm6JFMg/2N7Ht7jtMJukfN+U3dQaoYXPbhRJ+heEtDpXV/WP4AlfbfpIOgTPW3mcmQCwKFNhS00vEsQA4728FfXZzDDmZCa3hwg51wDbL7XUOr0OePgzi86lt0Q193K6CkGqEAa1vFIb//ElEfBYIwdATbmcoAsM3mHhz58X7c1qf8LNuB93o/1N2xXXZI3NWOhOjlviTc2DAhffXDwlMJSYUhldnwtDKmLM1mooJzLgm2p9w7gRD7WPqEqZFq9uFDK69P9uX5T9hFHg=,iv:rAE4sYxxLou4tyD4RWTp3LjQP0cya95coy1MvwfEK/U=,tag:u4SSk8SZFlj0ks7d6tDocw==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2KzdNUWhEcDB6QmtUTnVh\nNS9Nc2I4UjAzekxhRXo1UmY3SklPejV1TURJCm9NY2lVOERoMDFKTU56Mmh1NHEr\naGV4M1RoVldHV0xyc3Z0MnVqakpjMFUKLS0tIEYxSk9OUm9kMkdtcG5POWRGQVkx\nY1FEaXYwMGo0L0Z0aTVTZDA5aUFDWEUKJ+e/7lR/rNPNVnIy+wkiKiAYMxWp4L7q\nwnSTx451vSnxv9j3JWB43Y7XQC08cisWDj06ULw8FnEbKYOvTYj9mQ==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBwOTU3dUEzaXM5T1VkbDRO\nMm14OG1mUkk2bDRhdnBsMHBkc3kvUzlyNlQwCktFSHJhMnhoQ2J6bC9vUHNLWTRC\nRFpYeHo3N2xjWUhjQnRwQ2Nrc1pRUmsKLS0tIDdPeFBVdkxDd1JWSmcxQ0tLMTBD\ncHU3VExZOUhYUlJvbGNoK3FMK2VIbGMKFk94P9aBY04CPIi983f3Aalgh4fnU+/K\n2mxawSMf9jz8704N5XJfmr2hwNy8hqLIn8bjsEMAPTfE1YBGga4w0g==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-05-24T09:42:23Z",
15 "mac": "ENC[AES256_GCM,data:diCeJGvBmM0Ng722eKoFwDe7pqZrdLPSLn5j9LfdaFI64BAbSbA5bAq4NFXqdJ1vttarD2A5rEafYoXUxP8228x2GhNyWUGW5AWgBjVPUc59gjs4wYKR5HlkVMIadhTwNheEyoEjrxX40GNBgCG7X3ocOtOYKbKECp433gdAPDg=,iv:d+yJMWj2RyFnveo2ZNrpNeV+amXM+H7vdC0A2F7mwjA=,tag:yjibG2iusdprp0ORghYWhw==,type:str]",
16 "unencrypted_suffix": "_unencrypted",
17 "version": "3.10.2"
18 }
19}
diff --git a/hosts/surtr/dns/zones/email.nights.soa b/hosts/surtr/dns/zones/email.nights.soa
index 913a88d4..34209a99 100644
--- a/hosts/surtr/dns/zones/email.nights.soa
+++ b/hosts/surtr/dns/zones/email.nights.soa
@@ -1,7 +1,7 @@
1$ORIGIN nights.email. 1$ORIGIN nights.email.
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 2023013000 ; serial 4 2025060700 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -27,11 +27,7 @@ $TTL 3600
27 27
28_acme-challenge IN NS ns.yggdrasil.li. 28_acme-challenge IN NS ns.yggdrasil.li.
29 29
30ymir._domainkey IN TXT ( 30ymir._domainkey IN CNAME ymir._domainkey.yggdrasil.li.
31 "v=DKIM1;k=rsa;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq3cCKlk+VPhyAanLZTM0BCzUT/+fmxHioZcFk0uJk1akBYj7BRofR7eVNcLKpm3rwYMQgE+9vJH9p8SV6tws9EcWc8SMCqqGZlREYM7PmLDiTSK/vjCzkygfgFCb0EBNsY2A/fpP4rTeoxrbcBSvMkq97iY5rwyw4wXZVZXLiDaCj23s8POoxTk1ClqUJZJQ5x2"
32 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
33 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
34)
35 31
36_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li. 32_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
37_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li. 33_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/li.141.soa b/hosts/surtr/dns/zones/li.141.soa
index ab117f09..78d137bb 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 2025020900 ; serial 4 2025060701 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -45,11 +45,8 @@ ymir IN AAAA 2a03:4000:6:d004::
45ymir IN MX 0 ymir.yggdrasil.li 45ymir IN MX 0 ymir.yggdrasil.li
46ymir IN TXT "v=spf1 redirect=ymir.yggdrasil.li" 46ymir IN TXT "v=spf1 redirect=ymir.yggdrasil.li"
47 47
48ymir._domainkey IN TXT ( 48ymir._domainkey IN CNAME ymir._domainkey.yggdrasil.li.
49 "v=DKIM1;k=rsa;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq3cCKlk+VPhyAanLZTM0BCzUT/+fmxHioZcFk0uJk1akBYj7BRofR7eVNcLKpm3rwYMQgE+9vJH9p8SV6tws9EcWc8SMCqqGZlREYM7PmLDiTSK/vjCzkygfgFCb0EBNsY2A/fpP4rTeoxrbcBSvMkq97iY5rwyw4wXZVZXLiDaCj23s8POoxTk1ClqUJZJQ5x2" 49surtr._domainkey IN CNAME surtr._domainkey.yggdrasil.li.
50 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
51 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
52)
53 50
54_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li. 51_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
55_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li. 52_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/li.kleen.soa b/hosts/surtr/dns/zones/li.kleen.soa
index a1c7d35a..5dd3e697 100644
--- a/hosts/surtr/dns/zones/li.kleen.soa
+++ b/hosts/surtr/dns/zones/li.kleen.soa
@@ -1,7 +1,7 @@
1$ORIGIN kleen.li. 1$ORIGIN kleen.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 2023013000 ; serial 4 2025060701 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -27,11 +27,8 @@ $TTL 3600
27 27
28_acme-challenge IN NS ns.yggdrasil.li. 28_acme-challenge IN NS ns.yggdrasil.li.
29 29
30ymir._domainkey IN TXT ( 30ymir._domainkey IN CNAME ymir._domainkey.yggdrasil.li.
31 "v=DKIM1;k=rsa;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq3cCKlk+VPhyAanLZTM0BCzUT/+fmxHioZcFk0uJk1akBYj7BRofR7eVNcLKpm3rwYMQgE+9vJH9p8SV6tws9EcWc8SMCqqGZlREYM7PmLDiTSK/vjCzkygfgFCb0EBNsY2A/fpP4rTeoxrbcBSvMkq97iY5rwyw4wXZVZXLiDaCj23s8POoxTk1ClqUJZJQ5x2" 31surtr._domainkey IN CNAME surtr._domainkey.yggdrasil.li.
32 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
33 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
34)
35 32
36_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li. 33_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
37_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li. 34_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/li.synapse.soa b/hosts/surtr/dns/zones/li.synapse.soa
index 086d4a85..247cf025 100644
--- a/hosts/surtr/dns/zones/li.synapse.soa
+++ b/hosts/surtr/dns/zones/li.synapse.soa
@@ -1,7 +1,7 @@
1$ORIGIN synapse.li. 1$ORIGIN synapse.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 2023092100 ; serial 4 2025060701 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
diff --git a/hosts/surtr/dns/zones/li.xmpp.soa b/hosts/surtr/dns/zones/li.xmpp.soa
deleted file mode 100644
index a9e98fb4..00000000
--- a/hosts/surtr/dns/zones/li.xmpp.soa
+++ /dev/null
@@ -1,43 +0,0 @@
1$ORIGIN xmpp.li.
2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2023013000 ; serial
5 10800 ; refresh
6 3600 ; retry
7 604800 ; expire
8 3600 ; min TTL
9)
10 IN NS ns.yggdrasil.li.
11 IN NS ns.inwx.de.
12 IN NS ns2.inwx.de.
13 IN NS ns3.inwx.eu.
14
15@ IN CAA 128 issue "letsencrypt.org; validationmethods=dns-01"
16@ IN CAA 128 iodef "mailto:caa@yggdrasil.li"
17
18@ IN A 188.68.51.254
19@ IN AAAA 2a03:4000:6:d004::
20@ IN MX 0 ymir.yggdrasil.li.
21@ IN TXT "v=spf1 redirect=yggdrasil.li"
22
23* IN A 188.68.51.254
24* IN AAAA 2a03:4000:6:d004::
25* IN MX 0 ymir.yggdrasil.li.
26* IN TXT "v=spf1 redirect=yggdrasil.li"
27
28_acme-challenge IN NS ns.yggdrasil.li.
29
30ymir._domainkey IN TXT (
31 "v=DKIM1;k=rsa;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq3cCKlk+VPhyAanLZTM0BCzUT/+fmxHioZcFk0uJk1akBYj7BRofR7eVNcLKpm3rwYMQgE+9vJH9p8SV6tws9EcWc8SMCqqGZlREYM7PmLDiTSK/vjCzkygfgFCb0EBNsY2A/fpP4rTeoxrbcBSvMkq97iY5rwyw4wXZVZXLiDaCj23s8POoxTk1ClqUJZJQ5x2"
32 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
33 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
34)
35
36_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
37_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li.
38
39_infinoted._tcp IN SRV 5 0 6523 ymir.yggdrasil.li.
40
41_submission._tcp IN SRV 5 0 25 ymir.yggdrasil.li.
42_imap._tcp IN SRV 5 0 143 ymir.yggdrasil.li.
43_imaps._tcp IN SRV 5 0 993 ymir.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/li.yggdrasil.soa b/hosts/surtr/dns/zones/li.yggdrasil.soa
index 2ef120e6..500194ae 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 2025021901 ; serial 4 2025060700 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -93,12 +93,30 @@ _acme-challenge.hledger IN NS ns.yggdrasil.li.
93 93
94hledger IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::" 94hledger IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
95 95
96audiobookshelf IN A 202.61.241.61
97audiobookshelf IN AAAA 2a03:4000:52:ada::
98audiobookshelf IN MX 0 surtr.yggdrasil.li
99audiobookshelf IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
100_acme-challenge.audiobookshelf IN NS ns.yggdrasil.li.
101
102audiobookshelf IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
103
104kimai IN A 202.61.241.61
105kimai IN AAAA 2a03:4000:52:ada::
106kimai IN MX 0 surtr.yggdrasil.li
107kimai IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
108_acme-challenge.kimai IN NS ns.yggdrasil.li.
109
110kimai IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
111
96vidhar IN AAAA 2a03:4000:52:ada:4:1:: 112vidhar IN AAAA 2a03:4000:52:ada:4:1::
97vidhar IN MX 0 ymir.yggdrasil.li 113vidhar IN MX 0 ymir.yggdrasil.li
98vidhar IN TXT "v=spf1 redirect=yggdrasil.li" 114vidhar IN TXT "v=spf1 redirect=yggdrasil.li"
99 115
100mailout IN A 188.68.51.254 116mailout IN A 188.68.51.254
101mailout IN AAAA 2a03:4000:6:d004:: 117mailout IN AAAA 2a03:4000:6:d004::
118mailout IN A 202.61.241.61
119mailout IN AAAA 2a03:4000:52:ada::
102mailout IN MX 0 ymir.yggdrasil.li 120mailout IN MX 0 ymir.yggdrasil.li
103mailout IN TXT "v=spf1 redirect=yggdrasil.li" 121mailout IN TXT "v=spf1 redirect=yggdrasil.li"
104 122
diff --git a/hosts/surtr/dns/zones/org.dirty-haskell.soa b/hosts/surtr/dns/zones/org.dirty-haskell.soa
deleted file mode 100644
index 27f0d7f9..00000000
--- a/hosts/surtr/dns/zones/org.dirty-haskell.soa
+++ /dev/null
@@ -1,34 +0,0 @@
1$ORIGIN dirty-haskell.org.
2$TTL 3600
3@ IN SOA ns.yggdrasil.li. hostmaster.yggdrasil.li (
4 2023013000 ; serial
5 10800 ; refresh
6 3600 ; retry
7 604800 ; expire
8 3600 ; min TTL
9)
10 IN NS ns.yggdrasil.li.
11 IN NS ns.inwx.de.
12 IN NS ns2.inwx.de.
13 IN NS ns3.inwx.eu.
14
15@ IN CAA 128 issue "letsencrypt.org; validationmethods=dns-01"
16@ IN CAA 128 iodef "mailto:caa@yggdrasil.li"
17
18@ IN A 188.68.51.254
19@ IN AAAA 2a03:4000:6:d004::
20@ IN MX 10 ymir.yggdrasil.li.
21@ IN TXT "v=spf1 redirect=yggdrasil.li"
22
23* IN A 188.68.51.254
24* IN AAAA 2a03:4000:6:d004::
25* IN MX 0 ymir.yggdrasil.li.
26* IN TXT "v=spf1 redirect=yggdrasil.li"
27
28_acme-challenge IN NS ns.yggdrasil.li.
29
30ymir._domainkey IN TXT (
31 "v=DKIM1;k=rsa;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq3cCKlk+VPhyAanLZTM0BCzUT/+fmxHioZcFk0uJk1akBYj7BRofR7eVNcLKpm3rwYMQgE+9vJH9p8SV6tws9EcWc8SMCqqGZlREYM7PmLDiTSK/vjCzkygfgFCb0EBNsY2A/fpP4rTeoxrbcBSvMkq97iY5rwyw4wXZVZXLiDaCj23s8POoxTk1ClqUJZJQ5x2"
32 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
33 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
34)
diff --git a/hosts/surtr/dns/zones/org.praseodym.soa b/hosts/surtr/dns/zones/org.praseodym.soa
index df505b4c..2b97ca19 100644
--- a/hosts/surtr/dns/zones/org.praseodym.soa
+++ b/hosts/surtr/dns/zones/org.praseodym.soa
@@ -1,7 +1,7 @@
1$ORIGIN praseodym.org. 1$ORIGIN praseodym.org.
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 2023013000 ; serial 4 2025060701 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -32,11 +32,8 @@ surtr IN AAAA 2a03:4000:52:ada::
32surtr IN MX 0 ymir.yggdrasil.li 32surtr IN MX 0 ymir.yggdrasil.li
33surtr IN TXT "v=spf1 redirect=yggdrasil.li" 33surtr IN TXT "v=spf1 redirect=yggdrasil.li"
34 34
35ymir._domainkey IN TXT ( 35ymir._domainkey IN CNAME ymir._domainkey.yggdrasil.li.
36 "v=DKIM1;k=rsa;p=MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAq3cCKlk+VPhyAanLZTM0BCzUT/+fmxHioZcFk0uJk1akBYj7BRofR7eVNcLKpm3rwYMQgE+9vJH9p8SV6tws9EcWc8SMCqqGZlREYM7PmLDiTSK/vjCzkygfgFCb0EBNsY2A/fpP4rTeoxrbcBSvMkq97iY5rwyw4wXZVZXLiDaCj23s8POoxTk1ClqUJZJQ5x2" 36surtr._domainkey IN CNAME surtr._domainkey.yggdrasil.li.
37 "qzrC0RfN5kLZ9A7Gq2jB09vNxpXHYqABA0bJv88JiZM7hfkp9IafJZ+yCVMaBcJs4DAxnTjNAuFD9gm+qSFVY8+yeXqL6Qjo5PbruhyZRBW8RgRYT8t5n07XRglMGKKGMwOGLanrltcyXqB+GsDZBD36RAAwjFadnxdpDyRv4SgRP7ff2tKRrORYpmpN+mKdqw5j3J/nP6bXV1oAkyh9XQkPEIDi81WT87EZziTElDzVp6A2qFOxqucAovoRk24"
38 "7vlsns1FApFRsp9mja0UZNObyKD1M6tP9Ep7lS76tFGMk+WDvXRJH5LEsyCpu7sSyl1r/O0M4K+KldRCqLlZd7rf8F5P8T0dn1azk05g7F4p0N/y9GNdzXbPZ9u0eZdI7SEdh8ZoOZp7NVZiBFfbWLSS5ZtyA2kbBa4i7GJ/cuAbEKOmqAkeQPiu96TGIcyjkXjS6mTPI+9UmKZYZC+OM8XdJ02y5KRoonCc19ZS8CAwEAAQ=="
39)
40 37
41_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li. 38_xmpp-client._tcp IN SRV 5 0 5222 ymir.yggdrasil.li.
42_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li. 39_xmpp-server._tcp IN SRV 5 0 5269 ymir.yggdrasil.li.
diff --git a/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py
index 00182523..7c931559 100644
--- a/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py
+++ b/hosts/surtr/email/ccert-policy-server/ccert_policy_server/__main__.py
@@ -28,10 +28,12 @@ class PolicyHandler(StreamRequestHandler):
28 28
29 allowed = False 29 allowed = False
30 user = None 30 user = None
31 relay_eligible = False
31 if self.args['sasl_username']: 32 if self.args['sasl_username']:
32 user = self.args['sasl_username'] 33 user = self.args['sasl_username']
33 if self.args['ccert_subject']: 34 if self.args['ccert_subject']:
34 user = self.args['ccert_subject'] 35 user = self.args['ccert_subject']
36 relay_eligible = True
35 37
36 if user: 38 if user:
37 with self.server.db_pool.connection() as conn: 39 with self.server.db_pool.connection() as conn:
@@ -44,10 +46,16 @@ class PolicyHandler(StreamRequestHandler):
44 46
45 with conn.cursor() as cur: 47 with conn.cursor() as cur:
46 cur.row_factory = namedtuple_row 48 cur.row_factory = namedtuple_row
47 cur.execute('SELECT "mailbox"."mailbox" as "user", "local", "extension", "domain" FROM "mailbox" INNER JOIN "mailbox_mapping" ON "mailbox".id = "mailbox_mapping"."mailbox" WHERE "mailbox"."mailbox" = %(user)s AND ("local" = %(local)s OR "local" IS NULL) AND ("extension" = %(extension)s OR "extension" IS NULL) AND "domain" = %(domain)s', params = {'user': user, 'local': local, 'extension': extension if extension is not None else '', 'domain': domain}, prepare=True) 49
48 for record in cur: 50 if relay_eligible:
49 logger.debug('Received result: %s', record) 51 cur.execute('SELECT EXISTS(SELECT true FROM "mailbox" INNER JOIN "relay_access" ON "mailbox".id = "relay_access"."mailbox" WHERE "mailbox"."mailbox" = %(user)s AND ("domain" = %(domain)s OR %(domain)s ilike CONCAT(\'%%_.\', "domain"))) as "exists"', params = {'user': user, 'domain': domain})
50 allowed = True 52 if (row := cur.fetchone()) is not None:
53 allowed = row.exists
54
55 if not allowed:
56 cur.execute('SELECT EXISTS(SELECT true FROM "mailbox" INNER JOIN "mailbox_mapping" ON "mailbox".id = "mailbox_mapping"."mailbox" WHERE "mailbox"."mailbox" = %(user)s AND ("local" = %(local)s OR "local" IS NULL) AND ("extension" = %(extension)s OR "extension" IS NULL) AND "domain" = %(domain)s) as "exists"', params = {'user': user, 'local': local, 'extension': extension if extension is not None else '', 'domain': domain}, prepare=True)
57 if (row := cur.fetchone()) is not None:
58 allowed = row.exists
51 59
52 action = '550 5.7.0 Sender address not authorized for current user' 60 action = '550 5.7.0 Sender address not authorized for current user'
53 if allowed: 61 if allowed:
diff --git a/hosts/surtr/email/default.nix b/hosts/surtr/email/default.nix
index a85b76aa..c993bb18 100644
--- a/hosts/surtr/email/default.nix
+++ b/hosts/surtr/email/default.nix
@@ -1,4 +1,4 @@
1{ config, pkgs, lib, flakeInputs, ... }: 1{ config, pkgs, lib, flake, flakeInputs, ... }:
2 2
3with lib; 3with lib;
4 4
@@ -15,7 +15,7 @@ let
15 15
16 for file in $out/pipe/bin/*; do 16 for file in $out/pipe/bin/*; do
17 wrapProgram $file \ 17 wrapProgram $file \
18 --set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin" 18 --set PATH "${makeBinPath (with pkgs; [coreutils rspamd])}"
19 done 19 done
20 ''; 20 '';
21 }; 21 };
@@ -33,12 +33,28 @@ let
33 }); 33 });
34 }); 34 });
35 }; 35 };
36 internal-policy-server =
37 let
38 workspace = flakeInputs.uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./internal-policy-server; };
39 pythonSet = flake.lib.pythonSet {
40 inherit pkgs;
41 python = pkgs.python312;
42 overlay = workspace.mkPyprojectOverlay {
43 sourcePreference = "wheel";
44 };
45 };
46 virtualEnv = pythonSet.mkVirtualEnv "internal-policy-server-env" workspace.deps.default;
47 in virtualEnv.overrideAttrs (oldAttrs: {
48 meta = (oldAttrs.meta or {}) // {
49 mainProgram = "internal-policy-server";
50 };
51 });
36 52
37 nftables-nologin-script = pkgs.writeScript "nftables-mail-nologin" '' 53 nftables-nologin-script = pkgs.resholve.writeScript "nftables-mail-nologin" {
38 #!${pkgs.zsh}/bin/zsh 54 inputs = with pkgs; [inetutils nftables gnugrep findutils];
39 55 interpreter = lib.getExe pkgs.zsh;
56 } ''
40 set -e 57 set -e
41 export PATH="${lib.makeBinPath (with pkgs; [inetutils nftables])}:$PATH"
42 58
43 typeset -a as_sets mnt_bys route route6 59 typeset -a as_sets mnt_bys route route6
44 as_sets=(${lib.escapeShellArgs config.services.email.nologin.ASSets}) 60 as_sets=(${lib.escapeShellArgs config.services.email.nologin.ASSets})
@@ -51,7 +67,7 @@ let
51 elif [[ "''${line}" =~ "^route6:\s+(.+)$" ]]; then 67 elif [[ "''${line}" =~ "^route6:\s+(.+)$" ]]; then
52 route6+=($match[1]) 68 route6+=($match[1])
53 fi 69 fi
54 done < <(whois -h whois.radb.net "!i''${as_set},1" | egrep -o 'AS[0-9]+' | xargs -- whois -h whois.radb.net -- -i origin) 70 done < <(whois -h whois.radb.net "!i''${as_set},1" | grep -Eo 'AS[0-9]+' | xargs whois -h whois.radb.net -- -i origin)
55 done 71 done
56 for mnt_by in $mnt_bys; do 72 for mnt_by in $mnt_bys; do
57 while IFS=$'\n' read line; do 73 while IFS=$'\n' read line; do
@@ -190,16 +206,19 @@ in {
190 "reject_unauth_destination" 206 "reject_unauth_destination"
191 "reject_unknown_recipient_domain" 207 "reject_unknown_recipient_domain"
192 "reject_unverified_recipient" 208 "reject_unverified_recipient"
209 "check_policy_service unix:/run/postfix-internal-policy.sock"
193 ]; 210 ];
194 unverified_recipient_reject_code = "550"; 211 unverified_recipient_reject_code = "550";
195 unverified_recipient_reject_reason = "Recipient address lookup failed"; 212 unverified_recipient_reject_reason = "Recipient address lookup failed";
196 address_verify_map = "internal:address_verify_map"; 213 address_verify_map = "internal:address_verify_map";
197 address_verify_positive_expire_time = "1h"; 214 address_verify_positive_expire_time = "1h";
198 address_verify_positive_refresh_time = "15m"; 215 address_verify_positive_refresh_time = "15m";
199 address_verify_negative_expire_time = "15s"; 216 address_verify_negative_expire_time = "5m";
200 address_verify_negative_refresh_time = "5s"; 217 address_verify_negative_refresh_time = "1m";
201 address_verify_cache_cleanup_interval = "5s"; 218 address_verify_cache_cleanup_interval = "12h";
219 address_verify_poll_count = "\${stress?15}\${stress:30}";
202 address_verify_poll_delay = "1s"; 220 address_verify_poll_delay = "1s";
221 address_verify_sender_ttl = "30045s";
203 222
204 smtpd_relay_restrictions = [ 223 smtpd_relay_restrictions = [
205 "check_ccert_access ${relay_ccert}" 224 "check_ccert_access ${relay_ccert}"
@@ -213,7 +232,7 @@ in {
213 smtpd_client_event_limit_exceptions = ""; 232 smtpd_client_event_limit_exceptions = "";
214 233
215 milter_default_action = "accept"; 234 milter_default_action = "accept";
216 smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"]; 235 smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock" "local:/run/postsrsd/postsrsd-milter.sock"];
217 non_smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"]; 236 non_smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"];
218 237
219 alias_maps = ""; 238 alias_maps = "";
@@ -235,11 +254,6 @@ in {
235 ::/0 silent-discard, dsn 254 ::/0 silent-discard, dsn
236 ''}"; 255 ''}";
237 256
238 sender_canonical_maps = "tcp:localhost:${toString config.services.postsrsd.forwardPort}";
239 sender_canonical_classes = "envelope_sender";
240 recipient_canonical_maps = "tcp:localhost:${toString config.services.postsrsd.reversePort}";
241 recipient_canonical_classes = ["envelope_recipient" "header_recipient"];
242
243 virtual_mailbox_domains = ''pgsql:${pkgs.writeText "virtual_mailbox_domains.cf" '' 257 virtual_mailbox_domains = ''pgsql:${pkgs.writeText "virtual_mailbox_domains.cf" ''
244 hosts = postgresql:///email 258 hosts = postgresql:///email
245 dbname = email 259 dbname = email
@@ -254,11 +268,24 @@ in {
254 virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp"; 268 virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp";
255 smtputf8_enable = false; 269 smtputf8_enable = false;
256 270
257 authorized_submit_users = "inline:{ root= postfwd= }"; 271 authorized_submit_users = "inline:{ root= postfwd= dovecot2= }";
272 authorized_flush_users = "inline:{ root= }";
273 authorized_mailq_users = "inline:{ root= }";
258 274
259 postscreen_access_list = ""; 275 postscreen_access_list = "";
260 postscreen_denylist_action = "drop"; 276 postscreen_denylist_action = "drop";
261 postscreen_greet_action = "enforce"; 277 postscreen_greet_action = "enforce";
278
279 sender_bcc_maps = ''pgsql:${pkgs.writeText "sender_bcc_maps.cf" ''
280 hosts = postgresql:///email
281 dbname = email
282 query = SELECT value FROM sender_bcc_maps WHERE key = '%s'
283 ''}'';
284 recipient_bcc_maps = ''pgsql:${pkgs.writeText "recipient_bcc_maps.cf" ''
285 hosts = postgresql:///email
286 dbname = email
287 query = SELECT value FROM recipient_bcc_maps WHERE key = '%s'
288 ''}'';
262 }; 289 };
263 masterConfig = { 290 masterConfig = {
264 "465" = { 291 "465" = {
@@ -283,7 +310,7 @@ in {
283 hosts = postgresql:///email 310 hosts = postgresql:///email
284 dbname = email 311 dbname = email
285 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' OR (lookup = regexp_replace('%s', '\+[^@]*@', '@') AND NOT EXISTS (SELECT 1 FROM virtual_mailbox_access WHERE lookup = '%s')) 312 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' OR (lookup = regexp_replace('%s', '\+[^@]*@', '@') AND NOT EXISTS (SELECT 1 FROM virtual_mailbox_access WHERE lookup = '%s'))
286 ''},permit_tls_all_clientcerts,reject}'' 313 ''},check_policy_service unix:/run/postfix-internal-policy.sock,permit_tls_all_clientcerts,reject}''
287 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject" 314 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject"
288 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" 315 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
289 "-o" "unverified_sender_reject_code=550" 316 "-o" "unverified_sender_reject_code=550"
@@ -313,7 +340,7 @@ in {
313 hosts = postgresql:///email 340 hosts = postgresql:///email
314 dbname = email 341 dbname = email
315 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' OR (lookup = regexp_replace('%s', '\+[^@]*@', '@') AND NOT EXISTS (SELECT 1 FROM virtual_mailbox_access WHERE lookup = '%s')) 342 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' OR (lookup = regexp_replace('%s', '\+[^@]*@', '@') AND NOT EXISTS (SELECT 1 FROM virtual_mailbox_access WHERE lookup = '%s'))
316 ''},permit_sasl_authenticated,reject}'' 343 ''},check_policy_service unix:/run/postfix-internal-policy.sock,permit_sasl_authenticated,reject}''
317 "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject" 344 "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject"
318 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" 345 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
319 "-o" "unverified_sender_reject_code=550" 346 "-o" "unverified_sender_reject_code=550"
@@ -364,17 +391,19 @@ in {
364 391
365 services.postsrsd = { 392 services.postsrsd = {
366 enable = true; 393 enable = true;
367 domain = "surtr.yggdrasil.li"; 394 domains = [ "surtr.yggdrasil.li" ] ++ concatMap (domain: [".${domain}" domain]) emailDomains;
368 separator = "+"; 395 separator = "+";
369 excludeDomains = [ "surtr.yggdrasil.li" 396 extraConfig = ''
370 ] ++ concatMap (domain: [".${domain}" domain]) emailDomains; 397 socketmap = unix:/run/postsrsd/postsrsd-socketmap.sock
398 milter = unix:/run/postsrsd/postsrsd-milter.sock
399 '';
371 }; 400 };
372 401
373 services.opendkim = { 402 services.opendkim = {
374 enable = true; 403 enable = true;
375 user = "postfix"; group = "postfix"; 404 user = "postfix"; group = "postfix";
376 socket = "local:/run/opendkim/opendkim.sock"; 405 socket = "local:/run/opendkim/opendkim.sock";
377 domains = ''csl:${concatStringsSep "," (["surtr.yggdrasil.li"] ++ emailDomains)}''; 406 domains = ''csl:${concatStringsSep "," (["surtr.yggdrasil.li" "yggdrasil.li" "141.li" "kleen.li" "synapse.li" "praseodym.org"] ++ emailDomains)}'';
378 selector = "surtr"; 407 selector = "surtr";
379 configFile = builtins.toFile "opendkim.conf" '' 408 configFile = builtins.toFile "opendkim.conf" ''
380 Syslog true 409 Syslog true
@@ -673,7 +702,7 @@ in {
673 plugin { 702 plugin {
674 plugin = fts fts_xapian 703 plugin = fts fts_xapian
675 fts = xapian 704 fts = xapian
676 fts_xapian = partial=2 full=20 attachments=1 verbose=1 705 fts_xapian = partial=3 full=20 attachments=1 verbose=1
677 706
678 fts_autoindex = yes 707 fts_autoindex = yes
679 708
@@ -693,7 +722,7 @@ in {
693 startAt = "*-*-* 22:00:00 Europe/Berlin"; 722 startAt = "*-*-* 22:00:00 Europe/Berlin";
694 serviceConfig = { 723 serviceConfig = {
695 Type = "oneshot"; 724 Type = "oneshot";
696 ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A"; 725 ExecStart = "${getExe' pkgs.dovecot "doveadm"} fts optimize -A";
697 PrivateDevices = true; 726 PrivateDevices = true;
698 PrivateNetwork = true; 727 PrivateNetwork = true;
699 ProtectKernelTunables = true; 728 ProtectKernelTunables = true;
@@ -778,7 +807,7 @@ in {
778 systemd.services.dovecot2 = { 807 systemd.services.dovecot2 = {
779 preStart = '' 808 preStart = ''
780 for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do 809 for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do
781 ${pkgs.dovecot_pigeonhole}/bin/sievec $f 810 ${getExe' pkgs.dovecot_pigeonhole "sievec"} $f
782 done 811 done
783 ''; 812 '';
784 813
@@ -845,15 +874,16 @@ in {
845 charset utf-8; 874 charset utf-8;
846 source_charset utf-8; 875 source_charset utf-8;
847 ''; 876 '';
848 root = pkgs.runCommand "mta-sts.${domain}" {} '' 877 root = pkgs.writeTextFile {
849 mkdir -p $out/.well-known 878 name = "mta-sts.${domain}";
850 cp ${pkgs.writeText "mta-sts.${domain}.txt" '' 879 destination = "/.well-known/mta-sts.txt";
880 text = ''
851 version: STSv1 881 version: STSv1
852 mode: enforce 882 mode: enforce
853 max_age: 2419200 883 max_age: 2419200
854 mx: mailin.${domain} 884 mx: mailin.${domain}
855 ''} $out/.well-known/mta-sts.txt 885 '';
856 ''; 886 };
857 }; 887 };
858 }) emailDomains); 888 }) emailDomains);
859 }; 889 };
@@ -870,7 +900,7 @@ in {
870 systemd.services.spm = { 900 systemd.services.spm = {
871 serviceConfig = { 901 serviceConfig = {
872 Type = "notify"; 902 Type = "notify";
873 ExecStart = "${pkgs.spm}/bin/spm-server"; 903 ExecStart = getExe' pkgs.spm "spm-server";
874 User = "spm"; 904 User = "spm";
875 Group = "spm"; 905 Group = "spm";
876 906
@@ -928,7 +958,7 @@ in {
928 serviceConfig = { 958 serviceConfig = {
929 Type = "notify"; 959 Type = "notify";
930 960
931 ExecStart = "${ccert-policy-server}/bin/ccert-policy-server"; 961 ExecStart = getExe' ccert-policy-server "ccert-policy-server";
932 962
933 Environment = [ 963 Environment = [
934 "PGDATABASE=email" 964 "PGDATABASE=email"
@@ -961,6 +991,53 @@ in {
961 }; 991 };
962 users.groups."postfix-ccert-sender-policy" = {}; 992 users.groups."postfix-ccert-sender-policy" = {};
963 993
994 systemd.sockets."postfix-internal-policy" = {
995 requiredBy = ["postfix.service"];
996 wants = ["postfix-internal-policy.service"];
997 socketConfig = {
998 ListenStream = "/run/postfix-internal-policy.sock";
999 };
1000 };
1001 systemd.services."postfix-internal-policy" = {
1002 after = [ "postgresql.service" ];
1003 bindsTo = [ "postgresql.service" ];
1004
1005 serviceConfig = {
1006 Type = "notify";
1007
1008 ExecStart = lib.getExe internal-policy-server;
1009
1010 Environment = [
1011 "PGDATABASE=email"
1012 ];
1013
1014 DynamicUser = false;
1015 User = "postfix-internal-policy";
1016 Group = "postfix-internal-policy";
1017 ProtectSystem = "strict";
1018 SystemCallFilter = "@system-service";
1019 NoNewPrivileges = true;
1020 ProtectKernelTunables = true;
1021 ProtectKernelModules = true;
1022 ProtectKernelLogs = true;
1023 ProtectControlGroups = true;
1024 MemoryDenyWriteExecute = true;
1025 RestrictSUIDSGID = true;
1026 KeyringMode = "private";
1027 ProtectClock = true;
1028 RestrictRealtime = true;
1029 PrivateDevices = true;
1030 PrivateTmp = true;
1031 ProtectHostname = true;
1032 ReadWritePaths = ["/run/postgresql"];
1033 };
1034 };
1035 users.users."postfix-internal-policy" = {
1036 isSystemUser = true;
1037 group = "postfix-internal-policy";
1038 };
1039 users.groups."postfix-internal-policy" = {};
1040
964 services.postfwd = { 1041 services.postfwd = {
965 enable = true; 1042 enable = true;
966 cache = false; 1043 cache = false;
diff --git a/hosts/surtr/email/internal-policy-server/.envrc b/hosts/surtr/email/internal-policy-server/.envrc
new file mode 100644
index 00000000..2c909235
--- /dev/null
+++ b/hosts/surtr/email/internal-policy-server/.envrc
@@ -0,0 +1,4 @@
1use flake
2
3[[ -d ".venv" ]] || ( uv venv && uv sync )
4. .venv/bin/activate
diff --git a/hosts/surtr/email/internal-policy-server/.gitignore b/hosts/surtr/email/internal-policy-server/.gitignore
new file mode 100644
index 00000000..4ccfae70
--- /dev/null
+++ b/hosts/surtr/email/internal-policy-server/.gitignore
@@ -0,0 +1,2 @@
1.venv
2**/__pycache__
diff --git a/hosts/surtr/email/internal-policy-server/internal_policy_server/__init__.py b/hosts/surtr/email/internal-policy-server/internal_policy_server/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/hosts/surtr/email/internal-policy-server/internal_policy_server/__init__.py
diff --git a/hosts/surtr/email/internal-policy-server/internal_policy_server/__main__.py b/hosts/surtr/email/internal-policy-server/internal_policy_server/__main__.py
new file mode 100644
index 00000000..04f1a59a
--- /dev/null
+++ b/hosts/surtr/email/internal-policy-server/internal_policy_server/__main__.py
@@ -0,0 +1,106 @@
1from systemd.daemon import listen_fds
2from sdnotify import SystemdNotifier
3from socketserver import StreamRequestHandler, ThreadingMixIn
4from systemd_socketserver import SystemdSocketServer
5import sys
6from threading import Thread
7from psycopg_pool import ConnectionPool
8from psycopg.rows import namedtuple_row
9
10import logging
11
12
13class PolicyHandler(StreamRequestHandler):
14 def handle(self):
15 logger.debug('Handling new connection...')
16
17 self.args = dict()
18
19 line = None
20 while line := self.rfile.readline().removesuffix(b'\n'):
21 if b'=' not in line:
22 break
23
24 key, val = line.split(sep=b'=', maxsplit=1)
25 self.args[key.decode()] = val.decode()
26
27 logger.info('Connection parameters: %s', self.args)
28
29 allowed = False
30 user = None
31 if self.args['sasl_username']:
32 user = self.args['sasl_username']
33 if self.args['ccert_subject']:
34 user = self.args['ccert_subject']
35
36 with self.server.db_pool.connection() as conn:
37 local, domain = self.args['recipient'].split(sep='@', maxsplit=1)
38 extension = None
39 if '+' in local:
40 local, extension = local.split(sep='+', maxsplit=1)
41
42 logger.debug('Parsed recipient address: %s', {'local': local, 'extension': extension, 'domain': domain})
43
44 with conn.cursor() as cur:
45 cur.row_factory = namedtuple_row
46 cur.execute('SELECT id, internal FROM "mailbox_mapping" WHERE ("local" = %(local)s OR "local" IS NULL) AND ("extension" = %(extension)s OR "extension" IS NULL) AND "domain" = %(domain)s', params = {'local': local, 'extension': extension if extension is not None else '', 'domain': domain}, prepare = True)
47 if (row := cur.fetchone()) is not None:
48 if not row.internal:
49 logger.debug('Recipient mailbox is not internal')
50 allowed = True
51 elif user:
52 cur.execute('SELECT EXISTS(SELECT true FROM "mailbox_mapping_access" INNER JOIN "mailbox" ON "mailbox".id = "mailbox_mapping_access"."mailbox" WHERE mailbox_mapping = %(mailbox_mapping)s AND "mailbox"."mailbox" = %(user)s) as "exists"', params = { 'mailbox_mapping': row.id, 'user': user }, prepare = True)
53 if (row := cur.fetchone()) is not None:
54 allowed = row.exists
55 else:
56 logger.debug('Recipient is not local')
57 allowed = True
58
59 action = '550 5.7.0 Recipient mailbox mapping not authorized for current user'
60 if allowed:
61 action = 'DUNNO'
62
63 logger.info('Reached verdict: %s', {'allowed': allowed, 'action': action})
64 self.wfile.write(f'action={action}\n\n'.encode())
65
66class ThreadedSystemdSocketServer(ThreadingMixIn, SystemdSocketServer):
67 def __init__(self, fd, RequestHandlerClass):
68 super().__init__(fd, RequestHandlerClass)
69
70 self.db_pool = ConnectionPool(min_size=1)
71 self.db_pool.wait()
72
73def main():
74 global logger
75 logger = logging.getLogger(__name__)
76 console_handler = logging.StreamHandler()
77 console_handler.setFormatter( logging.Formatter('[%(levelname)s](%(name)s): %(message)s') )
78 if sys.stderr.isatty():
79 console_handler.setFormatter( logging.Formatter('%(asctime)s [%(levelname)s](%(name)s): %(message)s') )
80 logger.addHandler(console_handler)
81 logger.setLevel(logging.DEBUG)
82
83 # log uncaught exceptions
84 def log_exceptions(type, value, tb):
85 global logger
86
87 logger.error(value)
88 sys.__excepthook__(type, value, tb) # calls default excepthook
89
90 sys.excepthook = log_exceptions
91
92 fds = listen_fds()
93 servers = [ThreadedSystemdSocketServer(fd, PolicyHandler) for fd in fds]
94
95 if servers:
96 for server in servers:
97 Thread(name=f'Server for fd{server.fileno()}', target=server.serve_forever).start()
98 else:
99 return 2
100
101 SystemdNotifier().notify('READY=1')
102
103 return 0
104
105if __name__ == '__main__':
106 sys.exit(main())
diff --git a/hosts/surtr/email/internal-policy-server/pyproject.toml b/hosts/surtr/email/internal-policy-server/pyproject.toml
new file mode 100644
index 00000000..c697cd01
--- /dev/null
+++ b/hosts/surtr/email/internal-policy-server/pyproject.toml
@@ -0,0 +1,18 @@
1[project]
2name = "internal-policy-server"
3version = "0.1.0"
4requires-python = ">=3.12"
5dependencies = [
6 "psycopg>=3.2.9",
7 "psycopg-binary>=3.2.9",
8 "psycopg-pool>=3.2.6",
9 "sdnotify>=0.3.2",
10 "systemd-socketserver>=1.0",
11]
12
13[project.scripts]
14internal-policy-server = "internal_policy_server.__main__:main"
15
16[build-system]
17requires = ["hatchling"]
18build-backend = "hatchling.build"
diff --git a/hosts/surtr/email/internal-policy-server/uv.lock b/hosts/surtr/email/internal-policy-server/uv.lock
new file mode 100644
index 00000000..f7a4e729
--- /dev/null
+++ b/hosts/surtr/email/internal-policy-server/uv.lock
@@ -0,0 +1,119 @@
1version = 1
2revision = 2
3requires-python = ">=3.12"
4
5[[package]]
6name = "internal-policy-server"
7version = "0.1.0"
8source = { editable = "." }
9dependencies = [
10 { name = "psycopg" },
11 { name = "psycopg-binary" },
12 { name = "psycopg-pool" },
13 { name = "sdnotify" },
14 { name = "systemd-socketserver" },
15]
16
17[package.metadata]
18requires-dist = [
19 { name = "psycopg", specifier = ">=3.2.9" },
20 { name = "psycopg-binary", specifier = ">=3.2.9" },
21 { name = "psycopg-pool", specifier = ">=3.2.6" },
22 { name = "sdnotify", specifier = ">=0.3.2" },
23 { name = "systemd-socketserver", specifier = ">=1.0" },
24]
25
26[[package]]
27name = "psycopg"
28version = "3.2.9"
29source = { registry = "https://pypi.org/simple" }
30dependencies = [
31 { name = "typing-extensions", marker = "python_full_version < '3.13'" },
32 { name = "tzdata", marker = "sys_platform == 'win32'" },
33]
34sdist = { url = "https://files.pythonhosted.org/packages/27/4a/93a6ab570a8d1a4ad171a1f4256e205ce48d828781312c0bbaff36380ecb/psycopg-3.2.9.tar.gz", hash = "sha256:2fbb46fcd17bc81f993f28c47f1ebea38d66ae97cc2dbc3cad73b37cefbff700", size = 158122, upload-time = "2025-05-13T16:11:15.533Z" }
35wheels = [
36 { url = "https://files.pythonhosted.org/packages/44/b0/a73c195a56eb6b92e937a5ca58521a5c3346fb233345adc80fd3e2f542e2/psycopg-3.2.9-py3-none-any.whl", hash = "sha256:01a8dadccdaac2123c916208c96e06631641c0566b22005493f09663c7a8d3b6", size = 202705, upload-time = "2025-05-13T16:06:26.584Z" },
37]
38
39[[package]]
40name = "psycopg-binary"
41version = "3.2.9"
42source = { registry = "https://pypi.org/simple" }
43wheels = [
44 { url = "https://files.pythonhosted.org/packages/29/6f/ec9957e37a606cd7564412e03f41f1b3c3637a5be018d0849914cb06e674/psycopg_binary-3.2.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be7d650a434921a6b1ebe3fff324dbc2364393eb29d7672e638ce3e21076974e", size = 4022205, upload-time = "2025-05-13T16:07:48.195Z" },
45 { url = "https://files.pythonhosted.org/packages/6b/ba/497b8bea72b20a862ac95a94386967b745a472d9ddc88bc3f32d5d5f0d43/psycopg_binary-3.2.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76b4722a529390683c0304501f238b365a46b1e5fb6b7249dbc0ad6fea51a0", size = 4083795, upload-time = "2025-05-13T16:07:50.917Z" },
46 { url = "https://files.pythonhosted.org/packages/42/07/af9503e8e8bdad3911fd88e10e6a29240f9feaa99f57d6fac4a18b16f5a0/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96a551e4683f1c307cfc3d9a05fec62c00a7264f320c9962a67a543e3ce0d8ff", size = 4655043, upload-time = "2025-05-13T16:07:54.857Z" },
47 { url = "https://files.pythonhosted.org/packages/28/ed/aff8c9850df1648cc6a5cc7a381f11ee78d98a6b807edd4a5ae276ad60ad/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:61d0a6ceed8f08c75a395bc28cb648a81cf8dee75ba4650093ad1a24a51c8724", size = 4477972, upload-time = "2025-05-13T16:07:57.925Z" },
48 { url = "https://files.pythonhosted.org/packages/5c/bd/8e9d1b77ec1a632818fe2f457c3a65af83c68710c4c162d6866947d08cc5/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad280bbd409bf598683dda82232f5215cfc5f2b1bf0854e409b4d0c44a113b1d", size = 4737516, upload-time = "2025-05-13T16:08:01.616Z" },
49 { url = "https://files.pythonhosted.org/packages/46/ec/222238f774cd5a0881f3f3b18fb86daceae89cc410f91ef6a9fb4556f236/psycopg_binary-3.2.9-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76eddaf7fef1d0994e3d536ad48aa75034663d3a07f6f7e3e601105ae73aeff6", size = 4436160, upload-time = "2025-05-13T16:08:04.278Z" },
50 { url = "https://files.pythonhosted.org/packages/37/78/af5af2a1b296eeca54ea7592cd19284739a844974c9747e516707e7b3b39/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:52e239cd66c4158e412318fbe028cd94b0ef21b0707f56dcb4bdc250ee58fd40", size = 3753518, upload-time = "2025-05-13T16:08:07.567Z" },
51 { url = "https://files.pythonhosted.org/packages/ec/ac/8a3ed39ea069402e9e6e6a2f79d81a71879708b31cc3454283314994b1ae/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:08bf9d5eabba160dd4f6ad247cf12f229cc19d2458511cab2eb9647f42fa6795", size = 3313598, upload-time = "2025-05-13T16:08:09.999Z" },
52 { url = "https://files.pythonhosted.org/packages/da/43/26549af068347c808fbfe5f07d2fa8cef747cfff7c695136172991d2378b/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1b2cf018168cad87580e67bdde38ff5e51511112f1ce6ce9a8336871f465c19a", size = 3407289, upload-time = "2025-05-13T16:08:12.66Z" },
53 { url = "https://files.pythonhosted.org/packages/67/55/ea8d227c77df8e8aec880ded398316735add8fda5eb4ff5cc96fac11e964/psycopg_binary-3.2.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:14f64d1ac6942ff089fc7e926440f7a5ced062e2ed0949d7d2d680dc5c00e2d4", size = 3472493, upload-time = "2025-05-13T16:08:15.672Z" },
54 { url = "https://files.pythonhosted.org/packages/3c/02/6ff2a5bc53c3cd653d281666728e29121149179c73fddefb1e437024c192/psycopg_binary-3.2.9-cp312-cp312-win_amd64.whl", hash = "sha256:7a838852e5afb6b4126f93eb409516a8c02a49b788f4df8b6469a40c2157fa21", size = 2927400, upload-time = "2025-05-13T16:08:18.652Z" },
55 { url = "https://files.pythonhosted.org/packages/28/0b/f61ff4e9f23396aca674ed4d5c9a5b7323738021d5d72d36d8b865b3deaf/psycopg_binary-3.2.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:98bbe35b5ad24a782c7bf267596638d78aa0e87abc7837bdac5b2a2ab954179e", size = 4017127, upload-time = "2025-05-13T16:08:21.391Z" },
56 { url = "https://files.pythonhosted.org/packages/bc/00/7e181fb1179fbfc24493738b61efd0453d4b70a0c4b12728e2b82db355fd/psycopg_binary-3.2.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:72691a1615ebb42da8b636c5ca9f2b71f266be9e172f66209a361c175b7842c5", size = 4080322, upload-time = "2025-05-13T16:08:24.049Z" },
57 { url = "https://files.pythonhosted.org/packages/58/fd/94fc267c1d1392c4211e54ccb943be96ea4032e761573cf1047951887494/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ab464bfba8c401f5536d5aa95f0ca1dd8257b5202eede04019b4415f491351", size = 4655097, upload-time = "2025-05-13T16:08:27.376Z" },
58 { url = "https://files.pythonhosted.org/packages/41/17/31b3acf43de0b2ba83eac5878ff0dea5a608ca2a5c5dd48067999503a9de/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e8aeefebe752f46e3c4b769e53f1d4ad71208fe1150975ef7662c22cca80fab", size = 4482114, upload-time = "2025-05-13T16:08:30.781Z" },
59 { url = "https://files.pythonhosted.org/packages/85/78/b4d75e5fd5a85e17f2beb977abbba3389d11a4536b116205846b0e1cf744/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7e4e4dd177a8665c9ce86bc9caae2ab3aa9360b7ce7ec01827ea1baea9ff748", size = 4737693, upload-time = "2025-05-13T16:08:34.625Z" },
60 { url = "https://files.pythonhosted.org/packages/3b/95/7325a8550e3388b00b5e54f4ced5e7346b531eb4573bf054c3dbbfdc14fe/psycopg_binary-3.2.9-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fc2915949e5c1ea27a851f7a472a7da7d0a40d679f0a31e42f1022f3c562e87", size = 4437423, upload-time = "2025-05-13T16:08:37.444Z" },
61 { url = "https://files.pythonhosted.org/packages/1a/db/cef77d08e59910d483df4ee6da8af51c03bb597f500f1fe818f0f3b925d3/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a1fa38a4687b14f517f049477178093c39c2a10fdcced21116f47c017516498f", size = 3758667, upload-time = "2025-05-13T16:08:40.116Z" },
62 { url = "https://files.pythonhosted.org/packages/95/3e/252fcbffb47189aa84d723b54682e1bb6d05c8875fa50ce1ada914ae6e28/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5be8292d07a3ab828dc95b5ee6b69ca0a5b2e579a577b39671f4f5b47116dfd2", size = 3320576, upload-time = "2025-05-13T16:08:43.243Z" },
63 { url = "https://files.pythonhosted.org/packages/1c/cd/9b5583936515d085a1bec32b45289ceb53b80d9ce1cea0fef4c782dc41a7/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:778588ca9897b6c6bab39b0d3034efff4c5438f5e3bd52fda3914175498202f9", size = 3411439, upload-time = "2025-05-13T16:08:47.321Z" },
64 { url = "https://files.pythonhosted.org/packages/45/6b/6f1164ea1634c87956cdb6db759e0b8c5827f989ee3cdff0f5c70e8331f2/psycopg_binary-3.2.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f0d5b3af045a187aedbd7ed5fc513bd933a97aaff78e61c3745b330792c4345b", size = 3477477, upload-time = "2025-05-13T16:08:51.166Z" },
65 { url = "https://files.pythonhosted.org/packages/7b/1d/bf54cfec79377929da600c16114f0da77a5f1670f45e0c3af9fcd36879bc/psycopg_binary-3.2.9-cp313-cp313-win_amd64.whl", hash = "sha256:2290bc146a1b6a9730350f695e8b670e1d1feb8446597bed0bbe7c3c30e0abcb", size = 2928009, upload-time = "2025-05-13T16:08:53.67Z" },
66]
67
68[[package]]
69name = "psycopg-pool"
70version = "3.2.6"
71source = { registry = "https://pypi.org/simple" }
72dependencies = [
73 { name = "typing-extensions" },
74]
75sdist = { url = "https://files.pythonhosted.org/packages/cf/13/1e7850bb2c69a63267c3dbf37387d3f71a00fd0e2fa55c5db14d64ba1af4/psycopg_pool-3.2.6.tar.gz", hash = "sha256:0f92a7817719517212fbfe2fd58b8c35c1850cdd2a80d36b581ba2085d9148e5", size = 29770, upload-time = "2025-02-26T12:03:47.129Z" }
76wheels = [
77 { url = "https://files.pythonhosted.org/packages/47/fd/4feb52a55c1a4bd748f2acaed1903ab54a723c47f6d0242780f4d97104d4/psycopg_pool-3.2.6-py3-none-any.whl", hash = "sha256:5887318a9f6af906d041a0b1dc1c60f8f0dda8340c2572b74e10907b51ed5da7", size = 38252, upload-time = "2025-02-26T12:03:45.073Z" },
78]
79
80[[package]]
81name = "sdnotify"
82version = "0.3.2"
83source = { registry = "https://pypi.org/simple" }
84sdist = { url = "https://files.pythonhosted.org/packages/ce/d8/9fdc36b2a912bf78106de4b3f0de3891ff8f369e7a6f80be842b8b0b6bd5/sdnotify-0.3.2.tar.gz", hash = "sha256:73977fc746b36cc41184dd43c3fe81323e7b8b06c2bb0826c4f59a20c56bb9f1", size = 2459, upload-time = "2017-08-02T20:03:44.395Z" }
85
86[[package]]
87name = "systemd-python"
88version = "235"
89source = { registry = "https://pypi.org/simple" }
90sdist = { url = "https://files.pythonhosted.org/packages/10/9e/ab4458e00367223bda2dd7ccf0849a72235ee3e29b36dce732685d9b7ad9/systemd-python-235.tar.gz", hash = "sha256:4e57f39797fd5d9e2d22b8806a252d7c0106c936039d1e71c8c6b8008e695c0a", size = 61677, upload-time = "2023-02-11T13:42:16.588Z" }
91
92[[package]]
93name = "systemd-socketserver"
94version = "1.0"
95source = { registry = "https://pypi.org/simple" }
96dependencies = [
97 { name = "systemd-python" },
98]
99wheels = [
100 { url = "https://files.pythonhosted.org/packages/d8/4f/b28b7f08880120a26669b080ca74487c8c67e8b54dcb0467a8f0c9f38ed6/systemd_socketserver-1.0-py3-none-any.whl", hash = "sha256:987a8bfbf28d959e7c2966c742ad7bad482f05e121077defcf95bb38267db9a8", size = 3248, upload-time = "2020-04-26T05:26:40.661Z" },
101]
102
103[[package]]
104name = "typing-extensions"
105version = "4.13.2"
106source = { registry = "https://pypi.org/simple" }
107sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
108wheels = [
109 { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
110]
111
112[[package]]
113name = "tzdata"
114version = "2025.2"
115source = { registry = "https://pypi.org/simple" }
116sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
117wheels = [
118 { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
119]
diff --git a/hosts/surtr/kimai.nix b/hosts/surtr/kimai.nix
new file mode 100644
index 00000000..454b3d80
--- /dev/null
+++ b/hosts/surtr/kimai.nix
@@ -0,0 +1,68 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "kimai.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."kimai" = {
13 servers = {
14 "[2a03:4000:52:ada:6::2]:80" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "kimai.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/kimai.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/kimai.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/kimai.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://kimai;
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 proxy_read_timeout 300;
52 '';
53 };
54 };
55 };
56 };
57
58 systemd.services.nginx = {
59 serviceConfig = {
60 LoadCredential = [
61 "kimai.yggdrasil.li.key.pem:${config.security.acme.certs."kimai.yggdrasil.li".directory}/key.pem"
62 "kimai.yggdrasil.li.pem:${config.security.acme.certs."kimai.yggdrasil.li".directory}/fullchain.pem"
63 "kimai.yggdrasil.li.chain.pem:${config.security.acme.certs."kimai.yggdrasil.li".directory}/chain.pem"
64 ];
65 };
66 };
67 };
68}
diff --git a/hosts/surtr/postgresql/default.nix b/hosts/surtr/postgresql/default.nix
index 059f4088..3786ea7c 100644
--- a/hosts/surtr/postgresql/default.nix
+++ b/hosts/surtr/postgresql/default.nix
@@ -280,6 +280,64 @@ in {
280 CREATE VIEW imap_user ("user", "password", quota_rule) AS SELECT mailbox.mailbox AS "user", "password", quota_rule FROM mailbox_quota_rule INNER JOIN mailbox ON mailbox_quota_rule.mailbox = mailbox.mailbox; 280 CREATE VIEW imap_user ("user", "password", quota_rule) AS SELECT mailbox.mailbox AS "user", "password", quota_rule FROM mailbox_quota_rule INNER JOIN mailbox ON mailbox_quota_rule.mailbox = mailbox.mailbox;
281 281
282 COMMIT; 282 COMMIT;
283
284 BEGIN;
285 SELECT _v.register_patch('013-internal', ARRAY['000-base'], null);
286
287 ALTER TABLE mailbox_mapping ADD COLUMN internal bool NOT NULL DEFAULT false;
288 CREATE TABLE mailbox_mapping_access (
289 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
290 mailbox_mapping uuid REFERENCES mailbox_mapping(id),
291 mailbox uuid REFERENCES mailbox(id)
292 );
293 CREATE USER "postfix-internal-policy";
294 GRANT CONNECT ON DATABASE "email" TO "postfix-internal-policy";
295 ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "postfix-internal-policy";
296 GRANT SELECT ON ALL TABLES IN SCHEMA public TO "postfix-internal-policy";
297
298 COMMIT;
299
300 BEGIN;
301 SELECT _v.register_patch('014-relay', ARRAY['000-base'], null);
302
303 CREATE TABLE relay_access (
304 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
305 mailbox uuid REFERENCES mailbox(id),
306 domain citext NOT NULL CONSTRAINT domain_non_empty CHECK (domain <> ''')
307 );
308
309 COMMIT;
310
311 BEGIN;
312 SELECT _v.register_patch('015-relay-unique', ARRAY['000-base', '014-relay'], null);
313
314 CREATE UNIQUE INDEX relay_unique ON relay_access (mailbox, domain);
315
316 COMMIT;
317
318 BEGIN;
319 SELECT _v.register_patch('015-sender_bcc', null, null);
320
321 CREATE TABLE sender_bcc_maps (
322 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
323 key text NOT NULL CONSTRAINT key_not_empty CHECK (key <> '''),
324 value text NOT NULL CONSTRAINT value_not_empty CHECK (value <> '''),
325 CONSTRAINT key_unique UNIQUE (key)
326 );
327
328 COMMIT;
329
330 BEGIN;
331 SELECT _v.register_patch('016-recipient_bcc', null, null);
332
333 CREATE TABLE recipient_bcc_maps (
334 id uuid PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(),
335 key text NOT NULL CONSTRAINT key_not_empty CHECK (key <> '''),
336 value text NOT NULL CONSTRAINT value_not_empty CHECK (value <> '''),
337 CONSTRAINT recipient_bcc_maps_key_unique UNIQUE (key)
338 );
339
340 COMMIT;
283 ''} 341 ''}
284 342
285 psql etebase postgres -eXf ${pkgs.writeText "etebase.sql" '' 343 psql etebase postgres -eXf ${pkgs.writeText "etebase.sql" ''
diff --git a/hosts/surtr/tls/tsig_keys/audiobookshelf.yggdrasil.li b/hosts/surtr/tls/tsig_keys/audiobookshelf.yggdrasil.li
new file mode 100644
index 00000000..8dd610dd
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/audiobookshelf.yggdrasil.li
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:r9jhdTlbDnCMq1QLJutn76uz1Ml8MFs7fXYRSiVYh1gafcXXsUZBq5+qqoQI,iv:un/luttuKpCiMf53fa2SRY0ffttGiYwT8DuHCKEnnEI=,tag:SkNULZSulQmP99aB/Ec+Fw==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBkZGJzaEsrSU4raHlTVDVB\nczRnWVlSTTRuNXU0T3F1RTkxKytXeVJRdGpFCk9WMzNBR1NaTTMzN3BGQ2JmTjVt\nRU4rSWxCYjJPYVRzLzR0OVRYQm45TUkKLS0tIDNyMnpPN2VKUFFadTkveXRYeWps\nYUNaTjRJLzdWUnREaUVIWkpFV0FTZ2MKJS0K49SdkLW4p67FlgboHy/OVvCiUA7g\nuv5b+yotkQmh5xJwr7CUvwRewqJh56mg1yhWmE8wzpgLZMIjRXcQCQ==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQQ1h3M1lXTXVNd0d6cmtU\nU2JtUzFFblJudmEycnJONkkwME9wWm5jWVFzCnRYVEFWaVNvSW9GZ05TRWF4L2ho\nanltVytEU3ZOdHk1VHY5aGJDUkdDdmcKLS0tIEtzOFVkbmpjbWN5d0c1VEpxc1Rr\nSzJwclYxeC9TVWNaK2gwUmJSY0x1ZVUKTNivp5iS+1tzVMjMn17/ncvHcELhjQ/B\n0OVz4VpKM2wv6CjEcIMxmchqT8p8GFYVRrKUdqO2GEKOoe8ANtidWA==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-05-09T17:07:16Z",
15 "mac": "ENC[AES256_GCM,data:SwS+8UQnPgHORobKLu+u2pNaMdKIvR+etUed8btbbne/IX/Wpxt0qyPYXNNGGRkN3KAxTHWjRRdrKU1bkuTU3ER1c94T935ExDESKJLVjzaEF5VSWCqLyUNCMsY2ANw84UES2swK4YI4zF1CP7rD8tKFFld78IWZoeQ7XNGDMRA=,iv:neLvamISgQ5+aqW1iRj9xJoXq1weNNyy7KCFG2+WRQE=,tag:66SDO61WnKU6DVElo9CImg==,type:str]",
16 "unencrypted_suffix": "_unencrypted",
17 "version": "3.10.2"
18 }
19}
diff --git a/hosts/surtr/tls/tsig_keys/kimai.yggdrasil.li b/hosts/surtr/tls/tsig_keys/kimai.yggdrasil.li
new file mode 100644
index 00000000..b9199975
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/kimai.yggdrasil.li
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:ATcU3Ix7o5d/49rD5H8je1ozTjoghrloMh5DIZ5WE3oYauUAknpGfr9xq92V,iv:vy9YK5Ot7CCjMtgAGVeAUQuaSw4F5kmmZ0GJYV9kCdQ=,tag:F/MXTUM2AI1fGXa9Ewn8yQ==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDMEF0cUdydERYVzJCa3pW\nTlo0NUFON0d5RGJFVnVTNVg3cjNEUERQMEdFClEvQW5odlNEd2F1VTFmMWQrL2RB\ncllFZVpIVVJrNTJsSGF4UEdZMnVmQzAKLS0tIFUrQkkzRVZiOFNiTnFCT1pEYVRM\nQm8wV1JkQ3RrR1dkL0FsNkhsY2kxa1kKGnAo/6oibgXexUU31THdLu6X+pRtrkjD\nZnXGPZ2xaESDVUVEYQPVpNrjt9brZGJBI1BasrkEwHAXMbJC236yYQ==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB3MGs1Z2ZqK2pqWHdVYTJH\naTlncHdPa3Zld0JhQW5Ccmc1SStWSnlDR0JrCmpML2d4TGdldUdoZCtaWVpPZVl0\nVm4waWVBS1orRS90ZS96N0Y2M29LY0UKLS0tIEI1Z2VVbVVxRUpOZEN4NnBRRklC\nQXloelZCb04xbmduTlVuL005TlRGMHMKfLB6zA3sj3HgDBC7VGfGVB6I1zJpt0PV\nkCV2yADgvAA2pT9HPg9IWAEpTPysOBiuE2jPNtFvylZYwTDHoumFnQ==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-05-24T09:42:23Z",
15 "mac": "ENC[AES256_GCM,data:0pk1LpWPmX9td/TwJFxwWp5pTDyW78UtHXMDah+V9Tmgi8hH7ONdysgjwpDwS/c4zGnMA3qtobEL286U3//CTXt2qVsiUGLsnngzs2E6yBg8oGMYlGrch4M355Fl5ZxYsc8QLA6qWcuZ4H3QW8PnoqdJixcHoYLoxG01dzh4Bc0=,iv:zchk4enI1D80BkJLji5RLm7OTk3GeF8nYHuwqBxCXIM=,tag:bgkknPMqkSidi6bDFfv6UQ==,type:str]",
16 "unencrypted_suffix": "_unencrypted",
17 "version": "3.10.2"
18 }
19}
diff --git a/hosts/surtr/vpn/default.nix b/hosts/surtr/vpn/default.nix
index 1bdcf74e..92223144 100644
--- a/hosts/surtr/vpn/default.nix
+++ b/hosts/surtr/vpn/default.nix
@@ -1,4 +1,4 @@
1{ pkgs, config, lib, ... }: 1{ flake, pkgs, config, lib, ... }:
2 2
3with lib; 3with lib;
4 4
@@ -22,7 +22,11 @@ in {
22 "--load-credential=surtr.priv:/run/credentials/container@vpn.service/surtr.priv" 22 "--load-credential=surtr.priv:/run/credentials/container@vpn.service/surtr.priv"
23 "--network-ipvlan=ens3:upstream" 23 "--network-ipvlan=ens3:upstream"
24 ]; 24 ];
25 config = { 25 config = let hostConfig = config; in { config, pkgs, ... }: {
26 system.stateVersion = lib.mkIf hostConfig.containers."vpn".ephemeral config.system.nixos.release;
27 system.configurationRevision = mkIf (flake ? rev) flake.rev;
28 nixpkgs.pkgs = hostConfig.nixpkgs.pkgs;
29
26 boot.kernel.sysctl = { 30 boot.kernel.sysctl = {
27 "net.core.rmem_max" = 4194304; 31 "net.core.rmem_max" = 4194304;
28 "net.core.wmem_max" = 4194304; 32 "net.core.wmem_max" = 4194304;
diff --git a/hosts/surtr/vpn/geri.pub b/hosts/surtr/vpn/geri.pub
index ed5de2b2..2cd9b24e 100644
--- a/hosts/surtr/vpn/geri.pub
+++ b/hosts/surtr/vpn/geri.pub
@@ -1 +1 @@
sYuQSNZHzfegv8HRz71jnZm2nFLGeRnaGwVonhKUj2k= hhER05bvstOTGfiAG3IJsFkBNWCUZHokBXwaiC5d534=
diff --git a/hosts/vidhar/audiobookshelf/abs-podcast-autoplaylist-gkleen.toml b/hosts/vidhar/audiobookshelf/abs-podcast-autoplaylist-gkleen.toml
new file mode 100644
index 00000000..a5319e38
--- /dev/null
+++ b/hosts/vidhar/audiobookshelf/abs-podcast-autoplaylist-gkleen.toml
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:60OmHwuLC7RJVNNn8lsCFjIFrtDlmmT3yAm3DYn/K2b8OJB/lzKBhMUCyPpoI2lfMm6y47/DMwXI3ExH3QwfgGRf4i/Tcv7p6FCkjFgDc0RhAM7cXNSnh1gKTff8QYtPoNIzmycFCThNr7iZsPsf2/1npVaVHTnt9nTc+cmDLc+lELlvjSE00JOXch/if7KPwFww9K83XlrFmoRvwybfXR0unJqxK2XLvj+dQuKD4Bhyb88iSgu4dX1yw2uBSZBD16S4Io0DaZ+as5Yw4Kon7WMj3Jd5kz8ZxK+0NCy1CVJHOfJIwgYl0SVPp4DpbAPtJO4R/ciXyDQ/XGpoLtHjxnKXaJlJoSiA7FhuSEk+jB/peLHrYV1obdIRE5Dstly01S5cydKlfQ+A0TSjxFSWBYMEiD89sD09Br3iSJX5FejOoS8d2IQJ5faVzgQl4T5aBKsxCNNwmYrEe8m9HN7o2eer8nTKMln5IxZi3ZWhnjgJfrJ4QTXFndxCb78jo8HroN3+7VhoM136UZkqH1OMrIgAH/XSlW08G8m9MRamKsAWklq9aVflcEsPWTHmYW7rjAapQYf+jyK6BbfHcYmyKM82TFZ5iNB60Pth6EJgb2V8PZiChGvDzQvFYYOO3p9a/J8bVqsnPZBXXYcIBt42ZuRPvyyUTfM+75V1eYE9ZGFML+QlofwNCAg+/Rnl+RRy4z+8xQxd8Dn06geDpHsr4yND72FRUTKLbjxF5xfbzBRcZEXjGkyFdEAF7rB78I8xIqii+n6Yt8uEURmd4geI9KWXRQnwofTz9pklaAnRbER8zy/BJIiIYy8zecUHJn9v/DPnsnksfL6RRmG4tHaRBDbpAag0kVkCrpO/flK6dZOl/wvoVVVqT2O69a9/RpHLSV2f//ZS6L9s6vaYe4pXL0M6QymgA22sNHaws6XggJlTxVOFGRejMGYrKqVWtC+2UNbnel+/J0N1qj4luWfQaf9+1j+fq7vyLSzXYFCiyOLAznpqOhzKu6VWy2IbR0UnCoL5ZbhIba9e2MXM7Czy9Yee4xc=,iv:M0GbtFFl1XUeq+y9H+MiD+9z/ASB9hsd06KhpPzSwEo=,tag:vTLIIf+CeZN6DU25CSP8tw==,type:str]",
3 "sops": {
4 "age": [
5 {
6 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
7 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA0aE1XNUNCM1Q5V0d3R2JG\nbjJZTmdvQ21JbmtyR0ZmODFMdVBGejRoam1vCjMzMGdTb3BReDVCa2JJU0JrSHFP\ndTdicU5TRjIrTWpteDMzeGtDT0xaelkKLS0tIFhaSlFrbzFDUjRZV0lGR0cydVdZ\nY2xma0VSVXlTM1JucFJUSys4dlRvdEUK9gQNQEdKDDf1ikWzd6uTlE50WsfO/EB0\nGH2Ono6oNWbKWTyl/wRO8NzXx0nudwqq66s0oBLIdTMQOpIBBNI0XQ==\n-----END AGE ENCRYPTED FILE-----\n"
8 },
9 {
10 "recipient": "age1qffdqvy9arld9zd5a5cylt0n98xhcns5shxhrhwjq5g4qa844ejselaa4l",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBiRWFqSHNlY1IvMkkwaEto\ncHZHa2p1Y25SakFkS2JYMlRFcFhnZGY1dVRFCkxSWmxvcHZMampQKzdKRHI0ZVMx\nUTFtR0pHbzFaQ0xQUFA2ZERDSWpwS0UKLS0tIFBaSGczY3VWdy9TKzRDZWZ2SElY\nbVQ4dDNhQllmVmViWGs5c3V4TmNscjQKeugevQJFAN/8JrzeAm4hm2JsQGb26BCb\n3dKYnN1kJU7oVHr1aVfXwMpELNYt9poX6WTY2h9lsdHuRlqoFXAA5Q==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2025-05-10T10:25:15Z",
15 "mac": "ENC[AES256_GCM,data:dhj7e+vF3uiR6I22PR5tdNdM8EyrWmGGTIqjj8H7IdNIsZBHzjeHlBDFOwN7z/JMO0BVwIi4DmhApg2BSPGsQZGDQZ28UTCC8TDtd1zmfGtSP8R8AFHADYdLK/desMtHg6BZTnLv5tpba34WWdflMNOQpwgWPZsIk/DkLaoXdvk=,iv:qkoAZngTz2sfWdxDs+h8Mb2IrkF8gqnQoR5iRoeKjbY=,tag:zXrkBJmPM4ItJxMnX8IDxQ==,type:str]",
16 "unencrypted_suffix": "_unencrypted",
17 "version": "3.10.2"
18 }
19}
diff --git a/hosts/vidhar/audiobookshelf/default.nix b/hosts/vidhar/audiobookshelf/default.nix
new file mode 100644
index 00000000..136bcaff
--- /dev/null
+++ b/hosts/vidhar/audiobookshelf/default.nix
@@ -0,0 +1,21 @@
1{ config, pkgs, lib, ... }:
2
3{
4 config = {
5 services.audiobookshelf = {
6 enable = true;
7 host = "2a03:4000:52:ada:4:1::";
8 port = 28982;
9 };
10
11 users.groups.audiobookshelf.members = [ "gkleen" ];
12
13 services.abs-podcast-autoplaylist = {
14 gkleen = {};
15 };
16 sops.secrets.${config.services.abs-podcast-autoplaylist.gkleen.configSecret} = {
17 format = "binary";
18 sopsFile = ./abs-podcast-autoplaylist-gkleen.toml;
19 };
20 };
21}
diff --git a/hosts/vidhar/default.nix b/hosts/vidhar/default.nix
index 90ab40dd..7da17e6f 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 ./paperless ./hledger 7 ./zfs.nix ./network ./samba.nix ./dns ./prometheus ./borg ./pgbackrest ./postgresql.nix ./immich.nix ./paperless ./hledger ./audiobookshelf ./kimai
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/kimai/default.nix b/hosts/vidhar/kimai/default.nix
new file mode 100644
index 00000000..0258697b
--- /dev/null
+++ b/hosts/vidhar/kimai/default.nix
@@ -0,0 +1,89 @@
1{ flake, config, ... }:
2
3{
4 config = {
5 boot.enableContainers = true;
6 boot.kernel.sysctl = {
7 "net.netfilter.nf_log_all_netns" = true;
8 };
9
10 containers."kimai" = {
11 autoStart = true;
12 ephemeral = true;
13 bindMounts = {
14 "/var/lib/kimai" = {
15 hostPath = "/var/lib/kimai/state";
16 isReadOnly = false;
17 };
18 "/var/lib/mysql" = {
19 hostPath = "/var/lib/kimai/mysql";
20 isReadOnly = false;
21 };
22 };
23 privateNetwork = true;
24 # forwardPorts = [
25 # { containerPort = 80;
26 # hostPort = 28983;
27 # }
28 # ];
29 hostAddress = "192.168.52.113";
30 localAddress = "192.168.52.114";
31 hostAddress6 = "2a03:4000:52:ada:6::1";
32 localAddress6 = "2a03:4000:52:ada:6::2";
33 config = let hostConfig = config; in { config, pkgs, lib, ... }: {
34 system.stateVersion = lib.mkIf hostConfig.containers."kimai".ephemeral config.system.nixos.release;
35 system.configurationRevision = lib.mkIf (flake ? rev) flake.rev;
36 nixpkgs.pkgs = hostConfig.nixpkgs.pkgs;
37
38 services.kimai.sites."kimai.yggdrasil.li" = {
39 database.socket = "/run/mysqld/mysqld.sock";
40 };
41
42 networking = {
43 useDHCP = false;
44 useNetworkd = true;
45 useHostResolvConf = false;
46 firewall.enable = false;
47 nftables = {
48 enable = true;
49 rulesetFile = ./ruleset.nft;
50 };
51 };
52
53 services.resolved.fallbackDns = [
54 "9.9.9.10#dns10.quad9.net"
55 "149.112.112.10#dns10.quad9.net"
56 "2620:fe::10#dns10.quad9.net"
57 "2620:fe::fe:10#dns10.quad9.net"
58 ];
59
60 systemd.network = {
61 networks.upstream = {
62 name = "eth0";
63 matchConfig = {
64 Name = "eth0";
65 };
66 linkConfig = {
67 RequiredForOnline = true;
68 };
69 networkConfig = {
70 Address = [ "192.168.52.114/32" "2a03:4000:52:ada:6::2/128" ];
71 LLMNR = false;
72 MulticastDNS = false;
73 };
74 routes = [
75 { Destination = "192.168.52.113/32"; }
76 { Destination = "2a03:4000:52:ada:6::1/128"; }
77 { Destination = "0.0.0.0/0";
78 Gateway = "192.168.52.113";
79 }
80 { Destination = "::/0";
81 Gateway = "2a03:4000:52:ada:6::1";
82 }
83 ];
84 };
85 };
86 };
87 };
88 };
89}
diff --git a/hosts/vidhar/kimai/ruleset.nft b/hosts/vidhar/kimai/ruleset.nft
new file mode 100644
index 00000000..ad4db6d5
--- /dev/null
+++ b/hosts/vidhar/kimai/ruleset.nft
@@ -0,0 +1,149 @@
1define icmp_protos = {ipv6-icmp, icmp, igmp}
2
3table arp filter {
4 limit lim_arp {
5 rate over 50 mbytes/second burst 50 mbytes
6 }
7
8 counter arp-rx {}
9 counter arp-tx {}
10
11 counter arp-ratelimit-rx {}
12 counter arp-ratelimit-tx {}
13
14 chain input {
15 type filter hook input priority filter
16 policy accept
17
18 limit name lim_arp counter name arp-ratelimit-rx drop
19
20 counter name arp-rx
21 }
22
23 chain output {
24 type filter hook output priority filter
25 policy accept
26
27 limit name lim_arp counter name arp-ratelimit-tx drop
28
29 counter name arp-tx
30 }
31}
32
33table inet filter {
34 limit lim_reject {
35 rate over 1000/second burst 1000 packets
36 }
37
38 limit lim_icmp {
39 rate over 50 mbytes/second burst 50 mbytes
40 }
41
42 counter invalid-fw {}
43 counter fw-lo {}
44
45 counter reject-ratelimit-fw {}
46 counter reject-fw {}
47 counter reject-tcp-fw {}
48 counter reject-icmp-fw {}
49
50 counter drop-fw {}
51
52 counter invalid-rx {}
53
54 counter rx-lo {}
55 counter invalid-local4-rx {}
56 counter invalid-local6-rx {}
57
58 counter icmp-ratelimit-rx {}
59 counter icmp-rx {}
60
61 counter kimai-rx {}
62
63 counter established-rx {}
64
65 counter reject-ratelimit-rx {}
66 counter reject-rx {}
67 counter reject-tcp-rx {}
68 counter reject-icmp-rx {}
69
70 counter drop-rx {}
71
72 counter tx-lo {}
73
74 counter icmp-ratelimit-tx {}
75 counter icmp-tx {}
76
77 counter kimai-tx {}
78
79 counter tx {}
80
81 chain forward {
82 type filter hook forward priority filter
83 policy drop
84
85
86 ct state invalid log level debug prefix "kimai: drop invalid forward: " counter name invalid-fw drop
87
88
89 iifname lo counter name fw-lo accept
90
91
92 limit name lim_reject log level debug prefix "kimai: drop forward: " counter name reject-ratelimit-fw drop
93 log level debug prefix "kimai: reject forward: " counter name reject-fw
94 meta l4proto tcp ct state new counter name reject-tcp-fw reject with tcp reset
95 ct state new counter name reject-icmp-fw reject
96
97
98 counter name drop-fw
99 }
100
101 chain input {
102 type filter hook input priority filter
103 policy drop
104
105
106 ct state invalid log level debug prefix "kimai: drop invalid input: " counter name invalid-rx drop
107
108
109 iifname lo counter name rx-lo accept
110 iif != lo ip daddr 127.0.0.1/8 counter name invalid-local4-rx reject
111 iif != lo ip6 daddr ::1/128 counter name invalid-local6-rx reject
112
113
114 meta l4proto $icmp_protos limit name lim_icmp counter name icmp-ratelimit-rx drop
115 meta l4proto $icmp_protos counter name icmp-rx accept
116
117
118 tcp dport 80 counter name kimai-rx accept
119
120
121 ct state { established, related } counter name established-rx accept
122
123
124 limit name lim_reject log level debug prefix "kimai: drop input: " counter name reject-ratelimit-rx drop
125 log level debug prefix "kimai: reject input: " counter name reject-rx
126 meta l4proto tcp ct state new counter name reject-tcp-rx reject with tcp reset
127 ct state new counter name reject-icmp-rx reject
128
129
130 counter name drop-rx
131 }
132
133 chain output {
134 type filter hook output priority filter
135 policy accept
136
137
138 oifname lo counter name tx-lo accept
139
140 meta l4proto $icmp_protos limit name lim_icmp counter name icmp-ratelimit-tx drop
141 meta l4proto $icmp_protos counter name icmp-tx accept
142
143
144 tcp sport 80 counter name kimai-tx
145
146
147 counter name tx
148 }
149}
diff --git a/hosts/vidhar/network/default.nix b/hosts/vidhar/network/default.nix
index 0643f0bb..92d755f3 100644
--- a/hosts/vidhar/network/default.nix
+++ b/hosts/vidhar/network/default.nix
@@ -103,7 +103,14 @@ with lib;
103 /srv/nfs/nix-store 10.141.0.0/24(ro,async,root_squash) 2a03:4000:52:ada:1::/80(ro,async,root_squash) 103 /srv/nfs/nix-store 10.141.0.0/24(ro,async,root_squash) 2a03:4000:52:ada:1::/80(ro,async,root_squash)
104 ''; 104 '';
105 }; 105 };
106 settings.nfsd.vers3 = false; 106 settings.nfsd = {
107 rdma = true;
108 vers3 = false;
109 vers4 = true;
110 "vers4.0" = false;
111 "vers4.1" = false;
112 "vers4.2" = true;
113 };
107 }; 114 };
108 115
109 fileSystems = { 116 fileSystems = {
diff --git a/hosts/vidhar/network/dhcp/default.nix b/hosts/vidhar/network/dhcp/default.nix
index 07a83351..11460393 100644
--- a/hosts/vidhar/network/dhcp/default.nix
+++ b/hosts/vidhar/network/dhcp/default.nix
@@ -1,8 +1,33 @@
1{ flake, config, pkgs, lib, ... }: 1{ flake, config, pkgs, lib, sources, ... }:
2 2
3with lib; 3with lib;
4 4
5{ 5let
6 nfsrootBaseUrl = "http://nfsroot.vidhar.yggdrasil";
7 tftpIp = "10.141.0.1";
8 nfsIp = tftpIp;
9 ipxe = pkgs.ipxe.override {
10 additionalTargets = {
11 "bin-i386-efi/ipxe.efi" = "i386-ipxe.efi";
12 };
13 additionalOptions = [
14 "NSLOOKUP_CMD"
15 "PING_CMD"
16 "CONSOLE_CMD"
17 ];
18 embedScript = pkgs.writeText "yggdrasil.ipxe" ''
19 #!ipxe
20
21 cpair --background 9 1
22 cpair --background 9 3
23 cpair --background 9 6
24
25 set user-class iPXE-yggdrasil
26
27 autoboot
28 '';
29 };
30in {
6 config = { 31 config = {
7 services.kea = { 32 services.kea = {
8 dhcp4 = { 33 dhcp4 = {
@@ -23,41 +48,67 @@ with lib;
23 }; 48 };
24 49
25 client-classes = [ 50 client-classes = [
26 { name = "eostre-ipxe"; 51 { name = "ipxe-eostre";
27 test = "hexstring(pkt4.mac, ':') == '00:d8:61:79:c5:40' and option[77].hex == 'iPXE'"; 52 test = "hexstring(pkt4.mac, ':') == '00:d8:61:79:c5:40' and option[77].hex == 'iPXE-yggdrasil'";
28 next-server = "10.141.0.1"; 53 next-server = tftpIp;
29 boot-file-name = "http://nfsroot.vidhar.yggdrasil/eostre/netboot.ipxe"; 54 boot-file-name = "${nfsrootBaseUrl}/eostre.menu.ipxe";
55 only-if-required = true;
56 }
57 { name = "ipxe-yggdrasil";
58 test = "option[77].hex == 'iPXE-yggdrasil'";
59 next-server = tftpIp;
60 boot-file-name = "${nfsrootBaseUrl}/installer-x86_64-linux.menu.ipxe";
61 only-if-required = true;
62 }
63
64 { name = "uefi-http";
65 test = "option[client-system].hex == 0x0010";
66 option-data = [
67 { name = "vendor-class-identifier"; data = "HTTPClient"; }
68 ];
69 boot-file-name = "${nfsrootBaseUrl}/ipxe.efi";
70 only-if-required = true;
71 }
72
73 { name = "ipxe-uefi-64";
74 test = "option[77].hex == 'iPXE' and (substring(option[60].hex,0,20) == 'PXEClient:Arch:00007' or substring(option[60].hex,0,20) == 'PXEClient:Arch:00008' or substring(option[60].hex,0,20) == 'PXEClient:Arch:00009')";
75 boot-file-name = "${nfsrootBaseUrl}/ipxe.efi";
76 only-if-required = true;
77 }
78 { name = "ipxe-uefi-32";
79 test = "option[77].hex == 'iPXE' and (substring(option[60].hex,0,20) == 'PXEClient:Arch:00002' or substring(option[60].hex,0,20) == 'PXEClient:Arch:00006')";
80 boot-file-name = "${nfsrootBaseUrl}/i386-ipxe.efi";
30 only-if-required = true; 81 only-if-required = true;
31 } 82 }
32 { name = "ipxe"; 83 { name = "ipxe-legacy";
33 test = "option[77].hex == 'iPXE'"; 84 test = "option[77].hex == 'iPXE' and substring(option[60].hex,0,20) == 'PXEClient:Arch:00000'";
34 next-server = "10.141.0.1"; 85 boot-file-name = "${nfsrootBaseUrl}/ipxe.lkrn";
35 boot-file-name = "http://nfsroot.vidhar.yggdrasil/installer-x86_64-linux/netboot.ipxe";
36 only-if-required = true; 86 only-if-required = true;
37 } 87 }
88
38 { name = "uefi-64"; 89 { name = "uefi-64";
39 test = "substring(option[60].hex,0,20) == 'PXEClient:Arch:00007' or substring(option[60].hex,0,20) == 'PXEClient:Arch:00008' or substring(option[60].hex,0,20) == 'PXEClient:Arch:00009'"; 90 test = "substring(option[60].hex,0,20) == 'PXEClient:Arch:00007' or substring(option[60].hex,0,20) == 'PXEClient:Arch:00008' or substring(option[60].hex,0,20) == 'PXEClient:Arch:00009'";
40 only-if-required = true;
41 option-data = [ 91 option-data = [
42 { name = "tftp-server-name"; data = "10.141.0.1"; } 92 { name = "tftp-server-name"; data = tftpIp; }
43 ]; 93 ];
44 boot-file-name = "ipxe.efi"; 94 boot-file-name = "ipxe.efi";
95 only-if-required = true;
45 } 96 }
46 { name = "uefi-32"; 97 { name = "uefi-32";
47 test = "substring(option[60].hex,0,20) == 'PXEClient:Arch:00002' or substring(option[60].hex,0,20) == 'PXEClient:Arch:00006'"; 98 test = "substring(option[60].hex,0,20) == 'PXEClient:Arch:00002' or substring(option[60].hex,0,20) == 'PXEClient:Arch:00006'";
48 only-if-required = true;
49 option-data = [ 99 option-data = [
50 { name = "tftp-server-name"; data = "10.141.0.1"; } 100 { name = "tftp-server-name"; data = tftpIp; }
51 ]; 101 ];
52 boot-file-name = "i386-ipxe.efi"; 102 boot-file-name = "i386-ipxe.efi";
103 only-if-required = true;
53 } 104 }
54 { name = "legacy"; 105 { name = "legacy";
55 test = "substring(option[60].hex,0,20) == 'PXEClient:Arch:00000'"; 106 test = "substring(option[60].hex,0,20) == 'PXEClient:Arch:00000'";
56 only-if-required = true;
57 option-data = [ 107 option-data = [
58 { name = "tftp-server-name"; data = "10.141.0.1"; } 108 { name = "tftp-server-name"; data = tftpIp; }
59 ]; 109 ];
60 boot-file-name = "undionly.kpxe"; 110 boot-file-name = "ipxe.lkrn";
111 only-if-required = true;
61 } 112 }
62 ]; 113 ];
63 114
@@ -252,34 +303,78 @@ with lib;
252 name = "nfsroot.vidhar.yggdrasil"; 303 name = "nfsroot.vidhar.yggdrasil";
253 paths = 304 paths =
254 (map (system: 305 (map (system:
255 let 306 pkgs.symlinkJoin {
256 installerBuild = (flake.nixosConfigurations.${"installer-${system}-nfsroot"}.extendModules { 307 name = "installer-${system}";
257 modules = [ 308 paths = [
258 ({ ... }: { 309 (builtins.addErrorContext "while evaluating installer-${system}-nfsroot" (let
259 config.nfsroot.storeDevice = "10.141.0.1:nix-store"; 310 installerBuild' = (flake.nixosConfigurations.${"installer-${system}-nfsroot"}.extendModules {
260 config.nfsroot.registrationUrl = "http://nfsroot.vidhar.yggdrasil/installer-${system}/registration"; 311 modules = [
261 }) 312 ({ ... }: {
262 ]; 313 config.nfsroot.storeDevice = "${nfsIp}:nix-store";
263 }).config.system.build; 314 config.nfsroot.registrationUrl = "${nfsrootBaseUrl}/installer-${system}/registration";
264 in builtins.toPath (pkgs.runCommandLocal "install-${system}" {} '' 315 config.system.nixos.label = "installer-${system}";
265 mkdir -p $out/installer-${system} 316 })
266 install -m 0444 -t $out/installer-${system} \ 317 ];
267 ${installerBuild.initialRamdisk}/initrd \ 318 });
268 ${installerBuild.kernel}/bzImage \ 319 installerBuild = installerBuild'.config.system.build;
269 ${installerBuild.netbootIpxeScript}/netboot.ipxe \ 320 in builtins.toPath (pkgs.runCommandLocal "installer-${system}" {} ''
270 ${pkgs.closureInfo { rootPaths = installerBuild.storeContents; }}/registration 321 mkdir -p $out/installer-${system}
271 '') 322 install -m 0444 -t $out/installer-${system} \
272 ) ["x86_64-linux"] 323 ${installerBuild.initialRamdisk}/initrd \
324 ${installerBuild.kernel}/bzImage \
325 ${installerBuild.netbootIpxeScript}/netboot.ipxe \
326 ${pkgs.closureInfo { rootPaths = installerBuild.storeContents; }}/registration
327 install -m 0444 ${pkgs.writeText "installer-${system}.menu.ipxe" ''
328 #!ipxe
329
330 :start
331 menu iPXE boot menu for installer-${system}
332 item installer ${with installerBuild'; "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} (Linux ${config.boot.kernelPackages.kernel.modDirVersion})"}
333 item memtest memtest86plus
334 item netboot netboot.xyz
335 item shell iPXE shell
336 choose --timeout 0 --default installer selected || goto shell
337 goto ''${selected}
338
339 :shell
340 shell
341 goto start
342
343 :installer
344 chain installer-${system}/netboot.ipxe
345 goto start
346
347 :netboot
348 iseq ''${platform} efi && chain --autofree netboot.xyz.efi || chain --autofree netboot.xyz.lkrn
349 goto start
350
351 :memtest
352 iseq ''${platform} efi && chain --autofree memtest.efi || chain --autofree memtest.bin
353 goto start
354 ''} $out/installer-${system}.menu.ipxe
355 '')))
356 ];
357 }) ["x86_64-linux"]
273 ) ++ [ 358 ) ++ [
274 (let 359 (pkgs.runCommandLocal "utils" {} ''
275 eostreBuild = (flake.nixosConfigurations.eostre.extendModules { 360 mkdir $out
361 install -m 0444 -t $out \
362 ${ipxe}/{ipxe.efi,i386-ipxe.efi,ipxe.lkrn} \
363 ${pkgs.memtest86plus}/{memtest.efi,memtest.bin}
364 install -m 0444 ${sources.netbootxyz-efi.src} $out/netboot.xyz.efi
365 install -m 0444 ${sources.netbootxyz-lkrn.src} $out/netboot.xyz.lkrn
366 '')
367 (builtins.addErrorContext "while evaluating eostre" (let
368 eostreBuild' = (flake.nixosConfigurations.eostre.extendModules {
276 modules = [ 369 modules = [
277 ({ ... }: { 370 ({ ... }: {
278 config.nfsroot.storeDevice = "10.141.0.1:nix-store"; 371 config.nfsroot.storeDevice = "${nfsIp}:nix-store";
279 config.nfsroot.registrationUrl = "http://nfsroot.vidhar.yggdrasil/eostre/registration"; 372 config.nfsroot.registrationUrl = "${nfsrootBaseUrl}/eostre/registration";
373 config.system.nixos.label = "eostre";
280 }) 374 })
281 ]; 375 ];
282 }).config.system.build; 376 });
377 eostreBuild = eostreBuild'.config.system.build;
283 in builtins.toPath (pkgs.runCommandLocal "eostre" {} '' 378 in builtins.toPath (pkgs.runCommandLocal "eostre" {} ''
284 mkdir -p $out/eostre 379 mkdir -p $out/eostre
285 install -m 0444 -t $out/eostre \ 380 install -m 0444 -t $out/eostre \
@@ -287,7 +382,39 @@ with lib;
287 ${eostreBuild.kernel}/bzImage \ 382 ${eostreBuild.kernel}/bzImage \
288 ${eostreBuild.netbootIpxeScript}/netboot.ipxe \ 383 ${eostreBuild.netbootIpxeScript}/netboot.ipxe \
289 ${pkgs.closureInfo { rootPaths = eostreBuild.storeContents; }}/registration 384 ${pkgs.closureInfo { rootPaths = eostreBuild.storeContents; }}/registration
290 '')) 385 install -m 0444 ${pkgs.writeText "eostre.menu.ipxe" ''
386 #!ipxe
387
388 set menu-timeout 5000
389
390 :start
391 menu iPXE boot menu for eostre
392 item eostre ${with eostreBuild'; "${config.system.nixos.distroName} ${config.system.nixos.codeName} ${config.system.nixos.label} (Linux ${config.boot.kernelPackages.kernel.modDirVersion})"}
393 item memtest memtest86plus
394 item netboot netboot.xyz
395 item shell iPXE shell
396 choose --timeout ''${menu-timeout} --default eostre selected || goto shell
397 set menu-timeout 0
398 goto ''${selected}
399
400 :shell
401 set menu-timeout 0
402 shell
403 goto start
404
405 :eostre
406 chain eostre/netboot.ipxe
407 goto start
408
409 :netboot
410 iseq ''${platform} efi && chain --autofree netboot.xyz.efi || chain --autofree netboot.xyz.lkrn
411 goto start
412
413 :memtest
414 iseq ''${platform} efi && chain --autofree memtest.efi || chain --autofree memtest.bin
415 goto start
416 ''} $out/eostre.menu.ipxe
417 '')))
291 ]; 418 ];
292 }; 419 };
293 }; 420 };
@@ -298,20 +425,12 @@ with lib;
298 after = [ "network.target" ]; 425 after = [ "network.target" ];
299 wantedBy = [ "multi-user.target" ]; 426 wantedBy = [ "multi-user.target" ];
300 serviceConfig.ExecStart = let 427 serviceConfig.ExecStart = let
301 ipxe = pkgs.ipxe.override {
302 additionalTargets = {
303 "bin-i386-efi/ipxe.efi" = "i386-ipxe.efi";
304 };
305 additionalOptions = [
306 "NSLOOKUP_CMD"
307 ];
308 };
309 tftpRoot = pkgs.runCommandLocal "netboot" {} '' 428 tftpRoot = pkgs.runCommandLocal "netboot" {} ''
310 mkdir -p $out 429 mkdir -p $out
311 install -m 0444 -t $out \ 430 install -m 0444 -t $out \
312 ${ipxe}/ipxe.efi ${ipxe}/i386-ipxe.efi ${ipxe}/undionly.kpxe 431 ${ipxe}/{ipxe.efi,i386-ipxe.efi,ipxe.lkrn}
313 ''; 432 '';
314 in "${pkgs.atftp}/sbin/atftpd --daemon --no-fork --bind-address=10.141.0.1 ${tftpRoot}"; 433 in "${pkgs.atftp}/sbin/atftpd --daemon --no-fork --bind-address=${tftpIp} ${tftpRoot}";
315 }; 434 };
316 }; 435 };
317} 436}
diff --git a/hosts/vidhar/network/ruleset.nft b/hosts/vidhar/network/ruleset.nft
index 1edae167..7897fb3d 100644
--- a/hosts/vidhar/network/ruleset.nft
+++ b/hosts/vidhar/network/ruleset.nft
@@ -60,6 +60,7 @@ table inet filter {
60 counter fw-lo {} 60 counter fw-lo {}
61 counter fw-lan {} 61 counter fw-lan {}
62 counter fw-gpon {} 62 counter fw-gpon {}
63 counter fw-kimai {}
63 64
64 counter fw-cups {} 65 counter fw-cups {}
65 66
@@ -94,6 +95,8 @@ table inet filter {
94 counter immich-rx {} 95 counter immich-rx {}
95 counter paperless-rx {} 96 counter paperless-rx {}
96 counter hledger-rx {} 97 counter hledger-rx {}
98 counter audiobookshelf-rx {}
99 counter kimai-rx {}
97 100
98 counter established-rx {} 101 counter established-rx {}
99 102
@@ -125,6 +128,8 @@ table inet filter {
125 counter immich-tx {} 128 counter immich-tx {}
126 counter paperless-tx {} 129 counter paperless-tx {}
127 counter hledger-tx {} 130 counter hledger-tx {}
131 counter audiobookshelf-tx {}
132 counter kimai-tx {}
128 133
129 counter tx {} 134 counter tx {}
130 135
@@ -148,8 +153,13 @@ table inet filter {
148 153
149 oifname { lan, gpon, bifrost } meta l4proto $icmp_protos jump forward_icmp_accept 154 oifname { lan, gpon, bifrost } meta l4proto $icmp_protos jump forward_icmp_accept
150 iifname lan oifname { gpon, bifrost } counter name fw-lan accept 155 iifname lan oifname { gpon, bifrost } counter name fw-lan accept
156 iifname ve-kimai oifname gpon counter name fw-kimai accept
151 157
152 iifname gpon oifname lan ct state { established, related } counter name fw-gpon accept 158 iifname gpon oifname lan ct state { established, related } counter name fw-gpon accept
159 iifname gpon oifname ve-kimai ct state { established, related } counter name fw-kimai accept
160
161 iifname bifrost oifname ve-kimai tcp dport 80 ip6 saddr $bifrost_surtr ip6 daddr 2a03:4000:52:ada:6::2 counter name kimai-rx accept
162 iifname ve-kimai oifname bifrost tcp sport 80 ip6 saddr 2a03:4000:52:ada:6::2 ip6 daddr $bifrost_surtr counter name kimai-tx accept
153 163
154 164
155 limit name lim_reject log level debug prefix "drop forward: " counter name reject-ratelimit-fw drop 165 limit name lim_reject log level debug prefix "drop forward: " counter name reject-ratelimit-fw drop
@@ -203,6 +213,7 @@ table inet filter {
203 iifname bifrost tcp dport 2283 ip6 saddr $bifrost_surtr counter name immich-rx accept 213 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 214 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 215 iifname bifrost tcp dport 5000 ip6 saddr $bifrost_surtr counter name hledger-rx accept
216 iifname bifrost tcp dport 28982 ip6 saddr $bifrost_surtr counter name audiobookshelf-rx accept
206 217
207 ct state { established, related } counter name established-rx accept 218 ct state { established, related } counter name established-rx accept
208 219
@@ -254,6 +265,7 @@ table inet filter {
254 iifname bifrost tcp sport 2283 ip6 daddr $bifrost_surtr counter name immich-tx accept 265 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 266 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 267 iifname bifrost tcp sport 5000 ip6 daddr $bifrost_surtr counter name hledger-tx accept
268 iifname bifrost tcp sport 28982 ip6 daddr $bifrost_surtr counter name audiobookshelf-tx accept
257 269
258 270
259 counter name tx 271 counter name tx
@@ -262,7 +274,7 @@ table inet filter {
262 274
263table inet nat { 275table inet nat {
264 counter gpon-nat {} 276 counter gpon-nat {}
265 # counter container-nat {} 277 counter kimai-nat {}
266 278
267 chain postrouting { 279 chain postrouting {
268 type nat hook postrouting priority srcnat 280 type nat hook postrouting priority srcnat
@@ -270,7 +282,7 @@ table inet nat {
270 282
271 283
272 meta nfproto ipv4 oifname gpon counter name gpon-nat masquerade 284 meta nfproto ipv4 oifname gpon counter name gpon-nat masquerade
273 # iifname ve-* oifname gpon counter name container-nat masquerade 285 iifname ve-kimai oifname gpon counter name kimai-nat masquerade
274 } 286 }
275} 287}
276 288
diff --git a/hosts/vidhar/paperless/default.nix b/hosts/vidhar/paperless/default.nix
index 34cd18c4..dd02da38 100644
--- a/hosts/vidhar/paperless/default.nix
+++ b/hosts/vidhar/paperless/default.nix
@@ -4,7 +4,7 @@
4 config = { 4 config = {
5 services.paperless = { 5 services.paperless = {
6 enable = true; 6 enable = true;
7 address = "[2a03:4000:52:ada:4:1::]"; 7 address = "2a03:4000:52:ada:4:1::";
8 passwordFile = config.sops.secrets."paperless-rootpw".path; 8 passwordFile = config.sops.secrets."paperless-rootpw".path;
9 settings = { 9 settings = {
10 PAPERLESS_OCR_LANGUAGE = "deu+eng"; 10 PAPERLESS_OCR_LANGUAGE = "deu+eng";
diff --git a/hosts/vidhar/prometheus/default.nix b/hosts/vidhar/prometheus/default.nix
index d368ad52..094f9f7a 100644
--- a/hosts/vidhar/prometheus/default.nix
+++ b/hosts/vidhar/prometheus/default.nix
@@ -26,7 +26,8 @@ in {
26 enable = true; 26 enable = true;
27 27
28 extraFlags = [ 28 extraFlags = [
29 "--enable-feature=remote-write-receiver" 29 "--web.enable-remote-write-receiver"
30 "--storage.tsdb.retention.size=35GB"
30 ]; 31 ];
31 32
32 exporters = { 33 exporters = {
diff --git a/installer-profiles/cd-dvd.nix b/installer-profiles/cd-dvd.nix
index 45291bad..ac12d885 100644
--- a/installer-profiles/cd-dvd.nix
+++ b/installer-profiles/cd-dvd.nix
@@ -1,7 +1,13 @@
1{ flakeInputs, ... }: 1{ flakeInputs, lib, ... }:
2 2
3{ 3{
4 imports = [ 4 imports = [
5 "${flakeInputs.nixpkgs.outPath}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix" 5 "${flakeInputs.nixpkgs.outPath}/nixos/modules/installer/cd-dvd/installation-cd-minimal.nix"
6 ]; 6 ];
7
8 config = {
9 isoImage.squashfsCompression = "zstd -Xcompression-level 9";
10 system.installer.channel.enable = false;
11 boot.loader.grub.memtest86.enable = lib.mkForce false;
12 };
7} 13}
diff --git a/installer-profiles/netboot.nix b/installer-profiles/netboot.nix
index 28e8084d..6e39ebfb 100644
--- a/installer-profiles/netboot.nix
+++ b/installer-profiles/netboot.nix
@@ -4,4 +4,9 @@
4 imports = [ 4 imports = [
5 "${flakeInputs.nixpkgs.outPath}/nixos/modules/installer/netboot/netboot-minimal.nix" 5 "${flakeInputs.nixpkgs.outPath}/nixos/modules/installer/netboot/netboot-minimal.nix"
6 ]; 6 ];
7
8 config = {
9 netboot.squashfsCompression = "zstd -Xcompression-level 9";
10 system.installer.channel.enable = false;
11 };
7} 12}
diff --git a/installer-profiles/nfsroot.nix b/installer-profiles/nfsroot.nix
index 6bd875b4..a8f6def6 100644
--- a/installer-profiles/nfsroot.nix
+++ b/installer-profiles/nfsroot.nix
@@ -8,4 +8,6 @@
8 "${flakeInputs.nixpkgs.outPath}/nixos/modules/profiles/base.nix" 8 "${flakeInputs.nixpkgs.outPath}/nixos/modules/profiles/base.nix"
9 "${flakeInputs.nixpkgs.outPath}/nixos/modules/profiles/installation-device.nix" 9 "${flakeInputs.nixpkgs.outPath}/nixos/modules/profiles/installation-device.nix"
10 ]; 10 ];
11
12 config.system.installer.channel.enable = false;
11} 13}
diff --git a/installer/default.nix b/installer/default.nix
index ec47832a..26f38572 100644
--- a/installer/default.nix
+++ b/installer/default.nix
@@ -8,7 +8,7 @@ with lib;
8 ]; 8 ];
9 9
10 config = { 10 config = {
11 boot.initrd.availableKernelModules = [ "e1000e" ]; 11 boot.initrd.kernelModules = [ "e1000e" "virtio_net" ];
12 12
13 hardware.cpu.amd.updateMicrocode = config.hardware.enableRedistributableFirmware; 13 hardware.cpu.amd.updateMicrocode = config.hardware.enableRedistributableFirmware;
14 14
@@ -47,7 +47,7 @@ with lib;
47 services.xserver.videoDrivers = [ "nvidia" ]; 47 services.xserver.videoDrivers = [ "nvidia" ];
48 systemd.services.nvidia-control-devices = { 48 systemd.services.nvidia-control-devices = {
49 wantedBy = [ "multi-user.target" ]; 49 wantedBy = [ "multi-user.target" ];
50 serviceConfig.ExecStart = "${pkgs.linuxPackages.nvidia_x11.bin}/bin/nvidia-smi"; 50 serviceConfig.ExecStart = lib.getExe' pkgs.linuxPackages.nvidia_x11.bin "nvidia-smi";
51 }; 51 };
52 nixpkgs.externalConfig.allowUnfree = true; 52 nixpkgs.externalConfig.allowUnfree = true;
53 53
diff --git a/lib/pythonSet.nix b/lib/pythonSet.nix
new file mode 100644
index 00000000..9dfb25ff
--- /dev/null
+++ b/lib/pythonSet.nix
@@ -0,0 +1,28 @@
1{ uv2nix, pyproject-nix, pyproject-build-systems, ... }:
2{ pkgs, python, overlay, lib ? pkgs.lib }:
3(pkgs.callPackage pyproject-nix.build.packages {
4 inherit python;
5}).overrideScope
6 (
7 lib.composeManyExtensions [
8 pyproject-build-systems.overlays.default
9 overlay
10 (final: prev: {
11 sdnotify = (prev.sdnotify.override {
12 sourcePreference = "sdist";
13 }).overrideAttrs (oldAttrs: {
14 nativeBuildInputs = (oldAttrs.nativeBuildInputs or []) ++ [
15 (final.resolveBuildSystem { setuptools = []; })
16 ];
17 });
18 systemd-python = (prev.systemd-python.override {
19 sourcePreference = "sdist";
20 }).overrideAttrs (oldAttrs: {
21 nativeBuildInputs = (oldAttrs.nativeBuildInputs or []) ++ [
22 pkgs.pkg-config pkgs.systemd.dev
23 (final.resolveBuildSystem { setuptools = []; })
24 ];
25 });
26 })
27 ]
28 )
diff --git a/modules/abs-podcast-autoplaylist.nix b/modules/abs-podcast-autoplaylist.nix
new file mode 100644
index 00000000..f526a434
--- /dev/null
+++ b/modules/abs-podcast-autoplaylist.nix
@@ -0,0 +1,55 @@
1{ config, pkgs, lib, utils, ... }:
2
3let
4 cfg = config.services.abs-podcast-autoplaylist;
5
6 enabledAttrs = lib.filterAttrs (_name: { enable, ... }: enable) cfg;
7in {
8 options = {
9 services.abs-podcast-autoplaylist = lib.mkOption {
10 default = {};
11 type = lib.types.attrsOf (lib.types.submodule ({ name, ... }: {
12 options = {
13 enable = lib.mkEnableOption "this instance of abs-podcast-autoplaylist" // {
14 default = true;
15 };
16 cron = lib.mkOption {
17 type = lib.types.str;
18 default = "*-*-* *:00/30:00";
19 };
20 configSecret = lib.mkOption {
21 type = lib.types.str;
22 default = "abs-podcast-autoplaylist-${name}.toml";
23 };
24 };
25 }));
26 };
27 };
28
29 config = lib.mkIf (enabledAttrs != {}) {
30 systemd.services = {
31 "abs-podcast-autoplaylist@" = {
32 serviceConfig = {
33 WorkingDirectory = "%d";
34 DynamicUser = true;
35 ProtectHome = true;
36 PrivateTmp = true;
37 PrivateDevices = true;
38 Type = "oneshot";
39 ExecStart = "${lib.getExe pkgs.abs-podcast-autoplaylist} %I.toml";
40 TimeoutSec = "5min";
41 };
42 };
43 } // lib.mapAttrs' (name: { configSecret, ... }: lib.nameValuePair "abs-podcast-autoplaylist@${utils.escapeSystemdPath name}" {
44 overrideStrategy = "asDropin";
45 serviceConfig = {
46 LoadCredential = "${name}.toml:${config.sops.secrets.${configSecret}.path}";
47 };
48 }) enabledAttrs;
49
50 systemd.timers = lib.mapAttrs' (name: { cron, ... }: lib.nameValuePair "abs-podcast-autoplaylist@${utils.escapeSystemdPath name}" {
51 wantedBy = [ "timers.target" ];
52 timerConfig.OnCalendar = cron;
53 }) enabledAttrs;
54 };
55}
diff --git a/modules/borgcopy/default.nix b/modules/borgcopy/default.nix
index 475edbd9..8e1afc27 100644
--- a/modules/borgcopy/default.nix
+++ b/modules/borgcopy/default.nix
@@ -22,6 +22,7 @@ let
22 }; 22 };
23 23
24 copyService = name: opts: nameValuePair "copy-borg@${utils.escapeSystemdPath name}" { 24 copyService = name: opts: nameValuePair "copy-borg@${utils.escapeSystemdPath name}" {
25 restartIfChanged = false;
25 serviceConfig = { 26 serviceConfig = {
26 Type = "oneshot"; 27 Type = "oneshot";
27 ExecStart = "${copyBorg}/bin/copy_borg --verbosity ${toString opts.verbosity} ${utils.escapeSystemdExecArgs [opts.from opts.to]}"; 28 ExecStart = "${copyBorg}/bin/copy_borg --verbosity ${toString opts.verbosity} ${utils.escapeSystemdExecArgs [opts.from opts.to]}";
diff --git a/modules/i18n.nix b/modules/i18n.nix
new file mode 100644
index 00000000..f84e8b64
--- /dev/null
+++ b/modules/i18n.nix
@@ -0,0 +1,156 @@
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 aggregatedLocales =
9 (builtins.map
10 (l: (lib.replaceStrings [ "utf8" "utf-8" "UTF8" ] [ "UTF-8" "UTF-8" "UTF-8" ] l) + "/UTF-8")
11 (
12 [ config.i18n.defaultLocale ]
13 ++ (lib.optionals (builtins.isList config.i18n.extraLocales) config.i18n.extraLocales)
14 ++ (lib.attrValues (lib.filterAttrs (n: _v: lib.hasPrefix "LC_" n) config.i18n.extraLocaleSettings))
15 )
16 )
17 ++ (lib.optional (builtins.isString config.i18n.extraLocales) config.i18n.extraLocales);
18in
19{
20 disabledModules = [ "config/i18n.nix" ];
21
22 ###### interface
23
24 options = {
25
26 i18n = {
27 glibcLocales = lib.mkOption {
28 type = lib.types.path;
29 default = pkgs.glibcLocales.override {
30 allLocales = lib.any (x: x == "all") config.i18n.supportedLocales;
31 locales = config.i18n.supportedLocales;
32 };
33 defaultText = lib.literalExpression ''
34 pkgs.glibcLocales.override {
35 allLocales = lib.any (x: x == "all") config.i18n.supportedLocales;
36 locales = config.i18n.supportedLocales;
37 }
38 '';
39 example = lib.literalExpression "pkgs.glibcLocales";
40 description = ''
41 Customized pkg.glibcLocales package.
42
43 Changing this option can disable handling of i18n.defaultLocale
44 and supportedLocale.
45 '';
46 };
47
48 defaultLocale = lib.mkOption {
49 type = lib.types.str;
50 default = "en_US.UTF-8";
51 example = "nl_NL.UTF-8";
52 description = ''
53 The default locale. It determines the language for program
54 messages, the format for dates and times, sort order, and so on.
55 It also determines the character set, such as UTF-8.
56 '';
57 };
58
59 extraLocales = lib.mkOption {
60 type = lib.types.either (lib.types.listOf lib.types.str) (lib.types.enum [ "all" ]);
61 default = [ ];
62 example = [ "nl_NL.UTF-8" ];
63 description = ''
64 Additional locales that the system should support, besides the ones
65 configured with {option}`i18n.defaultLocale` and
66 {option}`i18n.extraLocaleSettings`.
67 Set this to `"all"` to install all available locales.
68 '';
69 };
70
71 extraLocaleSettings = lib.mkOption {
72 type = lib.types.attrsOf lib.types.str;
73 default = { };
74 example = {
75 LC_MESSAGES = "en_US.UTF-8";
76 LC_TIME = "de_DE.UTF-8";
77 };
78 description = ''
79 A set of additional system-wide locale settings other than
80 `LANG` which can be configured with
81 {option}`i18n.defaultLocale`.
82 '';
83 };
84
85 supportedLocales = lib.mkOption {
86 type = lib.types.listOf lib.types.str;
87 visible = false;
88 default = lib.unique (
89 [
90 "C.UTF-8/UTF-8"
91 "en_US.UTF-8/UTF-8"
92 ]
93 ++ aggregatedLocales
94 );
95 example = [
96 "en_US.UTF-8/UTF-8"
97 "nl_NL.UTF-8/UTF-8"
98 "nl_NL/ISO-8859-1"
99 ];
100 description = ''
101 List of locales that the system should support. The value
102 `"all"` means that all locales supported by
103 Glibc will be installed. A full list of supported locales
104 can be found at <https://sourceware.org/git/?p=glibc.git;a=blob;f=localedata/SUPPORTED>.
105 '';
106 };
107
108 };
109
110 };
111
112 ###### implementation
113
114 config = {
115 warnings =
116 lib.optional
117 (
118 !(
119 (lib.subtractLists config.i18n.supportedLocales aggregatedLocales) == [ ]
120 || lib.any (x: x == "all") config.i18n.supportedLocales
121 )
122 )
123 ''
124 `i18n.supportedLocales` is deprecated in favor of `i18n.extraLocales`,
125 and it seems you are using `i18n.supportedLocales` and forgot to
126 include some locales specified in `i18n.defaultLocale`,
127 `i18n.extraLocales` or `i18n.extraLocaleSettings`.
128
129 If you're trying to install additional locales not specified in
130 `i18n.defaultLocale` or `i18n.extraLocaleSettings`, consider adding
131 only those locales to `i18n.extraLocales`.
132 '';
133
134 environment.systemPackages =
135 # We increase the priority a little, so that plain glibc in systemPackages can't win.
136 lib.optional (config.i18n.supportedLocales != [ ]) (lib.setPrio (-1) config.i18n.glibcLocales);
137
138 environment.sessionVariables = {
139 LANG = config.i18n.defaultLocale;
140 LOCALE_ARCHIVE = "/run/current-system/sw/lib/locale/locale-archive";
141 } // config.i18n.extraLocaleSettings;
142
143 systemd.globalEnvironment = lib.mkIf (config.i18n.supportedLocales != [ ]) {
144 LOCALE_ARCHIVE = "${config.i18n.glibcLocales}/lib/locale/locale-archive";
145 };
146
147 # ‘/etc/locale.conf’ is used by systemd.
148 environment.etc."locale.conf".source = pkgs.writeText "locale.conf" ''
149 LANG=${config.i18n.defaultLocale}
150 ${lib.concatStringsSep "\n" (
151 lib.mapAttrsToList (n: v: "${n}=${v}") config.i18n.extraLocaleSettings
152 )}
153 '';
154
155 };
156}
diff --git a/modules/installer.nix b/modules/installer.nix
new file mode 100644
index 00000000..3e5c6d5b
--- /dev/null
+++ b/modules/installer.nix
@@ -0,0 +1,56 @@
1{ flake, config, lib, pkgs, ... }:
2
3let
4 cfg = config.installer.links;
5
6 installerOutPath = {
7 "cd-dvd" = _: installerBuild: "${installerBuild.config.system.build.isoImage}/iso";
8 "netboot" = {system, variant}: installerBuild: pkgs.runCommandLocal "${system}-${variant}" {} ''
9 mkdir $out
10 install -m 0444 -t $out \
11 ${installerBuild.config.system.build.netbootRamdisk}/initrd \
12 ${installerBuild.config.system.build.kernel}/${config.system.boot.loader.kernelFile} \
13 ${installerBuild.config.system.build.netbootIpxeScript}/netboot.ipxe \
14 ${pkgs.ipxe.override {
15 additionalTargets = {
16 "bin-i386-efi/ipxe.efi" = "i386-ipxe.efi";
17 };
18 additionalOptions = [
19 "NSLOOKUP_CMD"
20 "PING_CMD"
21 "CONSOLE_CMD"
22 ];
23 embedScript = pkgs.writeText "netboot.ipxe" ''
24 #!ipxe
25
26 chain netboot.ipxe
27 '';
28 }}/{ipxe.efi,i386-ipxe.efi,ipxe.lkrn}
29 '';
30 };
31in {
32 options = {
33 installer.links = lib.mkOption {
34 type = lib.types.listOf (lib.types.submodule {
35 options = {
36 system = lib.mkOption {
37 type = lib.types.str;
38 };
39 variant = lib.mkOption {
40 type = lib.types.str;
41 };
42 };
43 });
44 default = [];
45 };
46 };
47
48 config = lib.mkIf (cfg != []) {
49 systemd.tmpfiles.rules = map (installer'@{system, variant}:
50 let
51 installer = "${system}-${variant}";
52 installerBuild = builtins.addErrorContext "while evaluating installer-${installer}" flake.nixosConfigurations.${"installer-${installer}"};
53 in "L+ /run/installer-${installer} - - - - ${installerOutPath.${variant} installer' installerBuild}"
54 ) cfg;
55 };
56}
diff --git a/modules/pgbackrest.nix b/modules/pgbackrest.nix
index 81c74a8e..550e970b 100644
--- a/modules/pgbackrest.nix
+++ b/modules/pgbackrest.nix
@@ -43,6 +43,8 @@ let
43 loglevelType = types.enum ["off" "error" "warn" "info" "detail" "debug" "trace"]; 43 loglevelType = types.enum ["off" "error" "warn" "info" "detail" "debug" "trace"];
44 inherit (utils.systemdUtils.unitOptions) unitOption; 44 inherit (utils.systemdUtils.unitOptions) unitOption;
45in { 45in {
46 disabledModules = ["services/backup/pgbackrest.nix"];
47
46 options = { 48 options = {
47 services.pgbackrest = { 49 services.pgbackrest = {
48 enable = mkEnableOption "pgBackRest"; 50 enable = mkEnableOption "pgBackRest";
diff --git a/modules/postsrsd.nix b/modules/postsrsd.nix
new file mode 100644
index 00000000..205e669d
--- /dev/null
+++ b/modules/postsrsd.nix
@@ -0,0 +1,157 @@
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 cfg = config.services.postsrsd;
10 runtimeDirectoryName = "postsrsd";
11 runtimeDirectory = "/run/${runtimeDirectoryName}";
12 # TODO: follow RFC 42, but we need a libconfuse format first:
13 # https://github.com/NixOS/nixpkgs/issues/401565
14 # Arrays in `libconfuse` look like this: {"Life", "Universe", "Everything"}
15 # See https://www.nongnu.org/confuse/tutorial-html/ar01s03.html.
16 #
17 # Note: We're using `builtins.toJSON` to escape strings, but JSON strings
18 # don't have exactly the same semantics as libconfuse strings. For example,
19 # "${F}" gets treated as an env var reference, see above issue for details.
20 libconfuseDomains = "{ " + lib.concatMapStringsSep ", " builtins.toJSON cfg.domains + " }";
21 configFile = pkgs.writeText "postsrsd.conf" ''
22 secrets-file = "''${CREDENTIALS_DIRECTORY}/secrets-file"
23 domains = ${libconfuseDomains}
24 separator = "${cfg.separator}"
25
26 # Disable postsrsd's jailing in favor of confinement with systemd.
27 unprivileged-user = ""
28 chroot-dir = ""
29
30 ${cfg.extraConfig}
31 '';
32
33in
34{
35 imports =
36 map
37 (
38 name:
39 lib.mkRemovedOptionModule [ "services" "postsrsd" name ] ''
40 `postsrsd` was upgraded to `>= 2.0.0`, with some different behaviors and configuration settings:
41 - NixOS Release Notes: https://nixos.org/manual/nixos/unstable/release-notes#sec-nixpkgs-release-25.05-incompatibilities
42 - NixOS Options Reference: https://nixos.org/manual/nixos/unstable/options#opt-services.postsrsd.enable
43 - Migration instructions: https://github.com/roehling/postsrsd/blob/2.0.10/README.rst#migrating-from-version-1x
44 - Postfix Setup: https://github.com/roehling/postsrsd/blob/2.0.10/README.rst#postfix-setup
45 ''
46 )
47 [
48 "domain"
49 "forwardPort"
50 "reversePort"
51 "timeout"
52 "excludeDomains"
53 ];
54
55 disabledModules = [ "services/mail/postsrsd.nix" ];
56
57 options = {
58 services.postsrsd = {
59 enable = lib.mkOption {
60 type = lib.types.bool;
61 default = false;
62 description = "Whether to enable the postsrsd SRS server for Postfix.";
63 };
64
65 secretsFile = lib.mkOption {
66 type = lib.types.path;
67 default = "/var/lib/postsrsd/postsrsd.secret";
68 description = "Secret keys used for signing and verification";
69 };
70
71 domains = lib.mkOption {
72 type = lib.types.listOf lib.types.str;
73 description = "Domain names for rewrite";
74 default = [ config.networking.hostName ];
75 defaultText = lib.literalExpression "[ config.networking.hostName ]";
76 };
77
78 separator = lib.mkOption {
79 type = lib.types.enum [
80 "-"
81 "="
82 "+"
83 ];
84 default = "=";
85 description = "First separator character in generated addresses";
86 };
87
88 user = lib.mkOption {
89 type = lib.types.str;
90 default = "postsrsd";
91 description = "User for the daemon";
92 };
93
94 group = lib.mkOption {
95 type = lib.types.str;
96 default = "postsrsd";
97 description = "Group for the daemon";
98 };
99
100 extraConfig = lib.mkOption {
101 type = lib.types.lines;
102 default = "";
103 };
104 };
105 };
106
107 config = lib.mkIf cfg.enable {
108 users.users = lib.optionalAttrs (cfg.user == "postsrsd") {
109 postsrsd = {
110 group = cfg.group;
111 uid = config.ids.uids.postsrsd;
112 };
113 };
114
115 users.groups = lib.optionalAttrs (cfg.group == "postsrsd") {
116 postsrsd.gid = config.ids.gids.postsrsd;
117 };
118
119 systemd.services.postsrsd-generate-secrets = {
120 path = [ pkgs.coreutils ];
121 script = ''
122 if [ -e "${cfg.secretsFile}" ]; then
123 echo "Secrets file exists. Nothing to do!"
124 else
125 echo "WARNING: secrets file not found, autogenerating!"
126 DIR="$(dirname "${cfg.secretsFile}")"
127 install -m 750 -o ${cfg.user} -g ${cfg.group} -d "$DIR"
128 install -m 600 -o ${cfg.user} -g ${cfg.group} <(dd if=/dev/random bs=18 count=1 | base64) "${cfg.secretsFile}"
129 fi
130 '';
131 serviceConfig = {
132 Type = "oneshot";
133 };
134 };
135
136 systemd.services.postsrsd = {
137 description = "PostSRSd SRS rewriting server";
138 after = [
139 "network.target"
140 "postsrsd-generate-secrets.service"
141 ];
142 before = [ "postfix.service" ];
143 wantedBy = [ "multi-user.target" ];
144 requires = [ "postsrsd-generate-secrets.service" ];
145 confinement.enable = true;
146
147 serviceConfig = {
148 ExecStart = "${lib.getExe pkgs.postsrsd} -C ${configFile}";
149 User = cfg.user;
150 Group = cfg.group;
151 PermissionsStartOnly = true;
152 RuntimeDirectory = runtimeDirectoryName;
153 LoadCredential = "secrets-file:${cfg.secretsFile}";
154 };
155 };
156 };
157}
diff --git a/modules/systemd-run0.nix b/modules/systemd-run0.nix
new file mode 100644
index 00000000..8575ec7c
--- /dev/null
+++ b/modules/systemd-run0.nix
@@ -0,0 +1,4 @@
1{ config, lib, ... }:
2{
3 config.security.pam.services.systemd-run0 = lib.mkIf (lib.versionAtLeast config.systemd.package.version "256") {};
4}
diff --git a/modules/tzupdate.nix b/modules/tzupdate.nix
new file mode 100644
index 00000000..6465d33f
--- /dev/null
+++ b/modules/tzupdate.nix
@@ -0,0 +1,81 @@
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.tzupdate;
9in
10{
11 disabledModules = [ "services/misc/tzupdate.nix" ];
12
13 options.services.tzupdate = {
14 enable = lib.mkOption {
15 type = lib.types.bool;
16 default = false;
17 description = ''
18 Enable the tzupdate timezone updating service. This provides
19 a one-shot service which can be activated with systemctl to
20 update the timezone.
21 '';
22 };
23
24 package = lib.mkPackageOption pkgs "tzupdate" { };
25
26 timer.enable = lib.mkOption {
27 type = lib.types.bool;
28 default = true;
29 description = ''
30 Enable the tzupdate timer to update the timezone automatically.
31 '';
32 };
33
34 timer.interval = lib.mkOption {
35 type = lib.types.str;
36 default = "hourly";
37 description = ''
38 The interval at which the tzupdate timer should run. See
39 {manpage}`systemd.time(7)` to understand the format.
40 '';
41 };
42 };
43
44 config = lib.mkIf cfg.enable {
45 # We need to have imperative time zone management for this to work.
46 # This will give users an error if they have set an explicit time
47 # zone, which is better than silently overriding it.
48 time.timeZone = null;
49
50 # We provide a one-shot service that runs at startup once network
51 # interfaces are up, but we can’t ensure we actually have Internet access
52 # at that point. It can also be run manually with `systemctl start tzupdate`.
53 systemd.services.tzupdate = {
54 description = "tzupdate timezone update service";
55 wantedBy = [ "multi-user.target" ];
56 wants = [ "network-online.target" ];
57 after = [ "network-online.target" ];
58 script = ''
59 timezone="$(${lib.getExe cfg.package} --print-only)"
60 if [[ -n "$timezone" ]]; then
61 echo "Setting timezone to '$timezone'"
62 ${lib.getExe' config.systemd.package "timedatectl"} set-timezone "$timezone"
63 fi
64 '';
65
66 serviceConfig = {
67 Type = "oneshot";
68 };
69 };
70
71 systemd.timers.tzupdate = {
72 enable = cfg.timer.enable;
73 timerConfig = {
74 OnStartupSec = "30s";
75 OnCalendar = cfg.timer.interval;
76 Persistent = true;
77 };
78 wantedBy = [ "timers.target" ];
79 };
80 };
81}
diff --git a/modules/uucp.nix b/modules/uucp.nix
deleted file mode 100644
index 10f7297b..00000000
--- a/modules/uucp.nix
+++ /dev/null
@@ -1,373 +0,0 @@
1{ flake, config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 portSpec = name: node: concatStringsSep "\n" (map (port: ''
7 port ${name}.${port}
8 type pipe
9 protocol ${node.protocols}
10 reliable true
11 command ${pkgs.openssh}/bin/ssh -x -o batchmode=yes ${name}.${port}
12 '') node.hostnames);
13 sysSpec = name: node: ''
14 system ${name}
15 time any
16 chat-seven-bit false
17 chat . ""
18 protocol ${node.protocols}
19 command-path ${concatStringsSep " " cfg.commandPath}
20 commands ${concatStringsSep " " node.commands}
21 ${concatStringsSep "\nalternate\n" (map (port: ''
22 port ${name}.${port}
23 '') node.hostnames)}
24 '';
25 sshConfig = name: node: concatStringsSep "\n" (map (port: ''
26 Host ${name}.${port}
27 Hostname ${port}
28 IdentitiesOnly Yes
29 IdentityFile ${cfg.sshKeyDir}/${name}
30 '') node.hostnames);
31 sshKeyGen = name: node: ''
32 if [[ ! -e ${cfg.sshKeyDir}/${name} ]]; then
33 ${pkgs.openssh}/bin/ssh-keygen ${escapeShellArgs node.generateKey} -f ${cfg.sshKeyDir}/${name}
34 fi
35 '';
36 restrictKey = key: ''
37 restrict,command="${chat}" ${key}
38 '';
39 chat = pkgs.writeScript "chat" ''
40 #!${pkgs.stdenv.shell}
41
42 echo .
43 exec ${config.security.wrapperDir}/uucico
44 '';
45
46 nodeCfg = {
47 options = {
48 commands = mkOption {
49 type = types.listOf types.str;
50 default = cfg.defaultCommands;
51 defaultText = literalExpression "config.services.uucp.defaultCommands";
52 description = "Commands to allow for this remote";
53 };
54
55 protocols = mkOption {
56 type = types.separatedString "";
57 default = cfg.defaultProtocols;
58 defaultText = literalExpression "config.services.uucp.defaultProtocols";
59 description = "UUCP protocols to use for this remote";
60 };
61
62 publicKeys = mkOption {
63 type = types.listOf types.str;
64 default = [];
65 description = "SSH client public keys for this node";
66 };
67
68 generateKey = mkOption {
69 type = types.listOf types.str;
70 default = [ "-t" "ed25519" "-N" "" ];
71 description = "Arguments to pass to `ssh-keygen` to generate a keypair for communication with this host";
72 };
73
74 hostnames = mkOption {
75 type = types.listOf types.str;
76 default = [];
77 description = "Hostnames to try in order when connecting";
78 };
79 };
80 };
81
82 cfg = config.services.uucp;
83in {
84 options = {
85 services.uucp = {
86 enable = mkOption {
87 type = types.bool;
88 default = false;
89 description = ''
90 If enabled we set up an account accesible via uucp over ssh
91 '';
92 };
93
94 nodeName = mkOption {
95 type = types.str;
96 default = "nixos";
97 description = "uucp node name";
98 };
99
100 sshUser = mkOption {
101 type = types.attrs;
102 default = {};
103 description = "Overrides for the local uucp linux-user";
104 };
105
106 extraSSHConfig = mkOption {
107 type = types.str;
108 default = "";
109 description = "Extra SSH config";
110 };
111
112 remoteNodes = mkOption {
113 type = types.attrsOf (types.submodule nodeCfg);
114 default = {};
115 description = ''
116 Ports to set up
117 Names will probably need to be configured in sshConfig
118 '';
119 };
120
121 commandPath = mkOption {
122 type = types.listOf types.path;
123 default = [ "${pkgs.rmail}/bin" ];
124 defaultText = literalExpression ''[ "''${pkgs.rmail}/bin" ]'';
125 description = ''
126 Command search path for all systems
127 '';
128 };
129
130 defaultCommands = mkOption {
131 type = types.listOf types.str;
132 default = ["rmail"];
133 description = "Commands allowed for remotes without explicit override";
134 };
135
136 defaultProtocols = mkOption {
137 type = types.separatedString "";
138 default = "te";
139 description = "UUCP protocol to use within ssh unless overriden";
140 };
141
142 incomingProtocols = mkOption {
143 type = types.separatedString "";
144 default = "te";
145 description = "UUCP protocols to use when called";
146 };
147
148 homeDir = mkOption {
149 type = types.path;
150 default = "/var/uucp";
151 description = "Home of the uucp user";
152 };
153
154 sshKeyDir = mkOption {
155 type = types.path;
156 default = "${cfg.homeDir}/.ssh/";
157 defaultText = literalExpression ''''${config.services.uucp.homeDir}/.ssh/'';
158 description = "Directory to store ssh keypairs";
159 };
160
161 spoolDir = mkOption {
162 type = types.path;
163 default = "/var/spool/uucp";
164 description = "Spool directory";
165 };
166
167 lockDir = mkOption {
168 type = types.path;
169 default = "/var/spool/uucp";
170 description = "Lock directory";
171 };
172
173 pubDir = mkOption {
174 type = types.path;
175 default = "/var/spool/uucppublic";
176 description = "Public directory";
177 };
178
179 logFile = mkOption {
180 type = types.path;
181 default = "/var/log/uucp";
182 description = "Log file";
183 };
184
185 statFile = mkOption {
186 type = types.path;
187 default = "/var/log/uucp.stat";
188 description = "Statistics file";
189 };
190
191 debugFile = mkOption {
192 type = types.path;
193 default = "/var/log/uucp.debug";
194 description = "Debug file";
195 };
196
197 interval = mkOption {
198 type = types.nullOr types.str;
199 default = "1h";
200 description = ''
201 Specification of when to run `uucico' in format used by systemd timers
202 The default is to do so every hour
203 '';
204 };
205
206 nmDispatch = mkOption {
207 type = types.bool;
208 default = config.networking.networkmanager.enable;
209 defaultText = literalExpression "config.networking.networkmanager.enable";
210 description = ''
211 Install a network-manager dispatcher script to automatically
212 call all remotes when networking is available
213 '';
214 };
215
216 extraConfig = mkOption {
217 type = types.lines;
218 default = ''
219 run-uuxqt 1
220 '';
221 description = "Extra configuration to append verbatim to `/etc/uucp/config'";
222 };
223
224 extraSys = mkOption {
225 type = types.lines;
226 default = ''
227 protocol-parameter g packet-size 4096
228 '';
229 description = "Extra configuration to prepend verbatim to `/etc/uucp/sys`";
230 };
231 };
232 };
233
234 config = mkIf cfg.enable {
235 environment.etc."uucp/config" = {
236 text = ''
237 hostname ${cfg.nodeName}
238
239 spool ${cfg.spoolDir}
240 lockdir ${cfg.lockDir}
241 pubdir ${cfg.pubDir}
242 logfile ${cfg.logFile}
243 statfile ${cfg.statFile}
244 debugfile ${cfg.debugFile}
245
246 ${cfg.extraConfig}
247 '';
248 };
249
250 users.groups."uucp" = {};
251 users.users."uucp" = {
252 name = "uucp";
253 group = "uucp";
254 isSystemUser = true;
255 isNormalUser = false;
256 createHome = true;
257 home = cfg.homeDir;
258 description = "User for uucp over ssh";
259 useDefaultShell = true;
260 openssh.authorizedKeys.keys = map restrictKey (concatLists (mapAttrsToList (name: node: node.publicKeys) cfg.remoteNodes));
261 } // cfg.sshUser;
262
263 system.activationScripts."uucp-sshconfig" = ''
264 mkdir -p ${config.users.users."uucp".home}/.ssh
265 chown ${config.users.users."uucp".name}:${config.users.users."uucp".group} ${config.users.users."uucp".home}/.ssh
266 chmod 700 ${config.users.users."uucp".home}/.ssh
267 ln -fs ${builtins.toFile "ssh-config" ''
268 ${concatStringsSep "\n" (mapAttrsToList sshConfig cfg.remoteNodes)}
269
270 ${cfg.extraSSHConfig}
271 ''} ${config.users.users."uucp".home}/.ssh/config
272
273 mkdir -p ${cfg.sshKeyDir}
274 chown ${config.users.users."uucp".name}:${config.users.users."uucp".group} ${cfg.sshKeyDir}
275 chmod 700 ${cfg.sshKeyDir}
276
277 ${concatStringsSep "\n" (mapAttrsToList sshKeyGen cfg.remoteNodes)}
278 '';
279
280 system.activationScripts."uucp-logs" = ''
281 touch ${cfg.logFile}
282 chown ${config.users.users."uucp".name}:${config.users.users."uucp".group} ${cfg.logFile}
283 chmod 644 ${cfg.logFile}
284 touch ${cfg.statFile}
285 chown ${config.users.users."uucp".name}:${config.users.users."uucp".group} ${cfg.statFile}
286 chmod 644 ${cfg.statFile}
287 touch ${cfg.debugFile}
288 chown ${config.users.users."uucp".name}:${config.users.users."uucp".group} ${cfg.debugFile}
289 chmod 644 ${cfg.debugFile}
290 '';
291
292 environment.etc."uucp/port" = {
293 text = ''
294 port ssh
295 type stdin
296 protocol ${cfg.incomingProtocols}
297 '' + concatStringsSep "\n" (mapAttrsToList portSpec cfg.remoteNodes);
298 };
299 environment.etc."uucp/sys" = {
300 text = cfg.extraSys + "\n" + concatStringsSep "\n" (mapAttrsToList sysSpec cfg.remoteNodes);
301 };
302
303 security.wrappers = let
304 wrapper = p: {
305 name = p;
306 value = {
307 source = "${pkgs.uucp}/bin/${p}";
308 owner = "root";
309 group = "root";
310 setuid = true;
311 setgid = false;
312 };
313 };
314 in listToAttrs (map wrapper ["uucico" "cu" "uucp" "uuname" "uustat" "uux" "uuxqt"]);
315
316 nixpkgs.overlays = [(self: super: {
317 rmail = super.writeShellScriptBin "rmail" ''
318 # Dummy UUCP rmail command for postfix/qmail systems
319
320 IFS=" " read junk from junk junk junk junk junk junk junk relay
321
322 case "$from" in
323 *[@!]*) ;;
324 *) from="$from@$relay";;
325 esac
326
327 exec ${config.security.wrapperDir}/sendmail -G -i -f "$from" -- "$@"
328 '';
329 })];
330
331 environment.systemPackages = with pkgs; [
332 uucp
333 ];
334
335 systemd.services."uucico@" = {
336 serviceConfig = {
337 User = "uucp";
338 Type = "oneshot";
339 ExecStart = "${config.security.wrapperDir}/uucico -D -S %i";
340 };
341 };
342
343 systemd.timers."uucico@" = {
344 timerConfig.OnActiveSec = cfg.interval;
345 timerConfig.OnUnitActiveSec = cfg.interval;
346 };
347
348 systemd.targets."multi-user" = {
349 wants = mapAttrsToList (name: node: "uucico@${name}.timer") cfg.remoteNodes;
350 };
351
352 systemd.kill-user.enable = true;
353 systemd.targets."sleep" = {
354 after = [ "kill-user@uucp.service" ];
355 wants = [ "kill-user@uucp.service" ];
356 };
357
358 networking.networkmanager.dispatcherScripts = optional cfg.nmDispatch {
359 type = "basic";
360 source = pkgs.writeScript "callRemotes.sh" ''
361 #!${pkgs.stdenv.shell}
362
363 shopt -s extglob
364
365 case "''${2}" in
366 (?(vpn-)up)
367 ${concatStringsSep "\n " (mapAttrsToList (name: node: "${pkgs.systemd}/bin/systemctl start uucico@${name}.service") cfg.remoteNodes)}
368 ;;
369 esac
370 '';
371 };
372 };
373}
diff --git a/nvfetcher.toml b/nvfetcher.toml
index ecaebba0..72c0d99d 100644
--- a/nvfetcher.toml
+++ b/nvfetcher.toml
@@ -115,3 +115,11 @@ fetch.git = "https://github.com/emersion/mako"
115[swayosd] 115[swayosd]
116src.git = "https://github.com/ErikReider/SwayOSD" 116src.git = "https://github.com/ErikReider/SwayOSD"
117fetch.git = "https://github.com/ErikReider/SwayOSD" 117fetch.git = "https://github.com/ErikReider/SwayOSD"
118
119[netbootxyz-efi]
120src.github = "netbootxyz/netboot.xyz"
121fetch.url = "https://github.com/netbootxyz/netboot.xyz/releases/download/$ver/netboot.xyz.efi"
122
123[netbootxyz-lkrn]
124src.github = "netbootxyz/netboot.xyz"
125fetch.url = "https://github.com/netbootxyz/netboot.xyz/releases/download/$ver/netboot.xyz.lkrn"
diff --git a/overlays/abs-podcast-autoplaylist/.envrc b/overlays/abs-podcast-autoplaylist/.envrc
new file mode 100644
index 00000000..2c909235
--- /dev/null
+++ b/overlays/abs-podcast-autoplaylist/.envrc
@@ -0,0 +1,4 @@
1use flake
2
3[[ -d ".venv" ]] || ( uv venv && uv sync )
4. .venv/bin/activate
diff --git a/overlays/abs-podcast-autoplaylist/.gitignore b/overlays/abs-podcast-autoplaylist/.gitignore
new file mode 100644
index 00000000..4ccfae70
--- /dev/null
+++ b/overlays/abs-podcast-autoplaylist/.gitignore
@@ -0,0 +1,2 @@
1.venv
2**/__pycache__
diff --git a/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__init__.py b/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__init__.py
diff --git a/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__main__.py b/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__main__.py
new file mode 100644
index 00000000..fd739805
--- /dev/null
+++ b/overlays/abs-podcast-autoplaylist/abs_podcast_autoplaylist/__main__.py
@@ -0,0 +1,107 @@
1import click
2from pathlib import Path
3import tomllib
4import requests
5from urllib.parse import urljoin
6from operator import itemgetter
7import re
8from frozendict import frozendict
9
10class BearerAuth(requests.auth.AuthBase):
11 def __init__(self, token):
12 self.token = token
13 def __call__(self, r):
14 r.headers["authorization"] = "Bearer " + self.token
15 return r
16
17class ABSSession(requests.Session):
18 def __init__(self, config):
19 super().__init__()
20 self.base_url = config['instance']
21 self.auth = BearerAuth(config['api_token'])
22
23 def request(self, method, url, *args, **kwargs):
24 joined_url = urljoin(self.base_url, url)
25 return super().request(method, joined_url, *args, **kwargs)
26
27@click.command()
28@click.argument('config_file', type=click.Path(dir_okay=False, path_type=Path))
29def main(config_file: Path):
30 with config_file.open('rb') as fh:
31 config = tomllib.load(fh)
32
33 with ABSSession(config) as s:
34 libraries = s.get('/api/libraries').json()['libraries']
35 playlists = s.get('/api/playlists').json()['playlists']
36
37 for library_config in config['libraries']:
38 [library] = filter(lambda l: l['name'] == library_config['name'], libraries)
39 filtered_playlists = list(filter(lambda p: p['name'] == library_config['playlist'] and p['libraryId'] == library['id'], playlists))
40 def get_playlist():
41 playlist = None
42 if filtered_playlists:
43 [playlist] = filtered_playlists
44 if not playlist:
45 playlist = s.post('/api/playlists', json={
46 'libraryId': library['id'],
47 'name': library_config['playlist'],
48 }).json()
49 return playlist
50
51 podcasts = dict()
52 items = s.get('/api/libraries/{}/items'.format(library['id'])).json()['results']
53 for item in items:
54 item = s.get('/api/items/{}'.format(item['id']), json={'expanded': True}).json()
55 episodes = list()
56 for episode in sorted(item['media']['episodes'], key = itemgetter('publishedAt')):
57 progress = s.get('/api/me/progress/{}/{}'.format(episode['libraryItemId'], episode['id']))
58 if progress.ok and progress.json()["isFinished"]:
59 continue
60 episodes.append(episode)
61 podcasts[item['media']['metadata']['title']] = list(map(lambda x: frozendict({ 'libraryItemId': x['libraryItemId'], 'episodeId': x['id']}), episodes))
62 def lookup_podcast(expr):
63 expr = re.compile(expr, flags=re.I)
64 matches = filter(lambda t: expr.search(t), podcasts.keys())
65 match list(matches):
66 case [x]:
67 return (x,)
68 case _:
69 raise RuntimeError("No unique match for ‘{}’".format(expr))
70
71 priorities = [
72 [
73 k
74 for item in (section if type(section) is list else [section])
75 for k in lookup_podcast(item)
76 ]
77 for section in library_config['priorities']
78 ]
79
80 playlist_items = list()
81 for section in priorities:
82 while any(map(lambda item: item in podcasts, section)):
83 for item in section:
84 if not item in podcasts:
85 continue
86
87 if not podcasts[item]:
88 del podcasts[item]
89 continue
90
91 playlist_items.append(podcasts[item].pop(0))
92
93 playlist = get_playlist()
94 current_playlist_items = map(lambda item: frozendict({ k: v for k, v in item.items() if k in {'libraryItemId', 'episodeId'}}), playlist['items'])
95
96 if current_playlist_items == playlist_items:
97 continue
98
99 to_remove = set(current_playlist_items) - set(playlist_items)
100 if to_remove:
101 s.post('/api/playlists/{}/batch/remove'.format(playlist['id']), json={'items': list(to_remove)}).raise_for_status()
102 playlist = get_playlist()
103 to_add = set(playlist_items) - set(current_playlist_items)
104 if to_add:
105 s.post('/api/playlists/{}/batch/add'.format(playlist['id']), json={'items': list(to_add)}).raise_for_status()
106
107 r = s.patch('/api/playlists/{}'.format(playlist['id']), json={'items': playlist_items}).raise_for_status()
diff --git a/overlays/abs-podcast-autoplaylist/default.nix b/overlays/abs-podcast-autoplaylist/default.nix
new file mode 100644
index 00000000..843f1b65
--- /dev/null
+++ b/overlays/abs-podcast-autoplaylist/default.nix
@@ -0,0 +1,19 @@
1{ prev, final, flake, flakeInputs, ... }:
2
3let
4 workspace = flakeInputs.uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
5 pythonSet = flake.lib.pythonSet {
6 pkgs = final;
7 python = final.python312;
8 overlay = workspace.mkPyprojectOverlay {
9 sourcePreference = "wheel";
10 };
11 };
12 virtualEnv = pythonSet.mkVirtualEnv "abs-podcast-autoplaylist-env" workspace.deps.default;
13in {
14 abs-podcast-autoplaylist = virtualEnv.overrideAttrs (oldAttrs: {
15 meta = (oldAttrs.meta or {}) // {
16 mainProgram = "abs-podcast-autoplaylist";
17 };
18 });
19}
diff --git a/overlays/abs-podcast-autoplaylist/pyproject.toml b/overlays/abs-podcast-autoplaylist/pyproject.toml
new file mode 100644
index 00000000..f52a84bc
--- /dev/null
+++ b/overlays/abs-podcast-autoplaylist/pyproject.toml
@@ -0,0 +1,16 @@
1[project]
2name = "abs-podcast-autoplaylist"
3version = "0.1.0"
4requires-python = ">=3.12"
5dependencies = [
6 "click>=8.1.8",
7 "frozendict>=2.4.6",
8 "requests>=2.32.3",
9]
10
11[project.scripts]
12abs-podcast-autoplaylist = "abs_podcast_autoplaylist.__main__:main"
13
14[build-system]
15requires = ["hatchling"]
16build-backend = "hatchling.build"
diff --git a/overlays/abs-podcast-autoplaylist/uv.lock b/overlays/abs-podcast-autoplaylist/uv.lock
new file mode 100644
index 00000000..17de5f0e
--- /dev/null
+++ b/overlays/abs-podcast-autoplaylist/uv.lock
@@ -0,0 +1,129 @@
1version = 1
2revision = 2
3requires-python = ">=3.12"
4
5[[package]]
6name = "abs-podcast-autoplaylist"
7version = "0.1.0"
8source = { editable = "." }
9dependencies = [
10 { name = "click" },
11 { name = "frozendict" },
12 { name = "requests" },
13]
14
15[package.metadata]
16requires-dist = [
17 { name = "click", specifier = ">=8.1.8" },
18 { name = "frozendict", specifier = ">=2.4.6" },
19 { name = "requests", specifier = ">=2.32.3" },
20]
21
22[[package]]
23name = "certifi"
24version = "2025.4.26"
25source = { registry = "https://pypi.org/simple" }
26sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload_time = "2025-04-26T02:12:29.51Z" }
27wheels = [
28 { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload_time = "2025-04-26T02:12:27.662Z" },
29]
30
31[[package]]
32name = "charset-normalizer"
33version = "3.4.2"
34source = { registry = "https://pypi.org/simple" }
35sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload_time = "2025-05-02T08:34:42.01Z" }
36wheels = [
37 { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload_time = "2025-05-02T08:32:33.712Z" },
38 { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload_time = "2025-05-02T08:32:35.768Z" },
39 { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload_time = "2025-05-02T08:32:37.284Z" },
40 { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload_time = "2025-05-02T08:32:38.803Z" },
41 { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload_time = "2025-05-02T08:32:40.251Z" },
42 { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload_time = "2025-05-02T08:32:41.705Z" },
43 { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload_time = "2025-05-02T08:32:43.709Z" },
44 { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload_time = "2025-05-02T08:32:46.197Z" },
45 { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload_time = "2025-05-02T08:32:48.105Z" },
46 { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload_time = "2025-05-02T08:32:49.719Z" },
47 { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload_time = "2025-05-02T08:32:51.404Z" },
48 { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload_time = "2025-05-02T08:32:53.079Z" },
49 { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload_time = "2025-05-02T08:32:54.573Z" },
50 { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload_time = "2025-05-02T08:32:56.363Z" },
51 { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload_time = "2025-05-02T08:32:58.551Z" },
52 { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload_time = "2025-05-02T08:33:00.342Z" },
53 { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload_time = "2025-05-02T08:33:02.081Z" },
54 { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload_time = "2025-05-02T08:33:04.063Z" },
55 { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload_time = "2025-05-02T08:33:06.418Z" },
56 { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload_time = "2025-05-02T08:33:08.183Z" },
57 { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload_time = "2025-05-02T08:33:09.986Z" },
58 { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload_time = "2025-05-02T08:33:11.814Z" },
59 { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload_time = "2025-05-02T08:33:13.707Z" },
60 { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload_time = "2025-05-02T08:33:15.458Z" },
61 { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload_time = "2025-05-02T08:33:17.06Z" },
62 { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload_time = "2025-05-02T08:33:18.753Z" },
63 { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload_time = "2025-05-02T08:34:40.053Z" },
64]
65
66[[package]]
67name = "click"
68version = "8.1.8"
69source = { registry = "https://pypi.org/simple" }
70dependencies = [
71 { name = "colorama", marker = "sys_platform == 'win32'" },
72]
73sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload_time = "2024-12-21T18:38:44.339Z" }
74wheels = [
75 { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload_time = "2024-12-21T18:38:41.666Z" },
76]
77
78[[package]]
79name = "colorama"
80version = "0.4.6"
81source = { registry = "https://pypi.org/simple" }
82sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload_time = "2022-10-25T02:36:22.414Z" }
83wheels = [
84 { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload_time = "2022-10-25T02:36:20.889Z" },
85]
86
87[[package]]
88name = "frozendict"
89version = "2.4.6"
90source = { registry = "https://pypi.org/simple" }
91sdist = { url = "https://files.pythonhosted.org/packages/bb/59/19eb300ba28e7547538bdf603f1c6c34793240a90e1a7b61b65d8517e35e/frozendict-2.4.6.tar.gz", hash = "sha256:df7cd16470fbd26fc4969a208efadc46319334eb97def1ddf48919b351192b8e", size = 316416, upload_time = "2024-10-13T12:15:32.449Z" }
92wheels = [
93 { url = "https://files.pythonhosted.org/packages/04/13/d9839089b900fa7b479cce495d62110cddc4bd5630a04d8469916c0e79c5/frozendict-2.4.6-py311-none-any.whl", hash = "sha256:d065db6a44db2e2375c23eac816f1a022feb2fa98cbb50df44a9e83700accbea", size = 16148, upload_time = "2024-10-13T12:15:26.839Z" },
94 { url = "https://files.pythonhosted.org/packages/ba/d0/d482c39cee2ab2978a892558cf130681d4574ea208e162da8958b31e9250/frozendict-2.4.6-py312-none-any.whl", hash = "sha256:49344abe90fb75f0f9fdefe6d4ef6d4894e640fadab71f11009d52ad97f370b9", size = 16146, upload_time = "2024-10-13T12:15:28.16Z" },
95 { url = "https://files.pythonhosted.org/packages/a5/8e/b6bf6a0de482d7d7d7a2aaac8fdc4a4d0bb24a809f5ddd422aa7060eb3d2/frozendict-2.4.6-py313-none-any.whl", hash = "sha256:7134a2bb95d4a16556bb5f2b9736dceb6ea848fa5b6f3f6c2d6dba93b44b4757", size = 16146, upload_time = "2024-10-13T12:15:29.495Z" },
96]
97
98[[package]]
99name = "idna"
100version = "3.10"
101source = { registry = "https://pypi.org/simple" }
102sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload_time = "2024-09-15T18:07:39.745Z" }
103wheels = [
104 { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload_time = "2024-09-15T18:07:37.964Z" },
105]
106
107[[package]]
108name = "requests"
109version = "2.32.3"
110source = { registry = "https://pypi.org/simple" }
111dependencies = [
112 { name = "certifi" },
113 { name = "charset-normalizer" },
114 { name = "idna" },
115 { name = "urllib3" },
116]
117sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload_time = "2024-05-29T15:37:49.536Z" }
118wheels = [
119 { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload_time = "2024-05-29T15:37:47.027Z" },
120]
121
122[[package]]
123name = "urllib3"
124version = "2.4.0"
125source = { registry = "https://pypi.org/simple" }
126sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload_time = "2025-04-10T15:23:39.232Z" }
127wheels = [
128 { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload_time = "2025-04-10T15:23:37.377Z" },
129]
diff --git a/overlays/cake-prometheus-exporter/default.nix b/overlays/cake-prometheus-exporter/default.nix
index 3d0acc2d..69a5008c 100644
--- a/overlays/cake-prometheus-exporter/default.nix
+++ b/overlays/cake-prometheus-exporter/default.nix
@@ -1,19 +1,18 @@
1{ final, prev, ... }: 1{ final, prev, ... }:
2let 2let
3 inpPython = final.python310.override {}; 3 inpPython = final.python310.override {};
4 python = inpPython.withPackages (ps: with ps; []);
4in { 5in {
5 cake-prometheus-exporter = prev.stdenv.mkDerivation rec { 6 cake-prometheus-exporter = prev.stdenv.mkDerivation rec {
6 pname = "cake-prometheus-exporter"; 7 pname = "cake-prometheus-exporter";
7 version = "0.0.0"; 8 version = "0.0.0";
8 9
9 src = ./cake-prometheus-exporter.py; 10 src = prev.replaceVars ./cake-prometheus-exporter.py { inherit python; };
10 11
11 phases = [ "buildPhase" "checkPhase" "installPhase" ]; 12 phases = [ "unpackPhase" "checkPhase" "installPhase" ];
12 13
13 python = inpPython.withPackages (ps: with ps; []); 14 unpackPhase = ''
14 15 cp $src cake-prometheus-exporter
15 buildPhase = ''
16 substituteAll $src cake-prometheus-exporter
17 ''; 16 '';
18 17
19 doCheck = true; 18 doCheck = true;
diff --git a/overlays/deploy-rs.nix b/overlays/deploy-rs.nix
new file mode 100644
index 00000000..678c6f5f
--- /dev/null
+++ b/overlays/deploy-rs.nix
@@ -0,0 +1,16 @@
1{ final, prev, flakeInputs, ... }: prev.lib.composeExtensions
2 flakeInputs.deploy-rs.overlays.default
3 (final: prev: {
4 deploy-rs = prev.deploy-rs // {
5 deploy-rs = prev.symlinkJoin {
6 name = "${prev.deploy-rs.deploy-rs.name}-wrapped";
7 paths = [ prev.deploy-rs.deploy-rs ];
8 buildInputs = [ prev.makeWrapper ];
9 postBuild = ''
10 wrapProgram $out/bin/deploy \
11 --prefix PATH : ${prev.lib.makeBinPath (with final; [ nix-monitored ])}
12 '';
13 };
14 };
15 })
16 final prev
diff --git a/overlays/inwx-cdnskey/default.nix b/overlays/inwx-cdnskey/default.nix
index cd564f24..e1bee0f2 100644
--- a/overlays/inwx-cdnskey/default.nix
+++ b/overlays/inwx-cdnskey/default.nix
@@ -2,17 +2,16 @@
2let 2let
3 packageOverrides = final.callPackage ./python-packages.nix {}; 3 packageOverrides = final.callPackage ./python-packages.nix {};
4 inpPython = final.python39.override { inherit packageOverrides; }; 4 inpPython = final.python39.override { inherit packageOverrides; };
5 python = inpPython.withPackages (ps: with ps; [pyxdg inwx-domrobot configparser dnspython]);
5in { 6in {
6 inwx-cdnskey = prev.stdenv.mkDerivation rec { 7 inwx-cdnskey = prev.stdenv.mkDerivation rec {
7 name = "inwx-cdnskey"; 8 name = "inwx-cdnskey";
8 src = ./inwx-cdnskey.py; 9 src = prev.replaceVars ./inwx-cdnskey.py { inherit python; };
9 10
10 phases = [ "buildPhase" "checkPhase" "installPhase" ]; 11 phases = [ "unpackPhase" "checkPhase" "installPhase" ];
11 12
12 python = inpPython.withPackages (ps: with ps; [pyxdg inwx-domrobot configparser dnspython]); 13 unpackPhase = ''
13 14 cp $src inwx-cdnskey
14 buildPhase = ''
15 substituteAll $src inwx-cdnskey
16 ''; 15 '';
17 16
18 doCheck = true; 17 doCheck = true;
diff --git a/overlays/lesspipe.nix b/overlays/lesspipe.nix
index 3258eb70..b791f6e5 100644
--- a/overlays/lesspipe.nix
+++ b/overlays/lesspipe.nix
@@ -17,7 +17,7 @@
17 17
18 preFixup = '' 18 preFixup = ''
19 wrapProgram $out/bin/lesspipe.sh \ 19 wrapProgram $out/bin/lesspipe.sh \
20 --prefix PATH : ${final.python3.pkgs.pygments}/bin:${final.file}/bin:${final.ncurses}/bin 20 --prefix PATH : ${prev.lib.makeBinPath (with final; [ file ncurses binutils ])}
21 ''; 21 '';
22 }; 22 };
23} 23}
diff --git a/overlays/nftables-prometheus-exporter/default.nix b/overlays/nftables-prometheus-exporter/default.nix
index aab0c8e9..48f668c4 100644
--- a/overlays/nftables-prometheus-exporter/default.nix
+++ b/overlays/nftables-prometheus-exporter/default.nix
@@ -1,17 +1,16 @@
1{ final, prev, ... }: 1{ final, prev, ... }:
2let 2let
3 inpPython = final.python310; 3 inpPython = final.python310;
4 python = inpPython.withPackages (ps: with ps; []);
4in { 5in {
5 nftables-prometheus-exporter = prev.stdenv.mkDerivation rec { 6 nftables-prometheus-exporter = prev.stdenv.mkDerivation rec {
6 name = "nftables-prometheus-exporter"; 7 name = "nftables-prometheus-exporter";
7 src = ./nftables-prometheus-exporter.py; 8 src = prev.replaceVars ./nftables-prometheus-exporter.py { inherit python; };
8 9
9 phases = [ "buildPhase" "checkPhase" "installPhase" ]; 10 phases = [ "unpackPhase" "checkPhase" "installPhase" ];
10 11
11 python = inpPython.withPackages (ps: with ps; []); 12 unpackPhase = ''
12 13 cp $src nftables-prometheus-exporter
13 buildPhase = ''
14 substituteAll $src nftables-prometheus-exporter
15 ''; 14 '';
16 15
17 doCheck = true; 16 doCheck = true;
diff --git a/overlays/nix-direnv/default.nix b/overlays/nix-direnv/default.nix
new file mode 100644
index 00000000..7c488e4e
--- /dev/null
+++ b/overlays/nix-direnv/default.nix
@@ -0,0 +1,53 @@
1{ final, prev, ... }: {
2 nix-direnv = prev.resholve.mkDerivation rec {
3 pname = "nix-direnv";
4 version = "3.0.6";
5
6 patches = [
7 ./static-nix.patch
8 ];
9
10 src = prev.fetchFromGitHub {
11 owner = "nix-community";
12 repo = "nix-direnv";
13 rev = version;
14 hash = "sha256-oNqhPqgQT92yxbKmcgX4F3e2yTUPyXYG7b2xQm3TvQw=";
15 };
16
17 installPhase = ''
18 runHook preInstall
19 install -m400 -D direnvrc $out/share/nix-direnv/direnvrc
20 runHook postInstall
21 '';
22
23 solutions = {
24 default = {
25 scripts = [ "share/nix-direnv/direnvrc" ];
26 interpreter = "none";
27 inputs = with final; [ coreutils nix-monitored ];
28 fake = {
29 builtin = [
30 "PATH_add"
31 "direnv_layout_dir"
32 "has"
33 "log_error"
34 "log_status"
35 "watch_file"
36 ];
37 function = [
38 # not really a function - this is in an else branch for macOS/homebrew that
39 # cannot be reached when built with nix
40 "shasum"
41 ];
42 };
43 keep = {
44 "$cmd" = true;
45 "$direnv" = true;
46 };
47 execer = [
48 "cannot:${prev.lib.getExe' final.nix-monitored "nix"}"
49 ];
50 };
51 };
52 };
53}
diff --git a/overlays/nix-direnv/static-nix.patch b/overlays/nix-direnv/static-nix.patch
new file mode 100644
index 00000000..5de8193f
--- /dev/null
+++ b/overlays/nix-direnv/static-nix.patch
@@ -0,0 +1,62 @@
1diff --git i/direnvrc w/direnvrc
2index ddac0f5..fbcade6 100644
3--- i/direnvrc
4+++ w/direnvrc
5@@ -29,10 +29,8 @@ _nix_direnv_warning() {
6
7 _nix_direnv_error() { log_error "${_NIX_DIRENV_LOG_PREFIX}$*"; }
8
9-_nix_direnv_nix=""
10-
11 _nix() {
12- ${_nix_direnv_nix} --extra-experimental-features "nix-command flakes" "$@"
13+ nix --extra-experimental-features "nix-command flakes" "$@"
14 }
15
16 _require_version() {
17@@ -55,34 +53,6 @@ _require_cmd_version() {
18 _require_version "$cmd" "${BASH_REMATCH[1]}" "$required"
19 }
20
21-_nix_direnv_resolve_nix() {
22- local ambient_nix
23-
24- if ambient_nix=$(command -v nix); then
25- if _require_cmd_version "${ambient_nix}" "${NIX_MIN_VERSION}"; then
26- echo "${ambient_nix}"
27- return 0
28- else
29- _nix_direnv_warning "Nix version in PATH is too old, wanted ${NIX_MIN_VERSION}+, got $(${ambient_nix} --version), will attempt fallback"
30- fi
31- else
32- _nix_direnv_warning "Could not find Nix in PATH, will attempt fallback"
33- fi
34-
35- if [ -n "${NIX_DIRENV_FALLBACK_NIX}" ]; then
36- if _require_cmd_version "${NIX_DIRENV_FALLBACK_NIX}" "${NIX_MIN_VERSION}"; then
37- echo "${NIX_DIRENV_FALLBACK_NIX}"
38- return 0
39- else
40- _nix_direnv_error "Fallback Nix version is too old, wanted ${NIX_MIN_VERSION}+, got $(${NIX_DIRENV_FALLBACK_NIX} --version)"
41- return 1
42- fi
43- else
44- _nix_direnv_error "Could not find fallback Nix binary, please add Nix to PATH or set NIX_DIRENV_FALLBACK_NIX"
45- return 1
46- fi
47-}
48-
49 _nix_direnv_preflight() {
50 if [[ -z $direnv ]]; then
51 # shellcheck disable=2016
52@@ -102,10 +72,6 @@ _nix_direnv_preflight() {
53 fi
54 fi
55
56- if ! _nix_direnv_nix=$(_nix_direnv_resolve_nix); then
57- return 1
58- fi
59-
60 local layout_dir
61 layout_dir=$(direnv_layout_dir)
62
diff --git a/overlays/nix-monitored.nix b/overlays/nix-monitored.nix
new file mode 100644
index 00000000..9f6caff1
--- /dev/null
+++ b/overlays/nix-monitored.nix
@@ -0,0 +1,8 @@
1{ final, prev, flakeInputs, ... }: prev.lib.composeExtensions
2 flakeInputs.nix-monitored.overlays.default
3 (final: prev: {
4 nix-monitored = prev.nix-monitored.override {
5 withNotify = false;
6 };
7 })
8 final prev
diff --git a/overlays/persistent-nix-shell/default.nix b/overlays/persistent-nix-shell/default.nix
index c36b9e86..6067cade 100644
--- a/overlays/persistent-nix-shell/default.nix
+++ b/overlays/persistent-nix-shell/default.nix
@@ -5,10 +5,9 @@
5 5
6 phases = [ "buildPhase" "installPhase" ]; 6 phases = [ "buildPhase" "installPhase" ];
7 7
8 inherit (final) zsh;
9
10 buildPhase = '' 8 buildPhase = ''
11 substituteAll $src persistent-nix-shell 9 substitute $src persistent-nix-shell \
10 --subst-var-by zsh ${final.zsh}
12 ''; 11 '';
13 12
14 installPhase = '' 13 installPhase = ''
diff --git a/overlays/postsrsd.nix b/overlays/postsrsd.nix
new file mode 100644
index 00000000..cb1ccf30
--- /dev/null
+++ b/overlays/postsrsd.nix
@@ -0,0 +1,11 @@
1{ final, prev, ... }:
2{
3 postsrsd = prev.postsrsd.overrideAttrs (oldAttrs: {
4 cmakeFlags = (oldAttrs.cmakeFlags or []) ++ [
5 "-DWITH_MILTER=ON"
6 ];
7 buildInputs = (oldAttrs.buildInputs or []) ++ [
8 final.libmilter
9 ];
10 });
11}
diff --git a/overlays/scutiger.nix b/overlays/scutiger.nix
index 7a56adaf..fea15c5f 100644
--- a/overlays/scutiger.nix
+++ b/overlays/scutiger.nix
@@ -2,7 +2,7 @@
2 scutiger = final.rustPlatform.buildRustPackage { 2 scutiger = final.rustPlatform.buildRustPackage {
3 inherit (sources.scutiger) pname version src; 3 inherit (sources.scutiger) pname version src;
4 4
5 cargoHash = "sha256-d+wJ3trrldCVATsudsbglElU6q4LaS6feRocRyHal2k="; 5 cargoHash = if prev.lib.versionOlder prev.lib.version "24.05" then "sha256-d+wJ3trrldCVATsudsbglElU6q4LaS6feRocRyHal2k=" else "sha256-FTAEmRuO95ii84uwaALVuImiymnSAQkB2UwZ5yX0WPs=";
6 6
7 nativeBuildInputs = with final; [ pkg-config pcre2.dev zlib.dev git ]; 7 nativeBuildInputs = with final; [ pkg-config pcre2.dev zlib.dev git ];
8 }; 8 };
diff --git a/overlays/swayosd/default.nix b/overlays/swayosd/default.nix
index 2a3a0f2b..b4601a03 100644
--- a/overlays/swayosd/default.nix
+++ b/overlays/swayosd/default.nix
@@ -1,10 +1,10 @@
1{ final, prev, sources, ... }: { 1{ final, prev, sources, ... }: {
2 swayosd = prev.swayosd.overrideAttrs (oldAttrs: rec { 2 swayosd = prev.swayosd.overrideAttrs (oldAttrs: rec {
3 inherit (sources.swayosd) version src; 3 inherit (sources.swayosd) version src;
4 cargoDeps = prev.rustPlatform.fetchCargoTarball { 4 cargoDeps = prev.rustPlatform.fetchCargoVendor {
5 inherit (oldAttrs) pname; 5 inherit (oldAttrs) pname;
6 inherit version src; 6 inherit version src;
7 hash = "sha256-s2vgyQP6J3i0q2o/Tt8dYoamBH3vrI/FNJYteHO8v5Q="; 7 hash = "sha256-yWybf4GKxHrk4WrW5SmjfPD0Gv79tpXOwNLlWeykYy0=";
8 }; 8 };
9 patches = (oldAttrs.patches or []) ++ [ 9 patches = (oldAttrs.patches or []) ++ [
10 ./exponential.patch 10 ./exponential.patch
diff --git a/overlays/thunderbird.nix b/overlays/thunderbird.nix
deleted file mode 100644
index 48ae2ccb..00000000
--- a/overlays/thunderbird.nix
+++ /dev/null
@@ -1,11 +0,0 @@
1{ final, prev, ... }: {
2 thunderbird-unwrapped = prev.thunderbird-unwrapped.overrideAttrs (oldAttrs: {
3 patches = (oldAttrs.patches or [])
4 ++ prev.lib.optional (prev.lib.versionAtLeast oldAttrs.version "136") (prev.fetchpatch {
5 url = "https://hg.mozilla.org/comm-central/raw-rev/a82bd8fc0bc0";
6 hash = "sha256-7t8IqxwcZJqXmOevpGof1mcrFKZvXtCcY2EOIsP47EY=";
7 stripLen = 1;
8 extraPrefix = "comm/";
9 });
10 });
11}
diff --git a/overlays/uucp/default.nix b/overlays/uucp/default.nix
deleted file mode 100644
index 4189dbcc..00000000
--- a/overlays/uucp/default.nix
+++ /dev/null
@@ -1,9 +0,0 @@
1{ final, prev, ... }: {
2 uucp = prev.uucp.overrideAttrs (oldAttrs: {
3 configureFlags = (oldAttrs.configureFlags or []) ++ ["--with-newconfigdir=/etc/uucp"];
4 patches = (oldAttrs.patches or []) ++ [
5 ./mailprogram.patch
6 ];
7 NIX_CFLAGS_COMPILE = "${oldAttrs.NIX_CFLAGS_COMPILE or ""} -Wno-error=incompatible-pointer-types";
8 });
9}
diff --git a/overlays/uucp/mailprogram.patch b/overlays/uucp/mailprogram.patch
deleted file mode 100644
index 89ac8f31..00000000
--- a/overlays/uucp/mailprogram.patch
+++ /dev/null
@@ -1,16 +0,0 @@
1 policy.h | 2 +-
2 1 file changed, 1 insertion(+), 1 deletion(-)
3
4diff --git a/policy.h b/policy.h
5index 5afe34b..8e92c8b 100644
6--- a/policy.h
7+++ b/policy.h
8@@ -240,7 +240,7 @@
9 the sendmail choice below. Otherwise, select one of the other
10 choices as appropriate. */
11 #if 1
12-#define MAIL_PROGRAM "/usr/lib/sendmail -t"
13+#define MAIL_PROGRAM "${config.security.wrapperDir}/sendmail -t"
14 /* #define MAIL_PROGRAM "/usr/sbin/sendmail -t" */
15 #define MAIL_PROGRAM_TO_BODY 1
16 #define MAIL_PROGRAM_SUBJECT_BODY 1
diff --git a/overlays/waybar-systemd-inhibit/.envrc b/overlays/waybar-systemd-inhibit/.envrc
new file mode 100644
index 00000000..2c909235
--- /dev/null
+++ b/overlays/waybar-systemd-inhibit/.envrc
@@ -0,0 +1,4 @@
1use flake
2
3[[ -d ".venv" ]] || ( uv venv && uv sync )
4. .venv/bin/activate
diff --git a/overlays/waybar-systemd-inhibit/.gitignore b/overlays/waybar-systemd-inhibit/.gitignore
new file mode 100644
index 00000000..4ccfae70
--- /dev/null
+++ b/overlays/waybar-systemd-inhibit/.gitignore
@@ -0,0 +1,2 @@
1.venv
2**/__pycache__
diff --git a/overlays/waybar-systemd-inhibit/default.nix b/overlays/waybar-systemd-inhibit/default.nix
new file mode 100644
index 00000000..ae6b8c75
--- /dev/null
+++ b/overlays/waybar-systemd-inhibit/default.nix
@@ -0,0 +1,20 @@
1{ prev, final, flake, flakeInputs, ... }:
2
3let
4 workspace = flakeInputs.uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
5 pythonSet = flake.lib.pythonSet {
6 pkgs = final;
7 python = final.python312;
8 overlay = workspace.mkPyprojectOverlay {
9 sourcePreference = "wheel";
10 };
11 };
12 virtualEnv = pythonSet.mkVirtualEnv "waybar-systemd-inhibit-env" workspace.deps.default;
13in {
14 waybar-systemd-inhibit = virtualEnv.overrideAttrs (oldAttrs: {
15 meta = (oldAttrs.meta or {}) // {
16 mainProgram = "waybar-systemd-inhibit";
17 };
18 nativeBuildInputs = (oldAttrs.nativeBuildInputs or []) ++ [ final.gobject-introspection final.wrapGAppsHook ];
19 });
20}
diff --git a/overlays/waybar-systemd-inhibit/pyproject.toml b/overlays/waybar-systemd-inhibit/pyproject.toml
new file mode 100644
index 00000000..6c6240b8
--- /dev/null
+++ b/overlays/waybar-systemd-inhibit/pyproject.toml
@@ -0,0 +1,17 @@
1[project]
2name = "waybar-systemd-inhibit"
3version = "0.1.0"
4requires-python = ">=3.12"
5dependencies = [
6 "asyncclick>=8.1.8",
7 "asyncio>=3.4.3",
8 "dbus-next>=0.2.3",
9]
10
11[project.scripts]
12waybar-systemd-inhibit = "waybar_systemd_inhibit.__main__:main"
13waybar-systemd-inhibit-toggle = "waybar_systemd_inhibit.__main__:toggle"
14
15[build-system]
16requires = ["hatchling"]
17build-backend = "hatchling.build"
diff --git a/overlays/waybar-systemd-inhibit/uv.lock b/overlays/waybar-systemd-inhibit/uv.lock
new file mode 100644
index 00000000..4e10d145
--- /dev/null
+++ b/overlays/waybar-systemd-inhibit/uv.lock
@@ -0,0 +1,102 @@
1version = 1
2revision = 2
3requires-python = ">=3.12"
4
5[[package]]
6name = "anyio"
7version = "4.9.0"
8source = { registry = "https://pypi.org/simple" }
9dependencies = [
10 { name = "idna" },
11 { name = "sniffio" },
12 { name = "typing-extensions", marker = "python_full_version < '3.13'" },
13]
14sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
15wheels = [
16 { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
17]
18
19[[package]]
20name = "asyncclick"
21version = "8.1.8"
22source = { registry = "https://pypi.org/simple" }
23dependencies = [
24 { name = "anyio" },
25 { name = "colorama", marker = "sys_platform == 'win32'" },
26]
27sdist = { url = "https://files.pythonhosted.org/packages/cb/b5/e1e5fdf1c1bb7e6e614987c120a98d9324bf8edfaa5f5cd16a6235c9d91b/asyncclick-8.1.8.tar.gz", hash = "sha256:0f0eb0f280e04919d67cf71b9fcdfb4db2d9ff7203669c40284485c149578e4c", size = 232900, upload-time = "2025-01-06T09:46:52.694Z" }
28wheels = [
29 { url = "https://files.pythonhosted.org/packages/14/cc/a436f0fc2d04e57a0697e0f87a03b9eaed03ad043d2d5f887f8eebcec95f/asyncclick-8.1.8-py3-none-any.whl", hash = "sha256:eb1ccb44bc767f8f0695d592c7806fdf5bd575605b4ee246ffd5fadbcfdbd7c6", size = 99093, upload-time = "2025-01-06T09:46:51.046Z" },
30 { url = "https://files.pythonhosted.org/packages/92/c4/ae9e9d25522c6dc96ff167903880a0fe94d7bd31ed999198ee5017d977ed/asyncclick-8.1.8.0-py3-none-any.whl", hash = "sha256:be146a2d8075d4fe372ff4e877f23c8b5af269d16705c1948123b9415f6fd678", size = 99115, upload-time = "2025-01-06T09:50:52.72Z" },
31]
32
33[[package]]
34name = "asyncio"
35version = "3.4.3"
36source = { registry = "https://pypi.org/simple" }
37sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411, upload-time = "2015-03-10T14:11:26.494Z" }
38wheels = [
39 { url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767, upload-time = "2015-03-10T14:05:10.959Z" },
40]
41
42[[package]]
43name = "colorama"
44version = "0.4.6"
45source = { registry = "https://pypi.org/simple" }
46sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
47wheels = [
48 { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
49]
50
51[[package]]
52name = "dbus-next"
53version = "0.2.3"
54source = { registry = "https://pypi.org/simple" }
55sdist = { url = "https://files.pythonhosted.org/packages/ce/45/6a40fbe886d60a8c26f480e7d12535502b5ba123814b3b9a0b002ebca198/dbus_next-0.2.3.tar.gz", hash = "sha256:f4eae26909332ada528c0a3549dda8d4f088f9b365153952a408e28023a626a5", size = 71112, upload-time = "2021-07-25T22:11:28.398Z" }
56wheels = [
57 { url = "https://files.pythonhosted.org/packages/d2/fc/c0a3f4c4eaa5a22fbef91713474666e13d0ea2a69c84532579490a9f2cc8/dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b", size = 57885, upload-time = "2021-07-25T22:11:25.466Z" },
58]
59
60[[package]]
61name = "idna"
62version = "3.10"
63source = { registry = "https://pypi.org/simple" }
64sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
65wheels = [
66 { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
67]
68
69[[package]]
70name = "sniffio"
71version = "1.3.1"
72source = { registry = "https://pypi.org/simple" }
73sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
74wheels = [
75 { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
76]
77
78[[package]]
79name = "typing-extensions"
80version = "4.13.2"
81source = { registry = "https://pypi.org/simple" }
82sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
83wheels = [
84 { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
85]
86
87[[package]]
88name = "waybar-systemd-inhibit"
89version = "0.1.0"
90source = { editable = "." }
91dependencies = [
92 { name = "asyncclick" },
93 { name = "asyncio" },
94 { name = "dbus-next" },
95]
96
97[package.metadata]
98requires-dist = [
99 { name = "asyncclick", specifier = ">=8.1.8" },
100 { name = "asyncio", specifier = ">=3.4.3" },
101 { name = "dbus-next", specifier = ">=0.2.3" },
102]
diff --git a/overlays/waybar-systemd-inhibit/waybar_systemd_inhibit/__init__.py b/overlays/waybar-systemd-inhibit/waybar_systemd_inhibit/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/overlays/waybar-systemd-inhibit/waybar_systemd_inhibit/__init__.py
diff --git a/overlays/waybar-systemd-inhibit/waybar_systemd_inhibit/__main__.py b/overlays/waybar-systemd-inhibit/waybar_systemd_inhibit/__main__.py
new file mode 100644
index 00000000..35cc7fd1
--- /dev/null
+++ b/overlays/waybar-systemd-inhibit/waybar_systemd_inhibit/__main__.py
@@ -0,0 +1,117 @@
1import asyncclick as click
2from dbus_next.aio import MessageBus
3from dbus_next import BusType, Message, PropertyAccess
4import asyncio
5from functools import update_wrapper
6from dbus_next.service import ServiceInterface, method, dbus_property
7from dbus_next import Variant, DBusError
8import os
9import json
10
11class BlockInterface(ServiceInterface):
12 def __init__(self, system_bus, logind):
13 super().__init__('li.yggdrasil.WaybarSystemdInhibit')
14 self.system_bus = system_bus
15 self.logind = logind
16 self.fd = None
17
18 def Release(self):
19 if not self.fd:
20 return
21
22 os.close(self.fd)
23 self.fd = None
24 self.emit_properties_changed({'IsAcquired': False})
25
26 async def Acquire(self):
27 if self.fd:
28 return
29
30 res = await self.system_bus.call(Message(
31 destination='org.freedesktop.login1',
32 path='/org/freedesktop/login1',
33 interface='org.freedesktop.login1.Manager',
34 member='Inhibit',
35 signature='ssss',
36 body=[
37 "handle-lid-switch",
38 "waybar-systemd-inhibit",
39 "User request",
40 "block",
41 ],
42 ))
43 self.fd = res.unix_fds[res.body[0]]
44 self.emit_properties_changed({'IsAcquired': True})
45
46 @method()
47 async def ToggleBlock(self):
48 if self.fd:
49 self.Release()
50 else:
51 await self.Acquire()
52
53 @dbus_property(access=PropertyAccess.READ)
54 def IsAcquired(self) -> 'b':
55 return self.fd is not None
56
57
58@click.command()
59async def main():
60 system_bus = await MessageBus(bus_type=BusType.SYSTEM, negotiate_unix_fd=True).connect()
61 session_bus = await MessageBus(bus_type=BusType.SESSION).connect()
62
63 introspection = await system_bus.introspect('org.freedesktop.login1', '/org/freedesktop/login1')
64 obj = system_bus.get_proxy_object('org.freedesktop.login1', '/org/freedesktop/login1', introspection)
65 logind = obj.get_interface('org.freedesktop.login1.Manager')
66 properties = obj.get_interface('org.freedesktop.DBus.Properties')
67
68 def is_blocked_logind(what: str):
69 return "handle-lid-switch" in what.split(':')
70
71 def print_state(is_blocked: bool, is_acquired: bool = False):
72 icon = "&#xf0322;" if is_blocked else "&#xf06e7;"
73 text = f"<span font=\"Symbols Nerd Font Mono\">{icon}</span>"
74 if is_acquired:
75 text = f"<span color=\"#f28a21\">{text}</span>"
76 elif is_blocked:
77 text = f"<span color=\"#ffffff\">{text}</span>"
78 print(json.dumps({'text': text, 'tooltip': ("Manually inhibited" if is_acquired else None)}, separators=(',', ':')), flush=True)
79
80 print_state(is_blocked_logind(await logind.get_block_inhibited()))
81
82 async def get_inhibit():
83 introspection = await session_bus.introspect('li.yggdrasil.WaybarSystemdInhibit', '/li/yggdrasil/WaybarSystemdInhibit')
84 return session_bus.get_proxy_object('li.yggdrasil.WaybarSystemdInhibit', '/li/yggdrasil/WaybarSystemdInhibit', introspection)
85
86 async def on_logind_properties_changed(interface_name, changed_properties, invalidated_properties):
87 if 'BlockInhibited' not in changed_properties:
88 return
89
90 properties = (await get_inhibit()).get_interface('li.yggdrasil.WaybarSystemdInhibit')
91
92 print_state(is_blocked_logind(changed_properties['BlockInhibited'].value), await properties.get_is_acquired())
93
94 properties.on_properties_changed(on_logind_properties_changed)
95
96 session_bus.export('/li/yggdrasil/WaybarSystemdInhibit', BlockInterface(system_bus, logind))
97 await session_bus.request_name('li.yggdrasil.WaybarSystemdInhibit')
98
99 properties = (await get_inhibit()).get_interface('org.freedesktop.DBus.Properties')
100
101 async def on_inhibit_properties_changed(interface_name, changed_properties, invalidated_properties):
102 if 'IsAcquired' not in changed_properties:
103 return
104
105 print_state(is_blocked_logind(await logind.get_block_inhibited()), changed_properties['IsAcquired'].value)
106
107 properties.on_properties_changed(on_inhibit_properties_changed)
108
109 await session_bus.wait_for_disconnect()
110
111@click.command()
112async def toggle():
113 session_bus = await MessageBus(bus_type=BusType.SESSION).connect()
114 introspection = await session_bus.introspect('li.yggdrasil.WaybarSystemdInhibit', '/li/yggdrasil/WaybarSystemdInhibit')
115 obj = session_bus.get_proxy_object('li.yggdrasil.WaybarSystemdInhibit', '/li/yggdrasil/WaybarSystemdInhibit', introspection)
116 interface = obj.get_interface('li.yggdrasil.WaybarSystemdInhibit')
117 await interface.call_toggle_block()
diff --git a/overlays/worktime/.envrc b/overlays/worktime/.envrc
new file mode 100644
index 00000000..2c909235
--- /dev/null
+++ b/overlays/worktime/.envrc
@@ -0,0 +1,4 @@
1use flake
2
3[[ -d ".venv" ]] || ( uv venv && uv sync )
4. .venv/bin/activate
diff --git a/overlays/worktime/.gitignore b/overlays/worktime/.gitignore
new file mode 100644
index 00000000..4ccfae70
--- /dev/null
+++ b/overlays/worktime/.gitignore
@@ -0,0 +1,2 @@
1.venv
2**/__pycache__
diff --git a/overlays/worktime/default.nix b/overlays/worktime/default.nix
index 1d8433af..579cf7ad 100644
--- a/overlays/worktime/default.nix
+++ b/overlays/worktime/default.nix
@@ -1,13 +1,19 @@
1{ prev, ... }: 1{ prev, final, flake, flakeInputs, ... }:
2 2
3with prev.poetry2nix; 3let
4 4 workspace = flakeInputs.uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
5{ 5 pythonSet = flake.lib.pythonSet {
6 worktime = mkPoetryApplication { 6 pkgs = final;
7 python = prev.python312; 7 python = final.python312;
8 8 overlay = workspace.mkPyprojectOverlay {
9 projectDir = cleanPythonSources { src = ./.; }; 9 sourcePreference = "wheel";
10 10 };
11 meta.mainProgram = "worktime";
12 }; 11 };
12 virtualEnv = pythonSet.mkVirtualEnv "worktime" workspace.deps.default;
13in {
14 worktime = virtualEnv.overrideAttrs (oldAttrs: {
15 meta = (oldAttrs.meta or {}) // {
16 mainProgram = "worktime";
17 };
18 });
13} 19}
diff --git a/overlays/worktime/poetry.lock b/overlays/worktime/poetry.lock
deleted file mode 100644
index 7c1ca91d..00000000
--- a/overlays/worktime/poetry.lock
+++ /dev/null
@@ -1,284 +0,0 @@
1# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
2
3[[package]]
4name = "backoff"
5version = "2.2.1"
6description = "Function decoration for backoff and retry"
7optional = false
8python-versions = ">=3.7,<4.0"
9groups = ["main"]
10files = [
11 {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"},
12 {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"},
13]
14
15[[package]]
16name = "certifi"
17version = "2025.1.31"
18description = "Python package for providing Mozilla's CA Bundle."
19optional = false
20python-versions = ">=3.6"
21groups = ["main"]
22files = [
23 {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
24 {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
25]
26
27[[package]]
28name = "charset-normalizer"
29version = "3.4.1"
30description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
31optional = false
32python-versions = ">=3.7"
33groups = ["main"]
34files = [
35 {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
36 {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
37 {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"},
38 {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"},
39 {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"},
40 {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"},
41 {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"},
42 {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"},
43 {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"},
44 {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"},
45 {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"},
46 {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"},
47 {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"},
48 {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"},
49 {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"},
50 {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"},
51 {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"},
52 {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"},
53 {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"},
54 {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"},
55 {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"},
56 {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"},
57 {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"},
58 {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"},
59 {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"},
60 {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"},
61 {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"},
62 {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"},
63 {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"},
64 {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"},
65 {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"},
66 {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"},
67 {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"},
68 {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"},
69 {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"},
70 {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"},
71 {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"},
72 {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"},
73 {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"},
74 {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"},
75 {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"},
76 {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"},
77 {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"},
78 {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"},
79 {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"},
80 {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"},
81 {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"},
82 {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"},
83 {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"},
84 {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"},
85 {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"},
86 {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"},
87 {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"},
88 {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"},
89 {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"},
90 {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"},
91 {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"},
92 {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"},
93 {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"},
94 {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"},
95 {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"},
96 {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"},
97 {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"},
98 {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"},
99 {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"},
100 {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"},
101 {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"},
102 {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"},
103 {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"},
104 {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"},
105 {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"},
106 {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"},
107 {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"},
108 {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"},
109 {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"},
110 {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"},
111 {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"},
112 {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"},
113 {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"},
114 {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"},
115 {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"},
116 {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"},
117 {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"},
118 {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"},
119 {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"},
120 {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"},
121 {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"},
122 {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"},
123 {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"},
124 {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"},
125 {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"},
126 {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"},
127]
128
129[[package]]
130name = "idna"
131version = "3.10"
132description = "Internationalized Domain Names in Applications (IDNA)"
133optional = false
134python-versions = ">=3.6"
135groups = ["main"]
136files = [
137 {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
138 {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
139]
140
141[package.extras]
142all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
143
144[[package]]
145name = "jsonpickle"
146version = "4.0.5"
147description = "jsonpickle encodes/decodes any Python object to/from JSON"
148optional = false
149python-versions = ">=3.8"
150groups = ["main"]
151files = [
152 {file = "jsonpickle-4.0.5-py3-none-any.whl", hash = "sha256:b4ac7d0a75ddcdfd93445737f1d36ff28768690d43e54bf5d0ddb1d915e580df"},
153 {file = "jsonpickle-4.0.5.tar.gz", hash = "sha256:f299818b39367c361b3f26bdba827d4249ab5d383cd93144d0f94b5417aacb35"},
154]
155
156[package.extras]
157cov = ["pytest-cov"]
158dev = ["black", "pyupgrade"]
159docs = ["furo", "rst.linker (>=1.9)", "sphinx (>=3.5)"]
160packaging = ["build", "setuptools (>=61.2)", "setuptools-scm[toml] (>=6.0)", "twine"]
161testing = ["PyYAML", "atheris (>=2.3.0,<2.4.0) ; python_version < \"3.12\"", "bson", "ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=6.0,!=8.1.*)", "pytest-benchmark", "pytest-benchmark[histogram]", "pytest-checkdocs (>=1.2.3)", "pytest-enabler (>=1.0.1)", "pytest-ruff (>=0.2.1)", "scikit-learn", "scipy (>=1.9.3) ; python_version > \"3.10\"", "scipy ; python_version <= \"3.10\"", "simplejson", "sqlalchemy", "ujson"]
162
163[[package]]
164name = "python-dateutil"
165version = "2.9.0.post0"
166description = "Extensions to the standard Python datetime module"
167optional = false
168python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
169groups = ["main"]
170files = [
171 {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
172 {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
173]
174
175[package.dependencies]
176six = ">=1.5"
177
178[[package]]
179name = "pyxdg"
180version = "0.28"
181description = "PyXDG contains implementations of freedesktop.org standards in python."
182optional = false
183python-versions = "*"
184groups = ["main"]
185files = [
186 {file = "pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"},
187 {file = "pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4"},
188]
189
190[[package]]
191name = "requests"
192version = "2.32.3"
193description = "Python HTTP for Humans."
194optional = false
195python-versions = ">=3.8"
196groups = ["main"]
197files = [
198 {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
199 {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
200]
201
202[package.dependencies]
203certifi = ">=2017.4.17"
204charset-normalizer = ">=2,<4"
205idna = ">=2.5,<4"
206urllib3 = ">=1.21.1,<3"
207
208[package.extras]
209socks = ["PySocks (>=1.5.6,!=1.5.7)"]
210use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
211
212[[package]]
213name = "six"
214version = "1.17.0"
215description = "Python 2 and 3 compatibility utilities"
216optional = false
217python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
218groups = ["main"]
219files = [
220 {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
221 {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
222]
223
224[[package]]
225name = "tabulate"
226version = "0.9.0"
227description = "Pretty-print tabular data"
228optional = false
229python-versions = ">=3.7"
230groups = ["main"]
231files = [
232 {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"},
233 {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"},
234]
235
236[package.extras]
237widechars = ["wcwidth"]
238
239[[package]]
240name = "toml"
241version = "0.10.2"
242description = "Python Library for Tom's Obvious, Minimal Language"
243optional = false
244python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
245groups = ["main"]
246files = [
247 {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
248 {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
249]
250
251[[package]]
252name = "uritools"
253version = "4.0.3"
254description = "URI parsing, classification and composition"
255optional = false
256python-versions = ">=3.7"
257groups = ["main"]
258files = [
259 {file = "uritools-4.0.3-py3-none-any.whl", hash = "sha256:bae297d090e69a0451130ffba6f2f1c9477244aa0a5543d66aed2d9f77d0dd9c"},
260 {file = "uritools-4.0.3.tar.gz", hash = "sha256:ee06a182a9c849464ce9d5fa917539aacc8edd2a4924d1b7aabeeecabcae3bc2"},
261]
262
263[[package]]
264name = "urllib3"
265version = "2.3.0"
266description = "HTTP library with thread-safe connection pooling, file post, and more."
267optional = false
268python-versions = ">=3.9"
269groups = ["main"]
270files = [
271 {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"},
272 {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"},
273]
274
275[package.extras]
276brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
277h2 = ["h2 (>=4,<5)"]
278socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
279zstd = ["zstandard (>=0.18.0)"]
280
281[metadata]
282lock-version = "2.1"
283python-versions = "^3.12"
284content-hash = "2b335da94bf3e2d2bee7d8ca6e84cdb56e97ac29d1224d8c8dca98d93bbdcea2"
diff --git a/overlays/worktime/pyproject.toml b/overlays/worktime/pyproject.toml
index de4b9fd4..42da51f5 100644
--- a/overlays/worktime/pyproject.toml
+++ b/overlays/worktime/pyproject.toml
@@ -1,23 +1,28 @@
1[tool.poetry] 1[project]
2name = "worktime" 2name = "worktime"
3version = "0.1.0" 3version = "1.0.0"
4description = "" 4requires-python = "~=3.12"
5authors = ["Gregor Kleen <gkleen@yggdrasil.li>"] 5dependencies = [
6 "pyxdg>=0.28,<0.29",
7 "python-dateutil>=2.9.0.post0,<3",
8 "uritools>=4.0.3,<5",
9 "requests>=2.32.3,<3",
10 "tabulate>=0.9.0,<0.10",
11 "toml>=0.10.2,<0.11",
12 "jsonpickle>=4.0.5,<5",
13 "frozendict>=2.4.6",
14 "atomicwriter>=0.2.5",
15 "desktop-notify>=1.3.3",
16]
6 17
7[tool.poetry.dependencies] 18[project.scripts]
8python = "^3.12"
9pyxdg = "^0.28"
10python-dateutil = "^2.9.0.post0"
11uritools = "^4.0.3"
12requests = "^2.32.3"
13tabulate = "^0.9.0"
14backoff = "^2.2.1"
15toml = "^0.10.2"
16jsonpickle = "^4.0.5"
17
18[tool.poetry.scripts]
19worktime = "worktime.__main__:main" 19worktime = "worktime.__main__:main"
20worktime-ui = "worktime.__main__:ui"
21worktime-stop = "worktime.__main__:stop"
20 22
21[build-system] 23[build-system]
22requires = ["poetry-core"] 24requires = ["hatchling"]
23build-backend = "poetry.core.masonry.api" \ No newline at end of file 25build-backend = "hatchling.build"
26
27[dependency-groups]
28dev = []
diff --git a/overlays/worktime/uv.lock b/overlays/worktime/uv.lock
new file mode 100644
index 00000000..39de4ccf
--- /dev/null
+++ b/overlays/worktime/uv.lock
@@ -0,0 +1,248 @@
1version = 1
2revision = 2
3requires-python = ">=3.12, <4"
4
5[[package]]
6name = "atomicwriter"
7version = "0.2.5"
8source = { registry = "https://pypi.org/simple" }
9sdist = { url = "https://files.pythonhosted.org/packages/50/b4/dd04e186eb244d1ed84b1d0ebfba19ddc7f8886b98e345aaca4208b031d2/atomicwriter-0.2.5.tar.gz", hash = "sha256:5ced6afb0579377a13e191b17a16115e14c30ec00e6c38b60403f58235a867af", size = 64990, upload-time = "2025-05-24T20:35:42.538Z" }
10wheels = [
11 { url = "https://files.pythonhosted.org/packages/99/7c/672a0de09b0b355a2ffa521ef25cf106f1984823379dee37f7305fdc1774/atomicwriter-0.2.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c1fab874e62ebe96f1af0e965dc1e92c4c1ef2e2e9612a444371b8fc751ec43", size = 234141, upload-time = "2025-05-24T20:34:32.74Z" },
12 { url = "https://files.pythonhosted.org/packages/b9/0c/e1c5bad033284c212c0a77121b48dd4147f80e9a7cd82a9d2ce0a2160901/atomicwriter-0.2.5-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:8dbb67cc730be7d6bdfd5e991271bc17052be8fb2e4fa27854b47d8a76d36349", size = 245788, upload-time = "2025-05-24T20:34:33.897Z" },
13 { url = "https://files.pythonhosted.org/packages/f4/d3/7036e203cc5fc4c49bf916b4ba158e0d2779de127afad5963edd7e3b9400/atomicwriter-0.2.5-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a4e7f81932839c738425dc96ad98e4a7511b740cd3d75f480bfabbcf8e6f7eae", size = 260428, upload-time = "2025-05-24T20:34:35.533Z" },
14 { url = "https://files.pythonhosted.org/packages/e5/b9/9a4d235a8d67fb442302dc0f3ea2394b7bd994bfc99b1dc0f744c7852418/atomicwriter-0.2.5-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:de37a3a5d1b57b719cfb0b81a11cab2114acfdc2c36051bf0af72d05eb644411", size = 263648, upload-time = "2025-05-24T20:34:36.72Z" },
15 { url = "https://files.pythonhosted.org/packages/71/7c/32d4ddad53375de42f3e972bb0633ec76f2c31772f2e508479d4788651d9/atomicwriter-0.2.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b925e55750092fd482565b6068b8c8366fd79de526681af9e58eb209f0deeca", size = 323775, upload-time = "2025-05-24T20:34:37.968Z" },
16 { url = "https://files.pythonhosted.org/packages/06/fe/6a226368a3f7ea30001fbd165f6a97f28c8f1a884896357b3d694983f5d2/atomicwriter-0.2.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:538f78f25e01584535782397211c66b8b3c9de90c2d1fc01a668ddce73dd0cb2", size = 340819, upload-time = "2025-05-24T20:34:39.63Z" },
17 { url = "https://files.pythonhosted.org/packages/92/95/b035b2296c483fde5392c629e0b6e3844eba6e54ea965c4b8827379b0893/atomicwriter-0.2.5-cp312-cp312-win_amd64.whl", hash = "sha256:1d2d49a1b94ea7b289be9f7134d756bfb0bbf53eb0e58411334ed1b9958abe5e", size = 152789, upload-time = "2025-05-24T20:34:40.905Z" },
18 { url = "https://files.pythonhosted.org/packages/da/25/caa0959ae8ce24763e24e1f45be6cb897414545d224a155f929d496d6812/atomicwriter-0.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f5490fd5bec378509521f7c2a19a64031a0de07d368d76733c3f76a0b9f026b", size = 233830, upload-time = "2025-05-24T20:34:42.532Z" },
19 { url = "https://files.pythonhosted.org/packages/d2/76/3c41bfd4fd74bc63bec29f05a806a767258eea7cf151496b4ab015cb5323/atomicwriter-0.2.5-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:a4dada83ff1255c7e640363cc2a4399ab9a822d4dbc9c18f55bbf0c8b12ce056", size = 245461, upload-time = "2025-05-24T20:34:44.454Z" },
20 { url = "https://files.pythonhosted.org/packages/c3/1e/5512dbdfdc3f4ab12f5923c50ae4765cc2fc65a9f112bb9dccbcbe60b395/atomicwriter-0.2.5-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ef2cf15e67513f05ad37d4cec48e403982c6b3c07f491472effd76d2157de7e2", size = 259892, upload-time = "2025-05-24T20:34:45.688Z" },
21 { url = "https://files.pythonhosted.org/packages/e5/1d/2382b6cacb119115828eb519697a555900bcfdb062efeb0f82603295402d/atomicwriter-0.2.5-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:73618f74c3c5f5401d3da0a3cd3043f23de5b6bb4a3d85bc580940a441355d25", size = 263125, upload-time = "2025-05-24T20:34:47.205Z" },
22 { url = "https://files.pythonhosted.org/packages/07/d7/c4d68386161870db4a8d0452f0655a19902fa435b749c12e6ef800e89b19/atomicwriter-0.2.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbd5eda80710ddac7aefb421c79cef6b905852a827e764f0f12fcbaa88919f7a", size = 323503, upload-time = "2025-05-24T20:34:48.417Z" },
23 { url = "https://files.pythonhosted.org/packages/b7/08/0fc03c0736ab8466e1b47a3ee17a528da18019cff93b7c4c2b33df82c19e/atomicwriter-0.2.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4776aaca40bc3040c3716c2adad74625c42285083ff31e8bf24a95315225c7b", size = 340156, upload-time = "2025-05-24T20:34:50.389Z" },
24 { url = "https://files.pythonhosted.org/packages/fa/09/7ba888cf4d90bcabd9e82db3bdb9de50e4ef072e0ea0d375cd1931b79349/atomicwriter-0.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:225ed1fbfa1996d9b0b2252f8a5d81263e51cbc797086d830f488c35b1d2ab42", size = 152274, upload-time = "2025-05-24T20:34:51.785Z" },
25 { url = "https://files.pythonhosted.org/packages/2a/70/07d2ba2e0a126cfecfbfed46baf599c9e2155f4c8338fed4d3ae0041b133/atomicwriter-0.2.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:63b55982cfa47232f179689933bf003eefb2bd33464235883ed3ce7322cf38f3", size = 232879, upload-time = "2025-05-24T20:34:53.195Z" },
26 { url = "https://files.pythonhosted.org/packages/f6/4d/397eb5435917135df93b339d849884bb1125896b1e15163c5244aa590336/atomicwriter-0.2.5-cp313-cp313t-macosx_11_0_x86_64.whl", hash = "sha256:e33f40b2a27f8831beeabb485923acb6dd067cc70bba1a63096749b3dc4747ff", size = 244386, upload-time = "2025-05-24T20:34:54.852Z" },
27 { url = "https://files.pythonhosted.org/packages/8b/01/73f0b683fa55e61dd29d30e48e9a75ddb049e6dad0ac4ae1a29dbc05f21e/atomicwriter-0.2.5-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c646e115e88147d71f845a005fc53910f22c4dc65bd634768cb90b7f34259359", size = 258255, upload-time = "2025-05-24T20:34:56.046Z" },
28 { url = "https://files.pythonhosted.org/packages/4b/19/692387c1fb1b8714a9b2fab99a58850fd4136bed988814c8ff74d0c8de02/atomicwriter-0.2.5-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:47f974e986ff6514351c3ea75041009a514be0c34c225c062b0ad8a28ec9c0a3", size = 261768, upload-time = "2025-05-24T20:34:57.795Z" },
29 { url = "https://files.pythonhosted.org/packages/3e/f2/4d466f52ee635cc54011713272f302584c6d1ce612c331d9989fa6fa672f/atomicwriter-0.2.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e1db8b9004cd3f628166e83b25eb814b82345f9d6bc15e99b6d201c355455b45", size = 321975, upload-time = "2025-05-24T20:34:59.45Z" },
30 { url = "https://files.pythonhosted.org/packages/84/ad/0189ad9783ca6609df47e06cc0cd22866a8073d46478f59c6ab3ec13e0fb/atomicwriter-0.2.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a7da4a114121ab865663578b801a0520b2b518d4591af0bd294f6aac0dad243b", size = 338946, upload-time = "2025-05-24T20:35:01.501Z" },
31 { url = "https://files.pythonhosted.org/packages/94/79/2c4d8f75eeb09192cf572957f031271998f3c985fabd79d513fff66ac715/atomicwriter-0.2.5-cp313-cp313t-win_amd64.whl", hash = "sha256:7aab4b3956cc17219e7e4da76e8a1bceb3d3aeaf03234f89b90e234a2adcf27b", size = 151571, upload-time = "2025-05-24T20:35:02.747Z" },
32 { url = "https://files.pythonhosted.org/packages/32/19/d6a686d189c3577e7f08b33df398b959c24bf74b3cec34359104db1a24ff/atomicwriter-0.2.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d0fccac2dfe5d884d97edbda28be9c16d55faee9bdf66f53a99384ac387cc43", size = 239320, upload-time = "2025-05-24T20:35:04.028Z" },
33 { url = "https://files.pythonhosted.org/packages/8e/35/35571a4eed57816c3b5fdbefcb15f38563fbe4f3a4a7d1588c8ef899afaf/atomicwriter-0.2.5-cp39-abi3-macosx_11_0_x86_64.whl", hash = "sha256:6583c24333508839db2156d895cbbb5cd3ff20d4f9c698e341435e5b35990eaa", size = 250818, upload-time = "2025-05-24T20:35:05.21Z" },
34 { url = "https://files.pythonhosted.org/packages/81/d9/145093630bc25f115a49d32d9ef66745f5cdef787492d77fd27e74d20389/atomicwriter-0.2.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:136a9902ae3f1c0cb262a07dd3ac85069d71f8b11347cd740030567e67d611aa", size = 265796, upload-time = "2025-05-24T20:35:06.388Z" },
35 { url = "https://files.pythonhosted.org/packages/58/32/d1881adade2ebc70aa9dbb61cadabc2c00cfa99a7a5d6ba48f44e279056f/atomicwriter-0.2.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0b6830434b6a49c19473c3f3975dfa0a87dec95bee81297f7393e378f9a0b82f", size = 269378, upload-time = "2025-05-24T20:35:07.578Z" },
36 { url = "https://files.pythonhosted.org/packages/93/f5/2661ea763784a4991c4c7be5c932a468937bd1d4618b833a63ec638a3b76/atomicwriter-0.2.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53095a01891a2901aa04c10c8de52c0ba41e0d8a4a1893318cf34ccbdbde00b7", size = 328167, upload-time = "2025-05-24T20:35:08.764Z" },
37 { url = "https://files.pythonhosted.org/packages/ec/bc/e3aa521671a589bee9662d3e2108e4835a5d80e6da76e4d05d98d1c78005/atomicwriter-0.2.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ecf4dc3983bb1f28b21cb09c2d96b6936d8864c559dcf151b57813cb1eae998b", size = 347153, upload-time = "2025-05-24T20:35:10.507Z" },
38 { url = "https://files.pythonhosted.org/packages/59/b7/e190383e7240b1f247c6df9bc6667db8df10190cd0bb2dba8ea6bd704ea4/atomicwriter-0.2.5-cp39-abi3-win_amd64.whl", hash = "sha256:92cff264a20364301ab341b332fd0112866870b8cb35caf99a3f3fee0e6c19e8", size = 156374, upload-time = "2025-05-24T20:35:11.716Z" },
39]
40
41[[package]]
42name = "certifi"
43version = "2025.1.31"
44source = { registry = "https://pypi.org/simple" }
45sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577, upload-time = "2025-01-31T02:16:47.166Z" }
46wheels = [
47 { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393, upload-time = "2025-01-31T02:16:45.015Z" },
48]
49
50[[package]]
51name = "charset-normalizer"
52version = "3.4.1"
53source = { registry = "https://pypi.org/simple" }
54sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188, upload-time = "2024-12-24T18:12:35.43Z" }
55wheels = [
56 { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105, upload-time = "2024-12-24T18:10:38.83Z" },
57 { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404, upload-time = "2024-12-24T18:10:44.272Z" },
58 { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423, upload-time = "2024-12-24T18:10:45.492Z" },
59 { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184, upload-time = "2024-12-24T18:10:47.898Z" },
60 { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268, upload-time = "2024-12-24T18:10:50.589Z" },
61 { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601, upload-time = "2024-12-24T18:10:52.541Z" },
62 { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098, upload-time = "2024-12-24T18:10:53.789Z" },
63 { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520, upload-time = "2024-12-24T18:10:55.048Z" },
64 { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852, upload-time = "2024-12-24T18:10:57.647Z" },
65 { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488, upload-time = "2024-12-24T18:10:59.43Z" },
66 { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192, upload-time = "2024-12-24T18:11:00.676Z" },
67 { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550, upload-time = "2024-12-24T18:11:01.952Z" },
68 { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785, upload-time = "2024-12-24T18:11:03.142Z" },
69 { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698, upload-time = "2024-12-24T18:11:05.834Z" },
70 { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162, upload-time = "2024-12-24T18:11:07.064Z" },
71 { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263, upload-time = "2024-12-24T18:11:08.374Z" },
72 { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966, upload-time = "2024-12-24T18:11:09.831Z" },
73 { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992, upload-time = "2024-12-24T18:11:12.03Z" },
74 { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162, upload-time = "2024-12-24T18:11:13.372Z" },
75 { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972, upload-time = "2024-12-24T18:11:14.628Z" },
76 { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095, upload-time = "2024-12-24T18:11:17.672Z" },
77 { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668, upload-time = "2024-12-24T18:11:18.989Z" },
78 { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073, upload-time = "2024-12-24T18:11:21.507Z" },
79 { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732, upload-time = "2024-12-24T18:11:22.774Z" },
80 { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391, upload-time = "2024-12-24T18:11:24.139Z" },
81 { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702, upload-time = "2024-12-24T18:11:26.535Z" },
82 { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767, upload-time = "2024-12-24T18:12:32.852Z" },
83]
84
85[[package]]
86name = "dbus-next"
87version = "0.2.3"
88source = { registry = "https://pypi.org/simple" }
89sdist = { url = "https://files.pythonhosted.org/packages/ce/45/6a40fbe886d60a8c26f480e7d12535502b5ba123814b3b9a0b002ebca198/dbus_next-0.2.3.tar.gz", hash = "sha256:f4eae26909332ada528c0a3549dda8d4f088f9b365153952a408e28023a626a5", size = 71112, upload-time = "2021-07-25T22:11:28.398Z" }
90wheels = [
91 { url = "https://files.pythonhosted.org/packages/d2/fc/c0a3f4c4eaa5a22fbef91713474666e13d0ea2a69c84532579490a9f2cc8/dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b", size = 57885, upload-time = "2021-07-25T22:11:25.466Z" },
92]
93
94[[package]]
95name = "desktop-notify"
96version = "1.3.3"
97source = { registry = "https://pypi.org/simple" }
98dependencies = [
99 { name = "dbus-next" },
100]
101sdist = { url = "https://files.pythonhosted.org/packages/7a/d8/7ae5779257f5f1aa0a2d50c02d70b29522bd414692f3d3bd18ef119fe82d/desktop-notify-1.3.3.tar.gz", hash = "sha256:62934ad1f72f292f9a3af5ffe45af32814af18c396c00369385540c72bf08077", size = 7828, upload-time = "2021-01-03T16:46:36.483Z" }
102wheels = [
103 { url = "https://files.pythonhosted.org/packages/0a/cd/a7e3bd0262f3e8a9272fd24d0193e24dad7cb4e4edd27da48e74b5523e59/desktop_notify-1.3.3-py3-none-any.whl", hash = "sha256:8ad7ecc3a9a603dd5fa3cdc11cc6265cfbc7f6df9d8ed240f4663f43ef0de37a", size = 9937, upload-time = "2021-01-03T16:46:35.157Z" },
104]
105
106[[package]]
107name = "frozendict"
108version = "2.4.6"
109source = { registry = "https://pypi.org/simple" }
110sdist = { url = "https://files.pythonhosted.org/packages/bb/59/19eb300ba28e7547538bdf603f1c6c34793240a90e1a7b61b65d8517e35e/frozendict-2.4.6.tar.gz", hash = "sha256:df7cd16470fbd26fc4969a208efadc46319334eb97def1ddf48919b351192b8e", size = 316416, upload-time = "2024-10-13T12:15:32.449Z" }
111wheels = [
112 { url = "https://files.pythonhosted.org/packages/04/13/d9839089b900fa7b479cce495d62110cddc4bd5630a04d8469916c0e79c5/frozendict-2.4.6-py311-none-any.whl", hash = "sha256:d065db6a44db2e2375c23eac816f1a022feb2fa98cbb50df44a9e83700accbea", size = 16148, upload-time = "2024-10-13T12:15:26.839Z" },
113 { url = "https://files.pythonhosted.org/packages/ba/d0/d482c39cee2ab2978a892558cf130681d4574ea208e162da8958b31e9250/frozendict-2.4.6-py312-none-any.whl", hash = "sha256:49344abe90fb75f0f9fdefe6d4ef6d4894e640fadab71f11009d52ad97f370b9", size = 16146, upload-time = "2024-10-13T12:15:28.16Z" },
114 { url = "https://files.pythonhosted.org/packages/a5/8e/b6bf6a0de482d7d7d7a2aaac8fdc4a4d0bb24a809f5ddd422aa7060eb3d2/frozendict-2.4.6-py313-none-any.whl", hash = "sha256:7134a2bb95d4a16556bb5f2b9736dceb6ea848fa5b6f3f6c2d6dba93b44b4757", size = 16146, upload-time = "2024-10-13T12:15:29.495Z" },
115]
116
117[[package]]
118name = "idna"
119version = "3.10"
120source = { registry = "https://pypi.org/simple" }
121sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
122wheels = [
123 { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
124]
125
126[[package]]
127name = "jsonpickle"
128version = "4.0.5"
129source = { registry = "https://pypi.org/simple" }
130sdist = { url = "https://files.pythonhosted.org/packages/d6/33/4bda317ab294722fcdfff8f63aab74af9fda3675a4652d984a101aa7587e/jsonpickle-4.0.5.tar.gz", hash = "sha256:f299818b39367c361b3f26bdba827d4249ab5d383cd93144d0f94b5417aacb35", size = 315661, upload-time = "2025-03-29T19:22:56.92Z" }
131wheels = [
132 { url = "https://files.pythonhosted.org/packages/dc/1b/0e79cf115e0f54f1e8f56effb6ffd2ef8f92e9c324d692ede660067f1bfe/jsonpickle-4.0.5-py3-none-any.whl", hash = "sha256:b4ac7d0a75ddcdfd93445737f1d36ff28768690d43e54bf5d0ddb1d915e580df", size = 46382, upload-time = "2025-03-29T19:22:54.252Z" },
133]
134
135[[package]]
136name = "python-dateutil"
137version = "2.9.0.post0"
138source = { registry = "https://pypi.org/simple" }
139dependencies = [
140 { name = "six" },
141]
142sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
143wheels = [
144 { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
145]
146
147[[package]]
148name = "pyxdg"
149version = "0.28"
150source = { registry = "https://pypi.org/simple" }
151sdist = { url = "https://files.pythonhosted.org/packages/b0/25/7998cd2dec731acbd438fbf91bc619603fc5188de0a9a17699a781840452/pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4", size = 77776, upload-time = "2022-06-05T11:35:01Z" }
152wheels = [
153 { url = "https://files.pythonhosted.org/packages/e5/8d/cf41b66a8110670e3ad03dab9b759704eeed07fa96e90fdc0357b2ba70e2/pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab", size = 49520, upload-time = "2022-06-05T11:34:58.832Z" },
154]
155
156[[package]]
157name = "requests"
158version = "2.32.3"
159source = { registry = "https://pypi.org/simple" }
160dependencies = [
161 { name = "certifi" },
162 { name = "charset-normalizer" },
163 { name = "idna" },
164 { name = "urllib3" },
165]
166sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
167wheels = [
168 { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
169]
170
171[[package]]
172name = "six"
173version = "1.17.0"
174source = { registry = "https://pypi.org/simple" }
175sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
176wheels = [
177 { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
178]
179
180[[package]]
181name = "tabulate"
182version = "0.9.0"
183source = { registry = "https://pypi.org/simple" }
184sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" }
185wheels = [
186 { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" },
187]
188
189[[package]]
190name = "toml"
191version = "0.10.2"
192source = { registry = "https://pypi.org/simple" }
193sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
194wheels = [
195 { url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
196]
197
198[[package]]
199name = "uritools"
200version = "4.0.3"
201source = { registry = "https://pypi.org/simple" }
202sdist = { url = "https://files.pythonhosted.org/packages/d3/43/4182fb2a03145e6d38698e38b49114ce59bc8c79063452eb585a58f8ce78/uritools-4.0.3.tar.gz", hash = "sha256:ee06a182a9c849464ce9d5fa917539aacc8edd2a4924d1b7aabeeecabcae3bc2", size = 24184, upload-time = "2024-05-28T18:07:45.194Z" }
203wheels = [
204 { url = "https://files.pythonhosted.org/packages/e6/17/5a4510d9ca9cc8be217ce359eb54e693dca81cf4d442308b282d5131b17d/uritools-4.0.3-py3-none-any.whl", hash = "sha256:bae297d090e69a0451130ffba6f2f1c9477244aa0a5543d66aed2d9f77d0dd9c", size = 10304, upload-time = "2024-05-28T18:07:42.731Z" },
205]
206
207[[package]]
208name = "urllib3"
209version = "2.3.0"
210source = { registry = "https://pypi.org/simple" }
211sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268, upload-time = "2024-12-22T07:47:30.032Z" }
212wheels = [
213 { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" },
214]
215
216[[package]]
217name = "worktime"
218version = "1.0.0"
219source = { editable = "." }
220dependencies = [
221 { name = "atomicwriter" },
222 { name = "desktop-notify" },
223 { name = "frozendict" },
224 { name = "jsonpickle" },
225 { name = "python-dateutil" },
226 { name = "pyxdg" },
227 { name = "requests" },
228 { name = "tabulate" },
229 { name = "toml" },
230 { name = "uritools" },
231]
232
233[package.metadata]
234requires-dist = [
235 { name = "atomicwriter", specifier = ">=0.2.5" },
236 { name = "desktop-notify", specifier = ">=1.3.3" },
237 { name = "frozendict", specifier = ">=2.4.6" },
238 { name = "jsonpickle", specifier = ">=4.0.5,<5" },
239 { name = "python-dateutil", specifier = ">=2.9.0.post0,<3" },
240 { name = "pyxdg", specifier = ">=0.28,<0.29" },
241 { name = "requests", specifier = ">=2.32.3,<3" },
242 { name = "tabulate", specifier = ">=0.9.0,<0.10" },
243 { name = "toml", specifier = ">=0.10.2,<0.11" },
244 { name = "uritools", specifier = ">=4.0.3,<5" },
245]
246
247[package.metadata.requires-dev]
248dev = []
diff --git a/overlays/worktime/worktime/__main__.py b/overlays/worktime/worktime/__main__.py
index 4eee5dc2..bf24bbec 100755
--- a/overlays/worktime/worktime/__main__.py
+++ b/overlays/worktime/worktime/__main__.py
@@ -1,10 +1,12 @@
1import requests 1import requests
2from requests.exceptions import HTTPError 2from requests.exceptions import HTTPError
3from requests.auth import HTTPBasicAuth 3from requests.auth import HTTPBasicAuth
4from requests.adapters import HTTPAdapter, Retry
4from datetime import * 5from datetime import *
5from xdg import BaseDirectory 6from xdg import BaseDirectory
6import toml 7import toml
7from uritools import (uricompose) 8from uritools import uricompose
9from urllib.parse import urljoin
8 10
9from inspect import signature 11from inspect import signature
10 12
@@ -27,77 +29,76 @@ from sys import stderr, stdout
27 29
28from tabulate import tabulate 30from tabulate import tabulate
29 31
30from itertools import groupby, count 32from itertools import groupby, count, islice
31from functools import cache, partial 33from functools import cache, partial
32 34
33import backoff
34
35from pathlib import Path 35from pathlib import Path
36 36
37from collections import defaultdict 37from collections import defaultdict
38from collections.abc import Iterable, Generator
39from typing import Any
38 40
39import jsonpickle 41import jsonpickle
40from hashlib import blake2s 42from hashlib import blake2s
41import json 43import json
42 44
43class TogglAPISection(Enum): 45import asyncio
44 TOGGL = '/api/v9' 46
45 REPORTS = '/reports/api/v2' 47from frozendict import frozendict
46 48from contextlib import closing
47class TogglAPIError(Exception): 49import os
48 def __init__(self, response, *, http_error=None): 50from time import clock_gettime_ns, CLOCK_MONOTONIC
49 self.http_error = http_error 51from atomicwriter import AtomicWriter
50 self.response = response 52import desktop_notify.aio as notify
51 53
52 def __str__(self): 54class BearerAuth(requests.auth.AuthBase):
53 if not self.http_error is None: 55 def __init__(self, token):
54 return str(self.http_error) 56 self.token = token
55 else: 57 def __call__(self, r):
56 return self.response.text 58 r.headers["authorization"] = "Bearer " + self.token
57 59 return r
58class TogglAPI(object): 60
59 def __init__(self, api_token, workspace_id, client_ids): 61class KimaiSession(requests.Session):
60 self._api_token = api_token 62 def __init__(self, base_url: str, api_token: str):
61 self._workspace_id = workspace_id 63 super().__init__()
62 self._client_ids = set(map(int, client_ids.split(','))) if client_ids else None 64 self.base_url = base_url
63 65 self.auth = BearerAuth(api_token)
64 def _make_url(self, api=TogglAPISection.TOGGL, section=['me', 'time_entries', 'current'], params={}): 66 retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
65 if api is TogglAPISection.REPORTS: 67 super().mount(base_url, HTTPAdapter(max_retries=retries))
66 params.update({'user_agent': 'worktime', 'workspace_id': self._workspace_id}) 68
67 69 def request(self, method, url, *args, **kwargs):
68 api_path = api.value 70 joined_url = urljoin(self.base_url, url)
69 section_path = '/'.join(section) 71 return super().request(method, joined_url, *args, headers = {'Accept': 'application/json'} | (kwargs['headers'] if 'headers' in kwargs else {}), **{k: v for k, v in kwargs.items() if k not in ['headers']})
70 uri = uricompose(scheme='https', host='api.track.toggl.com', path=f"{api_path}/{section_path}", query=params) 72
71 73class KimaiAPI(object):
72 return uri 74 def __init__(self, base_url: str, api_token: str, clients: Iterable[str]):
73 75 self._session = KimaiSession(base_url, api_token)
74 def _query(self, url, method): 76 self._kimai_clients = self._session.get('/api/customers').json()
75 response = self._raw_query(url, method) 77 self._client_ids = self.resolve_clients(clients)
76 response.raise_for_status() 78 kimai_user = self._session.get('/api/users/me').json()
77 return response 79 self._tz = gettz(kimai_user['timezone'])
78 80
79 @backoff.on_predicate( 81 def resolve_clients(self, clients: Iterable[str]) -> frozenset[int]:
80 backoff.expo, 82 return frozenset({ client['id'] for client in self._kimai_clients if client['name'] in clients })
81 factor=0.1, max_value=2, 83
82 predicate=lambda r: r.status_code == 429, 84 def render_datetime(self, datetime: datetime) -> str:
83 max_time=10, 85 return datetime.astimezone(self._tz).strftime('%Y-%m-%dT%H:%M:%S')
84 ) 86
85 def _raw_query(self, url, method): 87 def get_timesheets(self, params: dict[str, Any] = {}) -> Generator[Any]:
86 headers = {'content-type': 'application/json', 'accept': 'application/json'} 88 for page in count(start=1):
87 response = None 89 resp = self._session.get('/api/timesheets', params=params | {'size': 100, 'page': page})
88 90 if resp.status_code == 404:
89 if method == 'GET': 91 break
90 response = requests.get(url, headers=headers, auth=HTTPBasicAuth(self._api_token, 'api_token')) 92 yield from resp.json()
91 elif method == 'POST':
92 response = requests.post(url, headers=headers, auth=HTTPBasicAuth(self._api_token, 'api_token'))
93 else:
94 raise ValueError(f"Undefined HTTP method “{method}”")
95
96 return response
97 93
98 def entry_durations(self, start_date, *, end_date, rounding=False, client_ids): 94 def entry_durations(self, start_date: datetime, *, end_date: datetime, clients: Iterable[str] | None = None) -> Generator[timedelta]:
99 if client_ids is not None and not client_ids: 95 client_ids = None
96 if clients is not None and not clients:
100 return 97 return
98 elif clients is None:
99 client_ids = self._client_ids
100 else:
101 client_ids = self.resolve_clients(clients)
101 102
102 cache_dir = Path(BaseDirectory.save_cache_path('worktime')) / 'entry_durations' 103 cache_dir = Path(BaseDirectory.save_cache_path('worktime')) / 'entry_durations'
103 step = timedelta(days = 120) 104 step = timedelta(days = 120)
@@ -116,11 +117,8 @@ class TogglAPI(object):
116 cache_key = blake2s(jsonpickle.encode({ 117 cache_key = blake2s(jsonpickle.encode({
117 'start': req_start, 118 'start': req_start,
118 'end': req_end, 119 'end': req_end,
119 'rounding': rounding, 120 'client_ids': client_ids,
120 'clients': client_ids, 121 }).encode('utf-8'), key = self._session.auth.token.encode('utf-8')).hexdigest()
121 'workspace': self._workspace_id,
122 'workspace_clients': self._client_ids
123 }).encode('utf-8'), key = self._api_token.encode('utf-8')).hexdigest()
124 cache_path = cache_dir / cache_key[:2] / cache_key[2:4] / f'{cache_key[4:]}.json' 122 cache_path = cache_dir / cache_key[:2] / cache_key[2:4] / f'{cache_key[4:]}.json'
125 try: 123 try:
126 with cache_path.open('r', encoding='utf-8') as ch: 124 with cache_path.open('r', encoding='utf-8') as ch:
@@ -130,85 +128,83 @@ class TogglAPI(object):
130 pass 128 pass
131 129
132 entries = list() 130 entries = list()
133 params = { 'since': (req_start - timedelta(days=1)).date().isoformat(), 131 params = {
134 'until': (req_end + timedelta(days=1)).date().isoformat(), 132 'begin': self.render_datetime(req_start),
135 'rounding': 'yes' if rounding else 'no', 133 'end': self.render_datetime(req_end),
136 'billable': 'yes' 134 'customers[]': list(client_ids),
137 } 135 'billable': 1,
138 if client_ids is not None: 136 }
139 params |= { 'client_ids': ','.join(map(str, client_ids)) } 137
140 for page in count(start = 1): 138 for entry in self.get_timesheets(params):
141 url = self._make_url(api = TogglAPISection.REPORTS, section = ['details'], params = params | { 'page': page }) 139 if entry['end'] is None:
142 r = self._query(url = url, method='GET') 140 continue
143 if not r or not r.json(): 141
144 raise TogglAPIError(r) 142 start = isoparse(entry['begin'])
145 report = r.json() 143 end = isoparse(entry['end'])
146 for entry in report['data']: 144
147 start = isoparse(entry['start']) 145 if start > req_end or end < req_start:
148 end = isoparse(entry['end']) 146 continue
149
150 if start > req_end or end < req_start:
151 continue
152 147
153 x = min(end, req_end) - max(start, req_start) 148 x = min(end, req_end) - max(start, req_start)
154 if cache_key: 149 if cache_key:
155 entries.append(x) 150 entries.append(x)
156 yield x 151 yield x
157 if not report['data']:
158 break
159 152
160 if cache_path: 153 if cache_path:
161 cache_path.parent.mkdir(parents=True, exist_ok=True) 154 cache_path.parent.mkdir(parents=True, exist_ok=True)
162 with cache_path.open('w', encoding='utf-8') as ch: 155 with cache_path.open('w', encoding='utf-8') as ch:
163 ch.write(jsonpickle.encode(entries)) 156 ch.write(jsonpickle.encode(entries))
164 # res = timedelta(milliseconds=report['total_billable']) if report['total_billable'] else timedelta(milliseconds=0)
165 # return res
166 157
167 def get_billable_hours(self, start_date, end_date=datetime.now(timezone.utc), rounding=False): 158 def get_billable_hours(self, start_date: datetime, end_date: datetime = datetime.now(timezone.utc)) -> timedelta:
168 billable_acc = timedelta(milliseconds = 0) 159 return sum(self.entry_durations(start_date, end_date=end_date), start=timedelta(milliseconds=0))
169 if 0 in self._client_ids:
170 url = self._make_url(api = TogglAPISection.TOGGL, section = ['workspaces', self._workspace_id, 'clients'])
171 r = self._query(url = url, method = 'GET')
172 if not r or not r.json():
173 raise TogglAPIError(r)
174 160
175 billable_acc += sum(self.entry_durations(start_date, end_date=end_date, rounding=rounding, client_ids=None), start=timedelta(milliseconds=0)) - sum(self.entry_durations(start_date, end_date=end_date, rounding=rounding, client_ids=frozenset(map(lambda c: c['id'], r.json()))), start=timedelta(milliseconds=0)) 161 def get_running_entry(self) -> Any | None:
176 162 kimai_entries = self._session.get('/api/timesheets/active').json()
177 billable_acc += sum(self.entry_durations(start_date, end_date=end_date, rounding=rounding, client_ids=frozenset(*(self._client_ids - {0}))), start=timedelta(milliseconds=0)) 163 if not kimai_entries:
178 164 return None
179 return billable_acc 165 entry = kimai_entries[0]
180 166
181 def get_running_clock(self, now=datetime.now(timezone.utc)): 167 if entry['project']['customer']['id'] not in self._client_ids:
182 url = self._make_url(api = TogglAPISection.TOGGL, section = ['me', 'time_entries', 'current']) 168 return None
183 r = self._query(url = url, method='GET')
184 169
185 if not r or (not r.json() and r.json() is not None): 170 return entry
186 raise TogglAPIError(r)
187 171
188 if not r.json() or not r.json()['billable']: 172 def get_running_clock(self, now: datetime = datetime.now(timezone.utc)) -> timedelta | None:
173 entry = self.get_running_entry()
174 if not entry:
189 return None 175 return None
176 start = isoparse(entry['begin'])
177 return now - start if start <= now else None
190 178
191 if self._client_ids is not None: 179 def get_recent_entries(self) -> Generator[Any]:
192 if 'pid' in r.json() and r.json()['pid']: 180 step = timedelta(days = 7)
193 url = self._make_url(api = TogglAPISection.TOGGL, section = ['projects', str(r.json()['pid'])]) 181 now = datetime.now().astimezone(timezone.utc)
194 pr = self._query(url = url, method = 'GET') 182 ids = set()
195 if not pr or not pr.json(): 183 for req_end in (now - step * i for i in count()):
196 raise TogglAPIError(pr) 184 params = {
185 'begin': self.render_datetime(req_end - step),
186 'end': self.render_datetime(req_end),
187 'full': 'true',
188 }
189 for entry in self.get_timesheets(params):
190 if entry['id'] in ids:
191 continue
192 ids.add(entry['id'])
193 yield entry
197 194
198 if not pr.json(): 195 def start_clock(self, project_id: int, activity_id: int, description: str | None = None, tags: Iterable[str] | None = None, billable: bool = True):
199 return None 196 self._session.post('/api/timesheets', json={
197 'begin': self.render_datetime(datetime.now()),
198 'project': project_id,
199 'activity': activity_id,
200 'description': description if description else '',
201 'tags': (','.join(tags)) if tags else '',
202 'billable': billable,
203 }).raise_for_status()
200 204
201 if 'cid' in pr.json() and pr.json()['cid']: 205 def stop_clock(self, running_id: int):
202 if pr.json()['cid'] not in self._client_ids: 206 self._session.patch(f'/api/timesheets/{running_id}/stop').raise_for_status()
203 return None
204 elif 0 not in self._client_ids:
205 return None
206 elif 0 not in self._client_ids:
207 return None
208 207
209 start = isoparse(r.json()['start'])
210
211 return now - start if start <= now else None
212 208
213class Worktime(object): 209class Worktime(object):
214 time_worked = timedelta() 210 time_worked = timedelta()
@@ -281,10 +277,10 @@ class Worktime(object):
281 277
282 config = Worktime.config() 278 config = Worktime.config()
283 config_dir = BaseDirectory.load_first_config('worktime') 279 config_dir = BaseDirectory.load_first_config('worktime')
284 api = TogglAPI( 280 api = KimaiAPI(
285 api_token=config.get("TOGGL", {}).get("ApiToken", None), 281 base_url=config.get("KIMAI", {}).get("BaseUrl", None),
286 workspace_id=config.get("TOGGL", {}).get("Workspace", None), 282 api_token=config.get("KIMAI", {}).get("ApiToken", None),
287 client_ids=config.get("TOGGL", {}).get("ClientIds", None) 283 clients=config.get("KIMAI", {}).get("Clients", None)
288 ) 284 )
289 date_format = config.get("WORKTIME", {}).get("DateFormat", '%Y-%m-%d') 285 date_format = config.get("WORKTIME", {}).get("DateFormat", '%Y-%m-%d')
290 286
@@ -496,7 +492,7 @@ class Worktime(object):
496 492
497 self.time_to_work += self.time_pulled_forward 493 self.time_to_work += self.time_pulled_forward
498 494
499 self.time_worked += api.get_billable_hours(self.start_date, self.now, rounding = config.get("WORKTIME", {}).get("rounding", True)) 495 self.time_worked += api.get_billable_hours(self.start_date, self.now)
500 496
501def format_days(worktime, days, date_format=None): 497def format_days(worktime, days, date_format=None):
502 if not date_format: 498 if not date_format:
@@ -886,7 +882,7 @@ def main():
886 882
887 config = Worktime.config() 883 config = Worktime.config()
888 884
889 parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using toggl API') 885 parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using Kimai API')
890 parser.add_argument('--time', dest = 'now', metavar = 'TIME', type = isotime, help = 'Time to calculate status for (default: current time)', default = datetime.now(tzlocal())) 886 parser.add_argument('--time', dest = 'now', metavar = 'TIME', type = isotime, help = 'Time to calculate status for (default: current time)', default = datetime.now(tzlocal()))
891 parser.add_argument('--start', dest = 'start_datetime', metavar = 'TIME', type = isotime, help = 'Time to calculate status from (default: None)', default = None) 887 parser.add_argument('--start', dest = 'start_datetime', metavar = 'TIME', type = isotime, help = 'Time to calculate status from (default: None)', default = None)
892 parser.add_argument('--no-running', dest = 'include_running', action = 'store_false') 888 parser.add_argument('--no-running', dest = 'include_running', action = 'store_false')
@@ -923,5 +919,139 @@ def main():
923 919
924 args.cmd(**vars(args)) 920 args.cmd(**vars(args))
925 921
922async def ui_update_options(api, cache_path):
923 options = set()
924 sort_order = dict()
925 entry_iter = enumerate(api.get_recent_entries())
926 loop = asyncio.get_event_loop()
927 start = clock_gettime_ns(CLOCK_MONOTONIC)
928 while item := await loop.run_in_executor(None, next, entry_iter):
929 ix, entry = item
930 if len(options) >= 20 or ix >= 1000:
931 break
932 elif len(options) >= 3:
933 now = clock_gettime_ns(CLOCK_MONOTONIC)
934 if now - start >= 4000000000:
935 break
936
937 option = frozendict({
938 'tags': frozenset(entry['tags']),
939 'activity': frozendict({'id': entry['activity']['id'], 'name': entry['activity']['name']}),
940 'project': frozendict({'id': entry['project']['id'], 'customer': entry['project']['customer']['name'], 'name': entry['project']['name']}),
941 'description': entry['description'] if entry['description'] else None,
942 'billable': entry['billable'],
943 })
944 sort_value = isoparse(entry['begin'])
945 if option in sort_order:
946 sort_value = max(sort_value, sort_order[option])
947 sort_order[option] = sort_value
948 options.add(option)
949
950 options = list(sorted(options, key = lambda o: sort_order[o], reverse = True))
951
952 with AtomicWriter(cache_path, overwrite=True) as ch:
953 ch.write_text(jsonpickle.encode(options))
954
955 return options
956
957def ui_render_option(option):
958 res = ''
959 if option['description']:
960 res += '„{}“, '.format(option['description'])
961 res += option['activity']['name'] + ', '
962 res += option['project']['name']
963 if option['project']['customer'] not in option['project']['name']:
964 res += ' ({})'.format(option['project']['customer'])
965 if option['tags']:
966 res += ', {}'.format(' '.join(map(lambda t: '#{}'.format(t), option['tags'])))
967 if not option['billable']:
968 res += ', not billable'
969 return res
970
971async def ui_main():
972 cache_path = Path(BaseDirectory.save_cache_path('worktime-ui')) / 'options.json'
973 options = None
974 try:
975 with cache_path.open('r', encoding='utf-8') as ch:
976 options = jsonpickle.decode(ch.read())
977 except FileNotFoundError:
978 pass
979
980 config = Worktime.config()
981 api = KimaiAPI(
982 base_url=config.get("KIMAI", {}).get("BaseUrl", None),
983 api_token=config.get("KIMAI", {}).get("ApiToken", None),
984 clients=config.get("KIMAI", {}).get("Clients", None)
985 )
986 running_entry = api.get_running_entry()
987
988 async with asyncio.TaskGroup() as tg:
989 update_options = tg.create_task(ui_update_options(api, cache_path))
990 if not options:
991 options = await update_options
992
993 read_fd, write_fd = os.pipe()
994 w_pipe = open(write_fd, 'wb', 0)
995 loop = asyncio.get_event_loop()
996 w_transport, _ = await loop.connect_write_pipe(
997 asyncio.Protocol,
998 w_pipe,
999 )
1000 r_pipe = open(read_fd, 'rb', 0)
1001
1002 proc = await asyncio.create_subprocess_exec(
1003 "fuzzel", "--dmenu", "--index", "--width=60",
1004 stdout = asyncio.subprocess.PIPE,
1005 stdin = r_pipe,
1006 )
1007
1008 with closing(w_transport) as t:
1009 if running_entry:
1010 t.write(b'Stop running timesheet\n')
1011 for option in options:
1012 t.write(ui_render_option(option).encode('utf-8') + b'\n')
1013
1014 stdout, _ = await proc.communicate()
1015 if proc.returncode != 0:
1016 return
1017 fuzzel_out = int(stdout.decode('utf-8'))
1018 if fuzzel_out < 0 or fuzzel_out >= len(options):
1019 return
1020 elif running_entry and fuzzel_out == 0:
1021 api.stop_clock(running_entry['id'])
1022 await notify.Server('worktime').Notify("Stopped running timesheet").set_timeout(65000).show()
1023 else:
1024 if running_entry:
1025 fuzzel_out -= 1
1026 option = options[fuzzel_out]
1027 api.start_clock(
1028 project_id = option['project']['id'],
1029 activity_id = option['activity']['id'],
1030 description = option['description'],
1031 tags = option['tags'],
1032 billable = option['billable'],
1033 )
1034 await notify.Server('worktime').Notify("Timesheet started…").set_timeout(65000).show()
1035
1036
1037def ui():
1038 asyncio.run(ui_main())
1039
1040async def stop_main():
1041 config = Worktime.config()
1042 api = KimaiAPI(
1043 base_url=config.get("KIMAI", {}).get("BaseUrl", None),
1044 api_token=config.get("KIMAI", {}).get("ApiToken", None),
1045 clients=config.get("KIMAI", {}).get("Clients", None)
1046 )
1047 if running_entry := api.get_running_entry():
1048 api.stop_clock(running_entry['id'])
1049 await notify.Server('worktime').Notify("Stopped running timesheet").set_timeout(65000).show()
1050 else:
1051 await notify.Server('worktime').Notify("No timesheet currently running").set_timeout(65000).show()
1052
1053def stop():
1054 asyncio.run(stop_main())
1055
926if __name__ == "__main__": 1056if __name__ == "__main__":
927 sys.exit(main()) 1057 sys.exit(main())
diff --git a/overlays/zte-prometheus-exporter/default.nix b/overlays/zte-prometheus-exporter/default.nix
index 2188e7b3..cd4207cd 100644
--- a/overlays/zte-prometheus-exporter/default.nix
+++ b/overlays/zte-prometheus-exporter/default.nix
@@ -2,17 +2,16 @@
2let 2let
3 packageOverrides = final.callPackage ./python-packages.nix {}; 3 packageOverrides = final.callPackage ./python-packages.nix {};
4 inpPython = final.python310.override { inherit packageOverrides; }; 4 inpPython = final.python310.override { inherit packageOverrides; };
5 python = inpPython.withPackages (ps: with ps; [pytimeparse requests]);
5in { 6in {
6 zte-prometheus-exporter = prev.stdenv.mkDerivation rec { 7 zte-prometheus-exporter = prev.stdenv.mkDerivation rec {
7 name = "zte-prometheus-exporter"; 8 name = "zte-prometheus-exporter";
8 src = ./zte-prometheus-exporter.py; 9 src = prev.replaceVars ./zte-prometheus-exporter.py { inherit python; };
9 10
10 phases = [ "buildPhase" "checkPhase" "installPhase" ]; 11 phases = [ "unpackPhase" "checkPhase" "installPhase" ];
11 12
12 python = inpPython.withPackages (ps: with ps; [pytimeparse requests]); 13 unpackPhase = ''
13 14 cp $src zte-prometheus-exporter
14 buildPhase = ''
15 substituteAll $src zte-prometheus-exporter
16 ''; 15 '';
17 16
18 doCheck = true; 17 doCheck = true;
diff --git a/shell.nix b/shell.nix
index 5bcb8180..1b334187 100644
--- a/shell.nix
+++ b/shell.nix
@@ -1,19 +1,29 @@
1inputs@{ system, self, deploy-rs, nvfetcher, nixpkgs, ca-util, ... }: 1inputs@{ system, self, nvfetcher, nixpkgs, ca-util, ... }:
2let 2let
3 pkgs = self.legacyPackages.${system}; 3 pkgs = self.legacyPackages.${system};
4 utils = import ./utils { inherit (nixpkgs) lib; }; 4 utils = import ./utils { inherit (nixpkgs) lib; };
5 inherit (utils) nixImport; 5 inherit (utils) nixImport;
6 uv-links = pkgs.symlinkJoin {
7 name = "uv-links";
8 paths = [
9 pkgs.python312.pkgs.pygobject3
10 ];
11 };
6in pkgs.mkShell { 12in pkgs.mkShell {
7 nativeBuildInputs = builtins.attrValues self.packages.${system} ++ (with pkgs; [ 13 nativeBuildInputs = builtins.attrValues self.packages.${system} ++ (with pkgs; [
8 sops 14 sops
9 wireguard-tools 15 wireguard-tools
10 gup 16 gup
11 nftables 17 nftables
12 deploy-rs.packages.${system}.deploy-rs 18 deploy-rs.deploy-rs
13 knot-dns 19 knot-dns
14 yq 20 yq
15 nvfetcher.packages.${system}.default 21 nvfetcher.packages.${system}.default
16 ca-util.packages.${system}.ca 22 ca-util.packages.${system}.ca
17 poetry 23 poetry uv
24 ninja pkg-config cairo.dev systemd.dev
18 ]); 25 ]);
26 shellHook = ''
27 export UV_FIND_LINKS=${uv-links}/lib/python3.12/site-packages
28 '';
19} 29}
diff --git a/system-profiles/core/default.nix b/system-profiles/core/default.nix
index b85aea4e..229a007e 100644
--- a/system-profiles/core/default.nix
+++ b/system-profiles/core/default.nix
@@ -127,36 +127,16 @@ in {
127 127
128 flake-registry = "${flakeInputs.flake-registry}/flake-registry.json"; 128 flake-registry = "${flakeInputs.flake-registry}/flake-registry.json";
129 }; 129 };
130 nixPath = [ 130 nixPath = map (flake: "${flake}=flake:${flake}") (attrNames config.nix.registry);
131 "nixpkgs=${pkgs.runCommand "nixpkgs" {} ''
132 mkdir $out
133 ln -s ${./nixpkgs.nix} $out/default.nix
134 ln -s /run/nixpkgs/lib $out/lib
135 ''}"
136 ];
137 registry = 131 registry =
138 let override = { self = "nixos"; }; 132 let override = { self = "nixos"; };
139 in mapAttrs' (inpName: inpFlake: nameValuePair 133 in mapAttrs' (inpName: inpFlake: nameValuePair
140 (override.${inpName} or inpName) 134 (override.${inpName} or inpName)
141 { flake = inpFlake; } ) flakeInputs; 135 { to = { type = "path"; path = inpFlake; }; } ) flakeInputs;
142 }; 136 };
143 137
144 systemd.tmpfiles.rules = [ 138 systemd.tmpfiles.rules = [
145 "L+ /run/nixpkgs - - - - ${flakeInputs.${config.nixpkgs.flakeInput}.outPath}" 139 "L+ /run/nixpkgs - - - - ${flakeInputs.${config.nixpkgs.flakeInput}.outPath}"
146 "L+ /run/nixpkgs-overlays.nix - - - - ${pkgs.writeText "overlays.nix" ''
147 with builtins;
148
149 attrValues (import
150 (
151 let lock = fromJSON (readFile ${flake + "/flake.lock"}); in
152 fetchTarball {
153 url = "https://github.com/edolstra/flake-compat/archive/''${lock.nodes.flake-compat.locked.rev}.tar.gz";
154 sha256 = lock.nodes.flake-compat.locked.narHash;
155 }
156 )
157 { src = ${flake}; }
158 ).defaultNix.overlays
159 ''}"
160 "L+ /etc/nixos - - - - ${flake}" 140 "L+ /etc/nixos - - - - ${flake}"
161 ] ++ map (input: "L+ /run/flake-inputs/${input} - - - - ${flakeInputs.${input}.outPath}") (attrNames flakeInputs); 141 ] ++ map (input: "L+ /run/flake-inputs/${input} - - - - ${flakeInputs.${input}.outPath}") (attrNames flakeInputs);
162 142
@@ -177,8 +157,6 @@ in {
177 { 157 {
178 manual.manpages.enable = true; 158 manual.manpages.enable = true;
179 systemd.user.startServices = "sd-switch"; 159 systemd.user.startServices = "sd-switch";
180
181 programs.ssh.internallyManaged = mkForce true;
182 } 160 }
183 ]; 161 ];
184 extraSpecialArgs = { inherit flake flakeInputs path; hostConfig = config; }; 162 extraSpecialArgs = { inherit flake flakeInputs path; hostConfig = config; };
diff --git a/system-profiles/default-locale.nix b/system-profiles/default-locale.nix
index 2d483f04..60d338cb 100644
--- a/system-profiles/default-locale.nix
+++ b/system-profiles/default-locale.nix
@@ -1,16 +1,23 @@
1{ lib, ... }: 1{ lib, options, ... }:
2 2
3with lib; 3with lib;
4 4
5{ 5{
6 i18n = { 6 config = foldr recursiveUpdate {} ([
7 defaultLocale = "en_DK.UTF-8"; 7 {
8 extraLocaleSettings = { 8 i18n = {
9 "TIME_STYLE" = "long-iso"; 9 defaultLocale = "en_DK.UTF-8";
10 }; 10 extraLocaleSettings = {
11 supportedLocales = [ "C.UTF-8/UTF-8" "en_US.UTF-8/UTF-8" "en_DK.UTF-8/UTF-8" ]; 11 "TIME_STYLE" = "long-iso";
12 }; 12 };
13 console.keyMap = mkDefault "dvorak-programmer"; 13 };
14 console.keyMap = mkDefault "dvorak-programmer";
14 15
15 time.timeZone = mkDefault "Europe/Berlin"; 16 time.timeZone = mkDefault "Europe/Berlin";
17 }
18 ] ++ (optional (options ? i18n.extraLocales) {
19 i18n.extraLocales = [ "C.UTF-8" "en_US.UTF-8" "en_DK.UTF-8" ];
20 }) ++ (optional (!(options ? i18n.extraLocales)) {
21 i18n.supportedLocales = [ "C.UTF-8/UTF-8" "en_US.UTF-8/UTF-8" "en_DK.UTF-8/UTF-8" ];
22 }));
16} 23}
diff --git a/system-profiles/nfsroot.nix b/system-profiles/nfsroot.nix
index 1cd930d9..b0116d61 100644
--- a/system-profiles/nfsroot.nix
+++ b/system-profiles/nfsroot.nix
@@ -48,7 +48,7 @@ in {
48 fileSystems."/nix/.ro-store" = mkImageMediaOverride 48 fileSystems."/nix/.ro-store" = mkImageMediaOverride
49 { fsType = "nfs4"; 49 { fsType = "nfs4";
50 device = cfg.storeDevice; 50 device = cfg.storeDevice;
51 options = [ "ro" ]; 51 options = [ "ro" "nfsvers=4.2" ];
52 neededForBoot = true; 52 neededForBoot = true;
53 }; 53 };
54 54
diff --git a/system-profiles/rebuild-machines/default.nix b/system-profiles/rebuild-machines/default.nix
index 544f47e1..de86cd74 100644
--- a/system-profiles/rebuild-machines/default.nix
+++ b/system-profiles/rebuild-machines/default.nix
@@ -25,16 +25,18 @@ let
25 25
26 phases = [ "buildPhase" "installPhase" ]; 26 phases = [ "buildPhase" "installPhase" ];
27 27
28 inherit (pkgs) zsh coreutils openssh;
29 inherit (cfg) scriptName;
30 inherit (cfg.flake) flakeOutput;
31 flake = cfg.flake.name;
32 nixosRebuild = config.system.build.nixos-rebuild;
33 inherit (config.security) wrapperDir;
34 inherit sshConfig;
35
36 buildPhase = '' 28 buildPhase = ''
37 substituteAll $src rebuild-machine.zsh 29 substitute $src rebuild-machine.zsh \
30 --subst-var-by zsh ${pkgs.zsh} \
31 --subst-var-by coreutils ${pkgs.coreutils} \
32 --subst-var-by openssh ${pkgs.openssh} \
33 --subst-var-by wrapperDir ${config.security.wrapperDir} \
34 --subst-var-by sshConfig ${sshConfig} \
35 --subst-var-by out "$out" \
36 --subst-var-by nixosRebuild ${config.system.build.nixos-rebuild} \
37 --subst-var-by flake ${cfg.flake.name} \
38 --subst-var-by scriptName ${cfg.scriptName} \
39 --subst-var-by flakeOutput ${cfg.flake.flakeOutput}
38 ''; 40 '';
39 41
40 installPhase = '' 42 installPhase = ''
diff --git a/user-profiles/core.nix b/user-profiles/core.nix
index a8af48b3..57fb7628 100644
--- a/user-profiles/core.nix
+++ b/user-profiles/core.nix
@@ -5,7 +5,11 @@ with lib;
5{ 5{
6 config = { 6 config = {
7 users.users.${userName} = {}; # Just make sure the user is created 7 users.users.${userName} = {}; # Just make sure the user is created
8 home-manager.users.${userName} = {}; 8 home-manager.users.${userName} = let sysConfig = config; in { config, ... }: {
9 config.nix.settings = {
10 inherit (sysConfig.nix.settings) use-xdg-base-directories;
11 };
12 };
9 13
10 systemd.services."home-manager-${utils.escapeSystemdPath userName}" = lib.mkIf (!config.home-manager.enableSystemd) { 14 systemd.services."home-manager-${utils.escapeSystemdPath userName}" = lib.mkIf (!config.home-manager.enableSystemd) {
11 restartIfChanged = false; # only run once on startup, deploy to running system with deploy-rs 15 restartIfChanged = false; # only run once on startup, deploy to running system with deploy-rs
diff --git a/user-profiles/feeds/alot.config b/user-profiles/feeds/alot.config
deleted file mode 100644
index a14d4539..00000000
--- a/user-profiles/feeds/alot.config
+++ /dev/null
@@ -1,50 +0,0 @@
1attachment_prefix="~/Downloads"
2bug_on_exit=true
3editor_cmd="false"
4tabwidth=2
5timestamp_format="%a %d %b %H:%M:%S %Y UTC%z"
6auto_remove_unread=True
7#initial_command="search ( tag:inbox ) AND NOT ( tag:killed )"
8initial_command="search ( tag:inbox ) AND NOT ( is:link OR is:media OR is:killed )"
9
10[accounts]
11 [[private]]
12 realname = @realname@
13 address = @address@
14
15[bindings]
16j =
17k =
18'g g' =
19G =
20I = search ( tag:inbox ) AND NOT ( is:killed )
21U = search ( tag:inbox ) AND NOT ( is:link OR is:media OR is:killed )
22V = search ( tag:inbox AND is:media OR ( is:live AND date:12h.. AND NOT is:unread ) ) AND NOT ( is:killed )
23W = search ( is:media ) AND NOT ( tag:inbox OR is:killed OR is:highlight )
24L = search ( tag:inbox AND is:link ) AND NOT ( is:killed )
25
26h = move first
27t = move up
28n = move down
29s = move last
30 [[search]]
31 a =
32 s =
33
34 u = toggletags unread
35 i = toggletags inbox
36 j = untag unread,inbox
37 r = toggletags later
38 [[thread]]
39 s =
40 S =
41 n =
42 'g j' =
43 'g k' =
44 'g l' =
45 w = save
46 W = save --all
47 'g h' = move parent
48 'g t' = move next sibling
49 'g n' = move previous sibling
50 'g s' = move first reply \ No newline at end of file
diff --git a/user-profiles/feeds/default.nix b/user-profiles/feeds/default.nix
deleted file mode 100644
index 82be90c7..00000000
--- a/user-profiles/feeds/default.nix
+++ /dev/null
@@ -1,11 +0,0 @@
1{ config, flakeInputs, pkgs, lib, userName, customUtils, ... }:
2{
3 home-manager.users.${userName} = {...}: {
4 imports = [
5 (customUtils.overrideModuleArgs
6 (import ./module.nix)
7 (inputs: inputs // { inherit flakeInputs; inherit (config.nixpkgs) system; })
8 )
9 ];
10 };
11}
diff --git a/user-profiles/feeds/imm-notmuch-insert.py b/user-profiles/feeds/imm-notmuch-insert.py
deleted file mode 100644
index b7eed292..00000000
--- a/user-profiles/feeds/imm-notmuch-insert.py
+++ /dev/null
@@ -1,52 +0,0 @@
1#!@python@/bin/python
2
3import json
4import sys
5import subprocess
6from io import BytesIO
7from email.message import EmailMessage
8import configparser
9from os import environ
10from datetime import *
11from dateutil.tz import *
12from dateutil.parser import isoparse
13from html2text import html2text
14
15def main():
16 notmuchConfig = configparser.ConfigParser()
17 notmuchConfig.read(environ.get('NOTMUCH_CONFIG'))
18
19 callbackMessage = json.load(sys.stdin)
20
21 msg = EmailMessage()
22 authors = ', '.join(map(lambda author: author['name'], callbackMessage['feed_item']['authors']))
23 if authors:
24 msg['From'] = f"{callbackMessage['feed_definition']['title']} ({authors}) <imm@imm.invalid>"
25 else:
26 msg['From'] = f"{callbackMessage['feed_definition']['title']} <imm@imm.invalid>"
27 msg['To'] = f"{notmuchConfig['user']['name']} <{notmuchConfig['user']['primary_email']}>"
28 if 'title' in callbackMessage['feed_item'] and callbackMessage['feed_item']['title']:
29 msg['Subject'] = callbackMessage['feed_item']['title']
30 msg['Item-Identifier'] = f"{callbackMessage['feed_item']['identifier']}"
31 for link in callbackMessage['feed_item']['links']:
32 msg.add_header('Link', link['uri'])
33 date = None
34 if 'date' in callbackMessage['feed_item']:
35 date = isoparse(callbackMessage['feed_item']['date'])
36 else:
37 date = datetime.now(tzlocal())
38 msg['Date'] = date.strftime('%a, %e %b %Y %T %z')
39
40 if 'content' in callbackMessage['feed_item'] and callbackMessage['feed_item']['content']:
41 msg.set_content(html2text(callbackMessage['feed_item']['content']))
42 msg.add_alternative(callbackMessage['feed_item']['content'], subtype='html')
43
44
45 subprocess.run(
46 args=['notmuch', 'insert'],
47 check=True,
48 input=bytes(msg)
49 )
50
51if __name__ == '__main__':
52 sys.exit(main())
diff --git a/user-profiles/feeds/module.nix b/user-profiles/feeds/module.nix
deleted file mode 100644
index 63e827eb..00000000
--- a/user-profiles/feeds/module.nix
+++ /dev/null
@@ -1,236 +0,0 @@
1{ config, flakeInputs, pkgs, lib, system, ... }:
2
3with lib;
4
5let
6 inherit (flakeInputs.home-manager.lib) hm;
7
8 databasePath = "${config.xdg.dataHome}/feeds";
9
10 imm =
11 let
12 hlib = pkgs.haskell.lib;
13 haskellPackages = pkgs.haskellPackages.override {
14 overrides = finalHaskell: prevHaskell: {
15 uri-bytestring = finalHaskell.callCabal2nix "uri-bytestring" (pkgs.fetchFromGitHub {
16 owner = "gkleen";
17 repo = "uri-bytestring";
18 rev = "5f7f32c8274bc4d1b81d99582f5148fe3e8b637e";
19 sha256 = "XLanwyCDIlMuOkpE5LbTNOBfL+1kZX+URfj9Bhs1Nsc=";
20 fetchSubmodules = true;
21 }) {};
22 atom-conduit = finalHaskell.callCabal2nix "atom-conduit" (pkgs.fetchFromGitHub {
23 owner = "gkleen";
24 repo = "atom-conduit";
25 rev = "022f0182a02373f87c06a0a09817c8c41efe2425";
26 sha256 = "8yEyh3ymqkoM/YP+eBqPq1I5ofzj0Qn7ojL7IWx1DPo=";
27 fetchSubmodules = true;
28 }) {};
29 rss-conduit = finalHaskell.callCabal2nix "rss-condit" (pkgs.fetchFromGitHub {
30 owner = "gkleen";
31 repo = "rss-conduit";
32 rev = "dbb0960a8d3dc519f1607aa0223b3a25a49282ef";
33 sha256 = "Md1XApZWkdv4JvNoaVnjz0S85LbEC6w9U3PUcwXfu94=";
34 fetchSubmodules = true;
35 }) {};
36 beam-core = hlib.doJailbreak (finalHaskell.callCabal2nix "beam-core" "${beamSrc}/beam-core" {});
37 beam-migrate = hlib.doJailbreak (finalHaskell.callCabal2nix "beam-migrate" "${beamSrc}/beam-migrate" {});
38 beam-sqlite = hlib.doJailbreak (finalHaskell.callCabal2nix "beam-sqlite" "${beamSrc}/beam-sqlite" {});
39
40 imm = finalHaskell.callCabal2nix "imm" (pkgs.fetchFromGitHub {
41 owner = "k0ral";
42 repo = "imm";
43 rev = "5033879667264cb44cee65671a66f6aa43f249e7";
44 sha256 = "PG22caLQmAGhLZP49HsazuNd8IFKKaTuhXIQBD8v4Fs=";
45 fetchSubmodules = true;
46 }) {};
47 };
48 };
49 beamSrc = pkgs.fetchFromGitHub {
50 owner = "haskell-beam";
51 repo = "beam";
52 rev = "efd464b079755a781c2bb7a2fc030d6c141bbb8a";
53 sha256 = "8nTuBP/vD0L/qMo4h3XNrGZvpIwXuMVdj40j5gvHU6w=";
54 fetchSubmodules = true;
55 };
56 in haskellPackages.imm;
57 immWrapped = pkgs.runCommand "${imm.name}-wrapped-${config.home.username}"
58 { nativeBuildInputs = with pkgs; [ makeWrapper ];
59 } ''
60 mkdir -p $out/bin
61 makeWrapper ${imm}/bin/imm $out/bin/imm \
62 --add-flags --callbacks=${notmuchCallbacks}
63 '';
64
65 notmuchCallbacks = pkgs.writeText "imm-callbacks-${config.home.username}.dhall" ''
66 [ { _executable = "${immNotmuchInsert}/bin/imm-notmuch-insert"
67 , _arguments = [] : List Text
68 }
69 ]
70 '';
71
72 immNotmuchInsert = pkgs.stdenv.mkDerivation rec {
73 name = "imm-notmuch-insert-${config.home.username}";
74 src = ./imm-notmuch-insert.py;
75
76 phases = [ "buildPhase" "checkPhase" "installPhase" "fixupPhase" ];
77
78 python = pkgs.python39.withPackages (ps: with ps; [ configparser python-dateutil html2text ]);
79
80 nativeBuildInputs = with pkgs; [ makeWrapper ];
81
82 buildPhase = ''
83 substituteAll $src imm-notmuch-insert
84 '';
85
86 doCheck = true;
87 checkPhase = ''
88 ${python}/bin/python -m py_compile imm-notmuch-insert
89 '';
90
91 installPhase = ''
92 install -m 0755 -D -t $out/bin \
93 imm-notmuch-insert
94 '';
95
96 fixupPhase = ''
97 wrapProgram $out/bin/imm-notmuch-insert \
98 --prefix PATH : ${pkgs.notmuch}/bin \
99 --set NOTMUCH_CONFIG ${configPath}
100 '';
101 };
102
103 mkIniKeyValue = key: value:
104 let
105 tweakVal = v:
106 if isString v then
107 v
108 else if isList v then
109 concatMapStringsSep ";" tweakVal v
110 else if isBool v then
111 (if v then "true" else "false")
112 else
113 toString v;
114 in "${key}=${tweakVal value}";
115
116 notmuchIni = {
117 database = { path = databasePath; };
118
119 maildir = { synchronize_flags = false; };
120
121 new = {
122 ignore = [];
123 tags = ["new"];
124 };
125
126 user = {
127 name = config.home.username;
128 primary_email = "${config.home.username}@imm.invalid";
129 };
130
131 search = { exclude_tags = ["deleted"]; };
132 };
133 configPath = pkgs.writeText "notmuchrc" (generators.toINI { mkKeyValue = mkIniKeyValue; } notmuchIni);
134
135 afewConfigDir = pkgs.symlinkJoin {
136 name = "afew-config";
137 paths = [
138 (pkgs.writeTextDir "config" ''
139 [InboxFilter]
140 '')
141 ];
142 };
143
144 notmuchHooksDir =
145 let
146 afewHook = pkgs.writeShellScript "afew" ''
147 exec -- ${pkgs.afew}/bin/afew -c ${afewConfigDir} -C ${configPath} --tag --new -vv
148 '';
149 in pkgs.linkFarm "notmuch-hooks" [
150 { name = "post-new";
151 path = afewHook;
152 }
153 { name = "post-insert";
154 path = afewHook;
155 }
156 ];
157
158 notmuchWrapped = pkgs.runCommand "${pkgs.notmuch.name}-wrapped-${config.home.username}"
159 { nativeBuildInputs = with pkgs; [ makeWrapper ];
160 } ''
161 mkdir -p $out/bin
162 makeWrapper ${pkgs.notmuch}/bin/notmuch $out/bin/notmuch-feeds \
163 --set NOTMUCH_CONFIG ${configPath}
164 '';
165 alotWrapped = pkgs.runCommand "${pkgs.alot.name}-wrapped-${config.home.username}"
166 { nativeBuildInputs = with pkgs; [ makeWrapper gnused ];
167 } ''
168 mkdir -p $out/bin
169 makeWrapper ${pkgs.alot}/bin/alot $out/bin/alot-feeds \
170 --prefix MAILCAPS : ${alotMailcaps} \
171 --add-flags --config=${alotConfig} \
172 --add-flags --notmuch-config=${configPath}
173
174 mkdir $out/share
175 ln -s ${pkgs.alot}/share/alot $out/share
176 mkdir -p $out/share/applications
177 sed -r 's/alot/alot-feeds/g' ${pkgs.alot}/share/applications/alot.desktop > $out/share/applications/alot-feeds.desktop
178 mkdir -p $out/share/zsh/site-functions
179 sed -r 's/alot/alot-feeds/g' ${pkgs.alot}/share/zsh/site-functions/_alot > $out/share/zsh/site-functions/_alot-feeds
180 '';
181
182 alotConfig = pkgs.runCommand "alot" {
183 realname = notmuchIni.user.name;
184 address = notmuchIni.user.primary_email;
185 } "substituteAll ${./alot.config} $out";
186 alotMailcaps = pkgs.writeText "mailcaps" ''
187 text/html; ${pkgs.lynx}/bin/lynx -dump -dont_wrap_pre -assume_charset=utf-8 -display_charset=utf-8 "%s"; nametemplate=%s.html; copiousoutput
188 '';
189in {
190 config = {
191 home.packages = [ immWrapped notmuchWrapped pkgs.notmuch.man alotWrapped ];
192
193 home.activation.createImm = hm.dag.entryAfter ["writeBoundary"] ''
194 $DRY_RUN_CMD mkdir -p $VERBOSE_ARG ${config.xdg.configHome}/imm
195 '';
196
197 home.activation.createFeedsDatabase = hm.dag.entryAfter ["linkGeneration" "writeBoundary"] ''
198 $DRY_RUN_CMD mkdir -p -m 0750 $VERBOSE_ARG ${databasePath}
199 $DRY_RUN_CMD mkdir -p $VERBOSE_ARG ${databasePath}/new ${databasePath}/cur ${databasePath}/tmp
200 if ! [[ -d ${databasePath}/.notmuch ]]; then
201 NOTMUCH_VERBOSE_ARG="--quiet"
202 if [[ -v VERBOSE ]]; then
203 NOTMUCH_VERBOSE_ARG="--verbose"
204 fi
205 NOTMUCH_CONFIG=${configPath} $DRY_RUN_CMD ${pkgs.notmuch}/bin/notmuch new $NOTMUCH_VERBOSE_ARG
206 fi
207 $DRY_RUN_CMD ln -Tsf $VERBOSE_ARG ${notmuchHooksDir} ${databasePath}/.notmuch/hooks
208 '';
209
210 systemd.user.services."logrotate-imm" = {
211 Unit = {
212 Description = "Rotate imm logfile";
213 };
214 Service = {
215 Type = "oneshot";
216 ExecStart = ''
217 ${pkgs.logrotate}/bin/logrotate --state ${config.xdg.configHome}/imm/imm.logrotate ${pkgs.writeText "logrotate.conf" ''
218 ${config.xdg.configHome}/imm/imm.log {
219 rotate 5
220 size 1024k
221 }
222 ''}
223 '';
224 };
225 };
226 systemd.user.timers."logrotate-imm" = {
227 Timer = {
228 OnActiveSec = "6h";
229 OnUnitActiveSec = "6h";
230 };
231 Install = {
232 WantedBy = ["default.target"];
233 };
234 };
235 };
236}
diff --git a/user-profiles/tmux/default.nix b/user-profiles/tmux/default.nix
index 11c53788..dc4e791f 100644
--- a/user-profiles/tmux/default.nix
+++ b/user-profiles/tmux/default.nix
@@ -1,10 +1,11 @@
1{ userName, pkgs, lib, ... }: 1{ userName, pkgs, lib, ... }:
2{ 2{
3 home-manager.users.${userName} = { 3 home-manager.users.${userName} = { config, ... }: {
4 programs.tmux = { 4 programs.tmux = {
5 enable = true; 5 enable = true;
6 clock24 = true; 6 clock24 = true;
7 historyLimit = 50000; 7 historyLimit = 50000;
8 mouse = true;
8 extraConfig = lib.readFile (pkgs.stdenv.mkDerivation { 9 extraConfig = lib.readFile (pkgs.stdenv.mkDerivation {
9 name = "tmux.conf"; 10 name = "tmux.conf";
10 src = ./tmux.conf; 11 src = ./tmux.conf;
@@ -13,11 +14,10 @@
13 14
14 phases = [ "installPhase" ]; 15 phases = [ "installPhase" ];
15 16
16 inherit (pkgs) zsh;
17 mandb = pkgs.man-db;
18
19 installPhase = '' 17 installPhase = ''
20 substituteAll $src $out 18 substitute $src $out \
19 --subst-var-by zsh ${config.programs.zsh.package} \
20 --subst-var-by man ${config.programs.man.package}
21 ''; 21 '';
22 }); 22 });
23 }; 23 };
diff --git a/user-profiles/tmux/tmux.conf b/user-profiles/tmux/tmux.conf
index 415d13e7..9e658800 100644
--- a/user-profiles/tmux/tmux.conf
+++ b/user-profiles/tmux/tmux.conf
@@ -1,23 +1,20 @@
1set-option -g history-limit 50000
2set-option -g status-bg black 1set-option -g status-bg black
3set-option -g status-fg white 2set-option -g status-fg white
4set-option -g clock-mode-colour white 3set-option -g clock-mode-colour white
5set-option -g clock-mode-style 24
6set-option -g bell-action any 4set-option -g bell-action any
7set-option -g default-shell @zsh@/bin/zsh 5set-option -g default-shell @zsh@
8set-option -g update-environment 'DISPLAY SSH_ASKPASS SSH_AUTH_SOCK SSH_AGENT_PID SSH_CONNECTION WINDOWID XAUTHORITY PROMPT_INFO PATH PGHOST PGLOG' 6set-option -g update-environment 'DISPLAY SSH_ASKPASS SSH_AUTH_SOCK SSH_AGENT_PID SSH_CONNECTION WINDOWID XAUTHORITY PROMPT_INFO PATH PGHOST PGLOG'
9set-option -g mouse on
10set-option -g set-clipboard on 7set-option -g set-clipboard on
11set-option -g terminal-overrides 'rxvt-uni*:XT:Ms=\E]52;%p1%s;%p2%s\007' 8set-option -g terminal-overrides 'rxvt-uni*:XT:Ms=\E]52;%p1%s;%p2%s\007'
12 9
13set-environment -g LESS " -R " 10set-environment -g LESS " -R "
14 11
15## determine if we should enable 256-colour support 12## determine if we should enable 256-colour support
16if "[[ ''${TERM} =~ 256color || ''${TERM} == fbterm || ''${TERM} =~ alacritty ]]" 'set -g default-terminal tmux-256color' 13if "[[ ''${TERM} =~ 256color || ''${TERM} == fbterm || ''${TERM} =~ alacritty || ''${TERM} =~ kitty ]]" 'set -g default-terminal tmux-256color'
17 14
18set-option -g status-right "" 15set-option -g status-right ""
19 16
20bind / command-prompt "split-window -h 'exec @mandb@/bin/man %%'" 17bind / command-prompt "split-window -h 'exec @man@ %%'"
21bind C clock-mode 18bind C clock-mode
22bind r switch-client -r 19bind r switch-client -r
23 20
diff --git a/user-profiles/utils.nix b/user-profiles/utils.nix
index 13eb6033..da79e336 100644
--- a/user-profiles/utils.nix
+++ b/user-profiles/utils.nix
@@ -1,19 +1,6 @@
1{ userName, lib, pkgs, config, ... }: 1{ userName, lib, pkgs, config, ... }:
2let 2let
3 cfg = config.home-manager.users.${userName}; 3 cfg = config.home-manager.users.${userName};
4
5 wrappedLess = pkgs.less.overrideAttrs (oldAttrs: {
6 pname = "${oldAttrs.pname or "less"}-wrapper";
7
8 nativeBuildInputs = (oldAttrs.nativeBuildInputs or []) ++ (with pkgs; [makeWrapper]);
9
10 postInstall = ''
11 ${oldAttrs.postInstall or ""}
12
13 wrapProgram $out/bin/less \
14 --prefix PATH : ${lib.makeBinPath (with pkgs; [binutils])}
15 '';
16 });
17in { 4in {
18 home-manager.users.${userName} = { 5 home-manager.users.${userName} = {
19 programs = { 6 programs = {
@@ -55,19 +42,23 @@ in {
55 }; 42 };
56 43
57 jq.enable = true; 44 jq.enable = true;
45
46 lesspipe.enable = true;
47
48 man.enable = true;
58 }; 49 };
59 50
60 home.sessionVariables = { 51 home.sessionVariables = {
61 LESSCOLORIZER = "pygmentize -O style=rrt"; 52 LESSCOLORIZER = "${lib.getExe' pkgs.python3Packages.pygments "pygmentize"} -O style=rrt";
62 }; 53 };
63 54
64 home.packages = with pkgs; [ 55 home.packages = with pkgs; [
65 autossh usbutils pciutils eza silver-searcher pwgen xkcdpass 56 autossh usbutils pciutils eza silver-searcher pwgen xkcdpass
66 unzip magic-wormhole qrencode tty-clock dnsutils openssl sshfs 57 unzip magic-wormhole dnsutils openssl sshfs
67 psmisc mosh tree vnstat file pv bc zip nmap aspell 58 psmisc mosh tree vnstat file pv bc zip nmap aspell
68 aspellDicts.de aspellDicts.en borgbackup man-pages rsync socat 59 aspellDicts.de aspellDicts.en borgbackup man-pages rsync socat
69 inetutils yq cached-nix-shell persistent-nix-shell rage 60 inetutils yq cached-nix-shell persistent-nix-shell rage
70 smartmontools hdparm nix-output-monitor wrappedLess dscp 61 smartmontools hdparm nix-output-monitor less dscp
71 iputils 62 iputils
72 ]; 63 ];
73 }; 64 };
diff --git a/user-profiles/yt-dlp.nix b/user-profiles/yt-dlp.nix
index 9e77f734..ef0be87e 100644
--- a/user-profiles/yt-dlp.nix
+++ b/user-profiles/yt-dlp.nix
@@ -28,7 +28,7 @@
28 # "youtube:formats=dashy" 28 # "youtube:formats=dashy"
29 # ]; 29 # ];
30 remux-video = "mp4>mkv"; 30 remux-video = "mp4>mkv";
31 output = "\"%(title)s [%(uploader)s %(webpage_url)s].%(ext)s\""; 31 output = lib.mkDefault "\"%(modified_date>%Y%m%d,release_date>%Y%m%d,upload_date>%Y%m%d)s %(title)s [%(uploader)s %(webpage_url)s].%(ext)s\"";
32 }; 32 };
33 }; 33 };
34 }; 34 };
diff --git a/user-profiles/zsh/default.nix b/user-profiles/zsh/default.nix
index 428e2459..ab523a52 100644
--- a/user-profiles/zsh/default.nix
+++ b/user-profiles/zsh/default.nix
@@ -1,38 +1,72 @@
1{ userName, pkgs, customUtils, lib, config, ... }: 1{ userName, pkgs, customUtils, lib, config, ... }:
2let 2{
3 dotDir = ".config/zsh"; 3 config = {
4 p10kZsh = "${dotDir}/.p10k.zsh"; 4 home-manager.users.${userName} = let sysConfig = config; in { config, ... }: {
5 cfg = config.home-manager.users.${userName}; 5 config = {
6in { 6 programs.zsh = {
7 home-manager.users.${userName} = { 7 dotDir = ".config/zsh";
8 programs.zsh = { 8 enable = true;
9 inherit dotDir; 9 autocd = true;
10 enable = true; 10 enableCompletion = true;
11 autocd = true; 11 enableVteIntegration = true;
12 enableCompletion = true; 12 history = {
13 append = true;
14 expireDuplicatesFirst = true;
15 extended = true;
16 findNoDups = true;
17 };
18 syntaxHighlighting.enable = true;
19 zsh-abbr = {
20 enable = true;
21 abbreviations = {
22 re = "systemctl restart";
23 ure = "systemctl --user restart";
24 st = "systemctl status";
25 ust = "systemctl --user status";
26 };
27 globalAbbreviations = {
28 "L" = "| less";
29 "S" = "&> /dev/null";
30 "G" = "| grep";
31 "B" = "&> /dev/null &";
32 "BB" = "&> /dev/null &!";
33 "J" = lib.mkIf config.programs.jq.enable "| jq '.'";
34 };
35 };
13 36
14 plugins = [ 37 plugins = [
15 { name = "powerlevel10k"; 38 { name = "powerlevel10k";
16 file = "share/zsh-powerlevel10k/powerlevel10k.zsh-theme"; 39 file = "share/zsh-powerlevel10k/powerlevel10k.zsh-theme";
17 src = pkgs.zsh-powerlevel10k; 40 src = pkgs.zsh-powerlevel10k;
18 } 41 }
19 ]; 42 ];
20 initExtraFirst = '' 43 initContent = lib.mkMerge [
21 if [[ $TERM == "dumb" ]]; then 44 (lib.mkBefore ''
22 unsetopt zle 45 if [[ $TERM == "dumb" ]]; then
23 PS1='$ ' 46 unsetopt zle
24 return 47 PS1='$ '
25 fi 48 return
26 ''; 49 fi
27 initExtraBeforeCompInit = '' 50 '')
28 source "${cfg.home.homeDirectory}/${p10kZsh}" 51 (lib.mkOrder 550 ''
29 ''; 52 source "$HOME/${config.xdg.configFile."zsh/.p10k.zsh".target}"
30 initExtra = lib.mkAfter '' 53 '')
31 source ${./zshrc} 54 (lib.mkAfter ''
32 source "${pkgs.zsh-syntax-highlighting}/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" 55 source ${./zshrc}
33 ''; 56 '')
57 ];
58 };
59
60 xdg.configFile."zsh/.p10k.zsh".source = ./p10k.zsh;
61 };
34 }; 62 };
35 63
36 home.file.${p10kZsh}.source = ./p10k.zsh; 64 programs.zsh.enable = true;
65 environment.pathsToLink = [ "/share/zsh" ];
66 environment.shellAliases = lib.mkOverride 90 {};
67
68 nixpkgs.externalConfig.allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
69 "zsh-abbr"
70 ];
37 }; 71 };
38} 72}
diff --git a/user-profiles/zsh/zshrc b/user-profiles/zsh/zshrc
index ed614182..af3aca64 100644
--- a/user-profiles/zsh/zshrc
+++ b/user-profiles/zsh/zshrc
@@ -33,9 +33,3 @@ zle -N self-insert url-quote-magic
33zle -N bracketed-paste bracketed-paste-magic 33zle -N bracketed-paste bracketed-paste-magic
34 34
35setopt extended_glob 35setopt extended_glob
36
37alias -g L='| less'
38alias -g S='&> /dev/null'
39alias -g G='| grep'
40alias -g B='&> /dev/null &'
41alias -g BB='&> /dev/null &!'
diff --git a/users/gkleen/default.nix b/users/gkleen/default.nix
index 5cc32521..5ce93de7 100644
--- a/users/gkleen/default.nix
+++ b/users/gkleen/default.nix
@@ -1,7 +1,7 @@
1{ flake, userName, pkgs, customUtils, lib, ... }: 1{ flake, userName, pkgs, customUtils, lib, ... }:
2{ 2{
3 imports = with flake.nixosModules.userProfiles.${userName}; [ 3 imports = with flake.nixosModules.userProfiles.${userName}; [
4 zsh tmux utils direnv 4 utils direnv
5 ]; 5 ];
6 6
7 users.users.${userName} = { 7 users.users.${userName} = {
diff --git a/users/root.nix b/users/root.nix
index b61f9cfd..ed1acd50 100644
--- a/users/root.nix
+++ b/users/root.nix
@@ -3,7 +3,7 @@ let
3 haveGKleen = flake.nixosModules.accounts ? "gkleen@${hostName}"; 3 haveGKleen = flake.nixosModules.accounts ? "gkleen@${hostName}";
4in { 4in {
5 imports = with flake.nixosModules.userProfiles.${userName}; [ 5 imports = with flake.nixosModules.userProfiles.${userName}; [
6 zsh tmux direnv utils 6 direnv utils
7 ]; 7 ];
8 8
9 users.users.${userName} = lib.mkIf haveGKleen { 9 users.users.${userName} = lib.mkIf haveGKleen {