summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.sops.yaml9
-rw-r--r--_sources/generated.json174
-rw-r--r--_sources/generated.nix136
-rw-r--r--accounts/gkleen@eostre.nix8
-rw-r--r--accounts/gkleen@installer.nix8
-rw-r--r--accounts/gkleen@sif/default.nix740
-rw-r--r--accounts/gkleen@sif/dunst-settings.nix45
-rw-r--r--accounts/gkleen@sif/dunstrc.d/00-urgency_low.conf4
-rw-r--r--accounts/gkleen@sif/dunstrc.d/01-urgency_normal.conf4
-rw-r--r--accounts/gkleen@sif/dunstrc.d/02-urgency_critical.conf4
-rw-r--r--accounts/gkleen@sif/dunstrc.d/10-brightness.conf5
-rw-r--r--accounts/gkleen@sif/dunstrc.d/10-pulseaudio-ctl.conf5
-rw-r--r--accounts/gkleen@sif/dunstrc.d/20-element.conf3
-rw-r--r--accounts/gkleen@sif/dunstrc.d/20-kitty.conf3
-rw-r--r--accounts/gkleen@sif/dunstrc.d/20-mail.conf3
-rw-r--r--accounts/gkleen@sif/dunstrc.d/20-zulip.conf3
-rw-r--r--accounts/gkleen@sif/emacs.el7
-rw-r--r--accounts/gkleen@sif/firefox-chrome.css30
-rw-r--r--accounts/gkleen@sif/hyprland.nix424
-rw-r--r--accounts/gkleen@sif/libvirt/default.nix21
-rw-r--r--accounts/gkleen@sif/niri/default.nix978
-rw-r--r--accounts/gkleen@sif/niri/mako.nix112
-rw-r--r--accounts/gkleen@sif/shell/default.nix115
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt168
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp83
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp55
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp102
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp52
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp18
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp21
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp198
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp147
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/customplugin.h7
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/default.nix30
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.DBus.Properties.xml28
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.login1.Manager.xml445
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.login1.Session.xml146
-rw-r--r--accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.systemd1.Manager.xml817
-rw-r--r--accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml172
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Bar.qml117
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml136
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml117
-rw-r--r--accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml84
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Clock.qml295
-rw-r--r--accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml112
-rw-r--r--accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml47
-rw-r--r--accounts/gkleen@sif/shell/quickshell/LockSurface.qml227
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Lockscreen.qml132
-rw-r--r--accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml35
-rw-r--r--accounts/gkleen@sif/shell/quickshell/NiriIdle.qml30
-rw-r--r--accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml340
-rw-r--r--accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml266
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml483
-rw-r--r--accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml49
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml75
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml18
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/InhibitorState.qml22
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/MprisProxy.qml8
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml194
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml162
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml63
-rw-r--r--accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml8
-rw-r--r--accounts/gkleen@sif/shell/quickshell/SystemTray.qml201
-rw-r--r--accounts/gkleen@sif/shell/quickshell/UnixIPC.qml97
-rw-r--r--accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml163
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml89
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml56
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml204
-rw-r--r--accounts/gkleen@sif/shell/quickshell/WorktimeWidget.qml120
-rw-r--r--accounts/gkleen@sif/shell/quickshell/displaymanager.qml115
-rw-r--r--accounts/gkleen@sif/shell/quickshell/shell.qml53
-rw-r--r--accounts/gkleen@sif/ssh-hosts.nix85
-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.nix254
-rw-r--r--accounts/gkleen@sif/taffybar/default.nix2
-rw-r--r--accounts/gkleen@sif/taffybar/gkleen-sif-taffybar.cabal32
-rw-r--r--accounts/gkleen@sif/taffybar/src/System/Taffybar/Widget/Clock.hs111
-rw-r--r--accounts/gkleen@sif/taffybar/src/System/Taffybar/Widget/TooltipBattery.hs101
-rw-r--r--accounts/gkleen@sif/taffybar/src/taffybar.hs89
-rw-r--r--accounts/gkleen@sif/taffybar/taffybar.css146
-rw-r--r--accounts/gkleen@sif/utils/async-yt-dlp.nix57
-rw-r--r--accounts/gkleen@sif/utils/pdf2pdf.nix8
-rw-r--r--accounts/gkleen@sif/utils/sieve-edit.nix24
-rw-r--r--accounts/gkleen@sif/xmonad/.gitignore4
-rw-r--r--accounts/gkleen@sif/xmonad/default.nix7
-rw-r--r--accounts/gkleen@sif/xmonad/lib/XMonad/Mpv.hs127
-rw-r--r--accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MyPass.hs94
-rw-r--r--accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MyShell.hs105
-rw-r--r--accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MySsh.hs246
-rw-r--r--accounts/gkleen@sif/xmonad/package.yaml31
-rw-r--r--accounts/gkleen@sif/xmonad/stack.nix17
-rw-r--r--accounts/gkleen@sif/xmonad/stack.yaml10
-rw-r--r--accounts/gkleen@sif/xmonad/xmonad-yggdrasil.nix21
-rw-r--r--accounts/gkleen@sif/xmonad/xmonad.hs939
-rw-r--r--accounts/gkleen@sif/zshrc153
-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.lock584
-rw-r--r--flake.nix90
-rw-r--r--home-modules/nixpkgs-release-check.nix4
-rw-r--r--home-modules/pandoc/default.nix27
-rw-r--r--home-modules/pandoc/german_abbreviations.txt1423
-rwxr-xr-xhome-modules/pandoc/german_abbreviations.txt.gup35
-rw-r--r--home-modules/quickshell.nix68
-rw-r--r--hosts/eostre/default.nix21
-rw-r--r--hosts/sif/default.nix213
-rw-r--r--hosts/sif/email/default.nix111
-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/greetd/default.nix92
-rw-r--r--hosts/sif/hw.nix19
-rw-r--r--hosts/sif/libvirt/default.nix1
-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.nix24
-rw-r--r--hosts/surtr/dns/Gupfile2
-rw-r--r--hosts/surtr/dns/default.nix8
-rw-r--r--hosts/surtr/dns/key.gup2
-rw-r--r--hosts/surtr/dns/keys/audiobookshelf.yggdrasil.li_acme19
-rw-r--r--hosts/surtr/dns/keys/hledger.yggdrasil.li_acme24
-rw-r--r--hosts/surtr/dns/keys/immich.yggdrasil.li_acme24
-rw-r--r--hosts/surtr/dns/keys/kimai.yggdrasil.li_acme19
-rw-r--r--hosts/surtr/dns/keys/paperless.yggdrasil.li_acme24
-rw-r--r--hosts/surtr/dns/zones/email.nights.soa8
-rw-r--r--hosts/surtr/dns/zones/li.141.soa11
-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.soa47
-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__.py18
-rw-r--r--hosts/surtr/email/default.nix273
-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/hledger.nix66
-rw-r--r--hosts/surtr/http/default.nix2
-rw-r--r--hosts/surtr/immich.nix66
-rw-r--r--hosts/surtr/kimai.nix68
-rw-r--r--hosts/surtr/paperless.nix66
-rw-r--r--hosts/surtr/postgresql/default.nix62
-rw-r--r--hosts/surtr/tls/default.nix2
-rw-r--r--hosts/surtr/tls/tsig_key.gup4
-rw-r--r--hosts/surtr/tls/tsig_keys/audiobookshelf.yggdrasil.li19
-rw-r--r--hosts/surtr/tls/tsig_keys/hledger.yggdrasil.li24
-rw-r--r--hosts/surtr/tls/tsig_keys/immich.yggdrasil.li24
-rw-r--r--hosts/surtr/tls/tsig_keys/kimai.yggdrasil.li19
-rw-r--r--hosts/surtr/tls/tsig_keys/paperless.yggdrasil.li24
-rw-r--r--hosts/surtr/vpn/default.nix8
-rw-r--r--hosts/surtr/vpn/geri.pub2
-rw-r--r--hosts/surtr/zfs.nix2
-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.nix6
-rw-r--r--hosts/vidhar/hledger/default.nix83
-rw-r--r--hosts/vidhar/hledger/htpasswd24
-rw-r--r--hosts/vidhar/immich.nix10
-rw-r--r--hosts/vidhar/kimai/default.nix89
-rw-r--r--hosts/vidhar/kimai/ruleset.nft149
-rw-r--r--hosts/vidhar/network/default.nix17
-rw-r--r--hosts/vidhar/network/dhcp/default.nix221
-rw-r--r--hosts/vidhar/network/pap-secrets26
-rw-r--r--hosts/vidhar/network/pppoe.nix (renamed from hosts/vidhar/network/gpon.nix)87
-rw-r--r--hosts/vidhar/network/ruleset.nft97
-rw-r--r--hosts/vidhar/paperless/default.nix25
-rw-r--r--hosts/vidhar/paperless/rootpw24
-rw-r--r--hosts/vidhar/pgbackrest/default.nix9
-rw-r--r--hosts/vidhar/postgresql.nix36
-rw-r--r--hosts/vidhar/prometheus/default.nix71
-rw-r--r--hosts/vidhar/prometheus/zte_dsl01.mgmt.yggdrasil26
-rw-r--r--hosts/vidhar/zfs.nix2
-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.nix8
-rw-r--r--lib/pythonSet.nix42
-rw-r--r--modules/abs-podcast-autoplaylist.nix55
-rw-r--r--modules/backup-utils.nix3
-rw-r--r--modules/borgcopy/.envrc4
-rw-r--r--modules/borgcopy/.gitignore2
-rw-r--r--modules/borgcopy/default.nix40
-rw-r--r--modules/borgcopy/poetry.lock180
-rw-r--r--modules/borgcopy/pyproject.toml33
-rw-r--r--modules/borgcopy/uv.lock146
-rw-r--r--modules/envfs.nix77
-rw-r--r--modules/i18n.nix156
-rw-r--r--modules/impermanence-timezone.nix41
-rw-r--r--modules/impermanence.nix6
-rw-r--r--modules/installer.nix56
-rw-r--r--modules/niri.nix6
-rw-r--r--modules/nix-access-tokens/default.nix24
-rw-r--r--modules/nix-access-tokens/nix.conf32
-rw-r--r--modules/pgbackrest.nix3
-rw-r--r--modules/postsrsd.nix163
-rw-r--r--modules/systemd-run0.nix4
-rw-r--r--modules/tzupdate.nix81
-rw-r--r--modules/uucp.nix398
-rw-r--r--nvfetcher.toml32
-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/batman-adv.nix15
-rw-r--r--overlays/cake-prometheus-exporter/default.nix11
-rw-r--r--overlays/deploy-rs.nix16
-rw-r--r--overlays/etesync-dav.nix49
-rw-r--r--overlays/inwx-cdnskey/default.nix11
-rw-r--r--overlays/keepassxc/database-open-dialog.patch129
-rw-r--r--overlays/keepassxc/default.nix8
-rw-r--r--overlays/lesspipe.nix2
-rw-r--r--overlays/mako.nix5
-rw-r--r--overlays/nftables-prometheus-exporter/default.nix11
-rw-r--r--overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py22
-rw-r--r--overlays/niri.nix9
-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/preserve-dscp/default.nix2
-rw-r--r--overlays/prometheus-lvm-exporter.nix2
-rw-r--r--overlays/quickshell/default.nix13
-rw-r--r--overlays/quickshell/greetd-response.patch16
-rw-r--r--overlays/quickshell/io.patch13
-rw-r--r--overlays/quickshell/lock-state-changed.patch12
-rw-r--r--overlays/quickshell/pipewire.patch488
-rw-r--r--overlays/scutiger.nix2
-rw-r--r--overlays/spice-record.nix2
-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/waybar.nix11
-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.lock248
-rw-r--r--overlays/worktime/pyproject.toml41
-rw-r--r--overlays/worktime/uv.lock248
-rwxr-xr-xoverlays/worktime/worktime/__main__.py669
-rw-r--r--overlays/wttrbar/default.nix7
-rw-r--r--overlays/wttrbar/icons.patch154
-rw-r--r--overlays/yt-dlp.nix2
-rw-r--r--overlays/zte-prometheus-exporter/default.nix13
-rw-r--r--overlays/zte-prometheus-exporter/zte-prometheus-exporter.py65
-rw-r--r--shell.nix16
-rw-r--r--system-profiles/bcachefs.nix12
-rw-r--r--system-profiles/core/default.nix43
-rw-r--r--system-profiles/default-locale.nix27
-rw-r--r--system-profiles/initrd-all-crypto-modules.nix2
-rw-r--r--system-profiles/lanzaboote.nix14
-rw-r--r--system-profiles/nfsroot.nix6
-rw-r--r--system-profiles/niri-flake.nix4
-rw-r--r--system-profiles/niri-unstable.nix11
-rw-r--r--system-profiles/rebuild-machines/default.nix20
-rw-r--r--system-profiles/zfs.nix6
-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/mpv/default.nix8
-rw-r--r--user-profiles/tmux/default.nix10
-rw-r--r--user-profiles/tmux/tmux.conf9
-rw-r--r--user-profiles/utils.nix25
-rw-r--r--user-profiles/yt-dlp.nix18
-rw-r--r--user-profiles/zsh/default.nix100
-rw-r--r--user-profiles/zsh/zshrc7
-rw-r--r--users/gkleen/default.nix34
-rw-r--r--users/gkleen/gitignore2
-rw-r--r--users/root.nix2
291 files changed, 16682 insertions, 6065 deletions
diff --git a/.gitignore b/.gitignore
index f30fe710..8d4a68d6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,5 @@
1**/result 1**/result
2**/result-* 2**/result-*
3**/#*#
4**/.#*
5**/.gup 3**/.gup
6.direnv 4.direnv
7 5
diff --git a/.sops.yaml b/.sops.yaml
index 7b0507ee..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 ]
@@ -26,3 +32,6 @@ creation_rules:
26 - path_regex: ^hosts/sif/ 32 - path_regex: ^hosts/sif/
27 key_groups: 33 key_groups:
28 - age: [ *admin_gkleen, *machine_sif ] 34 - age: [ *admin_gkleen, *machine_sif ]
35 - path_regex: ^modules/nix-access-tokens/
36 key_groups:
37 - age: [ *admin_gkleen, *machine_sif, *machine_surtr, *machine_vidhar ]
diff --git a/_sources/generated.json b/_sources/generated.json
index 06346969..dd73e455 100644
--- a/_sources/generated.json
+++ b/_sources/generated.json
@@ -20,23 +20,9 @@
20 }, 20 },
21 "version": "8ef9a5b73e5d1063cf912c70027c655fb19d1109" 21 "version": "8ef9a5b73e5d1063cf912c70027c655fb19d1109"
22 }, 22 },
23 "batman-adv": {
24 "cargoLocks": null,
25 "date": null,
26 "extract": null,
27 "name": "batman-adv",
28 "passthru": null,
29 "pinned": false,
30 "src": {
31 "sha256": "sha256-VYyIkH5IFfKN6EOHZxSx6AaepD3a22/hhmLhqkle5Z0=",
32 "type": "tarball",
33 "url": "https://downloads.open-mesh.org/batman/stable/sources/batman-adv/batman-adv-2024.4.tar.gz"
34 },
35 "version": "2024.4"
36 },
37 "bpf-examples": { 23 "bpf-examples": {
38 "cargoLocks": null, 24 "cargoLocks": null,
39 "date": "2024-01-31", 25 "date": "2025-09-19",
40 "extract": null, 26 "extract": null,
41 "name": "bpf-examples", 27 "name": "bpf-examples",
42 "passthru": null, 28 "passthru": null,
@@ -48,12 +34,12 @@
48 "name": null, 34 "name": null,
49 "owner": "xdp-project", 35 "owner": "xdp-project",
50 "repo": "bpf-examples", 36 "repo": "bpf-examples",
51 "rev": "5343ed3377471c7b7ef2237526c8bdc0f00a0cef", 37 "rev": "d621b4fb25c4877415a563887606ab0fe47ad59a",
52 "sha256": "sha256-vKVI8pQ17BNWLKm8wwpyNkLslnB9E2CAZTS6EP5lDT0=", 38 "sha256": "sha256-IQBTYtqHsghbb/Mpx29Hjr9AsLVG6w3BqfJYSKoMotU=",
53 "sparseCheckout": [], 39 "sparseCheckout": [],
54 "type": "github" 40 "type": "github"
55 }, 41 },
56 "version": "5343ed3377471c7b7ef2237526c8bdc0f00a0cef" 42 "version": "d621b4fb25c4877415a563887606ab0fe47ad59a"
57 }, 43 },
58 "emacs-scratch_el": { 44 "emacs-scratch_el": {
59 "cargoLocks": null, 45 "cargoLocks": null,
@@ -90,12 +76,12 @@
90 "name": null, 76 "name": null,
91 "owner": "Mange", 77 "owner": "Mange",
92 "repo": "emoji-data", 78 "repo": "emoji-data",
93 "rev": "v2.6", 79 "rev": "v2.7",
94 "sha256": "sha256-6nBiT9q139P1pXLqkV1JejE0s2rZn1gUbNsejXJR6RU=", 80 "sha256": "sha256-bUFh0Q7xcnKTBgVBUJU8BH6zzq1Y3krLfJJAgx5TqKs=",
95 "sparseCheckout": [], 81 "sparseCheckout": [],
96 "type": "github" 82 "type": "github"
97 }, 83 },
98 "version": "v2.6" 84 "version": "v2.7"
99 }, 85 },
100 "lesspipe": { 86 "lesspipe": {
101 "cargoLocks": null, 87 "cargoLocks": null,
@@ -105,11 +91,31 @@
105 "passthru": null, 91 "passthru": null,
106 "pinned": false, 92 "pinned": false,
107 "src": { 93 "src": {
108 "sha256": "sha256-s6oV77sOPYAd8CA51KZK6nydWIh8oK2+SUpPfm7yehg=", 94 "sha256": "sha256-yb3IzdaMiv1PwqHOfSyHvmWXyStvK/XXC49saXVAJFU=",
109 "type": "tarball", 95 "type": "tarball",
110 "url": "https://github.com/wofr06/lesspipe/archive/refs/tags/v2.16.tar.gz" 96 "url": "https://github.com/wofr06/lesspipe/archive/refs/tags/v2.20.tar.gz"
97 },
98 "version": "2.20"
99 },
100 "mako": {
101 "cargoLocks": null,
102 "date": "2025-09-11",
103 "extract": null,
104 "name": "mako",
105 "passthru": null,
106 "pinned": false,
107 "src": {
108 "deepClone": false,
109 "fetchSubmodules": false,
110 "leaveDotGit": false,
111 "name": null,
112 "rev": "8318972590420c042c0177af16e26a1768550fab",
113 "sha256": "sha256-Y/exF/Pv60E31Zl+M1zboWkmkZgOUCA3l93OKbtvZ+g=",
114 "sparseCheckout": [],
115 "type": "git",
116 "url": "https://github.com/emersion/mako"
111 }, 117 },
112 "version": "2.16" 118 "version": "8318972590420c042c0177af16e26a1768550fab"
113 }, 119 },
114 "mpv-autosave": { 120 "mpv-autosave": {
115 "cargoLocks": null, 121 "cargoLocks": null,
@@ -196,7 +202,7 @@
196 }, 202 },
197 "mpv-reload": { 203 "mpv-reload": {
198 "cargoLocks": null, 204 "cargoLocks": null,
199 "date": "2024-03-22", 205 "date": "2025-02-07",
200 "extract": null, 206 "extract": null,
201 "name": "mpv-reload", 207 "name": "mpv-reload",
202 "passthru": null, 208 "passthru": null,
@@ -208,16 +214,16 @@
208 "name": null, 214 "name": null,
209 "owner": "4e6", 215 "owner": "4e6",
210 "repo": "mpv-reload", 216 "repo": "mpv-reload",
211 "rev": "1a6a9383ba1774708fddbd976e7a9b72c3eec938", 217 "rev": "60e6fb1c578aa9af80d725857dac8e439095b033",
212 "sha256": "sha256-BshxCjec/UNGyiC0/g1Rai2NvG2qOIHXDDEUYwwdij0=", 218 "sha256": "sha256-elA9bi5ov5MbehLD1kyS4Z8zKgTc+8dcOPq32muRGcE=",
213 "sparseCheckout": [], 219 "sparseCheckout": [],
214 "type": "github" 220 "type": "github"
215 }, 221 },
216 "version": "1a6a9383ba1774708fddbd976e7a9b72c3eec938" 222 "version": "60e6fb1c578aa9af80d725857dac8e439095b033"
217 }, 223 },
218 "mpv-subselect": { 224 "mpv-subselect": {
219 "cargoLocks": null, 225 "cargoLocks": null,
220 "date": "2024-08-31", 226 "date": "2025-04-04",
221 "extract": null, 227 "extract": null,
222 "name": "mpv-subselect", 228 "name": "mpv-subselect",
223 "passthru": null, 229 "passthru": null,
@@ -227,13 +233,13 @@
227 "fetchSubmodules": false, 233 "fetchSubmodules": false,
228 "leaveDotGit": false, 234 "leaveDotGit": false,
229 "name": null, 235 "name": null,
230 "rev": "9b0043ba6042ba01fdd2533281313b70cbc98be4", 236 "rev": "26d24a0fd1d69988eaedda6056a2c87d0a55b6cb",
231 "sha256": "sha256-ZLWwkwrFaOg7PnuC23VaZ0P3zMhm1JmVf0eH9lqO0BY=", 237 "sha256": "sha256-+eVga4b7KIBnfrtmlgq/0HNjQVS3SK6YWVXCPvOeOOc=",
232 "sparseCheckout": [], 238 "sparseCheckout": [],
233 "type": "git", 239 "type": "git",
234 "url": "https://github.com/CogentRedTester/mpv-sub-select" 240 "url": "https://github.com/CogentRedTester/mpv-sub-select"
235 }, 241 },
236 "version": "9b0043ba6042ba01fdd2533281313b70cbc98be4" 242 "version": "26d24a0fd1d69988eaedda6056a2c87d0a55b6cb"
237 }, 243 },
238 "mpv-youtube-quality": { 244 "mpv-youtube-quality": {
239 "cargoLocks": null, 245 "cargoLocks": null,
@@ -255,6 +261,36 @@
255 }, 261 },
256 "version": "1f8c31457459ffc28cd1c3f3c2235a53efad7148" 262 "version": "1f8c31457459ffc28cd1c3f3c2235a53efad7148"
257 }, 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-ipbZJ0mPCuwzb/TDtXXUBTuWOcSsKGAJ1GEGIgB2G7E=",
274 "type": "url",
275 "url": "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.88/netboot.xyz.efi"
276 },
277 "version": "2.0.88"
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-igy3O30noS25dU7ZnHuKrWqLLkjjd/L46IdCTd038dI=",
289 "type": "url",
290 "url": "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.88/netboot.xyz.lkrn"
291 },
292 "version": "2.0.88"
293 },
258 "postfix-mta-sts-resolver": { 294 "postfix-mta-sts-resolver": {
259 "cargoLocks": null, 295 "cargoLocks": null,
260 "date": null, 296 "date": null,
@@ -263,11 +299,11 @@
263 "passthru": null, 299 "passthru": null,
264 "pinned": false, 300 "pinned": false,
265 "src": { 301 "src": {
266 "sha256": "sha256-wpBbT/KXd2iU6Jn6Y/6i5C+Wv3OsUTjFcJDhUm6w46I=", 302 "sha256": "sha256-DrPWxAlzdtb5K0Z+yVi+rL1h7CyLj0/Fiio8B2H/Ssg=",
267 "type": "tarball", 303 "type": "tarball",
268 "url": "https://github.com/Snawoot/postfix-mta-sts-resolver/archive/refs/tags/v1.4.0.tar.gz" 304 "url": "https://github.com/Snawoot/postfix-mta-sts-resolver/archive/refs/tags/v1.5.0.tar.gz"
269 }, 305 },
270 "version": "1.4.0" 306 "version": "1.5.0"
271 }, 307 },
272 "postfwd": { 308 "postfwd": {
273 "cargoLocks": null, 309 "cargoLocks": null,
@@ -291,11 +327,11 @@
291 "passthru": null, 327 "passthru": null,
292 "pinned": false, 328 "pinned": false,
293 "src": { 329 "src": {
294 "sha256": "sha256-mA84Bnq5JF0BGfqHhcCzTef5nDotLgQuiyg3/zOPqTE=", 330 "sha256": "sha256-mg4iyp/heYzSoK+pGSMYfZb5UauoBMrEL1QPH6EoJ8o=",
295 "type": "tarball", 331 "type": "tarball",
296 "url": "https://github.com/hansmi/prometheus-lvm-exporter/archive/refs/tags/v0.3.3.tar.gz" 332 "url": "https://github.com/hansmi/prometheus-lvm-exporter/archive/refs/tags/v0.6.1.tar.gz"
297 }, 333 },
298 "version": "0.3.3" 334 "version": "0.6.1"
299 }, 335 },
300 "psql-versioning": { 336 "psql-versioning": {
301 "cargoLocks": null, 337 "cargoLocks": null,
@@ -317,6 +353,26 @@
317 }, 353 },
318 "version": "330cb9da36651b701085ad53ae75ff296d02202a" 354 "version": "330cb9da36651b701085ad53ae75ff296d02202a"
319 }, 355 },
356 "quickshell": {
357 "cargoLocks": null,
358 "date": "2025-09-19",
359 "extract": null,
360 "name": "quickshell",
361 "passthru": null,
362 "pinned": false,
363 "src": {
364 "deepClone": false,
365 "fetchSubmodules": false,
366 "leaveDotGit": false,
367 "name": null,
368 "rev": "e9a574d919a89602d2868621576b2ccae54a5cb0",
369 "sha256": "sha256-wOv1guIi9THD1NjOtBU2Xh/Avg9xv7nIjsfFSkr1NeQ=",
370 "sparseCheckout": [],
371 "type": "git",
372 "url": "https://git.outfoxxed.me/quickshell/quickshell.git"
373 },
374 "version": "e9a574d919a89602d2868621576b2ccae54a5cb0"
375 },
320 "scutiger": { 376 "scutiger": {
321 "cargoLocks": null, 377 "cargoLocks": null,
322 "date": null, 378 "date": null,
@@ -359,6 +415,26 @@
359 }, 415 },
360 "version": "0.2.1" 416 "version": "0.2.1"
361 }, 417 },
418 "swayosd": {
419 "cargoLocks": null,
420 "date": "2025-07-07",
421 "extract": null,
422 "name": "swayosd",
423 "passthru": null,
424 "pinned": false,
425 "src": {
426 "deepClone": false,
427 "fetchSubmodules": false,
428 "leaveDotGit": false,
429 "name": null,
430 "rev": "73aed75146b81aaf67c4301353790ff5a17aed1f",
431 "sha256": "sha256-p31HNelptAw7Sk0NmYP4FkoUCdA5uAsrXC20JJp24Vw=",
432 "sparseCheckout": [],
433 "type": "git",
434 "url": "https://github.com/ErikReider/SwayOSD"
435 },
436 "version": "73aed75146b81aaf67c4301353790ff5a17aed1f"
437 },
362 "tomorrow-night-paradise-theme": { 438 "tomorrow-night-paradise-theme": {
363 "cargoLocks": null, 439 "cargoLocks": null,
364 "date": "2012-06-04", 440 "date": "2012-06-04",
@@ -381,7 +457,7 @@
381 }, 457 },
382 "v4l2loopback": { 458 "v4l2loopback": {
383 "cargoLocks": null, 459 "cargoLocks": null,
384 "date": "2024-11-26", 460 "date": "2025-08-18",
385 "extract": null, 461 "extract": null,
386 "name": "v4l2loopback", 462 "name": "v4l2loopback",
387 "passthru": null, 463 "passthru": null,
@@ -393,16 +469,16 @@
393 "name": null, 469 "name": null,
394 "owner": "umlaeute", 470 "owner": "umlaeute",
395 "repo": "v4l2loopback", 471 "repo": "v4l2loopback",
396 "rev": "e750af9eb17d729b8c5257a4bcd2faba2b28029c", 472 "rev": "5eaa59e7c41d0e6f35a6c14c3b756d94d25f58ed",
397 "sha256": "sha256-ePA1LcxQInrLLpbZ7Wljv75lWl6V6s9KkdMp0tF1vhk=", 473 "sha256": "sha256-YcSpNfItvUdPVirlDyGdYuCnVvxHhh780x+OI5VNZmE=",
398 "sparseCheckout": [], 474 "sparseCheckout": [],
399 "type": "github" 475 "type": "github"
400 }, 476 },
401 "version": "e750af9eb17d729b8c5257a4bcd2faba2b28029c" 477 "version": "5eaa59e7c41d0e6f35a6c14c3b756d94d25f58ed"
402 }, 478 },
403 "xcompose": { 479 "xcompose": {
404 "cargoLocks": null, 480 "cargoLocks": null,
405 "date": "2022-09-14", 481 "date": "2025-06-05",
406 "extract": null, 482 "extract": null,
407 "name": "xcompose", 483 "name": "xcompose",
408 "passthru": null, 484 "passthru": null,
@@ -414,12 +490,12 @@
414 "name": null, 490 "name": null,
415 "owner": "kragen", 491 "owner": "kragen",
416 "repo": "xcompose", 492 "repo": "xcompose",
417 "rev": "cd8d3e622f547ec9f83d7f64f51d4a27ee812681", 493 "rev": "4d8eab4d05a19537ce79294ae0459fdae78ffb20",
418 "sha256": "sha256-fkl2lDv/DdrqPjVsEUKSRD3BNGwTjTsA0ovI8akFI6U=", 494 "sha256": "sha256-vKY4u5Z2IL111orLLkkF4AoVzqluKG/VQhNUUCqO/k8=",
419 "sparseCheckout": [], 495 "sparseCheckout": [],
420 "type": "github" 496 "type": "github"
421 }, 497 },
422 "version": "cd8d3e622f547ec9f83d7f64f51d4a27ee812681" 498 "version": "4d8eab4d05a19537ce79294ae0459fdae78ffb20"
423 }, 499 },
424 "yt-dlp": { 500 "yt-dlp": {
425 "cargoLocks": null, 501 "cargoLocks": null,
@@ -430,10 +506,10 @@
430 "pinned": false, 506 "pinned": false,
431 "src": { 507 "src": {
432 "name": null, 508 "name": null,
433 "sha256": "sha256-dD2+CB6ocb4/X/CD4s2V2oZt6nc/xwrmsQmDjPv3KsQ=", 509 "sha256": "sha256-koKtHerbTJCy5tO8+fNgq/iMXy5LqDba17UTh7CG11c=",
434 "type": "url", 510 "type": "url",
435 "url": "https://pypi.org/packages/source/y/yt_dlp/yt_dlp-2024.12.6.tar.gz" 511 "url": "https://pypi.org/packages/source/y/yt_dlp/yt_dlp-2025.9.23.tar.gz"
436 }, 512 },
437 "version": "2024.12.6" 513 "version": "2025.9.23"
438 } 514 }
439} \ No newline at end of file 515} \ No newline at end of file
diff --git a/_sources/generated.nix b/_sources/generated.nix
index b07979a7..8eac064b 100644
--- a/_sources/generated.nix
+++ b/_sources/generated.nix
@@ -16,25 +16,17 @@
16 }; 16 };
17 date = "2021-05-30"; 17 date = "2021-05-30";
18 }; 18 };
19 batman-adv = {
20 pname = "batman-adv";
21 version = "2024.4";
22 src = fetchTarball {
23 url = "https://downloads.open-mesh.org/batman/stable/sources/batman-adv/batman-adv-2024.4.tar.gz";
24 sha256 = "sha256-VYyIkH5IFfKN6EOHZxSx6AaepD3a22/hhmLhqkle5Z0=";
25 };
26 };
27 bpf-examples = { 19 bpf-examples = {
28 pname = "bpf-examples"; 20 pname = "bpf-examples";
29 version = "5343ed3377471c7b7ef2237526c8bdc0f00a0cef"; 21 version = "d621b4fb25c4877415a563887606ab0fe47ad59a";
30 src = fetchFromGitHub { 22 src = fetchFromGitHub {
31 owner = "xdp-project"; 23 owner = "xdp-project";
32 repo = "bpf-examples"; 24 repo = "bpf-examples";
33 rev = "5343ed3377471c7b7ef2237526c8bdc0f00a0cef"; 25 rev = "d621b4fb25c4877415a563887606ab0fe47ad59a";
34 fetchSubmodules = true; 26 fetchSubmodules = true;
35 sha256 = "sha256-vKVI8pQ17BNWLKm8wwpyNkLslnB9E2CAZTS6EP5lDT0="; 27 sha256 = "sha256-IQBTYtqHsghbb/Mpx29Hjr9AsLVG6w3BqfJYSKoMotU=";
36 }; 28 };
37 date = "2024-01-31"; 29 date = "2025-09-19";
38 }; 30 };
39 emacs-scratch_el = { 31 emacs-scratch_el = {
40 pname = "emacs-scratch_el"; 32 pname = "emacs-scratch_el";
@@ -50,23 +42,37 @@
50 }; 42 };
51 emoji-data = { 43 emoji-data = {
52 pname = "emoji-data"; 44 pname = "emoji-data";
53 version = "v2.6"; 45 version = "v2.7";
54 src = fetchFromGitHub { 46 src = fetchFromGitHub {
55 owner = "Mange"; 47 owner = "Mange";
56 repo = "emoji-data"; 48 repo = "emoji-data";
57 rev = "v2.6"; 49 rev = "v2.7";
58 fetchSubmodules = true; 50 fetchSubmodules = true;
59 sha256 = "sha256-6nBiT9q139P1pXLqkV1JejE0s2rZn1gUbNsejXJR6RU="; 51 sha256 = "sha256-bUFh0Q7xcnKTBgVBUJU8BH6zzq1Y3krLfJJAgx5TqKs=";
60 }; 52 };
61 }; 53 };
62 lesspipe = { 54 lesspipe = {
63 pname = "lesspipe"; 55 pname = "lesspipe";
64 version = "2.16"; 56 version = "2.20";
65 src = fetchTarball { 57 src = fetchTarball {
66 url = "https://github.com/wofr06/lesspipe/archive/refs/tags/v2.16.tar.gz"; 58 url = "https://github.com/wofr06/lesspipe/archive/refs/tags/v2.20.tar.gz";
67 sha256 = "sha256-s6oV77sOPYAd8CA51KZK6nydWIh8oK2+SUpPfm7yehg="; 59 sha256 = "sha256-yb3IzdaMiv1PwqHOfSyHvmWXyStvK/XXC49saXVAJFU=";
68 }; 60 };
69 }; 61 };
62 mako = {
63 pname = "mako";
64 version = "8318972590420c042c0177af16e26a1768550fab";
65 src = fetchgit {
66 url = "https://github.com/emersion/mako";
67 rev = "8318972590420c042c0177af16e26a1768550fab";
68 fetchSubmodules = false;
69 deepClone = false;
70 leaveDotGit = false;
71 sparseCheckout = [ ];
72 sha256 = "sha256-Y/exF/Pv60E31Zl+M1zboWkmkZgOUCA3l93OKbtvZ+g=";
73 };
74 date = "2025-09-11";
75 };
70 mpv-autosave = { 76 mpv-autosave = {
71 pname = "mpv-autosave"; 77 pname = "mpv-autosave";
72 version = "744c3ee61d2f0a8e9bb4e308dec6897215ae4704"; 78 version = "744c3ee61d2f0a8e9bb4e308dec6897215ae4704";
@@ -118,29 +124,29 @@
118 }; 124 };
119 mpv-reload = { 125 mpv-reload = {
120 pname = "mpv-reload"; 126 pname = "mpv-reload";
121 version = "1a6a9383ba1774708fddbd976e7a9b72c3eec938"; 127 version = "60e6fb1c578aa9af80d725857dac8e439095b033";
122 src = fetchFromGitHub { 128 src = fetchFromGitHub {
123 owner = "4e6"; 129 owner = "4e6";
124 repo = "mpv-reload"; 130 repo = "mpv-reload";
125 rev = "1a6a9383ba1774708fddbd976e7a9b72c3eec938"; 131 rev = "60e6fb1c578aa9af80d725857dac8e439095b033";
126 fetchSubmodules = false; 132 fetchSubmodules = false;
127 sha256 = "sha256-BshxCjec/UNGyiC0/g1Rai2NvG2qOIHXDDEUYwwdij0="; 133 sha256 = "sha256-elA9bi5ov5MbehLD1kyS4Z8zKgTc+8dcOPq32muRGcE=";
128 }; 134 };
129 date = "2024-03-22"; 135 date = "2025-02-07";
130 }; 136 };
131 mpv-subselect = { 137 mpv-subselect = {
132 pname = "mpv-subselect"; 138 pname = "mpv-subselect";
133 version = "9b0043ba6042ba01fdd2533281313b70cbc98be4"; 139 version = "26d24a0fd1d69988eaedda6056a2c87d0a55b6cb";
134 src = fetchgit { 140 src = fetchgit {
135 url = "https://github.com/CogentRedTester/mpv-sub-select"; 141 url = "https://github.com/CogentRedTester/mpv-sub-select";
136 rev = "9b0043ba6042ba01fdd2533281313b70cbc98be4"; 142 rev = "26d24a0fd1d69988eaedda6056a2c87d0a55b6cb";
137 fetchSubmodules = false; 143 fetchSubmodules = false;
138 deepClone = false; 144 deepClone = false;
139 leaveDotGit = false; 145 leaveDotGit = false;
140 sparseCheckout = [ ]; 146 sparseCheckout = [ ];
141 sha256 = "sha256-ZLWwkwrFaOg7PnuC23VaZ0P3zMhm1JmVf0eH9lqO0BY="; 147 sha256 = "sha256-+eVga4b7KIBnfrtmlgq/0HNjQVS3SK6YWVXCPvOeOOc=";
142 }; 148 };
143 date = "2024-08-31"; 149 date = "2025-04-04";
144 }; 150 };
145 mpv-youtube-quality = { 151 mpv-youtube-quality = {
146 pname = "mpv-youtube-quality"; 152 pname = "mpv-youtube-quality";
@@ -156,12 +162,28 @@
156 }; 162 };
157 date = "2020-02-10"; 163 date = "2020-02-10";
158 }; 164 };
165 netbootxyz-efi = {
166 pname = "netbootxyz-efi";
167 version = "2.0.88";
168 src = fetchurl {
169 url = "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.88/netboot.xyz.efi";
170 sha256 = "sha256-ipbZJ0mPCuwzb/TDtXXUBTuWOcSsKGAJ1GEGIgB2G7E=";
171 };
172 };
173 netbootxyz-lkrn = {
174 pname = "netbootxyz-lkrn";
175 version = "2.0.88";
176 src = fetchurl {
177 url = "https://github.com/netbootxyz/netboot.xyz/releases/download/2.0.88/netboot.xyz.lkrn";
178 sha256 = "sha256-igy3O30noS25dU7ZnHuKrWqLLkjjd/L46IdCTd038dI=";
179 };
180 };
159 postfix-mta-sts-resolver = { 181 postfix-mta-sts-resolver = {
160 pname = "postfix-mta-sts-resolver"; 182 pname = "postfix-mta-sts-resolver";
161 version = "1.4.0"; 183 version = "1.5.0";
162 src = fetchTarball { 184 src = fetchTarball {
163 url = "https://github.com/Snawoot/postfix-mta-sts-resolver/archive/refs/tags/v1.4.0.tar.gz"; 185 url = "https://github.com/Snawoot/postfix-mta-sts-resolver/archive/refs/tags/v1.5.0.tar.gz";
164 sha256 = "sha256-wpBbT/KXd2iU6Jn6Y/6i5C+Wv3OsUTjFcJDhUm6w46I="; 186 sha256 = "sha256-DrPWxAlzdtb5K0Z+yVi+rL1h7CyLj0/Fiio8B2H/Ssg=";
165 }; 187 };
166 }; 188 };
167 postfwd = { 189 postfwd = {
@@ -174,10 +196,10 @@
174 }; 196 };
175 prometheus-lvm-exporter = { 197 prometheus-lvm-exporter = {
176 pname = "prometheus-lvm-exporter"; 198 pname = "prometheus-lvm-exporter";
177 version = "0.3.3"; 199 version = "0.6.1";
178 src = fetchTarball { 200 src = fetchTarball {
179 url = "https://github.com/hansmi/prometheus-lvm-exporter/archive/refs/tags/v0.3.3.tar.gz"; 201 url = "https://github.com/hansmi/prometheus-lvm-exporter/archive/refs/tags/v0.6.1.tar.gz";
180 sha256 = "sha256-mA84Bnq5JF0BGfqHhcCzTef5nDotLgQuiyg3/zOPqTE="; 202 sha256 = "sha256-mg4iyp/heYzSoK+pGSMYfZb5UauoBMrEL1QPH6EoJ8o=";
181 }; 203 };
182 }; 204 };
183 psql-versioning = { 205 psql-versioning = {
@@ -194,6 +216,20 @@
194 }; 216 };
195 date = "2023-11-23"; 217 date = "2023-11-23";
196 }; 218 };
219 quickshell = {
220 pname = "quickshell";
221 version = "e9a574d919a89602d2868621576b2ccae54a5cb0";
222 src = fetchgit {
223 url = "https://git.outfoxxed.me/quickshell/quickshell.git";
224 rev = "e9a574d919a89602d2868621576b2ccae54a5cb0";
225 fetchSubmodules = false;
226 deepClone = false;
227 leaveDotGit = false;
228 sparseCheckout = [ ];
229 sha256 = "sha256-wOv1guIi9THD1NjOtBU2Xh/Avg9xv7nIjsfFSkr1NeQ=";
230 };
231 date = "2025-09-19";
232 };
197 scutiger = { 233 scutiger = {
198 pname = "scutiger"; 234 pname = "scutiger";
199 version = "0.2.0"; 235 version = "0.2.0";
@@ -218,6 +254,20 @@
218 sha256 = "sha256-7d/0fepOvdswuBGJCCMULB2kXOFBLP78yqX4NmByCF8="; 254 sha256 = "sha256-7d/0fepOvdswuBGJCCMULB2kXOFBLP78yqX4NmByCF8=";
219 }; 255 };
220 }; 256 };
257 swayosd = {
258 pname = "swayosd";
259 version = "73aed75146b81aaf67c4301353790ff5a17aed1f";
260 src = fetchgit {
261 url = "https://github.com/ErikReider/SwayOSD";
262 rev = "73aed75146b81aaf67c4301353790ff5a17aed1f";
263 fetchSubmodules = false;
264 deepClone = false;
265 leaveDotGit = false;
266 sparseCheckout = [ ];
267 sha256 = "sha256-p31HNelptAw7Sk0NmYP4FkoUCdA5uAsrXC20JJp24Vw=";
268 };
269 date = "2025-07-07";
270 };
221 tomorrow-night-paradise-theme = { 271 tomorrow-night-paradise-theme = {
222 pname = "tomorrow-night-paradise-theme"; 272 pname = "tomorrow-night-paradise-theme";
223 version = "70225a5bf90d495e13a9260bfdc268632ece0801"; 273 version = "70225a5bf90d495e13a9260bfdc268632ece0801";
@@ -234,34 +284,34 @@
234 }; 284 };
235 v4l2loopback = { 285 v4l2loopback = {
236 pname = "v4l2loopback"; 286 pname = "v4l2loopback";
237 version = "e750af9eb17d729b8c5257a4bcd2faba2b28029c"; 287 version = "5eaa59e7c41d0e6f35a6c14c3b756d94d25f58ed";
238 src = fetchFromGitHub { 288 src = fetchFromGitHub {
239 owner = "umlaeute"; 289 owner = "umlaeute";
240 repo = "v4l2loopback"; 290 repo = "v4l2loopback";
241 rev = "e750af9eb17d729b8c5257a4bcd2faba2b28029c"; 291 rev = "5eaa59e7c41d0e6f35a6c14c3b756d94d25f58ed";
242 fetchSubmodules = true; 292 fetchSubmodules = true;
243 sha256 = "sha256-ePA1LcxQInrLLpbZ7Wljv75lWl6V6s9KkdMp0tF1vhk="; 293 sha256 = "sha256-YcSpNfItvUdPVirlDyGdYuCnVvxHhh780x+OI5VNZmE=";
244 }; 294 };
245 date = "2024-11-26"; 295 date = "2025-08-18";
246 }; 296 };
247 xcompose = { 297 xcompose = {
248 pname = "xcompose"; 298 pname = "xcompose";
249 version = "cd8d3e622f547ec9f83d7f64f51d4a27ee812681"; 299 version = "4d8eab4d05a19537ce79294ae0459fdae78ffb20";
250 src = fetchFromGitHub { 300 src = fetchFromGitHub {
251 owner = "kragen"; 301 owner = "kragen";
252 repo = "xcompose"; 302 repo = "xcompose";
253 rev = "cd8d3e622f547ec9f83d7f64f51d4a27ee812681"; 303 rev = "4d8eab4d05a19537ce79294ae0459fdae78ffb20";
254 fetchSubmodules = false; 304 fetchSubmodules = false;
255 sha256 = "sha256-fkl2lDv/DdrqPjVsEUKSRD3BNGwTjTsA0ovI8akFI6U="; 305 sha256 = "sha256-vKY4u5Z2IL111orLLkkF4AoVzqluKG/VQhNUUCqO/k8=";
256 }; 306 };
257 date = "2022-09-14"; 307 date = "2025-06-05";
258 }; 308 };
259 yt-dlp = { 309 yt-dlp = {
260 pname = "yt-dlp"; 310 pname = "yt-dlp";
261 version = "2024.12.6"; 311 version = "2025.9.23";
262 src = fetchurl { 312 src = fetchurl {
263 url = "https://pypi.org/packages/source/y/yt_dlp/yt_dlp-2024.12.6.tar.gz"; 313 url = "https://pypi.org/packages/source/y/yt_dlp/yt_dlp-2025.9.23.tar.gz";
264 sha256 = "sha256-dD2+CB6ocb4/X/CD4s2V2oZt6nc/xwrmsQmDjPv3KsQ="; 314 sha256 = "sha256-koKtHerbTJCy5tO8+fNgq/iMXy5LqDba17UTh7CG11c=";
265 }; 315 };
266 }; 316 };
267} 317}
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 00707e87..36b722e4 100644
--- a/accounts/gkleen@sif/default.nix
+++ b/accounts/gkleen@sif/default.nix
@@ -4,33 +4,6 @@ with lib;
4 4
5let 5let
6 cfg = config.home-manager.users.${userName}; 6 cfg = config.home-manager.users.${userName};
7 emacsScratch = pkgs.stdenv.mkDerivation (sources.emacs-scratch_el // rec {
8 phases = [ "installPhase" ];
9
10 installPhase = ''
11 mkdir -p $out/share/emacs/site-lisp
12 cp $src/scratch.el $out/share/emacs/site-lisp/default.el
13 '';
14 });
15 muteScript = pkgs.stdenv.mkDerivation {
16 name = "mute";
17 src = ./scripts/mute.zsh;
18
19 buildInputs = with pkgs; [ makeWrapper ];
20
21 phases = [ "installPhase" ];
22
23 installPhase = ''
24 mkdir -p $out/bin
25 install -m 0755 $src $out/bin/mute
26 wrapProgram $out/bin/mute \
27 --prefix PATH : ${pkgs.zsh}/bin \
28 --prefix PATH : ${pkgs.findutils}/bin \
29 --prefix PATH : ${pkgs.util-linux}/bin \
30 --prefix PATH : ${pkgs.coreutils}/bin \
31 --prefix PATH : ${pkgs.pulseaudio}/bin
32 '';
33 };
34 wrapElectron = { package, bin ? package.meta.mainProgram or package.pname or (pkgs.lib.strings.nameFromURL package.name "-"), outBin ? bin, sandbox ? true }: pkgs.symlinkJoin { 7 wrapElectron = { package, bin ? package.meta.mainProgram or package.pname or (pkgs.lib.strings.nameFromURL package.name "-"), outBin ? bin, sandbox ? true }: pkgs.symlinkJoin {
35 name = "${package.name}-wrapped"; 8 name = "${package.name}-wrapped";
36 buildInputs = with pkgs; [ makeWrapper ]; 9 buildInputs = with pkgs; [ makeWrapper ];
@@ -47,10 +20,6 @@ let
47 ''; 20 '';
48 }; 21 };
49 22
50 wrappedChrome = wrapElectron { package = pkgs.google-chrome; outBin = "google-chrome"; };
51 wrappedZulip = wrapElectron { package = pkgs.zulip; bin = "zulip"; outBin = "zulip"; };
52 wrappedElementDesktop = wrapElectron { package = pkgs.element-desktop; bin = "element-desktop"; };
53 wrappedRocketChatDesktop = wrapElectron { package = pkgs.rocketchat-desktop; bin = "rocketchat-desktop"; outBin = "rocketchat"; };
54 wrappedYTMDesktop = wrapElectron { package = pkgs.ytmdesktop; sandbox = false; }; 23 wrappedYTMDesktop = wrapElectron { package = pkgs.ytmdesktop; sandbox = false; };
55 24
56 wrappedKeepassxc = pkgs.symlinkJoin { 25 wrappedKeepassxc = pkgs.symlinkJoin {
@@ -63,7 +32,7 @@ let
63 text = '' 32 text = ''
64 [D-BUS Service] 33 [D-BUS Service]
65 Name=org.keepassxc.KeePassXC.MainWindow 34 Name=org.keepassxc.KeePassXC.MainWindow
66 Exec=${pkgs.coreutils}/bin/false 35 Exec=${lib.getExe' pkgs.coreutils "false"}
67 SystemdService=keepassxc.service 36 SystemdService=keepassxc.service
68 ''; 37 '';
69 }) 38 })
@@ -73,35 +42,48 @@ let
73 text = '' 42 text = ''
74 [D-BUS Service] 43 [D-BUS Service]
75 Name=org.freedesktop.secrets 44 Name=org.freedesktop.secrets
76 Exec=${pkgs.coreutils}/bin/false 45 Exec=${lib.getExe' pkgs.coreutils "false"}
77 SystemdService=keepassxc.service 46 SystemdService=keepassxc.service
78 ''; 47 '';
79 }) 48 })
80 ]; 49 ];
81 }; 50 };
82 51
83 lockCommand = "${config.systemd.package}/bin/systemctl --user start gtklock.service"; 52 # lockCommand = "${lib.getExe' config.systemd.package "systemctl"} --user start gtklock.service";
53 lockCommand = "${lib.getExe' cfg.programs.quickshell.package "qs"} ipc call Lockscreen setLocked true";
54
55 editor = pkgs.symlinkJoin {
56 inherit (cfg.services.emacs.package) name;
57 buildInputs = with pkgs; [ makeWrapper ];
58 paths = [ cfg.services.emacs.package ];
59 postBuild = ''
60 wrapProgram $out/bin/emacsclient \
61 --inherit-argv0 \
62 --add-flags ${lib.escapeShellArg (lib.escapeShellArgs cfg.services.emacs.client.arguments)}
63 '';
64 };
84in { 65in {
85 imports = with flake.nixosModules.userProfiles.${userName}; [ 66 imports = with flake.nixosModules.userProfiles.${userName}; [
86 mpv yt-dlp (args: import ./xcompose.nix (inputs // args)) 67 zsh tmux mpv yt-dlp (args: import ./xcompose.nix (inputs // args))
87 ]; 68 ];
88 69
89 config = { 70 config = {
90 services.displayManager.defaultSession = "Hyprland"; # "none+xmonad";
91
92 home-manager.users.${userName} = { 71 home-manager.users.${userName} = {
93 imports = [ 72 imports = [
94 ./libvirt 73 ./libvirt
95 flakeInputs.nix-index-database.hmModules.nix-index 74 ./niri
75 ./shell
76 ./synadm
77 flakeInputs.nix-index-database.homeModules.nix-index
96 flakeInputs.impermanence.nixosModules.home-manager.impermanence 78 flakeInputs.impermanence.nixosModules.home-manager.impermanence
97 ]; 79 ];
98 80
99 home.stateVersion = "20.09"; 81 home.stateVersion = "20.09";
100 82
101 nixpkgs.config = { 83 # nixpkgs.config = {
102 allowUnfree = true; 84 # allowUnfree = true;
103 zathura.useMupdf = false; 85 # zathura.useMupdf = false;
104 }; 86 # };
105 87
106 nix.registry = { 88 nix.registry = {
107 "flk" = { 89 "flk" = {
@@ -111,14 +93,14 @@ in {
111 }; 93 };
112 to = { 94 to = {
113 type = "git"; 95 type = "git";
114 url = "file:///home/gkleen/config/nixos-flakes"; 96 url = "file:///home/gkleen/projects/machines";
115 }; 97 };
116 }; 98 };
117 }; 99 };
118 100
119 programs = { 101 programs = {
120 ssh = { 102 ssh = {
121 matchBlocks = import ./ssh-hosts.nix { inherit pkgs; }; # customUtils.nixImport { dir = ./ssh-hosts; }; 103 matchBlocks = import ./ssh-hosts.nix inputs; # customUtils.nixImport { dir = ./ssh-hosts; };
122 extraConfig = '' 104 extraConfig = ''
123 Match host uniworx3.ifi.lmu.de,uniworx4.ifi.lmu.de,uniworx5.ifi.lmu.de,uni2workgw.ifi.lmu.de,blackbeard.tcs.ifi.lmu.de,gitlab2.rz.ifi.lmu.de,oregon.tcs.ifi.lmu.de !exec "nc -z -w 1 %h %p &>/dev/null" 105 Match host uniworx3.ifi.lmu.de,uniworx4.ifi.lmu.de,uniworx5.ifi.lmu.de,uni2workgw.ifi.lmu.de,blackbeard.tcs.ifi.lmu.de,gitlab2.rz.ifi.lmu.de,oregon.tcs.ifi.lmu.de !exec "nc -z -w 1 %h %p &>/dev/null"
124 ProxyJump remote.cip.ifi.lmu.de 106 ProxyJump remote.cip.ifi.lmu.de
@@ -136,8 +118,8 @@ in {
136 ''} 118 ''}
137 119
138 Match host *.mathinst.loc,*.math.lmu.de !host ssh.math.lmu.de !exec "nc -z -w 1 %h %p &>/dev/null" 120 Match host *.mathinst.loc,*.math.lmu.de !host ssh.math.lmu.de !exec "nc -z -w 1 %h %p &>/dev/null"
139 # ProxyCommand ${pkgs.socat}/bin/socat - SOCKS4A:127.0.0.1:%h:%p,socksport=8118 121 ProxyCommand ${lib.getExe pkgs.socat} - SOCKS4A:127.0.0.1:%h:%p,socksport=8118
140 ProxyJump ssh.math.lmu.de 122 # ProxyJump ssh.math.lmu.de
141 123
142 Match host *.cipmath.loc !host cip04.cipmath.loc,mgmt-cls01.cipmath.loc !exec "nc -z -w 1 %h %p &>/dev/null" 124 Match host *.cipmath.loc !host cip04.cipmath.loc,mgmt-cls01.cipmath.loc !exec "nc -z -w 1 %h %p &>/dev/null"
143 ProxyJump cip04 125 ProxyJump cip04
@@ -154,22 +136,31 @@ in {
154 136
155 emacs = { 137 emacs = {
156 enable = true; 138 enable = true;
157 package = pkgs.emacs29-pgtk; 139 package = pkgs.emacs-pgtk;
158 extraPackages = epkgs: with epkgs; [ 140 extraPackages = epkgs: with epkgs; [
159 evil evil-dvorak undo-tree magit haskell-tng-mode nix-mode 141 evil evil-dvorak undo-tree magit haskell-tng-mode nix-mode
160 yaml-mode json-mode shakespeare-mode smart-mode-line 142 yaml-mode json-mode shakespeare-mode smart-mode-line
161 highlight-parentheses highlight-symbol ag sass-mode lua-mode 143 highlight-parentheses highlight-symbol ag sass-mode
162 fira-code-mode use-package wanderlust # notmuch 144 lua-mode fira-code-mode use-package wanderlust # notmuch
163 use-package-ensure-system-package git-gutter emacsScratch 145 git-gutter scratch edit-server mediawiki editorconfig
164 edit-server mediawiki editorconfig typescript-mode 146 typescript-mode markdown-mode nftables-mode rustic
165 markdown-mode nftables-mode rustic lsp-mode lsp-ui 147 lsp-mode lsp-ui direnv company projectile
166 direnv company projectile tomorrow-night-paradise-theme 148 tomorrow-night-paradise-theme
167 treesit-grammars.with-all-grammars magit-delta scad-mode 149 treesit-grammars.with-all-grammars magit-delta scad-mode
168 ]; 150 ];
169 overrides = self: super: { 151 overrides = self: super: {
170 tomorrow-night-paradise-theme = super.trivialBuild { 152 tomorrow-night-paradise-theme = super.trivialBuild {
171 inherit (sources.tomorrow-night-paradise-theme) pname version src; 153 inherit (sources.tomorrow-night-paradise-theme) pname version src;
172 }; 154 };
155 scratch = pkgs.stdenv.mkDerivation {
156 inherit (sources.emacs-scratch_el) pname version src;
157
158 phases = [ "unpackPhase" "installPhase" ];
159
160 installPhase = ''
161 install -Dt $out/share/emacs/site-lisp scratch.el
162 '';
163 };
173 }; 164 };
174 }; 165 };
175 firefox = { 166 firefox = {
@@ -183,8 +174,14 @@ in {
183 }; 174 };
184 }; 175 };
185 }; 176 };
177 chromium.enable = true;
186 178
187 zathura.enable = true; 179 zathura = {
180 enable = true;
181 options = {
182 scroll-page-aware = true;
183 };
184 };
188 imv.enable = true; 185 imv.enable = true;
189 186
190 mpv.config = { 187 mpv.config = {
@@ -193,13 +190,93 @@ in {
193 gpu-api = "vulkan"; 190 gpu-api = "vulkan";
194 }; 191 };
195 192
196 zsh.initExtra = '' 193 zsh.initContent = let
197 source ${./zshrc} 194 zshrc = pkgs.resholve.mkDerivation {
195 pname = "zshrc";
196 version = "0.0.0";
197
198 src = ./zshrc;
199
200 dontUnpack = true;
201 dontConfigure = true;
202 dontBuild = true;
203
204 installPhase = ''
205 mkdir -p $out/share
206 install "$src" $out/share/zshrc
207 '';
208
209 solutions = {
210 default = {
211 scripts = [ "share/zshrc" ];
212 interpreter = "none";
213 inputs = with pkgs; [
214 coreutils
215 rpm
216 binutils
217 squashfsTools
218 unzip
219 cfg.programs.git.package
220 magickWrapped
221 curl
222 file
223 gnutar
224 cpio
225 magic-wormhole
226 cfg.programs.zsh.package
227 fuse
228 util-linux
229 findutils
230 qrencode
231 tty-clock
232 cfg.programs.jq.package
233 eza
234 less
235 config.systemd.package
236 config.programs.ssh.package
237 gnused
238 miniserve
239 p7zip
240 ];
241 execer = with pkgs; [
242 "cannot:${lib.getExe' rpm "rpm2cpio"}"
243 "cannot:${lib.getExe' squashfsTools "unsquashfs"}"
244 "cannot:${lib.getExe' unzip "unzip"}"
245 "cannot:${lib.getExe cfg.programs.git.package}"
246 "cannot:${lib.getExe cpio}"
247 "cannot:${lib.getExe' magic-wormhole "wormhole"}"
248 "cannot:${lib.getExe' fuse "fusermount"}"
249 "cannot:${lib.getExe less}"
250 "cannot:${lib.getExe' config.systemd.package "systemctl"}"
251 "cannot:${lib.getExe config.programs.ssh.package}"
252 "cannot:${lib.getExe' p7zip "7z"}"
253 ];
254 wrapper = with pkgs; [
255 "${lib.getExe' magickWrapped "magick"}:${lib.getExe' imagemagick "magick"}"
256 ];
257 fake = {
258 builtin = ["print"];
259 external = ["sudo" "umount"];
260 };
261 };
262 };
263 };
264 magickWrapped = pkgs.symlinkJoin {
265 inherit (pkgs.imagemagick) name;
266 paths = [ pkgs.imagemagick ];
267
268 buildInputs = with pkgs; [ makeWrapper ];
269 postBuild = ''
270 wrapProgram $out/bin/magick \
271 --prefix PATH : ${lib.makeBinPath (with pkgs; [ ghostscript ])}
272 '';
273 };
274 in ''
275 source ${zshrc}/share/zshrc
198 ''; 276 '';
199 zsh.dirHashes = let 277 zsh.dirHashes = let
200 flakeHashes = mapAttrs' (n: v: nameValuePair (inputNames.${n} or n) (toString v)) flakeInputs; 278 flakeHashes = mapAttrs' (n: v: nameValuePair (inputNames.${n} or n) (toString v)) flakeInputs;
201 inputNames = { 279 inputNames = {
202 "nixpkgs" = "nixos";
203 }; 280 };
204 in flakeHashes // { 281 in flakeHashes // {
205 u2w = "$HOME/projects/uni2work"; 282 u2w = "$HOME/projects/uni2work";
@@ -211,6 +288,16 @@ in {
211 pro = "$HOME/projects/pro"; 288 pro = "$HOME/projects/pro";
212 media = "$HOME/media"; 289 media = "$HOME/media";
213 }; 290 };
291 jq.colors = {
292 arrays = "1;37";
293 "false" = "0;37";
294 "null" = "2;37";
295 numbers = "0;37";
296 objectKeys = "1;34";
297 objects = "1;37";
298 strings = "0;32";
299 "true" = "0;37";
300 };
214 301
215 obs-studio = { 302 obs-studio = {
216 enable = true; 303 enable = true;
@@ -220,7 +307,7 @@ in {
220 gh = { 307 gh = {
221 enable = true; 308 enable = true;
222 settings = { 309 settings = {
223 editor = "${config.home-manager.users.${userName}.programs.emacs.package}/bin/emacsclient"; 310 editor = lib.getExe' editor "emacsclient";
224 gitProtocol = "ssh"; 311 gitProtocol = "ssh";
225 }; 312 };
226 }; 313 };
@@ -246,309 +333,23 @@ in {
246 # notify_on_cmd_finish = "invisible 120"; 333 # notify_on_cmd_finish = "invisible 120";
247 }; 334 };
248 keybindings = { 335 keybindings = {
249 "kitty_mod+n" = "detach_window"; 336 "kitty_mod+n" = "new_os_window_with_cwd";
250 "kitty_mod+m" = "detach_window ask"; 337 "kitty_mod+m" = "detach_window ask";
251 }; 338 "kitty_mod+enter" = "new_window_with_cwd";
252 }; 339 "kitty_mod+t" = "new_tab_with_cwd";
253 waybar = {
254 enable = true;
255 systemd = {
256 enable = true;
257 target = "hyprland-session.target";
258 };
259 settings = let
260 windowRewrites = {
261 "(.*) — Mozilla Firefox" = "$1";
262 "(.*) - Mozilla Thunderbird" = "$1";
263 "(.*) - mpv" = "$1";
264 };
265 iconSize = 11;
266 in [
267 {
268 layer = "top";
269 position = "top";
270 height = 14;
271 output = [ "eDP-1" "DP-2" "DP-3" ];
272 modules-left = [ "hyprland/workspaces" ];
273 modules-center = [ "hyprland/window" ];
274 modules-right = [ "custom/worktime" "custom/worktime-today" "custom/weather" "custom/keymap" "privacy" "tray" "wireplumber" "backlight" "battery" "idle_inhibitor" "clock" ];
275
276 "custom/weather" = {
277 format = "{}";
278 tooltip = true;
279 interval = 3600;
280 exec = "${lib.getExe pkgs.wttrbar} --hide-conditions --custom-indicator \"<span font=\\\"Symbols Nerd Font Mono\\\" size=\\\"120%\\\">{ICON}</span> {FeelsLikeC}°\"";
281 return-type = "json";
282 };
283 "custom/keymap" = {
284 format = "{}";
285 tooltip = true;
286 return-type = "json";
287 exec = pkgs.writers.writePython3 "keymap" {} ''
288 import os
289 import socket
290 import re
291 import subprocess
292 import json
293
294
295 def output(keymap):
296 short = keymap
297 if keymap == "English (programmer Dvorak)":
298 short = "dvp"
299 elif keymap == "English (US)":
300 short = "<span color=\"#ffffff\">us</span>"
301 print(json.dumps({'text': short, 'tooltip': keymap}, separators=(',', ':')), flush=True) # noqa: E501
302
303
304 r = subprocess.run(["hyprctl", "devices", "-j"], check=True, stdout=subprocess.PIPE, text=True) # noqa: E501
305 for keyboard in json.loads(r.stdout)['keyboards']:
306 if keyboard['name'] != "at-translated-set-2-keyboard":
307 continue
308 output(keyboard['active_keymap'])
309
310 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
311 sock.connect(os.environ["XDG_RUNTIME_DIR"] + "/hypr/" + os.environ["HYPRLAND_INSTANCE_SIGNATURE"] + "/.socket2.sock") # noqa: E501
312 expected = re.compile(r'^activelayout>>at-translated-set-2-keyboard,(?P<keymap>.+)$') # noqa: E501
313 for line in sock.makefile(buffering=1, encoding='utf-8'):
314 if match := expected.match(line):
315 output(match.group("keymap"))
316 '';
317 on-click = "hyprctl switchxkblayout at-translated-set-2-keyboard next";
318 };
319 "custom/worktime" = {
320 interval = 60;
321 exec = getExe pkgs.worktime;
322 tooltip = false;
323 };
324 "custom/worktime-today" = {
325 interval = 60;
326 exec = "${getExe pkgs.worktime} today";
327 tooltip = false;
328 };
329 "hyprland/workspaces" = {
330 all-outputs = true;
331 };
332 "hyprland/window" = {
333 separate-outputs = true;
334 icon = true;
335 icon-size = 14;
336 rewrite = windowRewrites;
337 };
338 clock = {
339 interval = 1;
340 # timezone = "Europe/Berlin";
341 format = "W{:%V-%u %F %H:%M:%S%Ez}";
342 tooltip-format = "<tt><small>{calendar}</small></tt>";
343 calendar = {
344 mode = "year";
345 mode-mon-col = 3;
346 weeks-pos = "left";
347 on-scroll = 1;
348 format = {
349 months = "<span color='#ffead3'><b>{}</b></span>";
350 days = "{}";
351 weeks = "<span color='#99ffdd'><b>{}</b></span>";
352 weekdays = "<span color='#ffcc66'><b>{}</b></span>";
353 today = "<span color='#ff6699'><b>{}</b></span>";
354 };
355 };
356 };
357 battery = {
358 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
359 icon-size = iconSize - 2;
360 states = { warning = 30; critical = 15; };
361 format-icons = ["&#xf008e;" "&#xf007a;" "&#xf007b;" "&#xf007c;" "&#xf007d;" "&#xf007e;" "&#xf007f;" "&#xf0080;" "&#xf0081;" "&#xf0082;" "&#xf0079;" ];
362 format-charging = "&#xf0084;";
363 format-plugged = "&#xf06a5;";
364 tooltip-format = "{capacity}% {timeTo}";
365 interval = 20;
366 };
367 tray = {
368 icon-size = 16;
369 # show-passive-items = true;
370 spacing = 1;
371 };
372 privacy = {
373 icon-spacing = 7;
374 icon-size = iconSize;
375 modules = [
376 { type = "screenshare"; }
377 { type = "audio-in"; }
378 ];
379 };
380 idle_inhibitor = {
381 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
382 icon-size = iconSize;
383 format-icons = { activated = "&#xf0208;"; deactivated = "&#xf0209;"; };
384 timeout = 120;
385 };
386 backlight = {
387 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
388 icon-size = iconSize;
389 tooltip-format = "{percent}%";
390 format-icons = ["&#xf00da;" "&#xf00db;" "&#xf00dc;" "&#xf00dd;" "&#xf00de;" "&#xf00df;" "&#xf00e0;"];
391 on-scroll-up = "lightctl -d -e4 -n1 up";
392 on-scroll-down = "lightctl -d -e4 -n1 down";
393 };
394 wireplumber = {
395 format = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">{icon}</span>";
396 icon-size = iconSize;
397 tooltip-format = "{volume}% {node_name}";
398 format-icons = ["&#xf057f;" "&#xf0580;" "&#xf057e;"];
399 format-muted = "<span font=\"Symbols Nerd Font Mono\" size=\"90%\">&#xf075f;</span>";
400 # ignored-sinks = ["Easy Effects Sink"];
401 on-scroll-up = "volumectl -d -u up";
402 on-scroll-down = "volumectl -d -u down";
403 on-click = "volumectl -d toggle-mute";
404 };
405 }
406 {
407 layer = "top";
408 position = "top";
409 height = 14;
410 output = [ "!eDP-1" "!DP-2" "!DP-3" ];
411 modules-left = [ "hyprland/workspaces" ];
412 modules-center = [ "hyprland/window" ];
413 modules-right = [ "clock" ];
414
415 "hyprland/workspaces" = {
416 all-outputs = false;
417 };
418 "hyprland/window" = {
419 separate-outputs = true;
420 icon = true;
421 icon-size = 14;
422 rewrite = windowRewrites;
423 };
424 clock = {
425 interval = 1;
426 # timezone = "Europe/Berlin";
427 format = "{:%H:%M}";
428 tooltip-format = "W{:%V-%u %F %H:%M:%S%Ez}";
429 };
430 }
431 ];
432 style = ''
433 @define-color white #ffffff;
434 @define-color grey #555555;
435 @define-color blue #1a8fff;
436 @define-color green #23fd00;
437 @define-color orange #f28a21;
438 @define-color red #f2201f;
439
440 * {
441 border: none;
442 font-family: "Fira Sans Nerd Font";
443 font-size: 10pt;
444 min-height: 0;
445 }
446
447 window#waybar {
448 background-color: rgba(0, 0, 0, 0.66);
449 color: @white;
450 }
451
452 .modules-left {
453 margin-left: 9px;
454 }
455 .modules-right {
456 margin-right: 9px;
457 }
458
459 .module {
460 margin: 0 5px;
461 }
462
463 #workspaces button {
464 color: @grey;
465 }
466 #workspaces button.hosting-monitor {
467 color: @white;
468 }
469 #workspaces button.visible {
470 color: @blue;
471 }
472 #workspaces button.active {
473 color: @green;
474 }
475 #workspaces button.urgent {
476 color: @red;
477 }
478
479 #custom-weather, #custom-keymap, #custom-worktime, #custom-worktime-today {
480 color: @grey;
481 margin: 0 5px;
482 }
483 #custom-weather, #custom-worktime-today {
484 margin-right: 3px;
485 }
486 #custom-keymap, #custom-weather {
487 margin-left: 3px;
488 }
489
490 #tray {
491 margin: 0;
492 }
493 #battery, #idle_inhibitor, #backlight, #wireplumber {
494 color: @grey;
495 margin: 0 5px 0 2px;
496 }
497 #idle_inhibitor {
498 margin-right: 2px;
499 margin-left: 3px;
500 }
501 #battery {
502 margin-right: 3px;
503 }
504 #battery.discharging {
505 color: @white;
506 }
507 #battery.warning {
508 color: @orange;
509 }
510 #battery.critical {
511 color: @red;
512 }
513 #battery.charging {
514 color: @white;
515 }
516 #idle_inhibitor.activated {
517 color: @white;
518 }
519
520 #idle_inhibitor {
521 padding-top: 1px;
522 }
523
524 #privacy {
525 color: @red;
526 margin: -1px 2px 0px 5px;
527 }
528 #clock {
529 /* margin-right: 5px; */
530 }
531 '';
532 };
533 wpaperd = {
534 enable = true;
535 settings.default = {
536 path = "~/.wallpapers";
537 duration = "8h";
538 mode = "center";
539 }; 340 };
540 }; 341 };
541 fuzzel = { 342 fuzzel = {
542 enable = true; 343 enable = true;
543 settings = { 344 settings = {
544 main = { 345 main = {
545 terminal = lib.getExe pkgs.kitty; 346 terminal = lib.getExe cfg.programs.kitty.package;
546 layer = "overlay"; 347 layer = "overlay";
547 icon-theme = "Paper"; 348 icon-theme = "Paper";
548 font = "Fira Sans"; 349 font = "Fira Sans";
549 }; 350 };
550 colors = { 351 colors = {
551 background = "000000aa"; 352 background = "000000cc";
552 text = "cdd6f4ff"; 353 text = "cdd6f4ff";
553 match = "94e2d5ff"; 354 match = "94e2d5ff";
554 selection = "585b70ff"; 355 selection = "585b70ff";
@@ -561,34 +362,46 @@ in {
561 }; 362 };
562 }; 363 };
563 }; 364 };
365 pandoc = {
366 enable = true;
367 extraAbbreviations = ["i.A." "d.h." "D.h." "gdw."];
368 };
369 nushell = {
370 enable = true;
371 settings.show_banner = false;
372 };
373 fd.enable = true;
564 }; 374 };
565 375
566 services = { 376 services = {
567 dunst = { 377 wpaperd = {
568 settings = import ./dunst-settings.nix inputs; 378 enable = false;
569 iconTheme = { 379 settings.default = {
570 package = pkgs.paper-icon-theme; 380 path = "~/.wallpapers";
571 name = "Paper"; 381 duration = "15m";
382 mode = "center";
572 }; 383 };
573 enable = true;
574 }; 384 };
575 emacs = { 385 emacs = {
576 enable = true; 386 enable = true;
577 socketActivation.enable = true; 387 socketActivation.enable = true;
578 client = { 388 client = {
579 enable = true; 389 enable = true;
580 arguments = mkForce ["--reuse-frame" "--alternate-editor" "\"\""]; 390 arguments = mkForce ["--create-frame" "--alternate-editor" (lib.getExe cfg.services.emacs.package)];
581 }; 391 };
582 }; 392 };
583 gpg-agent = { 393 gpg-agent = {
584 enable = true; 394 enable = true;
585 enableSshSupport = true; 395 enableSshSupport = true;
586 extraConfig = '' 396 extraConfig = ''
587 pinentry-program ${pkgs.pinentry-gtk2}/bin/pinentry 397 pinentry-program ${lib.getExe' pkgs.pinentry-gtk2 "pinentry"}
588 grab 398 grab
589 ''; 399 '';
590 }; 400 };
591 xembed-sni-proxy.enable = true; 401 xembed-sni-proxy = {
402 enable = true;
403 package = pkgs.kdePackages.plasma-workspace;
404 };
592 udiskie = { 405 udiskie = {
593 enable = true; 406 enable = true;
594 automount = false; 407 automount = false;
@@ -599,6 +412,12 @@ in {
599 notification_actions = { 412 notification_actions = {
600 device_mounted = []; 413 device_mounted = [];
601 }; 414 };
415 device_config = [
416 { loop_file = "/nix/store/*-etc-metadata.erofs"; is_mounted = false; ignore = true; }
417 { mount_path = "/run/nixos-etc-metadata"; ignore = true; }
418 { mount_path = "/run/nixos-etc-metadata.*"; ignore = true; }
419 ];
420 icon_names.media = ["drive-removable-media-symbolic"];
602 }; 421 };
603 }; 422 };
604 network-manager-applet.enable = true; 423 network-manager-applet.enable = true;
@@ -615,7 +434,7 @@ in {
615 batch = "true"; 434 batch = "true";
616 log = "false"; 435 log = "false";
617 repeat = "watch"; 436 repeat = "watch";
618 sshcmd = "${pkgs.openssh}/bin/ssh"; 437 sshcmd = lib.getExe' pkgs.openssh "ssh";
619 ui = "text"; 438 ui = "text";
620 }; 439 };
621 }; 440 };
@@ -631,35 +450,7 @@ in {
631 serverUrl = "https://etesync.yggdrasil.li"; 450 serverUrl = "https://etesync.yggdrasil.li";
632 }; 451 };
633 452
634 swayidle = {
635 enable = true;
636 events = [
637 { event = "before-sleep"; command = lockCommand; }
638 { event = "after-resume"; command = "${cfg.wayland.windowManager.hyprland.package}/bin/hyprctl dispatch dpms on"; }
639 { event = "lock"; command = lockCommand; }
640 ];
641 timeouts = [
642 { timeout = 300;
643 command = "${cfg.wayland.windowManager.hyprland.package}/bin/hyprctl dispatch dpms off";
644 }
645 { timeout = 330; command = lockCommand; }
646 ];
647 extraArgs = [
648 "idlehint" "30"
649 ];
650 };
651 poweralertd.enable = true; 453 poweralertd.enable = true;
652 avizo = {
653 enable = true;
654 settings.default = {
655 time = "1.0";
656 background = "rgba(0, 0, 0, 0.8)";
657 border-color = "rgba(0, 0, 0, 1)";
658 bar-fg-color = "rgba(160, 160, 160, 1)";
659 bar-bg-color = "rgba(32, 32, 32, 0.96)";
660 # y-offset = "0.25";
661 };
662 };
663 }; 454 };
664 455
665 home.pointerCursor = { 456 home.pointerCursor = {
@@ -690,6 +481,15 @@ in {
690 name = "Paper-Mono-Dark"; 481 name = "Paper-Mono-Dark";
691 }; 482 };
692 }; 483 };
484 qt.enable = true;
485 qt.platformTheme.name = "gtk";
486
487 qt.kde.settings = {
488 kwalletrc = {
489 KSecretD.Enabled = false;
490 Wallet."Default Wallet" = "store";
491 };
492 };
693 493
694 xsession.preferStatusNotifierItems = true; 494 xsession.preferStatusNotifierItems = true;
695 495
@@ -699,18 +499,19 @@ in {
699 packages = with pkgs; [ 499 packages = with pkgs; [
700 fira fira-code pwvucontrol wrappedKeepassxc wl-clipboard-rs 500 fira fira-code pwvucontrol wrappedKeepassxc wl-clipboard-rs
701 mumble pulseaudio-ctl pamixer libnotify screen-message 501 mumble pulseaudio-ctl pamixer libnotify screen-message
702 wrappedYTMDesktop libsForQt5.qt5ct playerctl evince 502 wrappedYTMDesktop libsForQt5.qt5ct playerctl evince papers
703 thunderbird zoom-us steam steam-run wireshark virt-manager 503 thunderbird zoom-us xdg-desktop-portal steam steam-run
704 rclone cached-nix-shell worktime fira-code-symbols 504 wireshark virt-manager rclone cached-nix-shell worktime
705 libreoffice xournalpp google-chrome nixos-shell virt-viewer 505 fira-code-symbols libreoffice xournalpp
706 freerdp gnome-icon-theme paper-icon-theme sshpassSecret 506 nixos-shell virt-viewer freerdp gnome-icon-theme
707 weechat element-desktop matrix-synapse-tools.synadm 507 paper-icon-theme sshpassSecret weechat element-desktop
708 flakeInputs.deploy-rs.packages.${config.nixpkgs.system}.deploy-rs 508 sieve-connect gimp3 inkscape udiskie glab nitrokey-app
709 sieve-connect gimp inkscape udiskie glab nitrokey-app
710 pynitrokey gtklock wlrctl remmina openscad spice-record 509 pynitrokey gtklock wlrctl remmina openscad spice-record
711 libguestfs-with-appliance nerd-fonts.fira-mono 510 nerd-fonts.fira-mono
712 nerd-fonts.symbols-only nerd-fonts.fira-code powerline-fonts 511 nerd-fonts.symbols-only nerd-fonts.fira-code powerline-fonts
713 ]; 512 swtpm (hunspell.withDicts (dicts: with dicts; [en_GB-large de_DE]))
513 libation libqalculate
514 ] ++ mapAttrsToList (_name: pkg: pkgs.callPackage pkg {}) (customUtils.nixImport { dir = ./utils; });
714 515
715 file = { 516 file = {
716 ".backup-munin".source = ./backup-patterns; 517 ".backup-munin".source = ./backup-patterns;
@@ -727,15 +528,12 @@ in {
727 sessionVariables = { 528 sessionVariables = {
728 # GDK_SCALE = 96.0 / 282.0; 529 # GDK_SCALE = 96.0 / 282.0;
729 # QT_AUTO_SCREEN_SCALE_FACTOR = 1; 530 # QT_AUTO_SCREEN_SCALE_FACTOR = 1;
730 QT_QPA_PLATFORMTHEME = "qt5ct"; 531 QT_QPA_PLATFORMTHEME = lib.mkForce "gtk3";
731 LIBVIRT_DEFAULT_URI = "qemu:///system"; 532 LIBVIRT_DEFAULT_URI = "qemu:///system";
732 STACK_XDG = 1; 533 STACK_XDG = 1;
733 EDITOR = pkgs.writeShellScript "editor" '' 534 EDITOR = lib.getExe' editor "emacsclient";
734 args=("--reuse-frame" "--alternate-editor" "") 535 RCLONE_PASSWORD_COMMAND = "${lib.getExe' pkgs.libsecret "secret-tool"} lookup service rclone";
735 args+=("$@") 536 SYSTEMD_TINT_BACKGROUND = "false";
736 exec -a emacsclient ${cfg.services.emacs.package}/bin/emacsclient "''${args[@]}"
737 '';
738 RCLONE_PASSWORD_COMMAND = "${pkgs.libsecret}/bin/secret-tool lookup service rclone";
739 }; 537 };
740 538
741 extraProfileCommands = '' 539 extraProfileCommands = ''
@@ -744,18 +542,11 @@ in {
744 }; 542 };
745 543
746 xdg.configFile = { 544 xdg.configFile = {
747 "dunst/dunstrc.d" = {
748 source = ./dunstrc.d;
749 recursive = true;
750 onChange = ''
751 ${pkgs.systemd}/bin/systemctl --user try-restart dunst
752 '';
753 };
754 "wireplumber" = { 545 "wireplumber" = {
755 source = ./wireplumber; 546 source = ./wireplumber;
756 recursive = true; 547 recursive = true;
757 onChange = '' 548 onChange = ''
758 ${pkgs.systemd}/bin/systemctl --user try-restart wireplumber 549 ${lib.getExe' config.systemd.package "systemctl"} --user try-restart wireplumber
759 ''; 550 '';
760 }; 551 };
761 "stack/config.yaml" = { 552 "stack/config.yaml" = {
@@ -779,37 +570,36 @@ in {
779 General = { 570 General = {
780 dot_as_separator = 0; 571 dot_as_separator = 0;
781 }; 572 };
573 Mode = {
574 calculate_as_you_type = 1;
575 };
782 }; 576 };
783 }; 577 };
784 "emacs/init.el".source = ./emacs.el; 578 "emacs/init.el".source = pkgs.substitute {
579 src = ./emacs.el;
580 substitutions = [
581 "--subst-var-by" "ksshaskpass" (lib.getExe pkgs.kdePackages.ksshaskpass)
582 ];
583 };
584 # "systemd/user/xdg-desktop-portal.service.d/after-graphical-session.conf".text = ''
585 # [Unit]
586 # After=graphical-session.target
587 # '';
588 "systemd/user/home-manager.service.d/before-graphical-session.conf".text = ''
589 [Unit]
590 Before=graphical-session-pre.target
591 '';
592 "pdfpc/pdfpcrc".text = ''
593 mouse 8 prev
594 mouse 9 next
595 '';
785 }; 596 };
786 597
787 xdg.dataFile = { 598 xdg.dataFile = {
788 "pandoc/abbreviations" = {
789 source = pkgs.runCommand "pandoc-abbreviations" {
790 buildInputs = [ pkgs.pandoc pkgs.coreutils ];
791 } (let
792 germanAbbrevs = pkgs.fetchFromGitHub {
793 owner = "jfilter";
794 repo = "german-abbreviations";
795 rev = "8eb9dae93b6f05d7c53374cd217ab2dc89558e0c";
796 sha256 = "SaD3tSqzen6Y3SPICe6/9vhe4iMHlArZ3kFQaEk7Hps=";
797 };
798 in ''
799 cat \
800 <(pandoc --print-default-data-file=abbreviations) \
801 <(grep -E '^[^ ]+\.$' ${germanAbbrevs}/german_abbreviations.txt) \
802 ${pkgs.writeText "abbrevs.txt" ''
803 i.A.
804 d.h.
805 D.h.
806 gdw.
807 ''} \
808 | sort | uniq >$out
809 '');
810 };
811 "dbus-1/services/org.keepassxc.KeePassXC.service".source = "${wrappedKeepassxc}/share/dbus-1/services/org.keepassxc.KeePassXC.service"; 599 "dbus-1/services/org.keepassxc.KeePassXC.service".source = "${wrappedKeepassxc}/share/dbus-1/services/org.keepassxc.KeePassXC.service";
812 "dbus-1/services/org.freedesktop.secrets.service.service".source = "${wrappedKeepassxc}/share/dbus-1/services/org.freedesktop.secrets.service.service"; 600 "dbus-1/services/org.freedesktop.secrets.service.service".source = "${wrappedKeepassxc}/share/dbus-1/services/org.freedesktop.secrets.service.service";
601 "dbus-1/services/org.kde.kwalletd6.service".source = "${pkgs.kdePackages.kwallet}/share/dbus-1/services/org.kde.kwalletd6.service";
602 "dbus-1/services/org.kde.kwalletd5.service".source = "${pkgs.kdePackages.kwallet}/share/dbus-1/services/org.kde.kwalletd5.service";
813 "emoji-data/list.txt".source = pkgs.stdenv.mkDerivation { 603 "emoji-data/list.txt".source = pkgs.stdenv.mkDerivation {
814 inherit (sources.emoji-data) pname src; 604 inherit (sources.emoji-data) pname src;
815 version = lib.removePrefix "v" sources.emoji-data.version; 605 version = lib.removePrefix "v" sources.emoji-data.version;
@@ -895,19 +685,68 @@ in {
895 name = "Rainbow"; 685 name = "Rainbow";
896 exec = toString (pkgs.writeShellScript "rainbow" '' 686 exec = toString (pkgs.writeShellScript "rainbow" ''
897 exec -- \ 687 exec -- \
898 ${config.systemd.package}/bin/systemd-run --wait --user --slice-inherit \ 688 ${lib.getExe' config.systemd.package "systemd-run"} --wait --user --slice-inherit \
899 --property 'CPUAccounting=yes' --property 'CPUQuotaPeriodSec=50ms' \ 689 --property 'CPUAccounting=yes' --property 'CPUQuotaPeriodSec=50ms' \
900 --property 'Environment=DSCP=46' \ 690 -E DSCP=46 -E NIXOS_OZONE_WL \
901 -- ${pkgs.dscp}/bin/dscp ${pkgs.google-chrome}/bin/google-chrome-stable \ 691 -- ${lib.getExe pkgs.dscp} ${lib.getExe cfg.programs.chromium.package} \
902 --force-device-scale-factor=1.5 \
903 --class=Rainbow \ 692 --class=Rainbow \
904 --kiosk "https://web.openrainbow.com" \ 693 --app="https://web.openrainbow.com" \
905 --user-data-dir=''${HOME}/.config/google-chrome-rainbow 694 --user-data-dir=''${HOME}/.config/chromium-rainbow
906 ''); 695 '');
907 icon = pkgs.fetchurl { 696 icon = pkgs.fetchurl {
908 url = "https://web.openrainbow.com/rb/2.139.17/assets/skins/rainbow/images/homepage/logo__rainbow.svg"; 697 url = "https://web.openrainbow.com/rb/2.139.17/assets/skins/rainbow/images/homepage/logo__rainbow.svg";
909 hash = "sha256-5fmo8rDqVDpzkGaPjk4Y+SsSZpAsY7VUQSFW6WdHwuU="; 698 hash = "sha256-5fmo8rDqVDpzkGaPjk4Y+SsSZpAsY7VUQSFW6WdHwuU=";
910 }; 699 };
700 settings = {
701 StartupWMClass = "Rainbow";
702 };
703 };
704 kimai = {
705 name = "Kimai";
706 exec = toString (pkgs.writeShellScript "kimai" ''
707 exec -- \
708 ${lib.getExe cfg.programs.chromium.package} \
709 --class=Kimai \
710 --app="https://kimai.yggdrasil.li" \
711 --user-data-dir=''${HOME}/.config/chromium-kimai
712 '');
713 icon = pkgs.fetchurl {
714 url = "https://www.kimai.org/images/kimai_logo.png";
715 hash = "sha256-lnlOttzR2SwXA70R+egJUkeKr4U5V0avqTk8uX4bqfs=";
716 };
717 settings = {
718 StartupWMClass = "Kimai";
719 StartupNotify = "true";
720 };
721 };
722 audiobookshelf = {
723 name = "Audiobookshelf";
724 exec = toString (pkgs.writeShellScript "audiobookshelf" ''
725 exec -- \
726 ${lib.getExe cfg.programs.chromium.package} \
727 --class=Audiobookshelf \
728 --app="https://audiobookshelf.yggdrasil.li" \
729 --user-data-dir=''${HOME}/.config/chromium-audiobookshelf
730 '');
731 icon = pkgs.fetchurl {
732 url = "https://www.audiobookshelf.org/Logo.png";
733 hash = "sha256-JGPk+WNT1C4DC4lSMb0K0YmAMT5LvmSOeO0QRzkc7Lk=";
734 };
735 settings = {
736 StartupWMClass = "Audiobookshelf";
737 StartupNotify = "true";
738 };
739 };
740 thunderbird-lmu = {
741 name = "Thunderbird (LMU)";
742 exec = "thunderbird --name thunderbird -P lmu %U";
743 icon = "thunderbird";
744 genericName = "Email Client";
745 categories = [ "Network" "Chat" "Email" "Feed" "GTK" "News" ];
746 settings = {
747 StartupWMClass = "thunderbird";
748 StartupNotify = "true";
749 };
911 }; 750 };
912 }; 751 };
913 752
@@ -923,23 +762,6 @@ in {
923 color-scheme = "prefer-dark"; 762 color-scheme = "prefer-dark";
924 }; 763 };
925 }; 764 };
926
927 wayland.windowManager.hyprland = {
928 enable = true;
929 settings = import ./hyprland.nix inputs;
930 };
931
932 xdg.portal = {
933 enable = true;
934 xdgOpenUsePortal = true;
935 config = {
936 common.default = [ "gtk" ];
937 hyprland.default = [ "gtk" "kde" "hyprland" ];
938 };
939 extraPortals = with pkgs; [
940 xdg-desktop-portal-kde xdg-desktop-portal-gtk xdg-desktop-portal-wlr xdg-desktop-portal-hyprland
941 ];
942 };
943 }; 765 };
944 }; 766 };
945} 767}
diff --git a/accounts/gkleen@sif/dunst-settings.nix b/accounts/gkleen@sif/dunst-settings.nix
deleted file mode 100644
index 72687aea..00000000
--- a/accounts/gkleen@sif/dunst-settings.nix
+++ /dev/null
@@ -1,45 +0,0 @@
1{ pkgs, ... }:
2{
3 global = {
4 font = "Fira Sans 12";
5 markup = "full";
6 format = "<i>%s</i> %p\\n%b";
7 alignment = "left";
8 # geometry = "1216x10-32+64";
9 width = 500;
10 height = 100;
11 offset = "4x4";
12 origin = "top-right";
13 shrink = true;
14 monitor = 0;
15 follow = "none";
16 padding = 6;
17 horizontal_padding = 6;
18 separator_height = 1;
19 separator_color = "frame";
20 idle_threshold = 0;
21
22 transparency = 10;
23
24 frame_width = 1;
25 frame_color = "#999999";
26
27 word_wrap = true;
28 show_age_threshold = 15;
29 show_indicators = false;
30 icon_position = "right";
31 min_icon_size = 25;
32 max_icon_size = 25;
33 sort = false;
34 sticky_history = false;
35
36 dmenu = "fuzzel --dmenu";
37 browser = "${pkgs.xdg-utils}/bin/xdg-open";
38 };
39 # shortcuts = {
40 # close = "ctrl+space";
41 # close_all = "ctrl+shift+space";
42 # history = "ctrl+comma";
43 # context = "ctrl+period";
44 # };
45}
diff --git a/accounts/gkleen@sif/dunstrc.d/00-urgency_low.conf b/accounts/gkleen@sif/dunstrc.d/00-urgency_low.conf
deleted file mode 100644
index 98c94b64..00000000
--- a/accounts/gkleen@sif/dunstrc.d/00-urgency_low.conf
+++ /dev/null
@@ -1,4 +0,0 @@
1[urgency_low]
2background="#000000aa"
3foreground="#999999"
4timeout=5 \ No newline at end of file
diff --git a/accounts/gkleen@sif/dunstrc.d/01-urgency_normal.conf b/accounts/gkleen@sif/dunstrc.d/01-urgency_normal.conf
deleted file mode 100644
index f8fa8e2d..00000000
--- a/accounts/gkleen@sif/dunstrc.d/01-urgency_normal.conf
+++ /dev/null
@@ -1,4 +0,0 @@
1[urgency_normal]
2background="#000000aa"
3foreground="#ffffff"
4timeout=15 \ No newline at end of file
diff --git a/accounts/gkleen@sif/dunstrc.d/02-urgency_critical.conf b/accounts/gkleen@sif/dunstrc.d/02-urgency_critical.conf
deleted file mode 100644
index a08bf4b1..00000000
--- a/accounts/gkleen@sif/dunstrc.d/02-urgency_critical.conf
+++ /dev/null
@@ -1,4 +0,0 @@
1[urgency_critical]
2background="#900000aa"
3foreground="#ffffff"
4timeout=0
diff --git a/accounts/gkleen@sif/dunstrc.d/10-brightness.conf b/accounts/gkleen@sif/dunstrc.d/10-brightness.conf
deleted file mode 100644
index c54595ab..00000000
--- a/accounts/gkleen@sif/dunstrc.d/10-brightness.conf
+++ /dev/null
@@ -1,5 +0,0 @@
1[brightness]
2appname="brightness"
3set_stack_tag="brightness"
4set_transient=yes
5history_ignore=yes
diff --git a/accounts/gkleen@sif/dunstrc.d/10-pulseaudio-ctl.conf b/accounts/gkleen@sif/dunstrc.d/10-pulseaudio-ctl.conf
deleted file mode 100644
index 074f4535..00000000
--- a/accounts/gkleen@sif/dunstrc.d/10-pulseaudio-ctl.conf
+++ /dev/null
@@ -1,5 +0,0 @@
1[pulseaudio-ctl]
2body="Current is *"
3history_ignore=yes
4set_stack_tag="volume"
5summary="Volume *"
diff --git a/accounts/gkleen@sif/dunstrc.d/20-element.conf b/accounts/gkleen@sif/dunstrc.d/20-element.conf
deleted file mode 100644
index 5ff6031e..00000000
--- a/accounts/gkleen@sif/dunstrc.d/20-element.conf
+++ /dev/null
@@ -1,3 +0,0 @@
1[element-im]
2appname=Element
3timeout=0 \ No newline at end of file
diff --git a/accounts/gkleen@sif/dunstrc.d/20-kitty.conf b/accounts/gkleen@sif/dunstrc.d/20-kitty.conf
deleted file mode 100644
index b27ee27e..00000000
--- a/accounts/gkleen@sif/dunstrc.d/20-kitty.conf
+++ /dev/null
@@ -1,3 +0,0 @@
1[kitty]
2appname=kitty
3urgency=low
diff --git a/accounts/gkleen@sif/dunstrc.d/20-mail.conf b/accounts/gkleen@sif/dunstrc.d/20-mail.conf
deleted file mode 100644
index cb568e01..00000000
--- a/accounts/gkleen@sif/dunstrc.d/20-mail.conf
+++ /dev/null
@@ -1,3 +0,0 @@
1[element]
2appname="notmuch"
3timeout=0 \ No newline at end of file
diff --git a/accounts/gkleen@sif/dunstrc.d/20-zulip.conf b/accounts/gkleen@sif/dunstrc.d/20-zulip.conf
deleted file mode 100644
index d7fbd32c..00000000
--- a/accounts/gkleen@sif/dunstrc.d/20-zulip.conf
+++ /dev/null
@@ -1,3 +0,0 @@
1[zulip]
2appname="Zulip"
3timeout=0 \ No newline at end of file
diff --git a/accounts/gkleen@sif/emacs.el b/accounts/gkleen@sif/emacs.el
index b1b1b198..3beefba6 100644
--- a/accounts/gkleen@sif/emacs.el
+++ b/accounts/gkleen@sif/emacs.el
@@ -14,7 +14,7 @@
14(setq package-archives nil) 14(setq package-archives nil)
15(package-initialize) 15(package-initialize)
16(require 'use-package) 16(require 'use-package)
17(use-package use-package-ensure-system-package :ensure t) 17;; (use-package use-package-ensure-system-package :ensure t)
18 18
19(require 'evil) 19(require 'evil)
20(evil-mode 1) 20(evil-mode 1)
@@ -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)
@@ -228,6 +228,7 @@ necessarily running."
228 (global-set-key (kbd "C-x k") 'kill-buffer-with-special-emacsclient-handling)) 228 (global-set-key (kbd "C-x k") 'kill-buffer-with-special-emacsclient-handling))
229 229
230(add-hook 'server-switch-hook 'install-emacsclient-wrapped-kill-buffer) 230(add-hook 'server-switch-hook 'install-emacsclient-wrapped-kill-buffer)
231(add-hook 'server-switch-hook #'raise-frame)
231 232
232(defun move-file (new-location) 233(defun move-file (new-location)
233 "Write this file to NEW-LOCATION, and delete the old one." 234 "Write this file to NEW-LOCATION, and delete the old one."
@@ -253,3 +254,5 @@ necessarily running."
253(bind-key "C-x C-m" #'move-file) 254(bind-key "C-x C-m" #'move-file)
254 255
255(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/firefox-chrome.css b/accounts/gkleen@sif/firefox-chrome.css
index 8900e2b9..726f1e4b 100644
--- a/accounts/gkleen@sif/firefox-chrome.css
+++ b/accounts/gkleen@sif/firefox-chrome.css
@@ -4,6 +4,21 @@
4 font-size:12px; 4 font-size:12px;
5} 5}
6 6
7#sidebar-main:has([expanded]) {
8 min-width:20em !important;
9 max-width:20em !important;
10}
11
12#sidebar, #sidebar-box {
13 min-width:35em !important;
14 max-width:35em !important;
15}
16
17#sidebar-box {
18 margin-right: var(--space-small);
19}
20
21/*
7#sidebar { 22#sidebar {
8 min-width:20em !important; 23 min-width:20em !important;
9 max-width:20em !important; 24 max-width:20em !important;
@@ -19,8 +34,21 @@
19} 34}
20 35
21#toolbar-menubar[inactive="true"] + #TabsToolbar { 36#toolbar-menubar[inactive="true"] + #TabsToolbar {
22 visibility: collapse !important; 37 visibility: collapse !important;
23} 38}
24 39
25#sidebar-box[sidebarcommand="tabcenter-reborn_ariasuni-sidebar-action"] #sidebar-header { visibility: collapse !important; } 40#sidebar-box[sidebarcommand="tabcenter-reborn_ariasuni-sidebar-action"] #sidebar-header { visibility: collapse !important; }
26#sidebar-box[sidebarcommand="_3c078156-979c-498b-8990-85f7987dd929_-sidebar-action"] #sidebar-header { visibility: collapse !important; } 41#sidebar-box[sidebarcommand="_3c078156-979c-498b-8990-85f7987dd929_-sidebar-action"] #sidebar-header { visibility: collapse !important; }
42*/
43
44.titlebar-buttonbox-container{ display: none; }
45#vertical-spacer { display: none; }
46#tabbrowser-tabs[orient="vertical"] .tab-background {
47 border-radius: var(--border-radius-small) !important;
48}
49hbox:has(> #tabs-newtab-button) {
50 display: none;
51}
52#sidebar-main .tools-and-extensions {
53 justify-content: space-around !important;
54}
diff --git a/accounts/gkleen@sif/hyprland.nix b/accounts/gkleen@sif/hyprland.nix
deleted file mode 100644
index d3061c61..00000000
--- a/accounts/gkleen@sif/hyprland.nix
+++ /dev/null
@@ -1,424 +0,0 @@
1{ pkgs, lib, config, userName, ... }:
2let
3 cfg = config.home-manager.users.${userName};
4in {
5 monitor = [
6 ",preferred,auto,auto,bitdepth,8"
7 "eDP-1,3840x2160@60,auto,1.5,bitdepth,8"
8 ];
9
10 "$terminal" = "kitty";
11 "$menu" = "fuzzel";
12
13 exec-once = [
14 "wpaperd"
15 ];
16
17 env = [
18 "NIXOS_OZONE_WL,1"
19 "QT_QPA_PLATFORM,wayland"
20 "QT_WAYLAND_DISABLE_WINDOWDECORATION,1"
21 "GDK_BACKEND,wayland"
22 "GDK_SCALE,0.66"
23 "QT_AUTO_SCREEN_SCALE_FACTOR,1"
24 "SDL_VIDEODRIVER,wayland"
25 # "AQ_DRM_DEVICES,/dev/dri/by-path/pci-0000:01:00.0-card"
26 "__NV_PRIME_RENDER_OFFLOAD,1"
27 "__NV_PRIME_RENDER_OFFLOAD_PROVIDER,NVIDIA-G0"
28 "__GLX_VENDOR_LIBRARY_NAME,nvidia"
29 "__VK_LAYER_NV_optimus,NVIDIA_only"
30 ];
31
32 xwayland.force_zero_scaling = true;
33
34 general = {
35 gaps_in = 3;
36 gaps_out = 9;
37 "col.active_border" = "rgba(33ccffee) rgba(00ff95ee) 45deg";
38 "col.inactive_border" = "rgba(595959aa)";
39
40 resize_on_border = false;
41
42 allow_tearing = false;
43
44 layout = "dwindle";
45 };
46
47 decoration = {
48 rounding = 5;
49 dim_special = 0.0;
50 };
51
52 animations = {
53 enabled = true;
54 bezier = "myBezier, 0.05, 0.9, 0.1, 1.05";
55 animation = [
56 "windows, 1, 1, default, popin 80%"
57 "windowsMove, 0"
58 # "windows, 1, 7, myBezier"
59 # "windowsOut, 1, 7, myBezier, popin 80%"
60 "border, 1, 10, default"
61 "borderangle, 1, 8, default"
62 "fade, 1, 1, default"
63 "workspaces, 1, 1, default, fade"
64 # "workspaces, 1, 6, default"
65 ];
66 };
67
68 dwindle = {
69 pseudotile = false;
70 preserve_split = true;
71 };
72
73 master = {
74 new_status = "master";
75 };
76
77 misc = {
78 disable_hyprland_logo = true;
79 disable_splash_rendering = true;
80 # focus_on_activate = true;
81 mouse_move_enables_dpms = true;
82 key_press_enables_dpms = true;
83 new_window_takes_over_fullscreen = 1;
84 exit_window_retains_fullscreen = true;
85 };
86
87 cursor = {
88 hide_on_key_press = true;
89 };
90
91 input = {
92 kb_layout = "us,us";
93 kb_variant = "dvp,";
94 kb_model = "";
95 kb_options = "compose:caps,grp:win_space_toggle";
96 kb_rules = "";
97
98 follow_mouse = 1;
99
100 sensitivity = 0;
101
102 touchpad = {
103 natural_scroll = false;
104 };
105 };
106
107 device = [
108 { name = "synaptics-tm3512-010";
109 sensitivity = 0.4;
110 }
111 { name = "tpps/2-elan-trackpoint";
112 sensitivity = 0.2;
113 }
114 { name = "logitech-ergo-m575";
115 sensitivity = 1.333;
116 }
117 ];
118
119 gestures = {
120 workspace_swipe = false;
121 };
122
123 dwindle = {
124 # no_gaps_when_only = 1;
125 };
126
127 "$mainMod" = "SUPER";
128
129 bind = [
130 "$mainMod, return, exec, $terminal"
131 "$mainMod, Q, killactive"
132 "$mainMod SHIFT, Q, exec, hyprctl kill"
133 "$mainMod, V, togglefloating"
134 "$mainMod, D, exec, $menu"
135 "$mainMod SHIFT, D, exec, $menu --list-executables-in-path"
136 # "$mainMod, J, togglesplit,"
137
138 "$mainMod SHIFT, L, exec, loginctl lock-session"
139 "$mainMod SHIFT, E, exit"
140
141 "$mainMod, left, movefocus, l"
142 "$mainMod, right, movefocus, r"
143 "$mainMod, up, movefocus, u"
144 "$mainMod, down, movefocus, d"
145 "$mainMod SHIFT, left, swapwindow, l"
146 "$mainMod SHIFT, right, swapwindow, r"
147 "$mainMod SHIFT, up, swapwindow, u"
148 "$mainMod SHIFT, down, swapwindow, d"
149
150 "$mainMod, T, cyclenext"
151
152 "$mainMod, G, focusmonitor, 0"
153 "$mainMod, C, focusmonitor, 1"
154 "$mainMod, R, focusmonitor, 2"
155 "$mainMod, L, focusmonitor, 3"
156
157 "$mainMod CTRL, G, movecurrentworkspacetomonitor, 0"
158 "$mainMod CTRL, C, movecurrentworkspacetomonitor, 1"
159 "$mainMod CTRL, R, movecurrentworkspacetomonitor, 2"
160 "$mainMod CTRL, L, movecurrentworkspacetomonitor, 3"
161
162 "$mainMod, F, fullscreen, 1"
163 "$mainMod SHIFT, F, fullscreen, 0"
164 "$mainMod CTRL SHIFT, F, fullscreenstate, 1, 2"
165
166 "$mainMod, code:14, workspace, 1"
167 "$mainMod, code:17, workspace, 2"
168 "$mainMod, code:13, workspace, 3"
169 "$mainMod, code:18, workspace, 4"
170 "$mainMod, code:12, workspace, 5"
171 "$mainMod, code:19, workspace, 6"
172 "$mainMod, code:11, workspace, 7"
173 "$mainMod, code:20, workspace, 8"
174 "$mainMod, code:15, workspace, 9"
175 "$mainMod, code:16, workspace, 10"
176
177 "$mainMod SHIFT, code:14, movetoworkspacesilent, 1"
178 "$mainMod SHIFT, code:17, movetoworkspacesilent, 2"
179 "$mainMod SHIFT, code:13, movetoworkspacesilent, 3"
180 "$mainMod SHIFT, code:18, movetoworkspacesilent, 4"
181 "$mainMod SHIFT, code:12, movetoworkspacesilent, 5"
182 "$mainMod SHIFT, code:19, movetoworkspacesilent, 6"
183 "$mainMod SHIFT, code:11, movetoworkspacesilent, 7"
184 "$mainMod SHIFT, code:20, movetoworkspacesilent, 8"
185 "$mainMod SHIFT, code:15, movetoworkspacesilent, 9"
186 "$mainMod SHIFT, code:16, movetoworkspacesilent, 10"
187
188 "$mainMod, semicolon, exec, dunstctl close"
189 "$mainMod SHIFT, semicolon, exec, dunstctl close-all"
190 "$mainMod, period, exec, dunstctl context"
191 "$mainMod, comma, exec, dunstctl history-pop"
192
193 "$mainMod ALT, E, exec, emacsclient -c"
194 "$mainMod ALT, Y, exec, ${pkgs.writeShellScript "yt-dlp" ''
195 export PATH="${lib.makeBinPath (with pkgs; [ wl-clipboard-rs socat ])}:$PATH"
196 socat STDIO UNIX-CONNECT:"$XDG_RUNTIME_DIR"/yt-dlp.sock <<<$'{ "urls": ["'"$(wl-paste)"$'"] }'
197 ''}"
198 "$mainMod ALT, L, exec, ${pkgs.writeShellScript "mpv" ''
199 export PATH="${lib.makeBinPath (with pkgs; [ wl-clipboard-rs ])}:$PATH"
200 exec mpv "$(wl-paste)"
201 ''}"
202
203 ", Print, exec, ${pkgs.writeShellScript "screenshot" ''
204 export PATH="${lib.makeBinPath (with pkgs; [ grim slurp wl-clipboard-rs coreutils ])}:$PATH"
205
206 outFile="$HOME/screenshots/$(date +"%Y-%m-%dT%H:%M:%S").png"
207 grim -g "$(slurp -b 00000080 -c FFFFFFFF -s 00000000 -w 1)" "$outFile"
208 wl-copy --type image/png <"$outFile"
209 ''}"
210 "SHIFT, Print, exec, ${pkgs.writeShellScript "screenshot" ''
211 export PATH="${lib.makeBinPath (with pkgs; [ grim jq wl-clipboard-rs coreutils ])}:$PATH"
212
213 outFile="$HOME/screenshots/$(date +"%Y-%m-%dT%H:%M:%S").png"
214 grim -o "$(hyprctl monitors -j | jq -r '.[] | select(.focused) | .name')" "$outFile"
215 wl-copy --type image/png <"$outFile"
216 ''}"
217 "CTRL SHIFT, Print, exec, ${pkgs.runCommand "picker" {
218 buildInputs = [ pkgs.makeWrapper ];
219 } ''
220 makeWrapper ${lib.getExe pkgs.hyprpicker} $out \
221 --prefix PATH : ${lib.makeBinPath [pkgs.wl-clipboard-rs]}
222 ''} -a"
223 "$mainMod, M, exec, ${pkgs.writeShellScript "qalc-fuzzel" ''
224 export PATH="${lib.makeBinPath (with pkgs; [ wl-clipboard-rs libqalculate cfg.programs.fuzzel.package coreutils findutils libnotify gnugrep ])}:$PATH"
225
226 RESULTS_DIR="$HOME/.cache/qalc-fuzzel"
227 prev() {
228 FOUND=false
229 while IFS= read -r line; do
230 [[ -n "$line" ]] || continue
231 FOUND=true
232 echo $line
233 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)
234 $FOUND || echo
235 }
236 FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> ") || exit $?
237 if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then
238 QALC_RES="$FUZZEL_RES"
239 QALC_RET=0
240 else
241 QALC_RES=$(qalc "$FUZZEL_RES" 2>&1)
242 QALC_RET=$?
243 fi
244 [[ -n "$QALC_RES" ]] || exit 1
245 EXISTING=false
246 fgrep -xrl "$QALC_RES" "$RESULTS_DIR" | xargs -r touch
247 [[ ''${PIPESTATUS[0]} -eq 0 ]] && EXISTING=true
248 if [[ $QALC_RET -eq 0 ]] && ! $EXISTING; then
249 RES_FILE="$RESULTS_DIR"/$(date -uIs).$(tr -Cd 'a-zA-Z0-9' </dev/random | head -c 10)
250 cat >"$RES_FILE" <<<"$QALC_RES"
251 fi
252 [[ "$QALC_RES" =~ .*\ =\ (.*) ]] && QALC_RES="''${BASH_REMATCH[1]}"
253 [[ $QALC_RET -eq 0 ]] && wl-copy "$QALC_RES"
254 notify-send "$QALC_RES"
255 ''}"
256 "$mainMod, E, exec, ${pkgs.writeShellScript "emoji-fuzzel" ''
257 export PATH="${lib.makeBinPath (with pkgs; [ cfg.programs.fuzzel.package wtype wl-clipboard-rs ])}:$PATH"
258
259 FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " <$HOME/.local/share/emoji-data/list.txt) || exit $?
260 [[ -n "$FUZZEL_RES" ]] || exit 1
261 wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste
262 ''}"
263 "$mainMod, B, exec, ${pkgs.writeShellScript "bring" ''
264 export PATH="${lib.makeBinPath (with pkgs; [ cfg.programs.fuzzel.package gawk gojq cfg.wayland.windowManager.hyprland.package ])}:$PATH"
265
266 state="$(hyprctl -j clients)"
267 active_window="$(hyprctl -j activewindow)"
268 active_workspace="$(hyprctl -j activeworkspace | gojq -r '.id')"
269
270 current_addr="$(echo "$active_window" | gojq -r '.address')"
271
272 window="$(echo "$state" |
273 gojq -r '.[] | select(.workspace.id == '"$active_workspace"') | select(.monitor != -1 ) | "\(.title)\t\(.address)"' |
274 fuzzel --log-level=warning --dmenu)"
275
276 addr="$(echo "$window" | awk -F $'\t' '{print $2}')"
277
278 if [[ "$addr" = "$current_addr" ]]; then
279 exit 0
280 fi
281
282 fullscreen_on_same_ws="$(echo "$state" | gojq -r '.[] | select(.fullscreen == true) | select(.workspace.id == '"$active_workspace"') | .address')"
283
284 if [[ "$window" != "" ]]; then
285 if [[ "$fullscreen_on_same_ws" == "" ]]; then
286 hyprctl dispatch focuswindow address:"''${addr}"
287 else
288 # If we want to focus app_A and app_B is fullscreen on the same workspace,
289 # app_A will get focus, but app_B will remain on top.
290 # This monstrosity is to make sure app_A will end up on top instead.
291 # XXX: doesn't handle fullscreen 0, but I don't care.
292 hyprctl --batch "dispatch focuswindow address:''${fullscreen_on_same_ws}; dispatch fullscreen 1; dispatch focuswindow address:''${addr}; dispatch fullscreen 1"
293 fi
294 fi
295 ''}"
296 "$mainMod SHIFT, B, exec, ${pkgs.writeShellScript "bring" ''
297 export PATH="${lib.makeBinPath (with pkgs; [ cfg.programs.fuzzel.package gawk gojq cfg.wayland.windowManager.hyprland.package ])}:$PATH"
298
299 state="$(hyprctl -j clients)"
300 active_window="$(hyprctl -j activewindow)"
301
302 current_addr="$(echo "$active_window" | gojq -r '.address')"
303
304 window="$(echo "$state" |
305 gojq -r '.[] | select(.monitor != -1 ) | "\(.title)\t\(.workspace.name)\t\(.address)"' |
306 fuzzel --log-level=warning --dmenu)"
307
308 addr="$(echo "$window" | awk -F $'\t' '{print $3}')"
309 ws="$(echo "$window" | awk -F $'\t' '{print $2}')"
310
311 if [[ "$addr" = "$current_addr" ]]; then
312 exit 0
313 fi
314
315 fullscreen_on_same_ws="$(echo "$state" | gojq -r ".[] | select(.fullscreen == true) | select(.workspace.name == \"$ws\") | .address")"
316
317 if [[ "$window" != "" ]]; then
318 if [[ "$fullscreen_on_same_ws" == "" ]]; then
319 hyprctl dispatch focuswindow address:"''${addr}"
320 else
321 # If we want to focus app_A and app_B is fullscreen on the same workspace,
322 # app_A will get focus, but app_B will remain on top.
323 # This monstrosity is to make sure app_A will end up on top instead.
324 # XXX: doesn't handle fullscreen 0, but I don't care.
325 hyprctl --batch "dispatch focuswindow address:''${fullscreen_on_same_ws}; dispatch fullscreen 1; dispatch focuswindow address:''${addr}; dispatch fullscreen 1"
326 fi
327 fi
328 ''}"
329
330 "$mainMod CTRL, return, togglespecialworkspace, term"
331 "$mainMod CTRL, e, togglespecialworkspace, edit"
332 "$mainMod CTRL, a, togglespecialworkspace, pwvucontrol"
333 "$mainMod CTRL, o, togglespecialworkspace, easyeffects"
334 "$mainMod CTRL, b, togglespecialworkspace, blueman"
335 "$mainMod CTRL, p, togglespecialworkspace, keepass"
336 ];
337 bindm = [
338 "$mainMod, mouse:272, movewindow"
339 "$mainMod, mouse:273, resizewindow"
340 ];
341 bindel = [
342 ", XF86MonBrightnessUp, exec, lightctl -d -e4 -n1 up"
343 ", XF86MonBrightnessDown, exec, lightctl -d -e4 -n1 down"
344 ", XF86AudioRaiseVolume, exec, volumectl -d -u up"
345 ", XF86AudioLowerVolume, exec, volumectl -d -u down"
346 ];
347 bindl = [
348 ", XF86AudioMute, exec, volumectl -d toggle-mute"
349 ", XF86AudioMicMute, exec, volumectl -d -m toggle-mute"
350 "$mainMod SHIFT, S, exec, systemctl suspend"
351
352 ", switch:off:Lid Switch,exec,hyprctl dispatch dpms on eDP-1"
353 ", switch:on:Lid Switch,exec,hyprctl dispatch dpms off eDP-1"
354
355 ", switch:off:Lid Switch,exec,${pkgs.writeShellScript "clamshell-off" ''
356 export PATH="${lib.makeBinPath (with pkgs; [ jq ])}:$PATH"
357 [[ $(hyprctl monitors -j | jq '.[] | select(.name == "eDP-1") | .disabled') = "true" ]] || exit 0
358
359 hyprctl keyword monitor "eDP-1,3840x2160@60,auto,1.5"
360 ''}"
361 ", switch:on:Lid Switch,exec,${pkgs.writeShellScript "clamshell-on" ''
362 export PATH="${lib.makeBinPath (with pkgs; [ jq ])}:$PATH"
363
364 [[ $(hyprctl monitors -j | jq 'reduce (.[] | select(.disabled == false)) as $_ (0; .+1)') -gt 1 ]] || exit 0
365
366 hyprctl keyword monitor "eDP-1,disable"
367 ''}"
368 ];
369
370 windowrulev2 = [
371 "suppressevent maximize fullscreen, class:.*"
372
373 # "maximize, class:^(Element|thunderbird)$"
374 "workspace special:pwvucontrol, class:^com\.saivert\.pwvucontrol$"
375 "workspace special:easyeffects, class:^com\.github\.wwmm\.easyeffects$"
376 "workspace special:blueman, class:^\.blueman-manager-wrapped$"
377 "workspace special:keepass silent, class:^org\.keepassxc\.KeePassXC$, title:^(?!Unlock Database.*)(?!.*(Access Request|Passkey credentials))"
378 # "group set always lock invade, class:^Element$"
379 "workspace 2, class:^firefox$"
380 "workspace 4, class:^evince$"
381 "workspace 4, class:^imv$"
382 "workspace 4, class:^org\.pwmt\.zathura$"
383 "workspace 10, class:^mpv$"
384 "workspace 1, class:^Element$"
385 "workspace 1, class:^thunderbird$"
386 "workspace 5, class:^virt-manager$"
387 "workspace 5, class:^qemu$"
388 "float, class:^org\.keepassxc\.KeePassXC$, title:Access Request$"
389 "center, class:^org\.keepassxc\.KeePassXC$, title:Access Request$"
390 "float, class:^org\.keepassxc\.KeePassXC$, title:Passkey credentials$"
391 "center, class:^org\.keepassxc\.KeePassXC$, title:Passkey credentials$"
392 "float, class:^org\.keepassxc\.KeePassXC$, title:^Unlock Database"
393 "center, class:^org\.keepassxc\.KeePassXC$, title:^Unlock Database"
394 "float, class:^xdg-desktop-portal-gtk$"
395 "center, class:^xdg-desktop-portal-gtk$"
396
397 "bordercolor rgba(ffaa33ee) rgba(bfff00ee) 45deg, fullscreen:1"
398 "bordercolor rgba(3366ffee) rgba(6a00ffee) 45deg, xwayland:1"
399 "bordercolor rgba(6633ffee) rgba(ea00ffee) 45deg, xwayland:1, fullscreen:1"
400 ];
401
402 workspace = [
403 "s[true], gapsout:100"
404
405 "special:term, on-created-empty:kitty"
406 "special:edit, on-created-empty:emacsclient -c"
407 "special:pwvucontrol, on-created-empty:pwvucontrol"
408 "special:easyeffects, on-created-empty:easyeffects"
409 "special:blueman, on-created-empty:blueman-manager"
410 "special:keepass, on-created-empty:keepassxc"
411
412 "1, defaultName:comm"
413 "2, defaultName:web"
414 "3, defaultName:work"
415 "4, defaultName:read"
416 ];
417
418 layerrule = [
419 "blur, waybar"
420 "blur, launcher"
421 "noanim, notifications"
422 "blur, notifications"
423 ];
424}
diff --git a/accounts/gkleen@sif/libvirt/default.nix b/accounts/gkleen@sif/libvirt/default.nix
index f86a68a2..4e5a9b90 100644
--- a/accounts/gkleen@sif/libvirt/default.nix
+++ b/accounts/gkleen@sif/libvirt/default.nix
@@ -7,6 +7,7 @@ with flakeInputs.nixVirt.lib;
7 config = { 7 config = {
8 virtualisation.libvirt = { 8 virtualisation.libvirt = {
9 enable = true; 9 enable = true;
10 swtpm.enable = true;
10 connections."qemu:///session" = { 11 connections."qemu:///session" = {
11 domains = [ 12 domains = [
12 { definition = domain.writeXML (updateManyAttrsByPath [ 13 { definition = domain.writeXML (updateManyAttrsByPath [
@@ -16,8 +17,8 @@ with flakeInputs.nixVirt.lib;
16 memory = { count = 16; unit = "GiB"; }; 17 memory = { count = 16; unit = "GiB"; };
17 storage_vol = "/home/gkleen/.local/share/libvirt/images/lmmirzm-vmrz01.qcow2"; 18 storage_vol = "/home/gkleen/.local/share/libvirt/images/lmmirzm-vmrz01.qcow2";
18 nvram_path = "/home/gkleen/.local/share/libvirt/lmmirzm-vmrz01.nvram"; 19 nvram_path = "/home/gkleen/.local/share/libvirt/lmmirzm-vmrz01.nvram";
19 virtio_drive = false; 20 virtio_drive = true;
20 virtio_video = false; 21 virtio_video = true;
21 install_virtio = false; 22 install_virtio = false;
22 }) { 23 }) {
23 qemu-commandline.env = [ 24 qemu-commandline.env = [
@@ -33,11 +34,12 @@ with flakeInputs.nixVirt.lib;
33 os.bootmenu.enable = true; 34 os.bootmenu.enable = true;
34 devices.graphics = { 35 devices.graphics = {
35 listen.type = "address"; 36 listen.type = "address";
36 # gl.enable = true; 37 gl.enable = false;
37 }; 38 };
39 devices.video.model.acceleration.accel3d = false;
38 devices.interface = { 40 devices.interface = {
39 # model.type = "virtio"; 41 model.type = "virtio";
40 model.type = "e1000e"; 42 # model.type = "e1000e";
41 type = "bridge"; 43 type = "bridge";
42 mac.address = "52:54:00:b9:f3:ed"; 44 mac.address = "52:54:00:b9:f3:ed";
43 source.bridge = "rz-0971"; 45 source.bridge = "rz-0971";
@@ -47,6 +49,15 @@ with flakeInputs.nixVirt.lib;
47 type = "unix"; 49 type = "unix";
48 target = { type = "virtio"; name = "org.qemu.guest_agent.0"; }; 50 target = { type = "virtio"; name = "org.qemu.guest_agent.0"; };
49 } 51 }
52 {
53 type = "spicevmc";
54 target = { type = "virtio"; name = "com.redhat.spice.0"; };
55 }
56 {
57 type = "spiceport";
58 target = { type = "virtio"; name = "org.spice-space.webdav.0"; };
59 source.channel = "org.spice-space.webdav.0";
60 }
50 ]; 61 ];
51 devices.tpm.model = "tpm-tis"; 62 devices.tpm.model = "tpm-tis";
52 })); 63 }));
diff --git a/accounts/gkleen@sif/niri/default.nix b/accounts/gkleen@sif/niri/default.nix
new file mode 100644
index 00000000..5ae372c1
--- /dev/null
+++ b/accounts/gkleen@sif/niri/default.nix
@@ -0,0 +1,978 @@
1{ config, hostConfig, pkgs, lib, flakeInputs, ... }:
2let
3 cfg = config.programs.niri;
4
5 kdl = flakeInputs.niri-flake.lib.kdl;
6 sleaf = name: arg: kdl.node name [arg] [];
7
8 niri = cfg.package;
9 terminal = lib.getExe config.programs.kitty.package;
10
11 focus_or_spawn = pkgs.writeShellApplication {
12 name = "focus-or-spawn";
13 runtimeInputs = [ niri pkgs.gojq pkgs.gnugrep pkgs.socat ];
14 text = ''
15 window_select="$1"
16 shift
17 workspace_name="$1"
18 shift
19
20 workspaces_json="$(niri msg -j workspaces)"
21 workspace_output="$(jq -r --arg workspace_name "$workspace_name" '.[] | select(.name == $workspace_name) | .output' <<<"$workspaces_json")"
22 # active_workspace="$(jq -r --arg workspace_output "$workspace_output" '.[] | select(.output == $workspace_output and .is_active) | .id' <<<"$workspaces_json")"
23 active_output="$(jq -r '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
24 if [[ $workspace_output != "$active_output" ]]; then
25 niri msg action move-workspace-to-monitor --reference "$workspace_name" "$active_output"
26 # socat STDIO "$NIRI_SOCKET" <<<'{"Action":{"FocusWorkspace":{"reference":{"Id":'"''${active_workspace}"'}}}}'
27 # niri msg action move-workspace-to-index --reference "$workspace_name" 1
28 fi
29
30 while IFS=$'\n' read -r window_json; do
31 if [[ -n $(jq -c "$window_select" <<<"$window_json") ]]; then
32 if jq -e '.is_focused' <<<"$window_json" >/dev/null; then
33 niri msg action focus-workspace-previous
34 else
35 if [[ $(jq -r --arg workspace_name "$workspace_name" 'map(select(.name == $workspace_name)) | .[0].is_focused' <<<"$workspaces_json") != "true" ]] && [[ $(jq -r --arg workspace_name "$workspace_name" 'map(select(.name == $workspace_name)) | .[0].id' <<<"$workspaces_json") = $(jq -r '.workspace_id' <<<"$window_json") ]]; then
36 niri msg action focus-workspace "$workspace_name"
37 else
38 niri msg action focus-window --id "$(jq -r '.id' <<<"$window_json")"
39 fi
40 fi
41 exit 0
42 fi
43 done < <(niri msg -j windows | jq -c '.[]')
44
45 exec "$@"
46 '';
47 };
48 focus-or-spawn-action = config.lib.niri.actions.spawn (lib.getExe focus_or_spawn);
49
50 with_adjacent_workspace = pkgs.writeShellApplication {
51 name = "with-adjacent-workspace";
52 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
53 text = ''
54 blacklist="$1"
55 shift
56 direction="$1"
57 shift
58 action="$1"
59 shift
60
61 workspaces_json="$(niri msg -j workspaces)"
62 active_workspace="$(jq -r '.[] | select(.is_focused) | .id' <<<"$workspaces_json")"
63 workspace_output="$(jq -r --arg active_workspace "$active_workspace" '.[] | select(.id == ($active_workspace | tonumber)) | .output' <<<"$workspaces_json")"
64 workspace_idx="$(jq -r '.[] | select(.is_focused) | .idx' <<<"$workspaces_json")"
65
66 jq_script='map(select('
67 case "$direction" in
68 down)
69 # shellcheck disable=SC2016
70 jq_script=''${jq_script}'.idx > ($workspace_idx | tonumber)';;
71 up)
72 # shellcheck disable=SC2016
73 jq_script=''${jq_script}'.idx < ($workspace_idx | tonumber)';;
74 esac
75 # shellcheck disable=SC2016
76 jq_script=''${jq_script}' and .output == $workspace_output and ((.name == null) or (.name | test($blacklist) | not)))) | sort_by(.idx)'
77 [[ $direction == "up" ]] && jq_script=''${jq_script}' | reverse'
78 jq_script=''${jq_script}' | .[0]'
79
80 workspace_json=$(jq -c --arg blacklist "$blacklist" --arg workspace_output "$workspace_output" --arg workspace_idx "$workspace_idx" "$jq_script" <<<"$workspaces_json")
81 [[ -n $workspace_json && $workspace_json != null ]] || exit 0
82 jq --arg active_workspace "$active_workspace" -c "$action" <<<"$workspace_json" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
83 '';
84 };
85 with-adjacent-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_adjacent_workspace) "^${lib.concatMapStringsSep "|" ({ name, ...}: name) cfg.scratchspaces}$";
86 focus-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
87 move-column-to-adjacent-workspace = direction: with-adjacent-workspace-action direction ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}, "focus": true}}}'';
88
89 with_unnamed_workspace = pkgs.writeShellApplication {
90 name = "with-unnamed-workspace";
91 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
92 text = ''
93 action="$1"
94 shift
95
96 workspaces_json="$(niri msg -j workspaces)"
97 active_output="$(jq -r '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
98 active_workspace="$(jq -r '.[] | select(.is_focused) | .id' <<<"$workspaces_json")"
99
100 history_json="$(socat STDIO UNIX-CONNECT:"$XDG_RUNTIME_DIR"/niri-workspace-history.sock)"
101 workspace_json="$(jq -c --arg active_output "$active_output" --argjson history "$history_json" 'map(select(.output == $active_output and .name == null)) | map({"value": ., "history_idx": ((. as $workspace | ($history[$active_output] | index($workspace | .id))) as $active_idx | if $active_idx then $active_idx else ($history[$active_output] | length) + 1 end)}) | sort_by(.history_idx, .value.idx) | map(.value) | .[0]' <<<"$workspaces_json")"
102 [[ -n $workspace_json && $workspace_json != null ]] || exit 0
103 jq --arg active_workspace "$active_workspace" -c "$action" <<<"$workspace_json" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
104 '';
105 };
106 with-unnamed-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_unnamed_workspace);
107
108 with_empty_unnamed_workspace = pkgs.writeShellApplication {
109 name = "with-empty-unnamed-workspace";
110 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
111 text = ''
112 action="$1"
113 shift
114
115 workspaces_json="$(niri msg -j workspaces)"
116 active_output="$(jq '.[] | select(.is_focused) | .output' <<<"$workspaces_json")"
117 target_workspace_id="$(jq --argjson active_output "$active_output" 'map(select(.active_window_id == null and .name == null and .output == $active_output)) | sort_by(.idx) | .[0].id' <<<"$workspaces_json")"
118 jq --argjson workspace_id "$target_workspace_id" -nc "$action" | tee /dev/stderr | socat STDIO "$NIRI_SOCKET"
119 '';
120 };
121 with-empty-unnamed-workspace-action = config.lib.niri.actions.spawn (lib.getExe with_empty_unnamed_workspace);
122
123 with_select_window = pkgs.writeShellApplication {
124 name = "with-select-window";
125 runtimeInputs = [ niri pkgs.gojq pkgs.socat config.programs.fuzzel.package pkgs.gawk ];
126 text = ''
127 window_select="$1"
128 shift
129 action="$1"
130 shift
131
132 windows_json="$(niri msg -j windows)"
133 active_workspace="$(jq -r '.[] | select(.is_focused) | .workspace_id' <<<"$windows_json")"
134 window_ix="$(gojq -r --arg active_workspace "$active_workspace" '.[] | select('"$window_select"') | "\(.title)\u0000icon\u001f\(.app_id)"' <<<"$windows_json" | fuzzel --width=60 --log-level=warning --dmenu --index)"
135 # shellcheck disable=SC2016
136 window_json="$(gojq -rc --arg active_workspace "$active_workspace" --arg window_ix "$window_ix" 'map(select('"$window_select"')) | .[($window_ix | tonumber)]' <<<"$windows_json")"
137
138 [[ -z "$window_json" ]] && exit 1
139
140 jq -c "$action" <<<"$window_json" | socat STDIO "$NIRI_SOCKET"
141 '';
142 };
143 with-select-window-action = config.lib.niri.actions.spawn (lib.getExe with_select_window);
144
145 with_predicate_window = pred: pkgs.writeShellApplication {
146 name = "with-predicate-window";
147 runtimeInputs = [ niri pkgs.gojq pkgs.socat ];
148 text = ''
149 action="$1"
150 shift
151
152 windows_json="$(niri msg -j windows)"
153 window_json="$(gojq -rc 'map(select(${pred})) | .[0]' <<<"$windows_json")"
154
155 [[ -z "$window_json" || $window_json = "null" ]] && exit 1
156
157 jq -c "$action" <<<"$window_json" | socat STDIO "$NIRI_SOCKET"
158 '';
159 };
160
161 with-urgent-window-action = config.lib.niri.actions.spawn (lib.getExe (with_predicate_window ".is_urgent"));
162 with-focused-window-action = config.lib.niri.actions.spawn (lib.getExe (with_predicate_window ".is_focused"));
163in {
164 options = {
165 programs.niri.scratchspaces = lib.mkOption {
166 type = lib.types.listOf (lib.types.submodule ({ config, ... }: {
167 options = {
168 name = lib.mkOption {
169 type = lib.types.str;
170 };
171 match = lib.mkOption {
172 type = lib.types.listOf (lib.types.attrsOf kdl.types.kdl-args);
173 default = [];
174 };
175 exclude = lib.mkOption {
176 type = lib.types.listOf (lib.types.attrsOf kdl.types.kdl-args);
177 default = [];
178 };
179 windowRuleExtra = lib.mkOption {
180 type = kdl.types.kdl-nodes;
181 default = [];
182 };
183 key = lib.mkOption {
184 type = lib.types.nullOr lib.types.str;
185 default = null;
186 };
187 moveKey = lib.mkOption {
188 type = lib.types.nullOr lib.types.str;
189 default = let
190 keys = lib.splitString "+" config.key;
191 defMoveKey = lib.concatStringsSep "+" (lib.flatten [
192 (lib.take (lib.length keys - 1) keys)
193 ["Shift"]
194 (lib.takeEnd 1 keys)
195 ]);
196 in if config.key == null then null else defMoveKey;
197 };
198 spawn = lib.mkOption {
199 type = lib.types.nullOr (lib.types.listOf lib.types.str);
200 default = null;
201 };
202 app-id = lib.mkOption {
203 type = lib.types.nullOr lib.types.str;
204 default = null;
205 };
206 selector = lib.mkOption {
207 type = lib.types.nullOr lib.types.str;
208 default = null;
209 };
210 };
211
212 config = lib.mkMerge [
213 (lib.mkIf (config.app-id != null) {
214 match = lib.mkDefault [ { app-id = "^${lib.escapeRegex config.app-id}$"; } ];
215 selector = lib.mkDefault "select(.app_id == \"${config.app-id}\")";
216 })
217 ];
218 }));
219 default = [];
220 };
221 };
222
223 config = {
224 home.packages = [ pkgs.xwayland-satellite-unstable ];
225
226 systemd.user.sockets.niri-workspace-history = {
227 Socket = {
228 ListenStream = "%t/niri-workspace-history.sock";
229 SocketMode = "0600";
230 };
231 };
232 systemd.user.services.niri-workspace-history = {
233 Unit = {
234 BindsTo = [ "niri.service" ];
235 After = [ "niri.service" ];
236 };
237 Install = {
238 WantedBy = [ "niri.service" ];
239 };
240 Service = {
241 Type = "simple";
242 Sockets = [ "niri-workspace-history.socket" ];
243 ExecStart = pkgs.writers.writePython3 "niri-workspace-history" { flakeIgnore = ["E501"]; } ''
244 import os
245 import socket
246 import json
247 # import sys
248 from collections import defaultdict
249 from threading import Thread, Lock
250 from socketserver import StreamRequestHandler, ThreadingTCPServer
251 from contextlib import contextmanager
252 from io import TextIOWrapper
253
254
255 @contextmanager
256 def detaching(thing):
257 try:
258 yield thing
259 finally:
260 thing.detach()
261
262
263 workspace_history = defaultdict(list)
264 history_lock = Lock()
265
266
267 def monitor_niri():
268 workspaces = list()
269
270 def focus_workspace(output, workspace):
271 with history_lock:
272 workspace_history[output] = [workspace] + [ws for ws in workspace_history[output] if ws != workspace]
273 # print(json.dumps(workspace_history), file=sys.stderr)
274
275 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
276 sock.connect(os.environ["NIRI_SOCKET"])
277 sock.send(b"\"EventStream\"\n")
278 for line in sock.makefile(buffering=1, encoding='utf-8'):
279 if line_json := json.loads(line):
280 if "WorkspacesChanged" in line_json:
281 workspaces = line_json["WorkspacesChanged"]["workspaces"]
282 for ws in workspaces:
283 if ws["is_focused"]:
284 focus_workspace(ws["output"], ws["id"])
285 if "WorkspaceActivated" in line_json:
286 for ws in workspaces:
287 if ws["id"] != line_json["WorkspaceActivated"]["id"]:
288 continue
289 focus_workspace(ws["output"], ws["id"])
290 break
291
292
293 class RequestHandler(StreamRequestHandler):
294 def handle(self):
295 with detaching(TextIOWrapper(self.wfile, encoding='utf-8', write_through=True)) as out:
296 with history_lock:
297 json.dump(workspace_history, out)
298
299
300 class Server(ThreadingTCPServer):
301 def __init__(self):
302 ThreadingTCPServer.__init__(self, ("", 8000), RequestHandler, bind_and_activate=False)
303 self.socket = socket.fromfd(3, self.address_family, self.socket_type)
304
305
306 def run_server():
307 with Server() as server:
308 server.serve_forever()
309
310
311 niri = Thread(target=monitor_niri)
312 niri.daemon = True
313 niri.start()
314
315 server_thread = Thread(target=run_server)
316 server_thread.daemon = True
317 server_thread.start()
318
319 while True:
320 server_thread.join(timeout=0.5)
321 niri.join(timeout=0.5)
322
323 if not (niri.is_alive() and server_thread.is_alive()):
324 break
325 '';
326 };
327 };
328 systemd.user.services.niri-workspace-sort = {
329 Unit = {
330 BindsTo = [ "niri.service" ];
331 After = [ "niri.service" ];
332 };
333 Install = {
334 WantedBy = [ "niri.service" ];
335 };
336 Service = {
337 Type = "simple";
338 ExecStart = pkgs.writers.writePython3 "niri-workspace-sort" { flakeIgnore = ["E501"]; } ''
339 import os
340 import sys
341 import socket
342 import json
343
344 outputs = None
345 only = {'HDMI-A-1': {'bmr'}, 'eDP-1': {'vid'}}
346
347
348 class Niri(socket.socket):
349 def __init__(self):
350 super().__init__(socket.AF_UNIX, socket.SOCK_STREAM)
351 super().connect(os.environ["NIRI_SOCKET"])
352 self.fh = super().makefile(mode='rw', buffering=1, encoding='utf-8')
353
354 def cmd(self, obj):
355 print(json.dumps(obj, separators=(',', ':')), flush=True, file=self.fh)
356
357 def event_stream(self):
358 self.cmd("EventStream")
359 return self.fh
360
361
362 with Niri() as niri, Niri().event_stream() as niri_stream:
363 for line in niri_stream:
364 workspaces = None
365 if line_json := json.loads(line):
366 if "WorkspacesChanged" in line_json:
367 workspaces = line_json["WorkspacesChanged"]["workspaces"]
368
369 if workspaces is None:
370 continue
371
372 old_outputs = outputs
373 outputs = {ws["output"] for ws in workspaces}
374 if old_outputs is None:
375 print("Initial outputs: {}".format(outputs), file=sys.stderr)
376 continue
377
378 new_outputs = outputs - old_outputs
379 if not new_outputs:
380 continue
381 print("New outputs: {}".format(new_outputs), file=sys.stderr)
382
383 relevant_workspaces = list(filter(lambda ws: (ws["name"] is not None) or (ws["active_window_id"] is not None), workspaces))
384 target_output = next(iter(outputs - set(only.keys())))
385 if not target_output:
386 continue
387 for ws in relevant_workspaces:
388 ws_ident = ws["name"] if ws["name"] is not None else (ws["output"], ws["idx"])
389 if ws["output"] not in set(only.keys()):
390 continue
391 if ws_ident in only[ws["output"]]:
392 continue
393
394 print("{} -> {}".format(ws_ident, target_output), file=sys.stderr)
395 niri.cmd({"Action": {"MoveWorkspaceToMonitor": {"reference": {"Id": ws["id"]}, "output": target_output}}})
396 '';
397 Restart = "on-failure";
398 RestartSec = 10;
399 };
400 };
401
402 programs.niri.scratchspaces = [
403 { name = "pwctl";
404 key = "Mod+Control+A";
405 spawn = ["pwvucontrol"];
406 app-id = "com.saivert.pwvucontrol";
407 }
408 { name = "kpxc";
409 exclude = [
410 { title = "^Unlock Database.*"; }
411 { title = "^Access Request.*"; }
412 { title = ".*Passkey credentials$"; }
413 ];
414 windowRuleExtra = with kdl; [
415 (sleaf "open-focused" false)
416 ];
417 key = "Mod+Control+P";
418 app-id = "org.keepassxc.KeePassXC";
419 spawn = [ "keepassxc" ];
420 }
421 { name = "bmgr";
422 key = "Mod+Control+B";
423 app-id = ".blueman-manager-wrapped";
424 spawn = [ "blueman-manager" ];
425 }
426 { name = "term";
427 key = "Mod+Control+Return";
428 app-id = "kitty-scratch";
429 spawn = [ "kitty" "--app-id" "kitty-scratch" ];
430 }
431 { name = "edit";
432 match = [ { title = "^scratch$"; app-id = "^emacs$"; } ];
433 key = "Mod+Control+E";
434 selector = "select(.app_id == \"emacs\" and .title == \"scratch\")";
435 spawn = [ "emacsclient" "-c" "--frame-parameters=(quote (name . \"scratch\"))" ];
436 }
437 { name = "eff";
438 key = "Mod+Control+O";
439 app-id = "com.github.wwmm.easyeffects";
440 spawn = [ "easyeffects" ];
441 }
442 { name = "time";
443 key = "Mod+Control+K";
444 app-id = "chrome-kimai.yggdrasil.li__-Default";
445 spawn = [ (toString (pkgs.resholve.writeScript "kimai" {
446 interpreter = pkgs.runtimeShell;
447 inputs = [ pkgs.dex ];
448 execer = [ "cannot:${lib.getExe pkgs.dex}" ];
449 } ''
450 exec dex $HOME/.local/state/nix/profile/share/applications/kimai.desktop
451 '')) ];
452 windowRuleExtra = with kdl; [
453 (sleaf "block-out-from" "screencast")
454 ];
455 }
456 ];
457 programs.niri.config =
458 let
459 inherit (kdl) node plain leaf flag;
460 optional-node = cond: v:
461 if cond
462 then v
463 else null;
464 opt-props = lib.filterAttrs (lib.const (value: value != null));
465 normalize-nodes = nodes: lib.remove null (lib.flatten nodes);
466 in
467 normalize-nodes [
468 (flag "prefer-no-csd")
469
470 (sleaf "screenshot-path" "~/screenshots/%Y-%m-%dT%H:%M:%S.png")
471
472 (plain "hotkey-overlay" [
473 (flag "skip-at-startup")
474 ])
475
476 (plain "input" [
477 (plain "keyboard" [
478 (sleaf "repeat-delay" 300)
479 (sleaf "repeat-rate" 50)
480
481 (plain "xkb" [
482 (sleaf "layout" "us,us")
483 (sleaf "variant" "dvp,")
484 (sleaf "options" "compose:caps,grp:win_space_toggle")
485 ])
486 ])
487
488 (flag "workspace-auto-back-and-forth")
489 # (sleaf "focus-follows-mouse" {})
490 # (flag "warp-mouse-to-focus")
491
492 # (plain "touchpad" [ (flag "off") ])
493 (plain "trackball" [
494 (sleaf "scroll-method" "on-button-down")
495 (sleaf "scroll-button" 278)
496 ])
497 (plain "touch" [
498 (sleaf "map-to-output" "eDP-1")
499 ])
500 ])
501
502 (plain "gestures" [
503 (plain "hot-corners" [(flag "off")])
504 ])
505
506 (plain "environment" (lib.mapAttrsToList sleaf {
507 NIXOS_OZONE_WL = "1";
508 QT_QPA_PLATFORM = "wayland";
509 QT_WAYLAND_DISABLE_WINDOWDECORATION = "1";
510 GDK_BACKEND = "wayland";
511 SDL_VIDEODRIVER = "wayland";
512 DISPLAY = ":0";
513 ELECTRON_OZONE_PLATFORM_HINT = "auto";
514 SSH_ASKPASS_REQUIRE = "prefer";
515 SSH_ASKPASS = lib.getExe pkgs.kdePackages.ksshaskpass;
516 SUDO_ASKPASS = lib.getExe pkgs.kdePackages.ksshaskpass;
517 }))
518
519 (node "output" ["eDP-1"] [
520 (sleaf "scale" 1.5)
521 (sleaf "position" { x = 0; y = 0; })
522 ])
523 (node "output" ["Ancor Communications Inc ASUS PB287Q 0x0000DD9B"] [
524 (sleaf "scale" 1.5)
525 (sleaf "position" { x = 2560; y = 0; })
526 ])
527 (node "output" ["HP Inc. HP 727pu CN4417143K"] [
528 (sleaf "mode" "2560x1440@119.998")
529 (sleaf "scale" 1)
530 (sleaf "position" { x = 2560; y = 0; })
531 (flag "variable-refresh-rate")
532 ])
533
534 (plain "debug" [
535 (sleaf "render-drm-device" "/dev/dri/by-path/pci-0000:00:02.0-render")
536 ])
537
538 (plain "animations" [
539 (sleaf "slowdown" 0.5)
540 (plain "workspace-switch" [(flag "off")])
541 ])
542
543 (plain "layout" [
544 (sleaf "gaps" 8)
545 (plain "struts" [
546 (sleaf "left" 26)
547 (sleaf "right" 26)
548 (sleaf "top" 0)
549 (sleaf "bottom" 0)
550 ])
551 (plain "border" [
552 (sleaf "width" 2)
553 (sleaf "active-gradient" {
554 from = "hsla(195 100% 45% 1)";
555 to = "hsla(155 100% 37.5% 1)";
556 angle = 29;
557 relative-to = "workspace-view";
558 })
559 (sleaf "inactive-gradient" {
560 from = "hsla(0 0% 27.7% 1)";
561 to = "hsla(0 0% 23% 1)";
562 angle = 29;
563 relative-to = "workspace-view";
564 })
565 ])
566 (plain "focus-ring" [
567 (flag "off")
568 ])
569
570 (plain "preset-column-widths" (map (prop: sleaf "proportion" prop) [
571 (1. / 4.) (1. / 3.) (1. / 2.) (2. / 3.) (3. / 4.) (1.)
572 ]))
573 (plain "default-column-width" [ (sleaf "proportion" (1. / 2.)) ])
574 (plain "preset-window-heights" (map (prop: sleaf "proportion" prop) [
575 (1. / 3.) (1. / 2.) (2. / 3.) (1.)
576 ]))
577
578 (flag "always-center-single-column")
579
580 (plain "tab-indicator" [
581 (sleaf "gap" 4)
582 (sleaf "width" 8)
583 (sleaf "gaps-between-tabs" 4)
584 (flag "place-within-column")
585 (sleaf "length" { total-proportion = 1.; })
586 (sleaf "active-gradient" {
587 from = "hsla(195 100% 60% 0.75)";
588 to = "hsla(155 100% 50% 0.75)";
589 angle = 29;
590 relative-to = "workspace-view";
591 })
592 (sleaf "inactive-gradient" {
593 from = "hsla(0 0% 42% 0.66)";
594 to = "hsla(0 0% 35% 0.66)";
595 angle = 29;
596 relative-to = "workspace-view";
597 })
598 ])
599 ])
600
601 (plain "cursor" [
602 (flag "hide-when-typing")
603 ])
604
605 (map (name:
606 (node "workspace" [name] [
607 (sleaf "open-on-output" "eDP-1")
608 ])
609 ) (map ({name, ...}: name) cfg.scratchspaces))
610 (map (name:
611 (sleaf "workspace" name)
612 ) ["comm" "web" "vid" "bmr"])
613
614 (plain "window-rule" [
615 (sleaf "clip-to-geometry" true)
616 ])
617
618 (plain "window-rule" [
619 (sleaf "match" { is-floating = true; })
620 (sleaf "geometry-corner-radius" 8)
621 (plain "shadow" [ (flag "on") ])
622 ])
623
624 (plain "window-rule" [
625 (sleaf "match" { app-id = "^org\\.keepassxc\\.KeePassXC$"; })
626 (sleaf "block-out-from" "screencast")
627 ])
628 (plain "window-rule" (normalize-nodes [
629 (map (title:
630 (sleaf "match" { app-id = "^org\\.keepassxc\\.KeePassXC$"; inherit title; })
631 ) ["^Unlock Database.*" "^Access Request.*" ".*Passkey credentials$" "Browser Access Request$"])
632 (sleaf "open-focused" true)
633 (sleaf "open-floating" true)
634 ]))
635
636 (map ({ name, match, exclude, windowRuleExtra, ... }:
637 (optional-node (match != []) (plain "window-rule" (normalize-nodes [
638 (map (sleaf "match") match)
639 (map (sleaf "exclude") exclude)
640 (sleaf "open-on-workspace" name)
641 (sleaf "open-maximized" true)
642 windowRuleExtra
643 ])))
644 ) cfg.scratchspaces)
645
646 (plain "window-rule" [
647 (sleaf "match" { app-id = "^emacs$"; })
648 (sleaf "match" { app-id = "^firefox$"; })
649 (plain "default-column-width" [(sleaf "proportion" (2. / 3.))])
650 ])
651 (plain "window-rule" [
652 (sleaf "match" { app-id = "^kitty$"; })
653 (sleaf "match" { app-id = "^kitty-play$"; })
654 (plain "default-column-width" [(sleaf "proportion" (1. / 3.))])
655 ])
656
657 (plain "window-rule" [
658 (sleaf "match" { app-id = "^thunderbird$"; })
659 (sleaf "match" { app-id = "^Element$"; })
660 (sleaf "match" { app-id = "^chrome-web\.openrainbow\.com__-Default$"; })
661 (sleaf "open-on-workspace" "comm")
662 ])
663 (plain "window-rule" [
664 (sleaf "match" { app-id = "^firefox$"; })
665 (sleaf "open-on-workspace" "web")
666 (sleaf "open-maximized" true)
667 ])
668 (plain "window-rule" [
669 (sleaf "match" { app-id = "^mpv$"; })
670 (sleaf "open-on-workspace" "vid")
671 (plain "default-column-width" [(sleaf "proportion" 1.)])
672 ])
673 (plain "window-rule" [
674 (sleaf "match" { app-id = "^kitty-play$"; })
675 (sleaf "open-on-workspace" "vid")
676 (sleaf "open-focused" false)
677 ])
678 (plain "window-rule" [
679 (sleaf "match" { app-id = "^chrome-audiobookshelf\.yggdrasil\.li__-Default$"; })
680 (sleaf "match" { app-id = "^YouTube Music Desktop App$"; })
681 (sleaf "open-on-workspace" "vid")
682 ])
683 (plain "window-rule" [
684 (sleaf "match" { app-id = "^pdfpc$"; })
685 (plain "default-column-width" [(sleaf "proportion" 1.)])
686 ])
687 (plain "window-rule" [
688 (sleaf "match" { app-id = "^pdfpc$"; title = "^.*presentation.*$"; })
689 (plain "default-column-width" [(sleaf "proportion" 1.)])
690 (sleaf "open-fullscreen" true)
691 (sleaf "open-on-workspace" "bmr")
692 (sleaf "open-focused" false)
693 ])
694 (plain "window-rule" (normalize-nodes [
695 (map (sleaf "match") [
696 { app-id = "^Gimp-"; title = "^Quit GIMP$"; }
697 { app-id = "^org\\.kde\\.polkit-kde-authentication-agent-1$"; }
698 { app-id = "^xdg-desktop-portal-gtk$"; }
699 ])
700 (sleaf "open-floating" true)
701 ]))
702 (plain "window-rule" [
703 (sleaf "match" { app-id = "^org\\.pwmt\\.zathura$"; })
704 (sleaf "match" { app-id = "^evince$"; })
705 (sleaf "match" { app-id = "^org\\.gnome\\.Papers$"; })
706 (sleaf "default-column-display" "tabbed")
707 ])
708
709 (plain "layer-rule" [
710 (sleaf "match" { namespace = "^notifications$"; })
711 (sleaf "match" { namespace = "^bar$"; })
712 (sleaf "match" { namespace = "^launcher$"; })
713 (sleaf "block-out-from" "screencast")
714 ])
715
716 (plain "binds"
717 (let
718 bind = name: cfg: node name [(lib.removeAttrs cfg ["action"])] (lib.mapAttrsToList leaf (lib.removeAttrs cfg.action ["__functor"]));
719 in
720 normalize-nodes [
721 (lib.mapAttrsToList bind (with config.lib.niri.actions; {
722 "Mod+Slash".action = show-hotkey-overlay;
723
724 "Mod+Return".action = spawn terminal;
725 "Mod+Shift+Return".action =
726 let
727 nushellKitty = pkgs.symlinkJoin {
728 name = "nushell-kitty";
729 paths = [ config.programs.kitty.package ];
730 buildInputs = [ pkgs.makeWrapper ];
731 postBuild = ''
732 wrapProgram $out/bin/kitty \
733 --add-flags "--config ${pkgs.writeText "kitty.conf" ''
734 include $HOME/${config.xdg.configFile."kitty/kitty.conf".target}
735 shell ${lib.getExe config.programs.nushell.package}
736 ''}"
737 '';
738 };
739 in spawn (lib.getExe' nushellKitty "kitty");
740 "Mod+Q".action = close-window;
741 "Mod+O".action = spawn (lib.getExe config.programs.fuzzel.package);
742 "Mod+Shift+O".action = spawn (lib.getExe config.programs.fuzzel.package) "--list-executables-in-path";
743
744 "Mod+Alt+E".action = spawn (lib.getExe' config.services.emacs.package "emacsclient") "-c";
745 "Mod+Alt+Y".action = spawn (lib.getExe (pkgs.writeShellApplication {
746 name = "queue-yt-dlp";
747 runtimeInputs = with pkgs; [ wl-clipboard-rs socat ];
748 text = ''
749 socat STDIO UNIX-CONNECT:"$XDG_RUNTIME_DIR"/yt-dlp.sock <<<$'{ "urls": ["'"$(wl-paste)"$'"] }'
750 '';
751 }));
752 "Mod+Alt+L".action = spawn (lib.getExe (pkgs.writeShellApplication {
753 name = "queue-yt-dlp";
754 runtimeInputs = with pkgs; [ wl-clipboard-rs config.programs.kitty.package ];
755 text = ''
756 exec -- kitty --app-id kitty-play --directory "$HOME"/media mpv "$(wl-paste)"
757 '';
758 }));
759 "Mod+Alt+M".action = spawn (lib.getExe' pkgs.screen-message "sm") "-n" "Fira Mono" "-a" "1" "-f" "#fff" "-b" "#000";
760
761 "Mod+U".action = spawn (lib.getExe (pkgs.writeShellApplication {
762 name = "qalc-fuzzel";
763 runtimeInputs = with pkgs; [ wl-clipboard-rs libqalculate config.programs.fuzzel.package coreutils findutils libnotify gnugrep ];
764 text = ''
765 RESULTS_DIR="$HOME/.cache/qalc-fuzzel"
766 prev() {
767 FOUND=false
768 while IFS= read -r line; do
769 [[ -n "$line" ]] || continue
770 FOUND=true
771 echo "$line"
772 done < <(export LC_ALL=C.UTF-8; echo; find "$RESULTS_DIR" -type f -printf $'%T@ %p\n' | sort -n | cut -d' ' -f2- | xargs -r cat)
773 $FOUND || echo
774 }
775 FUZZEL_RES=$(prev | fuzzel --dmenu --prompt "qalc> " --width=60) || exit $?
776 if [[ "$FUZZEL_RES" =~ .*\ =\ .* ]]; then
777 QALC_RES="$FUZZEL_RES"
778 QALC_RET=0
779 else
780 QALC_RES=$(qalc -set "autocalc off" "$FUZZEL_RES" 2>&1)
781 QALC_RET=$?
782 fi
783 [[ -n "$QALC_RES" ]] || exit 1
784 EXISTING=false
785 set +o pipefail
786 grep -Fxrl "$QALC_RES" "$RESULTS_DIR" | xargs -r touch
787 [[ ''${PIPESTATUS[0]} -eq 0 ]] && EXISTING=true
788 set -o pipefail
789 if [[ $QALC_RET -eq 0 ]] && ! $EXISTING; then
790 set +o pipefail
791 RES_FILE="$RESULTS_DIR"/$(date -uIs).$(tr -Cd 'a-zA-Z0-9' </dev/random | head -c 10)
792 set -o pipefail
793 cat >"$RES_FILE" <<<"$QALC_RES"
794 fi
795 [[ "$QALC_RES" =~ .*\ =\ (.*) ]] && QALC_RES="''${BASH_REMATCH[1]}"
796 [[ $QALC_RET -eq 0 ]] && wl-copy "$QALC_RES"
797 notify-send "$QALC_RES"
798 '';
799 }));
800 "Mod+Shift+U".action =
801 let
802 qalcKitty = pkgs.symlinkJoin {
803 name = "qalc-kitty";
804 paths = [ config.programs.kitty.package ];
805 buildInputs = [ pkgs.makeWrapper ];
806 postBuild = ''
807 wrapProgram $out/bin/kitty \
808 --add-flags "--config ${pkgs.writeText "kitty.conf" ''
809 include $HOME/${config.xdg.configFile."kitty/kitty.conf".target}
810 shell ${lib.getExe pkgs.libqalculate}
811 ''}"
812 '';
813 };
814 in spawn (lib.getExe' qalcKitty "kitty");
815 "Mod+E".action = spawn (lib.getExe (pkgs.writeShellApplication {
816 name = "emoji-fuzzel";
817 runtimeInputs = with pkgs; [ config.programs.fuzzel.package wtype wl-clipboard-rs ];
818 text = ''
819 FUZZEL_RES=$(fuzzel --dmenu --prompt "emoji> " --cache "$HOME"/.cache/fuzzel-emoji --width=60 <"$HOME"/.local/share/emoji-data/list.txt) || exit $?
820 [[ -n "$FUZZEL_RES" ]] || exit 1
821 wl-copy "$(cut -d ':' -f 1 <<<"$FUZZEL_RES" | tr -d '\n')" && wtype -k XF86Paste
822 '';
823 }));
824 "Print".action = screenshot;
825 "Control+Print".action = screenshot-window;
826 "Shift+Print".action = kdl.magic-leaf "screenshot-screen";
827 "Mod+B".action = with-select-window-action ".workspace_id == ($active_workspace | tonumber)" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
828 "Mod+Shift+B".action = with-select-window-action "true" "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
829
830 "Mod+Escape" = {
831 allow-inhibiting = false;
832 action = toggle-keyboard-shortcuts-inhibit;
833 };
834
835 "Mod+H".action = focus-column-left;
836 "Mod+T".action = focus-window-down;
837 "Mod+N".action = focus-window-up;
838 "Mod+S".action = focus-column-right;
839
840 "Mod+Shift+H".action = move-column-left;
841 "Mod+Shift+T".action = move-window-down;
842 "Mod+Shift+N".action = move-window-up;
843 "Mod+Shift+S".action = move-column-right;
844
845 "Mod+Control+H".action = focus-monitor-left;
846 "Mod+Control+T".action = focus-monitor-down;
847 "Mod+Control+N".action = focus-monitor-up;
848 "Mod+Control+S".action = focus-monitor-right;
849
850 "Mod+Shift+Control+H".action = move-workspace-to-monitor-left;
851 "Mod+Shift+Control+T".action = move-workspace-to-monitor-down;
852 "Mod+Shift+Control+N".action = move-workspace-to-monitor-up;
853 "Mod+Shift+Control+S".action = move-workspace-to-monitor-right;
854
855 "Mod+G".action = focus-adjacent-workspace "down";
856 "Mod+C".action = focus-adjacent-workspace "up";
857
858 "Mod+Shift+G".action = move-column-to-adjacent-workspace "down";
859 "Mod+Shift+C".action = move-column-to-adjacent-workspace "up";
860
861 "Mod+Shift+Control+G".action = move-workspace-down;
862 "Mod+Shift+Control+C".action = move-workspace-up;
863
864 "Mod+ParenLeft".action = focus-workspace "comm";
865 "Mod+Shift+ParenLeft".action = kdl.magic-leaf "move-column-to-workspace" "comm";
866
867 "Mod+ParenRight".action = focus-workspace "web";
868 "Mod+Shift+ParenRight".action = kdl.magic-leaf "move-column-to-workspace" "web";
869
870 "Mod+BraceRight".action = focus-workspace "read";
871 "Mod+Shift+BraceRight".action = kdl.magic-leaf "move-column-to-workspace" "read";
872
873 "Mod+BraceLeft".action = focus-workspace "mon";
874 "Mod+Shift+BraceLeft".action = kdl.magic-leaf "move-column-to-workspace" "mon";
875
876 "Mod+Asterisk".action = focus-workspace "vid";
877 "Mod+Shift+Asterisk".action = kdl.magic-leaf "move-column-to-workspace" "vid";
878
879 "Mod+Plus".action = with-unnamed-workspace-action ''{"Action":{"FocusWorkspace":{"reference":{"Id": .id}}}}'';
880 "Mod+Shift+Plus".action = with-unnamed-workspace-action ''{"Action":{"MoveColumnToWorkspace":{"reference":{"Id": .id}, "focus": true}}}'';
881
882 "Mod+M".action = consume-or-expel-window-left;
883 "Mod+W".action = consume-or-expel-window-right;
884
885 "Mod+Shift+M".action = toggle-column-tabbed-display;
886
887 "Mod+R".action = switch-preset-column-width;
888 "Mod+Shift+R".action = maximize-column;
889 "Mod+Shift+Ctrl+R".action = switch-preset-window-height;
890 "Mod+F".action = center-column;
891 "Mod+Shift+F".action = toggle-windowed-fullscreen;
892 "Mod+Ctrl+Shift+F".action = fullscreen-window;
893
894 "Mod+V".action = switch-focus-between-floating-and-tiling;
895 "Mod+Shift+V".action = toggle-window-floating;
896
897 "Mod+Left".action = set-column-width "-10%";
898 "Mod+Down".action = set-window-height "-10%";
899 "Mod+Up".action = set-window-height "+10%";
900 "Mod+Right".action = set-column-width "+10%";
901
902 "Mod+Shift+Z" = {
903 action = power-off-monitors;
904 allow-when-locked = true;
905 };
906 "Mod+Shift+E".action = quit;
907
908 # "Mod+Semicolon".action = spawn makoctl "dismiss" "--group";
909 # "Mod+Shift+Semicolon".action = spawn makoctl "dismiss" "--all";
910 # "Mod+Period".action = spawn makoctl "menu" "--" (lib.getExe config.programs.fuzzel.package) "--dmenu";
911 # "Mod+Comma".action = spawn makoctl "restore";
912
913 "Mod+Control+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"FocusWorkspace\":{\"reference\":{\"Id\": $workspace_id}}}}";
914 "Mod+Control+Shift+W".action = with-empty-unnamed-workspace-action "{\"Action\":{\"MoveColumnToWorkspace\":{\"reference\":{\"Id\": $workspace_id}, \"focus\": true}}}";
915
916 "Mod+X".action = set-dynamic-cast-window;
917 "Mod+Shift+X".action = set-dynamic-cast-monitor;
918 "Mod+Control+Shift+X".action = clear-dynamic-cast-target;
919
920 "Mod+D".action = with-urgent-window-action "{\"Action\":{\"FocusWindow\":{\"id\": .id}}}";
921 "Mod+Shift+D".action = with-focused-window-action "{\"Action\":{\"UnsetUrgent\":{\"id\": .id}}}";
922
923 "Mod+K".action = spawn (lib.getExe' pkgs.worktime "worktime-ui");
924 "Mod+Shift+K".action = spawn (lib.getExe' pkgs.worktime "worktime-stop");
925 }))
926 (lib.mapAttrsToList (name: cfg: node name [(lib.removeAttrs cfg ["action"])] [cfg.action]) (let
927 shell = obj: leaf "send-unix" [
928 { path = ''''${XDG_RUNTIME_DIR}/shell.sock''; }
929 (builtins.toJSON obj + "\n")
930 ];
931 in {
932 "XF86AudioRaiseVolume" = {
933 allow-when-locked = true;
934 action = shell { Volume.volume = "up"; };
935 };
936 "XF86AudioLowerVolume" = {
937 allow-when-locked = true;
938 action = shell { Volume.volume = "down"; };
939 };
940 "XF86AudioMute" = {
941 allow-when-locked = true;
942 action = shell { Volume.muted = "toggle"; };
943 };
944 "XF86AudioMicMute" = {
945 allow-when-locked = true;
946 action = shell { Volume."mic-muted" = "toggle"; };
947 };
948 "XF86MonBrightnessUp" = {
949 action = shell { Brightness = "up"; };
950 allow-when-locked = true;
951 };
952 "XF86MonBrightnessDown" = {
953 action = shell { Brightness = "down"; };
954 allow-when-locked = true;
955 };
956 "Mod+Shift+L".action = shell { LockSession = {}; };
957 "Mod+Shift+Minus" = {
958 action = shell { Suspend = {}; };
959 allow-when-locked = true;
960 };
961 "Mod+Shift+Control+Minus" = {
962 action = shell { Hibernate = {}; };
963 allow-when-locked = true;
964 };
965 "Mod+Shift+P" = {
966 action = shell { Mpris = { PauseAll = {}; }; };
967 allow-when-locked = true;
968 };
969 "Mod+Semicolon".action = shell { Notifications = { DismissGroup = {}; }; };
970 "Mod+Shift+Semicolon".action = shell { Notifications = { DismissAll = {}; }; };
971 }))
972 (map ({ name, selector, spawn, key, ...}: if key != null && selector != null && spawn != null then bind key { action = focus-or-spawn-action selector name spawn; } else null) cfg.scratchspaces)
973 (map ({ name, moveKey, ...}: if moveKey != null then bind moveKey { action = kdl.magic-leaf "move-column-to-workspace" name; } else null) cfg.scratchspaces)
974 ]
975 ))
976 ];
977 };
978}
diff --git a/accounts/gkleen@sif/niri/mako.nix b/accounts/gkleen@sif/niri/mako.nix
new file mode 100644
index 00000000..3d246d96
--- /dev/null
+++ b/accounts/gkleen@sif/niri/mako.nix
@@ -0,0 +1,112 @@
1{ config, lib, pkgs, ... }:
2{
3 config = lib.mkIf false {
4 services.mako = {
5 enable = true;
6 settings = {
7 font = "Fira Sans 10";
8 format = "<i>%s</i>\\n%b";
9 margin = "2";
10 max-visible = -1;
11 background-color = "#000000dd";
12 progress-color = "source #223544ff";
13 width = 384;
14 outer-margin = 1;
15 max-history = 100;
16 max-icon-size = 48;
17
18 grouped.format = "<b>(%g)</b> <i>%s</i>\\n%b";
19 "urgency=low".text-color = "#999999ff";
20 "urgency=critical".background-color = "#900000dd";
21 "app-name=Element".group-by = "summary";
22 "app-name=poweralertd" = {
23 history = false;
24 ignore-timeout = true;
25 default-timeout = 2000;
26 };
27 "app-name=worktime".history = false;
28 "mode=silent".invisible = true;
29 };
30 package = pkgs.symlinkJoin {
31 name = "${pkgs.mako.name}-wrapped";
32 paths = with pkgs; [ mako ];
33 inherit (pkgs.mako) meta;
34 postBuild = ''
35 rm -r $out/share/dbus-1
36 '';
37 };
38 };
39 systemd.user.services.mako = {
40 Unit = {
41 Description = "Mako notification daemon";
42 PartOf = [ "graphical-session.target" ];
43 };
44 Install = {
45 WantedBy = [ "graphical-session.target" ];
46 };
47 Service = {
48 Type = "dbus";
49 BusName = "org.freedesktop.Notifications";
50 ExecStart = lib.getExe config.services.mako.package;
51 RestartSec = 5;
52 Restart = "always";
53 };
54 };
55
56 systemd.user.services.mako-follows-focus = {
57 Unit = {
58 BindsTo = [ "niri.service" "mako.service" ];
59 After = [ "niri.service" "mako.service" ];
60 };
61 Service = {
62 Type = "simple";
63 Restart = "always";
64 ExecStart = pkgs.writers.writePython3 "mako-follows-focus" {
65 libraries = with pkgs.python3Packages; [];
66 } ''
67 import os
68 import socket
69 import json
70 import subprocess
71
72
73 current_output = None
74 workspaces = []
75
76
77 def output_changed(new_output):
78 global current_output
79
80 if current_output == new_output:
81 return
82
83 current_output = new_output
84 subprocess.run(["makoctl", "reload"])
85
86
87 sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
88 sock.connect(os.environ["NIRI_SOCKET"])
89 sock.send(b"\"EventStream\"\n")
90 for line in sock.makefile(buffering=1, encoding='utf-8'):
91 if line_json := json.loads(line):
92 if "WorkspacesChanged" in line_json:
93 workspaces = line_json["WorkspacesChanged"]["workspaces"]
94 for workspace in workspaces:
95 if not workspace["is_focused"]:
96 continue
97 output_changed(workspace["output"])
98 break
99 if "WorkspaceActivated" in line_json and line_json["WorkspaceActivated"]["focused"]: # noqa: E501
100 for workspace in workspaces:
101 if not workspace["id"] == line_json["WorkspaceActivated"]["id"]: # noqa: E501
102 continue
103 output_changed(workspace["output"])
104 break
105 '';
106 };
107 Install = {
108 WantedBy = [ "mako.service" ];
109 };
110 };
111 };
112}
diff --git a/accounts/gkleen@sif/shell/default.nix b/accounts/gkleen@sif/shell/default.nix
new file mode 100644
index 00000000..44462865
--- /dev/null
+++ b/accounts/gkleen@sif/shell/default.nix
@@ -0,0 +1,115 @@
1{ config, pkgs, lib, ... }:
2
3{
4 config = {
5 programs.quickshell = {
6 enable = true;
7 package = pkgs.symlinkJoin {
8 pname = pkgs.quickshell.pname + "-wrapped";
9 inherit (pkgs.quickshell) version meta;
10 paths = [ pkgs.quickshell ];
11 buildInputs = [ pkgs.makeWrapper ];
12 postBuild = ''
13 for binary in quickshell qs; do
14 wrapProgram $out/bin/$binary \
15 --prefix QML_IMPORT_PATH : ${pkgs.qt6Packages.callPackage ./quickshell-plugins {}}/${pkgs.qt6.qtbase.qtQmlPrefix}
16 done
17 '';
18 };
19 config = {
20 src = ./quickshell;
21 replacements = {
22 ignore_workspaces = builtins.toJSON (map ({ name, ... }: name) config.programs.niri.scratchspaces);
23 wallpapers = builtins.toJSON (pkgs.stdenvNoCC.mkDerivation {
24 name = "wallpapers";
25 srcs = [
26 (pkgs.fetchurl {
27 url = "https://esawebb.org/media/archives/images/publicationtiff10k/carinanebula3.tif";
28 hash = "sha256-YxZEweDKJfvfrdxb/QFmgJhcZDEJYxotoHrG+RRn1tw=";
29 })
30 (pkgs.fetchurl {
31 url = "https://esawebb.org/media/archives/images/original/pillarsofcreation_composite.tif";
32 hash = "sha256-qRiODxR0lZWdxgYXna0fNRFFDErpBJDwOJuQl6sNjRc=";
33 })
34 (pkgs.fetchurl {
35 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2212a.tif";
36 hash = "sha256-l2fqE/z//C1a0xkvZwsnwPbTSb+WuA11h+SUl3E1dhw=";
37 })
38 (pkgs.fetchurl {
39 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2415a.tif";
40 hash = "sha256-onBy7cPoUpDuzQStbY2E+qmlGgSLXPwFCLX53ukAb4c=";
41 })
42 (pkgs.fetchurl {
43 url = "https://esawebb.org/media/archives/images/publicationtiff10k/weic2330a.tif";
44 hash = "sha256-nn0ZtjZIrPcpj3YcLTsrL7XiXvyh3QYgCSmdDMD+3OM=";
45 })
46 (pkgs.fetchurl {
47 url = "https://esawebb.org/media/archives/images/original/weic2426a.tif";
48 hash = "sha256-EDnfPn3GE9jt6XPqiGInP7E2F3Az7d25NqATSWltDv0=";
49 })
50 (pkgs.fetchurl {
51 url = "https://esawebb.org/media/archives/images/original/weic2503a.tif";
52 hash = "sha256-3/RX6RQp8naELcgReHPd5/zhJkoCjnA10w5BEnNo+qI=";
53 })
54 (pkgs.fetchurl {
55 url = "https://esawebb.org/media/archives/images/original/weic2506a.tif";
56 hash = "sha256-aDld0aoY1owRxDVf7Jcyw71TH42M1foYotxn2thyFYw=";
57 })
58 (pkgs.fetchurl {
59 url = "https://esawebb.org/media/archives/images/original/weic2514a.tif";
60 hash = "sha256-jTi1G1Ofo5xsF6ggrbtYJHxqLaCQ7edM5B3uORiVQtg=";
61 })
62 (pkgs.fetchurl {
63 url = "https://esawebb.org/media/archives/images/original/weic2425c.tif";
64 hash = "sha256-oaEOexSJHEGj090dJF3ct5HAoR+Y5gRiPrUlxdvnTRo=";
65 })
66 ];
67
68 dontUnpack = true;
69
70 buildInputs = [ pkgs.imagemagick ];
71 buildPhase = ''
72 runHook preBuild
73
74 typeset sources=($srcs)
75
76 mkdir -p $out
77 magick ''${sources[0]} -crop 10000x5625+0+79 +repage -define jpeg:extent=10MB $out/carinanebula3.jpeg
78 magick ''${sources[1]} -crop 6716x3778+329+80 +repage -define jpeg:extent=10MB $out/pillarsofcreation_composite.jpeg
79 magick ''${sources[2]} -crop 10000x5625+0+79 +repage -define jpeg:extent=10MB $out/weic2212a.jpeg
80 magick ''${sources[3]} -crop 7650x4302+1166+389 +repage -define jpeg:extent=10MB $out/weic2415a.jpeg
81 magick ''${sources[4]} -crop 8732x4912+0+434 +repage -define jpeg:extent=10MB $out/weic2330a.jpeg
82 magick ''${sources[5]} -crop 5302x2982+636+0 +repage -define jpeg:extent=10MB $out/weic2426a.jpeg
83 magick ''${sources[6]} -crop 4328x2434+0+906 +repage -define jpeg:extent=10MB $out/weic2503a.jpeg
84 magick ''${sources[7]} -crop 4152x2335+0+666 +repage -define jpeg:extent=10MB $out/weic2506a.jpeg
85 magick ''${sources[8]} -crop 4320x2430+0+0 +repage -define jpeg:extent=10MB $out/weic2514a.jpeg
86 magick ''${sources[9]} -crop 5863x3298+0+477 +repage -define jpeg:extent=10MB $out/weic2425c.jpeg
87
88 runHook postBuild
89 '';
90 });
91 niri_session = builtins.toJSON [
92 (pkgs.writeShellScript "niri-session" ''
93 exec ${lib.getExe pkgs.dex} -w ${config.programs.niri.package}/share/wayland-sessions/niri.desktop &>/tmp/niri-session-$$.log
94 '')
95 # (lib.getExe pkgs.dex)
96 # "${config.programs.niri.package}/share/wayland-sessions/niri.desktop"
97 ];
98 username = builtins.toJSON config.home.username;
99 mdi = builtins.toJSON (pkgs.fetchFromGitHub {
100 owner = "Templarian";
101 repo = "MaterialDesign";
102 rev = "2424e748e0cc63ab7b9c095a099b9fe239b737c0";
103 hash = "sha256-QMGl7soAhErrrnY3aKOZpt49yebkSNzy10p/v5OaqQ0=";
104 });
105 worktime = builtins.toJSON (lib.getExe pkgs.worktime);
106 };
107 };
108 };
109 systemd.user.services.quickshell = {
110 Service = {
111 RuntimeDirectory = "quickshell";
112 };
113 };
114 };
115}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
new file mode 100644
index 00000000..020c0515
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/CMakeLists.txt
@@ -0,0 +1,168 @@
1set(INSTALL_QMLDIR "" CACHE STRING "QML install dir")
2set(INSTALL_QML_PREFIX "" CACHE STRING "QML install prefix")
3
4# There doesn't seem to be a standard cross-distro qml install path.
5if ("${INSTALL_QMLDIR}" STREQUAL "" AND "${INSTALL_QML_PREFIX}" STREQUAL "")
6 message(WARNING "Neither INSTALL_QMLDIR nor INSTALL_QML_PREFIX is set. QML modules will not be installed.")
7else()
8 if ("${INSTALL_QMLDIR}" STREQUAL "")
9 set(QML_FULL_INSTALLDIR "${CMAKE_INSTALL_PREFIX}/${INSTALL_QML_PREFIX}")
10 else()
11 set(QML_FULL_INSTALLDIR "${INSTALL_QMLDIR}")
12 endif()
13
14 message(STATUS "QML install dir: ${QML_FULL_INSTALLDIR}")
15endif()
16
17# Install a given target as a QML module. This is mostly pulled from ECM, as there does not seem
18# to be an official way to do it.
19# see https://github.com/KDE/extra-cmake-modules/blob/fe0f606bf7f222e36f7560fd7a2c33ef993e23bb/modules/ECMQmlModule6.cmake#L160
20function(install_qml_module arg_TARGET)
21 if (NOT DEFINED QML_FULL_INSTALLDIR)
22 return()
23 endif()
24
25 qt_query_qml_module(${arg_TARGET}
26 URI module_uri
27 VERSION module_version
28 PLUGIN_TARGET module_plugin_target
29 TARGET_PATH module_target_path
30 QMLDIR module_qmldir
31 TYPEINFO module_typeinfo
32 QML_FILES module_qml_files
33 RESOURCES module_resources
34 )
35
36 set(module_dir "${QML_FULL_INSTALLDIR}/${module_target_path}")
37
38 if (NOT TARGET "${module_plugin_target}")
39 message(FATAL_ERROR "install_qml_modules called for a target without a plugin")
40 endif()
41
42 get_target_property(target_type "${arg_TARGET}" TYPE)
43 if (NOT "${target_type}" STREQUAL "STATIC_LIBRARY")
44 install(
45 TARGETS "${arg_TARGET}"
46 LIBRARY DESTINATION "${module_dir}"
47 RUNTIME DESTINATION "${module_dir}"
48 )
49
50 install(
51 TARGETS "${module_plugin_target}"
52 LIBRARY DESTINATION "${module_dir}"
53 RUNTIME DESTINATION "${module_dir}"
54 )
55 endif()
56
57 install(FILES "${module_qmldir}" DESTINATION "${module_dir}")
58 install(FILES "${module_typeinfo}" DESTINATION "${module_dir}")
59
60 # Install QML files
61 list(LENGTH module_qml_files num_files)
62 if (NOT "${module_qml_files}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0)
63 qt_query_qml_module(${arg_TARGET} QML_FILES_DEPLOY_PATHS qml_files_deploy_paths)
64
65 math(EXPR last_index "${num_files} - 1")
66 foreach(i RANGE 0 ${last_index})
67 list(GET module_qml_files ${i} src_file)
68 list(GET qml_files_deploy_paths ${i} deploy_path)
69 get_filename_component(dst_name "${deploy_path}" NAME)
70 get_filename_component(dest_dir "${deploy_path}" DIRECTORY)
71 install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}")
72 endforeach()
73 endif()
74
75 # Install resources
76 list(LENGTH module_resources num_files)
77 if (NOT "${module_resources}" MATCHES "NOTFOUND" AND ${num_files} GREATER 0)
78 qt_query_qml_module(${arg_TARGET} RESOURCES_DEPLOY_PATHS resources_deploy_paths)
79
80 math(EXPR last_index "${num_files} - 1")
81 foreach(i RANGE 0 ${last_index})
82 list(GET module_resources ${i} src_file)
83 list(GET resources_deploy_paths ${i} deploy_path)
84 get_filename_component(dst_name "${deploy_path}" NAME)
85 get_filename_component(dest_dir "${deploy_path}" DIRECTORY)
86 install(FILES "${src_file}" DESTINATION "${module_dir}/${dest_dir}" RENAME "${dst_name}")
87 endforeach()
88 endif()
89endfunction()
90
91
92cmake_minimum_required(VERSION 3.20)
93project(custom LANGUAGES CXX)
94
95find_package(Qt6 REQUIRED COMPONENTS Core Qml DBus)
96
97qt_standard_project_setup(REQUIRES 6.6)
98
99qt6_policy(SET QTP0001 NEW)
100qt6_add_qml_module(customplugin
101 URI "Custom"
102 PLUGIN_TARGET customplugin
103)
104
105set_source_files_properties(org.keepassxc.KeePassXC.MainWindow.xml PROPERTIES
106 CLASSNAME DBusKeePassXC
107 NO_NAMESPACE TRUE
108)
109qt_add_dbus_interface(DBUS_INTERFACES
110 org.keepassxc.KeePassXC.MainWindow.xml
111 dbus_keepassxc
112)
113
114set_source_files_properties(org.freedesktop.systemd1.Manager.xml PROPERTIES
115 CLASSNAME DBusSystemdManager
116 NO_NAMESPACE TRUE
117)
118qt_add_dbus_interface(DBUS_INTERFACES
119 org.freedesktop.systemd1.Manager.xml
120 dbus_systemd_manager
121)
122
123set_source_files_properties(org.freedesktop.login1.Manager.xml PROPERTIES
124 CLASSNAME DBusLogindManager
125 NO_NAMESPACE TRUE
126)
127qt_add_dbus_interface(DBUS_INTERFACES
128 org.freedesktop.login1.Manager.xml
129 dbus_logind_manager
130)
131
132set_source_files_properties(org.freedesktop.login1.Session.xml PROPERTIES
133 CLASSNAME DBusLogindSession
134 NO_NAMESPACE TRUE
135)
136qt_add_dbus_interface(DBUS_INTERFACES
137 org.freedesktop.login1.Session.xml
138 dbus_logind_session
139)
140
141set_source_files_properties(org.freedesktop.DBus.Properties.xml PROPERTIES
142 CLASSNAME DBusProperties
143 NO_NAMESPACE TRUE
144)
145qt_add_dbus_interface(DBUS_INTERFACES
146 org.freedesktop.DBus.Properties.xml
147 dbus_properties
148)
149
150include_directories(${CMAKE_SOURCE_DIR}/build)
151
152target_compile_features(customplugin PUBLIC cxx_std_26)
153
154target_link_libraries(customplugin PRIVATE
155 Qt6::Core
156 Qt6::Qml
157 Qt6::DBus
158)
159
160target_sources(customplugin PRIVATE
161 Chrono.cpp Chrono.hpp
162 FileSelector.cpp FileSelector.hpp
163 KeePassXC.cpp KeePassXC.hpp
164 Systemd.cpp Systemd.hpp
165 ${DBUS_INTERFACES}
166)
167
168install_qml_module(customplugin)
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp
new file mode 100644
index 00000000..22b3469b
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.cpp
@@ -0,0 +1,83 @@
1#include "Chrono.hpp"
2
3#include <format>
4#include <iostream>
5#include <cmath>
6
7Chrono::Chrono(QObject* parent): QObject(parent) {
8 QObject::connect(&this->timer, &QTimer::timeout, this, &Chrono::onTimeout);
9 this->update();
10}
11
12bool Chrono::enabled() const { return this->mEnabled; }
13
14void Chrono::setEnabled(bool enabled) {
15 if (enabled == this->mEnabled) return;
16 this->mEnabled = enabled;
17 emit this->enabledChanged();
18 this->update();
19}
20
21Chrono::Precision Chrono::precision() const { return this->mPrecision; }
22
23void Chrono::setPrecision(Chrono::Precision precision) {
24 if (precision == this->mPrecision) return;
25 this->mPrecision = precision;
26 emit this->precisionChanged();
27 this->update();
28}
29
30void Chrono::onTimeout() {
31 this->setTime(this->targetTime);
32 this->schedule(this->targetTime);
33}
34
35void Chrono::update() {
36 if (this->mEnabled) {
37 this->setTime(std::chrono::time_point<std::chrono::system_clock>());
38 this->schedule(std::chrono::time_point<std::chrono::system_clock>());
39 } else {
40 this->timer.stop();
41 }
42}
43
44void Chrono::setTime(const std::chrono::time_point<std::chrono::system_clock>& targetTime) {
45 using namespace std::chrono_literals;
46
47 auto currentTime = std::chrono::system_clock::now();
48 auto offset = std::chrono::duration_cast<std::chrono::milliseconds>(targetTime - currentTime);
49 this->currentTime = abs(offset) < 500ms ? targetTime : currentTime;
50
51 switch (this->mPrecision) {
52 case Chrono::Hours: this->currentTime = std::chrono::time_point_cast<std::chrono::hours>(this->currentTime);
53 case Chrono::Minutes: this->currentTime = std::chrono::time_point_cast<std::chrono::minutes>(this->currentTime);
54 case Chrono::Seconds: this->currentTime = std::chrono::time_point_cast<std::chrono::seconds>(this->currentTime);
55 }
56
57 emit this->dateChanged();
58}
59
60void Chrono::schedule(const std::chrono::time_point<std::chrono::system_clock>& targetTime) {
61 using namespace std::chrono_literals;
62
63 auto currentTime = std::chrono::system_clock::now();
64 auto offset = std::chrono::duration_cast<std::chrono::milliseconds>(targetTime - currentTime);
65 auto nextTime = abs(offset) < 500ms ? targetTime : currentTime;
66
67 switch (this->mPrecision) {
68 case Chrono::Hours: nextTime = std::chrono::time_point_cast<std::chrono::hours>(nextTime) + 1h;
69 case Chrono::Minutes: nextTime = std::chrono::time_point_cast<std::chrono::minutes>(nextTime) + 1min;
70 case Chrono::Seconds: nextTime = std::chrono::time_point_cast<std::chrono::seconds>(nextTime) + 1s;
71 }
72
73 this->targetTime = nextTime;
74 this->timer.start(std::chrono::duration_cast<std::chrono::milliseconds>(nextTime - currentTime));
75}
76
77QString Chrono::format(const QString& fmt) const {
78 return QString::fromStdString(std::format(std::runtime_format(fmt.toStdString()), std::chrono::zoned_time(std::chrono::current_zone(), std::chrono::time_point_cast<std::chrono::seconds>(this->currentTime))));
79}
80
81QDateTime Chrono::date() const {
82 return QDateTime::fromStdTimePoint(std::chrono::time_point_cast<std::chrono::milliseconds>(this->currentTime));
83}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp
new file mode 100644
index 00000000..04080187
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Chrono.hpp
@@ -0,0 +1,55 @@
1#pragma once
2
3#include <chrono>
4
5#include <QDateTime>
6#include <QObject>
7#include <QTimer>
8
9#include <qqmlintegration.h>
10
11class Chrono : public QObject {
12 Q_OBJECT;
13 Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
14 Q_PROPERTY(Chrono::Precision precision READ precision WRITE setPrecision NOTIFY precisionChanged);
15 Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged)
16 QML_ELEMENT;
17
18public:
19 enum Precision : quint8 {
20 Hours = 1,
21 Minutes = 2,
22 Seconds = 3,
23 };
24 Q_ENUM(Precision);
25
26 explicit Chrono(QObject* parent = nullptr);
27
28 bool enabled() const;
29 void setEnabled(bool enabled);
30
31 Chrono::Precision precision() const;
32 void setPrecision(Chrono::Precision precision);
33
34 Q_INVOKABLE QString format(const QString& fmt) const;
35
36 QDateTime date() const;
37
38signals:
39 void enabledChanged();
40 void precisionChanged();
41 void dateChanged();
42
43private slots:
44 void onTimeout();
45
46private:
47 bool mEnabled = true;
48 Chrono::Precision mPrecision = Chrono::Seconds;
49 QTimer timer;
50 std::chrono::time_point<std::chrono::system_clock> currentTime, targetTime;
51
52 void update();
53 void setTime(const std::chrono::time_point<std::chrono::system_clock>& targetTime);
54 void schedule(const std::chrono::time_point<std::chrono::system_clock>& targetTime);
55};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp
new file mode 100644
index 00000000..d7051d2a
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.cpp
@@ -0,0 +1,102 @@
1#include "FileSelector.hpp"
2
3#include <sstream>
4#include <vector>
5#include <random>
6#include <algorithm>
7
8#include <iostream>
9#include <format>
10
11namespace fs = std::filesystem;
12
13FileSelector::FileSelector(QObject* parent): QObject(parent) {
14 QObject::connect(&this->timer, &QTimer::timeout, this, &FileSelector::onTimeout);
15 this->timer.setTimerType(Qt::PreciseTimer);
16}
17
18QString FileSelector::directory() const {
19 return QString::fromStdString(this->mDirectory->string());
20}
21void FileSelector::setDirectory(QString directory) {
22 this->mDirectory = directory.toStdString();
23 if (this->mDirectory && this->mEpoch)
24 this->update();
25 emit this->directoryChanged();
26}
27
28unsigned int FileSelector::epoch() const {
29 return std::chrono::duration_cast<std::chrono::milliseconds>(*this->mEpoch).count();
30}
31void FileSelector::setEpoch(unsigned int epoch) {
32 this->mEpoch = std::chrono::milliseconds{epoch};
33 if (this->mDirectory && this->mEpoch)
34 this->update();
35 emit this->epochChanged();
36}
37
38QString FileSelector::seed() const {
39 return this->mSeed;
40}
41void FileSelector::setSeed(QString seed) {
42 this->mSeed = seed;
43 emit this->seedChanged();
44 if (this->mDirectory && this->mEpoch)
45 emit this->selectedChanged();
46}
47
48QString FileSelector::selected() const {
49 if (!this->mDirectory || !this->mEpoch)
50 return QString();
51
52 std::vector<fs::path> shuffled(this->mFiles.begin(), this->mFiles.end());
53 std::sort(shuffled.begin(), shuffled.end());
54
55 auto currentTime = std::chrono::system_clock::now();
56 uint64_t currentEpoch = currentTime.time_since_epoch() / *this->mEpoch;
57 std::chrono::milliseconds timeInEpoch = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime.time_since_epoch()) % *this->mEpoch;
58
59 std::ostringstream seed;
60 seed << this->mSeed.size() << ";" << this->mSeed.toStdString() << ";";
61 seed << *this->mEpoch << ";";
62 seed << currentEpoch << ";";
63 seed << this->mDirectory->string().size() << ";" << *this->mDirectory << ";";
64 seed << this->mFiles.size() << ";";
65 for (const fs::path& p: this->mFiles)
66 seed << p.string().size() << ";" << p << ";";
67
68 std::vector<std::seed_seq::result_type> v;
69 v.reserve(seed.str().size());
70 for (const char& c: seed.str())
71 v.push_back(c);
72
73 std::seed_seq engine_seed(v.begin(), v.end());
74 std::mt19937 g(engine_seed);
75 std::shuffle(shuffled.begin(), shuffled.end(), g);
76
77 std::vector<fs::path>::size_type ix = shuffled.size() * timeInEpoch / *this->mEpoch;
78 return QString::fromStdString((*this->mDirectory / shuffled[ix]).string());
79}
80
81void FileSelector::onTimeout() {
82 if (!this->mFiles.size())
83 return;
84
85 auto currentTime = std::chrono::system_clock::now();
86 uint64_t currentMinorEpoch = currentTime.time_since_epoch() / (*this->mEpoch / this->mFiles.size());
87 auto nextTime = std::chrono::time_point<std::chrono::system_clock>((currentMinorEpoch + 1) * (*this->mEpoch / this->mFiles.size()));
88 this->timer.start(std::chrono::duration_cast<std::chrono::milliseconds>(nextTime - currentTime));
89
90 emit this->selectedChanged();
91}
92
93void FileSelector::update() {
94 this->mFiles = std::set<fs::path>{};
95 for (const fs::directory_entry& entry:
96 fs::recursive_directory_iterator(*this->mDirectory, fs::directory_options::follow_directory_symlink))
97 {
98 this->mFiles.insert(fs::relative(entry, *this->mDirectory));
99 }
100
101 this->onTimeout();
102}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp
new file mode 100644
index 00000000..72c4f2a7
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/FileSelector.hpp
@@ -0,0 +1,52 @@
1#pragma once
2
3#include <filesystem>
4#include <chrono>
5#include <set>
6#include <optional>
7
8#include <QObject>
9#include <QTimer>
10
11#include <qqmlintegration.h>
12
13class FileSelector : public QObject {
14 Q_OBJECT;
15 Q_PROPERTY(QString directory READ directory WRITE setDirectory NOTIFY directoryChanged REQUIRED);
16 Q_PROPERTY(unsigned int epoch READ epoch WRITE setEpoch NOTIFY epochChanged REQUIRED);
17 Q_PROPERTY(QString seed READ seed WRITE setSeed NOTIFY seedChanged);
18 Q_PROPERTY(QString selected READ selected NOTIFY selectedChanged);
19 QML_ELEMENT;
20
21public:
22 explicit FileSelector(QObject* parent = nullptr);
23
24 QString directory() const;
25 void setDirectory(QString directory);
26
27 unsigned int epoch() const;
28 void setEpoch(unsigned int epoch);
29
30 QString seed() const;
31 void setSeed(QString seed);
32
33 QString selected() const;
34
35signals:
36 void directoryChanged();
37 void epochChanged();
38 void seedChanged();
39 void selectedChanged();
40
41private slots:
42 void onTimeout();
43
44private:
45 std::optional<std::filesystem::path> mDirectory;
46 std::optional<std::chrono::milliseconds> mEpoch;
47 std::set<std::filesystem::path> mFiles;
48 QString mSeed;
49 QTimer timer;
50
51 void update();
52};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp
new file mode 100644
index 00000000..f6e4dd6e
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.cpp
@@ -0,0 +1,18 @@
1#include "KeePassXC.hpp"
2
3#include <QDBusConnection>
4
5KeePassXC::KeePassXC() {
6 this->service = new DBusKeePassXC(DBusKeePassXC::staticInterfaceName(), "/keepassxc", QDBusConnection::sessionBus(), this);
7}
8KeePassXC::~KeePassXC() {
9 if (this->service)
10 delete this->service;
11}
12
13void KeePassXC::lockAllDatabases() {
14 if (!this->service)
15 return;
16
17 this->service->lockAllDatabases();
18}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp
new file mode 100644
index 00000000..c4cd71e0
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/KeePassXC.hpp
@@ -0,0 +1,21 @@
1#pragma once
2
3#include "dbus_keepassxc.h"
4
5#include <QObject>
6#include <QtQmlIntegration/qqmlintegration.h>
7
8class KeePassXC : public QObject {
9 Q_OBJECT;
10 QML_SINGLETON;
11 QML_ELEMENT;
12
13public:
14 explicit KeePassXC();
15 ~KeePassXC();
16
17 Q_INVOKABLE void lockAllDatabases();
18
19private:
20 DBusKeePassXC* service = nullptr;
21};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp
new file mode 100644
index 00000000..308659e9
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.cpp
@@ -0,0 +1,198 @@
1#include "Systemd.hpp"
2
3#include <sstream>
4
5#include <QDBusConnection>
6#include <QDBusReply>
7#include <QDebug>
8#include <QDBusUnixFileDescriptor>
9#include <QDBusObjectPath>
10
11Systemd::Systemd(QObject* parent) : QObject(parent) {
12 this->systemdManager = new DBusSystemdManager(
13 "org.freedesktop.systemd1",
14 "/org/freedesktop/systemd1",
15 QDBusConnection::sessionBus(),
16 this
17 );
18 this->logindManager = new DBusLogindManager(
19 "org.freedesktop.login1",
20 "/org/freedesktop/login1",
21 QDBusConnection::systemBus(),
22 this
23 );
24
25 QDBusReply<QDBusObjectPath> sessionPath = this->logindManager->GetSession("auto");
26 qDebug() << sessionPath;
27 this->logindSession = new DBusLogindSession(
28 "org.freedesktop.login1",
29 sessionPath.value().path(),
30 QDBusConnection::systemBus(),
31 this
32 );
33 this->logindSessionProperties = new DBusProperties(
34 "org.freedesktop.login1",
35 sessionPath.value().path(),
36 QDBusConnection::systemBus(),
37 this
38 );
39
40 QObject::connect(this->logindManager, &DBusLogindManager::PrepareForShutdown, this, &Systemd::shutdown);
41 QObject::connect(this->logindManager, &DBusLogindManager::PrepareForSleep, this, &Systemd::sleep);
42 QObject::connect(this->logindSession, &DBusLogindSession::Lock, this, &Systemd::lock);
43 QObject::connect(this->logindSession, &DBusLogindSession::Unlock, this, &Systemd::unlock);
44 QObject::connect(this->logindSessionProperties, &DBusProperties::PropertiesChanged, this, &Systemd::onLogindSessionPropertiesChanged);
45}
46
47void Systemd::onLogindSessionPropertiesChanged(const QString& interface_name, const QVariantMap& changed_properties, const QStringList& invalidated_properties) {
48 if (changed_properties.contains("IdleHint") || invalidated_properties.contains("IdleHint"))
49 emit this->idleHintChanged();
50 if (changed_properties.contains("LockedHint") || invalidated_properties.contains("LockedHint"))
51 emit this->lockedHintChanged();
52}
53
54void Systemd::stopUserUnit(const QString& unit, const QString& mode) { this->systemdManager->StopUnit(unit, mode); }
55
56void Systemd::setBrightness(const QString& subsystem, const QString& name, quint32 brightness) { this->logindSession->SetBrightness(subsystem, name, brightness); }
57
58bool Systemd::idleHint() { return this->logindSession->idleHint(); }
59void Systemd::setIdleHint(bool idle) { this->logindSession->SetIdleHint(idle); }
60bool Systemd::lockedHint() { return this->logindSession->lockedHint(); }
61void Systemd::setLockedHint(bool locked) { this->logindSession->SetLockedHint(locked); }
62void Systemd::lockSession() { this->logindSession->call("Lock"); }
63void Systemd::suspend() { this->logindManager->Suspend(true); }
64void Systemd::hibernate() { this->logindManager->Hibernate(true); }
65
66std::string SystemdInhibitorParams::toString(SystemdInhibitorParams::WhatItem what) {
67 if (what == SystemdInhibitorParams::Shutdown)
68 return "shutdown";
69 else if (what == SystemdInhibitorParams::Sleep)
70 return "sleep";
71 else if (what == SystemdInhibitorParams::Idle)
72 return "idle";
73 else if (what == SystemdInhibitorParams::HandlePowerKey)
74 return "handle-power-key";
75 else if (what == SystemdInhibitorParams::HandleSuspendKey)
76 return "handle-suspend-key";
77 else if (what == SystemdInhibitorParams::HandleHibernateKey)
78 return "handle-hibernate-key";
79 else if (what == SystemdInhibitorParams::HandleLidSwitch)
80 return "handle-lid-switch";
81 return "";
82}
83
84std::string SystemdInhibitorParams::toString(SystemdInhibitorParams::What what) {
85 std::ostringstream res;
86 bool first = true;
87 for (const WhatItem& item: SystemdInhibitorParams::allWhatItems) {
88 if (!(what & item))
89 continue;
90
91 if (!first)
92 res << ":";
93 else
94 first = false;
95 res << SystemdInhibitorParams::toString(item);
96 }
97 return res.str();
98}
99
100std::string SystemdInhibitorParams::toString(SystemdInhibitorParams::Mode mode) {
101 if (mode == SystemdInhibitorParams::Block)
102 return "block";
103 else if (mode == SystemdInhibitorParams::BlockWeak)
104 return "block-weak";
105 else if (mode == SystemdInhibitorParams::Delay)
106 return "delay";
107 return "";
108}
109
110bool SystemdInhibitor::enabled() const { return static_cast<bool>(this->activeInhibitor); }
111void SystemdInhibitor::setEnabled(bool enabled) {
112 this->mEnabled = enabled;
113
114 if (enabled)
115 this->update();
116 else
117 this->release();
118}
119
120SystemdInhibitorParams::What SystemdInhibitor::what() const { return this->mWhat; }
121void SystemdInhibitor::setWhat(SystemdInhibitorParams::What what) {
122 this->mWhat = what;
123 this->update();
124}
125
126QString SystemdInhibitor::who() const { return this->mWho; }
127void SystemdInhibitor::setWho(QString who) {
128 this->mWho = who;
129 this->update();
130}
131
132QString SystemdInhibitor::why() const { return this->mWhy; }
133void SystemdInhibitor::setWhy(QString why) {
134 this->mWhy = why;
135 this->update();
136}
137
138SystemdInhibitorParams::Mode SystemdInhibitor::mode() const { return this->mMode; }
139void SystemdInhibitor::setMode(SystemdInhibitorParams::Mode mode) {
140 this->mMode = mode;
141 this->update();
142}
143
144SystemdInhibitor::ActiveSystemdInhibitor::ActiveSystemdInhibitor(SystemdInhibitorParams::What what_, QString who_, QString why_, SystemdInhibitorParams::Mode mode_): what(what_), who(who_), why(why_), mode(mode_) {
145 DBusLogindManager logindManager(
146 "org.freedesktop.login1",
147 "/org/freedesktop/login1",
148 QDBusConnection::systemBus()
149 );
150 QDBusReply<QDBusUnixFileDescriptor> fd_ = logindManager.Inhibit(QString::fromStdString(SystemdInhibitorParams::toString(what)), who, why, QString::fromStdString(SystemdInhibitorParams::toString(mode)));
151 if (fd_.error().isValid())
152 throw fd_.error();
153 this->fd = ::dup(fd_.value().fileDescriptor());
154}
155SystemdInhibitor::ActiveSystemdInhibitor::~ActiveSystemdInhibitor() {
156 if (this->fd != -1)
157 ::close(this->fd);
158}
159
160void SystemdInhibitor::release() {
161 if (!this->activeInhibitor)
162 return;
163
164 this->activeInhibitor.reset();
165 emit this->enabledChanged();
166}
167
168void SystemdInhibitor::update() {
169 if (!this->mWhat || this->mWho.isEmpty() || this->mWhy.isEmpty() || !this->mMode || !this->mEnabled)
170 if (this->activeInhibitor)
171 this->release();
172 else
173 return;
174
175 if (this->activeInhibitor && this->mWhat == this->activeInhibitor->what && this->mWho == this->activeInhibitor->who && this->mWhy == this->activeInhibitor->why && this->mMode == this->activeInhibitor->mode)
176 return;
177
178 std::unique_ptr<ActiveSystemdInhibitor> otherInhibitor;
179 try {
180 otherInhibitor.reset(new SystemdInhibitor::ActiveSystemdInhibitor(this->mWhat, this->mWho, this->mWhy, this->mMode));
181 } catch (const QDBusError& err) {
182 qCritical().noquote()
183 << err.name().toStdString() << " " << err.message().toStdString();
184 return;
185 }
186 this->activeInhibitor.swap(otherInhibitor);
187
188 if (!otherInhibitor && this->activeInhibitor)
189 emit this->enabledChanged();
190 if (otherInhibitor && otherInhibitor->what != this->activeInhibitor->what)
191 emit this->whatChanged();
192 if (otherInhibitor && otherInhibitor->who != this->activeInhibitor->who)
193 emit this->whoChanged();
194 if (otherInhibitor && otherInhibitor->why != this->activeInhibitor->why)
195 emit this->whyChanged();
196 if (otherInhibitor && otherInhibitor->mode != this->activeInhibitor->mode)
197 emit this->modeChanged();
198}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp
new file mode 100644
index 00000000..615024d2
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/Systemd.hpp
@@ -0,0 +1,147 @@
1#pragma once
2
3#include <cstdint>
4#include <memory>
5#include <string>
6
7#include <QObject>
8#include <QDBusInterface>
9#include <QtQmlIntegration/qqmlintegration.h>
10
11#include "dbus_systemd_manager.h"
12#include "dbus_logind_manager.h"
13#include "dbus_logind_session.h"
14#include "dbus_properties.h"
15
16class Systemd : public QObject {
17 Q_OBJECT;
18 QML_SINGLETON;
19 QML_ELEMENT;
20
21public:
22 explicit Systemd(QObject* parent = nullptr);
23
24 Q_PROPERTY(bool idleHint READ idleHint WRITE setIdleHint NOTIFY idleHintChanged)
25 Q_PROPERTY(bool lockedHint READ lockedHint WRITE setLockedHint NOTIFY lockedHintChanged)
26
27 Q_INVOKABLE void stopUserUnit(const QString& unit, const QString& mode);
28 Q_INVOKABLE void setBrightness(const QString& subsystem, const QString& name, quint32 brightness);
29 Q_INVOKABLE void setIdleHint(bool idle);
30 Q_INVOKABLE void setLockedHint(bool locked);
31 Q_INVOKABLE void lockSession();
32 Q_INVOKABLE void suspend();
33 Q_INVOKABLE void hibernate();
34
35 bool idleHint();
36 bool lockedHint();
37
38signals:
39 void shutdown(bool before);
40 void sleep(bool before);
41 void lock();
42 void unlock();
43 void idleHintChanged();
44 void lockedHintChanged();
45
46private slots:
47 void onLogindSessionPropertiesChanged(const QString& interface_name, const QVariantMap& changed_properties, const QStringList& invalidated_properties);
48
49private:
50 DBusSystemdManager* systemdManager;
51 DBusLogindManager* logindManager;
52 DBusLogindSession* logindSession;
53 DBusProperties* logindSessionProperties;
54};
55
56class SystemdInhibitorParams : public QObject {
57 Q_OBJECT;
58 QML_ELEMENT;
59 QML_SINGLETON;
60
61public:
62 enum WhatItem : uint8_t {
63 Shutdown = 0b1,
64 Sleep = 0b10,
65 Idle = 0b100,
66 HandlePowerKey = 0b1000,
67 HandleSuspendKey = 0b10000,
68 HandleHibernateKey = 0b100000,
69 HandleLidSwitch = 0b1000000,
70 };
71 Q_ENUM(WhatItem);
72 Q_DECLARE_FLAGS(What, WhatItem);
73
74 enum Mode : uint8_t {
75 Block = 1,
76 BlockWeak = 2,
77 Delay = 3,
78 };
79 Q_ENUM(Mode);
80
81 Q_INVOKABLE static std::string toString(WhatItem what);
82 Q_INVOKABLE static std::string toString(What what);
83 Q_INVOKABLE static std::string toString(Mode mode);
84
85 static constexpr WhatItem allWhatItems[] = { Shutdown, Sleep, Idle, HandlePowerKey, HandleSuspendKey, HandleHibernateKey, HandleLidSwitch };
86};
87Q_DECLARE_OPERATORS_FOR_FLAGS(SystemdInhibitorParams::What)
88
89class SystemdInhibitor : public QObject {
90 Q_OBJECT;
91 Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged);
92 Q_PROPERTY(SystemdInhibitorParams::What what READ what WRITE setWhat NOTIFY whatChanged);
93 Q_PROPERTY(QString who READ who WRITE setWho NOTIFY whoChanged);
94 Q_PROPERTY(QString why READ why WRITE setWhy NOTIFY whyChanged);
95 Q_PROPERTY(SystemdInhibitorParams::Mode mode READ mode WRITE setMode NOTIFY modeChanged);
96 QML_ELEMENT;
97
98public:
99 explicit SystemdInhibitor(QObject* parent = nullptr): QObject(parent) {}
100
101 bool enabled() const;
102 void setEnabled(bool enabled);
103
104 SystemdInhibitorParams::What what() const;
105 void setWhat(SystemdInhibitorParams::What what);
106
107 QString who() const;
108 void setWho(QString who);
109
110 QString why() const;
111 void setWhy(QString why);
112
113 SystemdInhibitorParams::Mode mode() const;
114 void setMode(SystemdInhibitorParams::Mode mode);
115
116 Q_INVOKABLE void release();
117
118signals:
119 void enabledChanged();
120 void whatChanged();
121 void whoChanged();
122 void whyChanged();
123 void modeChanged();
124
125private:
126 class ActiveSystemdInhibitor {
127 public:
128 uint32_t fd = -1;
129 SystemdInhibitorParams::What what;
130 QString who;
131 QString why;
132 SystemdInhibitorParams::Mode mode;
133
134 ActiveSystemdInhibitor(SystemdInhibitorParams::What what_, QString who_, QString why_, SystemdInhibitorParams::Mode mode_);
135 ~ActiveSystemdInhibitor();
136 };
137
138 void update();
139
140 bool mEnabled = true;
141 std::unique_ptr<ActiveSystemdInhibitor> activeInhibitor;
142 SystemdInhibitorParams::What mWhat = static_cast<SystemdInhibitorParams::What>(0);
143 QString mWho;
144 QString mWhy;
145 SystemdInhibitorParams::Mode mMode = static_cast<SystemdInhibitorParams::Mode>(0);
146};
147
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/customplugin.h b/accounts/gkleen@sif/shell/quickshell-plugins/customplugin.h
new file mode 100644
index 00000000..e66ba9e3
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/customplugin.h
@@ -0,0 +1,7 @@
1#include <QQmlEngineExtensionPlugin>
2
3class CustomPlugin : public QQmlEngineExtensionPlugin
4{
5 Q_OBJECT
6 Q_PLUGIN_METADATA(IID QQmlEngineExtensionInterface_iid)
7};
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/default.nix b/accounts/gkleen@sif/shell/quickshell-plugins/default.nix
new file mode 100644
index 00000000..20a195eb
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/default.nix
@@ -0,0 +1,30 @@
1{ lib
2, stdenv
3, cmake
4, qt6
5, fmt
6, keepassxc
7, systemd
8}:
9
10stdenv.mkDerivation rec {
11 name = "quickshell-custom";
12
13 src = ./.;
14
15 prePatch = ''
16 cp ${keepassxc.src}/src/gui/org.keepassxc.KeePassXC.MainWindow.xml .
17 '';
18
19 nativeBuildInputs = [ cmake qt6.wrapQtAppsHook ];
20 buildInputs = [
21 qt6.qtbase
22 qt6.qtdeclarative
23 ];
24
25 cmakeFlags = [
26 (lib.cmakeFeature "INSTALL_QML_PREFIX" qt6.qtbase.qtQmlPrefix)
27 ];
28
29 LC_ALL = "C.UTF-8";
30}
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.DBus.Properties.xml b/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.DBus.Properties.xml
new file mode 100644
index 00000000..7588e7a5
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.DBus.Properties.xml
@@ -0,0 +1,28 @@
1<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
2"https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
3<node>
4 <interface name="org.freedesktop.DBus.Properties">
5 <method name="Get">
6 <arg name="interface_name" direction="in" type="s"/>
7 <arg name="property_name" direction="in" type="s"/>
8 <arg name="value" direction="out" type="v"/>
9 </method>
10 <method name="GetAll">
11 <arg name="interface_name" direction="in" type="s"/>
12 <arg name="props" direction="out" type="a{sv}"/>
13 <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="QVariantMap"/>
14 </method>
15 <method name="Set">
16 <arg name="interface_name" direction="in" type="s"/>
17 <arg name="property_name" direction="in" type="s"/>
18 <arg name="value" direction="in" type="v"/>
19 </method>
20 <signal name="PropertiesChanged">
21 <arg type="s" name="interface_name"/>
22 <arg type="a{sv}" name="changed_properties"/>
23 <annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
24 <arg type="as" name="invalidated_properties"/>
25 </signal>
26 </interface>
27</node>
28
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.login1.Manager.xml b/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.login1.Manager.xml
new file mode 100644
index 00000000..120a06d9
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.login1.Manager.xml
@@ -0,0 +1,445 @@
1<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
2"https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
3<node>
4 <interface name="org.freedesktop.login1.Manager">
5 <property name="EnableWallMessages" type="b" access="readwrite">
6 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
7 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
8 </property>
9 <property name="WallMessage" type="s" access="readwrite">
10 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
11 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
12 </property>
13 <property name="NAutoVTs" type="u" access="read">
14 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
15 </property>
16 <property name="KillOnlyUsers" type="as" access="read">
17 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
18 </property>
19 <property name="KillExcludeUsers" type="as" access="read">
20 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
21 </property>
22 <property name="KillUserProcesses" type="b" access="read">
23 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
24 </property>
25 <property name="RebootParameter" type="s" access="read">
26 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
27 </property>
28 <property name="RebootToFirmwareSetup" type="b" access="read">
29 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
30 </property>
31 <property name="RebootToBootLoaderMenu" type="t" access="read">
32 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
33 </property>
34 <property name="RebootToBootLoaderEntry" type="s" access="read">
35 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
36 </property>
37 <property name="BootLoaderEntries" type="as" access="read">
38 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
39 </property>
40 <property name="IdleHint" type="b" access="read">
41 </property>
42 <property name="IdleSinceHint" type="t" access="read">
43 </property>
44 <property name="IdleSinceHintMonotonic" type="t" access="read">
45 </property>
46 <property name="BlockInhibited" type="s" access="read">
47 </property>
48 <property name="BlockWeakInhibited" type="s" access="read">
49 </property>
50 <property name="DelayInhibited" type="s" access="read">
51 </property>
52 <property name="InhibitDelayMaxUSec" type="t" access="read">
53 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
54 </property>
55 <property name="UserStopDelayUSec" type="t" access="read">
56 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
57 </property>
58 <property name="SleepOperation" type="as" access="read">
59 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
60 </property>
61 <property name="HandlePowerKey" type="s" access="read">
62 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
63 </property>
64 <property name="HandlePowerKeyLongPress" type="s" access="read">
65 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
66 </property>
67 <property name="HandleRebootKey" type="s" access="read">
68 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
69 </property>
70 <property name="HandleRebootKeyLongPress" type="s" access="read">
71 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
72 </property>
73 <property name="HandleSuspendKey" type="s" access="read">
74 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
75 </property>
76 <property name="HandleSuspendKeyLongPress" type="s" access="read">
77 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
78 </property>
79 <property name="HandleHibernateKey" type="s" access="read">
80 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
81 </property>
82 <property name="HandleHibernateKeyLongPress" type="s" access="read">
83 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
84 </property>
85 <property name="HandleLidSwitch" type="s" access="read">
86 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
87 </property>
88 <property name="HandleLidSwitchExternalPower" type="s" access="read">
89 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
90 </property>
91 <property name="HandleLidSwitchDocked" type="s" access="read">
92 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
93 </property>
94 <property name="HandleSecureAttentionKey" type="s" access="read">
95 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
96 </property>
97 <property name="HoldoffTimeoutUSec" type="t" access="read">
98 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
99 </property>
100 <property name="IdleAction" type="s" access="read">
101 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
102 </property>
103 <property name="IdleActionUSec" type="t" access="read">
104 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
105 </property>
106 <property name="PreparingForShutdown" type="b" access="read">
107 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
108 </property>
109 <property name="PreparingForShutdownWithMetadata" type="a{sv}" access="read">
110 <annotation name="org.qtproject.QtDBus.QtTypeName" value="QVariantMap"/>
111 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
112 </property>
113 <property name="PreparingForSleep" type="b" access="read">
114 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
115 </property>
116 <!-- <property name="ScheduledShutdown" type="(st)" access="read"> -->
117 <!-- </property> -->
118 <property name="DesignatedMaintenanceTime" type="s" access="read">
119 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
120 </property>
121 <property name="Docked" type="b" access="read">
122 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
123 </property>
124 <property name="LidClosed" type="b" access="read">
125 </property>
126 <property name="OnExternalPower" type="b" access="read">
127 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
128 </property>
129 <property name="RemoveIPC" type="b" access="read">
130 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
131 </property>
132 <property name="RuntimeDirectorySize" type="t" access="read">
133 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
134 </property>
135 <property name="RuntimeDirectoryInodesMax" type="t" access="read">
136 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
137 </property>
138 <property name="InhibitorsMax" type="t" access="read">
139 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
140 </property>
141 <property name="NCurrentInhibitors" type="t" access="read">
142 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
143 </property>
144 <property name="SessionsMax" type="t" access="read">
145 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
146 </property>
147 <property name="NCurrentSessions" type="t" access="read">
148 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
149 </property>
150 <property name="StopIdleSessionUSec" type="t" access="read">
151 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
152 </property>
153 <method name="GetSession">
154 <arg type="s" name="session_id" direction="in"/>
155 <arg type="o" name="object_path" direction="out"/>
156 </method>
157 <method name="GetSessionByPID">
158 <arg type="u" name="pid" direction="in"/>
159 <arg type="o" name="object_path" direction="out"/>
160 </method>
161 <method name="GetUser">
162 <arg type="u" name="uid" direction="in"/>
163 <arg type="o" name="object_path" direction="out"/>
164 </method>
165 <method name="GetUserByPID">
166 <arg type="u" name="pid" direction="in"/>
167 <arg type="o" name="object_path" direction="out"/>
168 </method>
169 <method name="GetSeat">
170 <arg type="s" name="seat_id" direction="in"/>
171 <arg type="o" name="object_path" direction="out"/>
172 </method>
173 <!-- <method name="ListSessions"> -->
174 <!-- <arg type="a(susso)" name="sessions" direction="out"/> -->
175 <!-- </method> -->
176 <!-- <method name="ListSessionsEx"> -->
177 <!-- <arg type="a(sussussbto)" name="sessions" direction="out"/> -->
178 <!-- </method> -->
179 <!-- <method name="ListUsers"> -->
180 <!-- <arg type="a(uso)" name="users" direction="out"/> -->
181 <!-- </method> -->
182 <!-- <method name="ListSeats"> -->
183 <!-- <arg type="a(so)" name="seats" direction="out"/> -->
184 <!-- </method> -->
185 <!-- <method name="ListInhibitors"> -->
186 <!-- <arg type="a(ssssuu)" name="inhibitors" direction="out"/> -->
187 <!-- </method> -->
188 <!-- <method name="CreateSession"> -->
189 <!-- <arg type="u" name="uid" direction="in"/> -->
190 <!-- <arg type="u" name="pid" direction="in"/> -->
191 <!-- <arg type="s" name="service" direction="in"/> -->
192 <!-- <arg type="s" name="type" direction="in"/> -->
193 <!-- <arg type="s" name="class" direction="in"/> -->
194 <!-- <arg type="s" name="desktop" direction="in"/> -->
195 <!-- <arg type="s" name="seat_id" direction="in"/> -->
196 <!-- <arg type="u" name="vtnr" direction="in"/> -->
197 <!-- <arg type="s" name="tty" direction="in"/> -->
198 <!-- <arg type="s" name="display" direction="in"/> -->
199 <!-- <arg type="b" name="remote" direction="in"/> -->
200 <!-- <arg type="s" name="remote_user" direction="in"/> -->
201 <!-- <arg type="s" name="remote_host" direction="in"/> -->
202 <!-- <arg type="a(sv)" name="properties" direction="in"/> -->
203 <!-- <arg type="s" name="session_id" direction="out"/> -->
204 <!-- <arg type="o" name="object_path" direction="out"/> -->
205 <!-- <arg type="s" name="runtime_path" direction="out"/> -->
206 <!-- <arg type="h" name="fifo_fd" direction="out"/> -->
207 <!-- <arg type="u" name="uid" direction="out"/> -->
208 <!-- <arg type="s" name="seat_id" direction="out"/> -->
209 <!-- <arg type="u" name="vtnr" direction="out"/> -->
210 <!-- <arg type="b" name="existing" direction="out"/> -->
211 <!-- <annotation name="org.freedesktop.systemd1.Privileged" value="true"/> -->
212 <!-- </method> -->
213 <!-- <method name="CreateSessionWithPIDFD"> -->
214 <!-- <arg type="u" name="uid" direction="in"/> -->
215 <!-- <arg type="h" name="pidfd" direction="in"/> -->
216 <!-- <arg type="s" name="service" direction="in"/> -->
217 <!-- <arg type="s" name="type" direction="in"/> -->
218 <!-- <arg type="s" name="class" direction="in"/> -->
219 <!-- <arg type="s" name="desktop" direction="in"/> -->
220 <!-- <arg type="s" name="seat_id" direction="in"/> -->
221 <!-- <arg type="u" name="vtnr" direction="in"/> -->
222 <!-- <arg type="s" name="tty" direction="in"/> -->
223 <!-- <arg type="s" name="display" direction="in"/> -->
224 <!-- <arg type="b" name="remote" direction="in"/> -->
225 <!-- <arg type="s" name="remote_user" direction="in"/> -->
226 <!-- <arg type="s" name="remote_host" direction="in"/> -->
227 <!-- <arg type="t" name="flags" direction="in"/> -->
228 <!-- <arg type="a(sv)" name="properties" direction="in"/> -->
229 <!-- <arg type="s" name="session_id" direction="out"/> -->
230 <!-- <arg type="o" name="object_path" direction="out"/> -->
231 <!-- <arg type="s" name="runtime_path" direction="out"/> -->
232 <!-- <arg type="h" name="fifo_fd" direction="out"/> -->
233 <!-- <arg type="u" name="uid" direction="out"/> -->
234 <!-- <arg type="s" name="seat_id" direction="out"/> -->
235 <!-- <arg type="u" name="vtnr" direction="out"/> -->
236 <!-- <arg type="b" name="existing" direction="out"/> -->
237 <!-- <annotation name="org.freedesktop.systemd1.Privileged" value="true"/> -->
238 <!-- </method> -->
239 <method name="ReleaseSession">
240 <arg type="s" name="session_id" direction="in"/>
241 </method>
242 <method name="ActivateSession">
243 <arg type="s" name="session_id" direction="in"/>
244 </method>
245 <method name="ActivateSessionOnSeat">
246 <arg type="s" name="session_id" direction="in"/>
247 <arg type="s" name="seat_id" direction="in"/>
248 </method>
249 <method name="LockSession">
250 <arg type="s" name="session_id" direction="in"/>
251 </method>
252 <method name="UnlockSession">
253 <arg type="s" name="session_id" direction="in"/>
254 </method>
255 <method name="LockSessions">
256 </method>
257 <method name="UnlockSessions">
258 </method>
259 <method name="KillSession">
260 <arg type="s" name="session_id" direction="in"/>
261 <arg type="s" name="whom" direction="in"/>
262 <arg type="i" name="signal_number" direction="in"/>
263 </method>
264 <method name="KillUser">
265 <arg type="u" name="uid" direction="in"/>
266 <arg type="i" name="signal_number" direction="in"/>
267 </method>
268 <method name="TerminateSession">
269 <arg type="s" name="session_id" direction="in"/>
270 </method>
271 <method name="TerminateUser">
272 <arg type="u" name="uid" direction="in"/>
273 </method>
274 <method name="TerminateSeat">
275 <arg type="s" name="seat_id" direction="in"/>
276 </method>
277 <method name="SetUserLinger">
278 <arg type="u" name="uid" direction="in"/>
279 <arg type="b" name="enable" direction="in"/>
280 <arg type="b" name="interactive" direction="in"/>
281 </method>
282 <method name="AttachDevice">
283 <arg type="s" name="seat_id" direction="in"/>
284 <arg type="s" name="sysfs_path" direction="in"/>
285 <arg type="b" name="interactive" direction="in"/>
286 </method>
287 <method name="FlushDevices">
288 <arg type="b" name="interactive" direction="in"/>
289 </method>
290 <method name="PowerOff">
291 <arg type="b" name="interactive" direction="in"/>
292 </method>
293 <method name="PowerOffWithFlags">
294 <arg type="t" name="flags" direction="in"/>
295 </method>
296 <method name="Reboot">
297 <arg type="b" name="interactive" direction="in"/>
298 </method>
299 <method name="RebootWithFlags">
300 <arg type="t" name="flags" direction="in"/>
301 </method>
302 <method name="Halt">
303 <arg type="b" name="interactive" direction="in"/>
304 </method>
305 <method name="HaltWithFlags">
306 <arg type="t" name="flags" direction="in"/>
307 </method>
308 <method name="Suspend">
309 <arg type="b" name="interactive" direction="in"/>
310 </method>
311 <method name="SuspendWithFlags">
312 <arg type="t" name="flags" direction="in"/>
313 </method>
314 <method name="Hibernate">
315 <arg type="b" name="interactive" direction="in"/>
316 </method>
317 <method name="HibernateWithFlags">
318 <arg type="t" name="flags" direction="in"/>
319 </method>
320 <method name="HybridSleep">
321 <arg type="b" name="interactive" direction="in"/>
322 </method>
323 <method name="HybridSleepWithFlags">
324 <arg type="t" name="flags" direction="in"/>
325 </method>
326 <method name="SuspendThenHibernate">
327 <arg type="b" name="interactive" direction="in"/>
328 </method>
329 <method name="SuspendThenHibernateWithFlags">
330 <arg type="t" name="flags" direction="in"/>
331 </method>
332 <method name="Sleep">
333 <arg type="t" name="flags" direction="in"/>
334 </method>
335 <method name="CanPowerOff">
336 <arg type="s" name="result" direction="out"/>
337 </method>
338 <method name="CanReboot">
339 <arg type="s" name="result" direction="out"/>
340 </method>
341 <method name="CanHalt">
342 <arg type="s" name="result" direction="out"/>
343 </method>
344 <method name="CanSuspend">
345 <arg type="s" name="result" direction="out"/>
346 </method>
347 <method name="CanHibernate">
348 <arg type="s" name="result" direction="out"/>
349 </method>
350 <method name="CanHybridSleep">
351 <arg type="s" name="result" direction="out"/>
352 </method>
353 <method name="CanSuspendThenHibernate">
354 <arg type="s" name="result" direction="out"/>
355 </method>
356 <method name="CanSleep">
357 <arg type="s" name="result" direction="out"/>
358 </method>
359 <method name="ScheduleShutdown">
360 <arg type="s" name="type" direction="in"/>
361 <arg type="t" name="usec" direction="in"/>
362 </method>
363 <method name="CancelScheduledShutdown">
364 <arg type="b" name="cancelled" direction="out"/>
365 </method>
366 <method name="Inhibit">
367 <arg type="s" name="what" direction="in"/>
368 <arg type="s" name="who" direction="in"/>
369 <arg type="s" name="why" direction="in"/>
370 <arg type="s" name="mode" direction="in"/>
371 <arg type="h" name="pipe_fd" direction="out"/>
372 </method>
373 <method name="CanRebootParameter">
374 <arg type="s" name="result" direction="out"/>
375 </method>
376 <method name="SetRebootParameter">
377 <arg type="s" name="parameter" direction="in"/>
378 </method>
379 <method name="CanRebootToFirmwareSetup">
380 <arg type="s" name="result" direction="out"/>
381 </method>
382 <method name="SetRebootToFirmwareSetup">
383 <arg type="b" name="enable" direction="in"/>
384 </method>
385 <method name="CanRebootToBootLoaderMenu">
386 <arg type="s" name="result" direction="out"/>
387 </method>
388 <method name="SetRebootToBootLoaderMenu">
389 <arg type="t" name="timeout" direction="in"/>
390 </method>
391 <method name="CanRebootToBootLoaderEntry">
392 <arg type="s" name="result" direction="out"/>
393 </method>
394 <method name="SetRebootToBootLoaderEntry">
395 <arg type="s" name="boot_loader_entry" direction="in"/>
396 </method>
397 <method name="SetWallMessage">
398 <arg type="s" name="wall_message" direction="in"/>
399 <arg type="b" name="enable" direction="in"/>
400 </method>
401 <signal name="SecureAttentionKey">
402 <arg type="s" name="seat_id"/>
403 <arg type="o" name="object_path"/>
404 </signal>
405 <signal name="SessionNew">
406 <arg type="s" name="session_id"/>
407 <arg type="o" name="object_path"/>
408 </signal>
409 <signal name="SessionRemoved">
410 <arg type="s" name="session_id"/>
411 <arg type="o" name="object_path"/>
412 </signal>
413 <signal name="UserNew">
414 <arg type="u" name="uid"/>
415 <arg type="o" name="object_path"/>
416 </signal>
417 <signal name="UserRemoved">
418 <arg type="u" name="uid"/>
419 <arg type="o" name="object_path"/>
420 </signal>
421 <signal name="SeatNew">
422 <arg type="s" name="seat_id"/>
423 <arg type="o" name="object_path"/>
424 </signal>
425 <signal name="SeatRemoved">
426 <arg type="s" name="seat_id"/>
427 <arg type="o" name="object_path"/>
428 </signal>
429 <signal name="PrepareForShutdown">
430 <arg type="b" name="start"/>
431 </signal>
432 <signal name="PrepareForShutdownWithMetadata">
433 <arg type="b" name="start"/>
434 <arg type="a{sv}" name="metadata"/>
435 <annotation name="org.qtproject.QtDBus.QtTypeName.Out1" value="QVariantMap"/>
436 </signal>
437 <signal name="PrepareForSleep">
438 <arg type="b" name="start"/>
439 </signal>
440 </interface>
441 <node name="user"/>
442 <node name="session"/>
443 <node name="seat"/>
444</node>
445
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.login1.Session.xml b/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.login1.Session.xml
new file mode 100644
index 00000000..7d6fc8ee
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.login1.Session.xml
@@ -0,0 +1,146 @@
1<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
2"https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
3<node>
4 <interface name="org.freedesktop.login1.Session">
5 <property name="Id" type="s" access="read">
6 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
7 </property>
8 <property name="User" type="o" access="read">
9 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
10 </property>
11 <property name="Name" type="s" access="read">
12 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
13 </property>
14 <property name="Timestamp" type="t" access="read">
15 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
16 </property>
17 <property name="TimestampMonotonic" type="t" access="read">
18 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
19 </property>
20 <property name="VTNr" type="u" access="read">
21 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
22 </property>
23 <property name="Seat" type="o" access="read">
24 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
25 </property>
26 <property name="TTY" type="s" access="read">
27 </property>
28 <property name="Display" type="s" access="read">
29 </property>
30 <property name="Remote" type="b" access="read">
31 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
32 </property>
33 <property name="RemoteHost" type="s" access="read">
34 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
35 </property>
36 <property name="RemoteUser" type="s" access="read">
37 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
38 </property>
39 <property name="Service" type="s" access="read">
40 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
41 </property>
42 <property name="Desktop" type="s" access="read">
43 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
44 </property>
45 <property name="Scope" type="s" access="read">
46 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
47 </property>
48 <property name="Leader" type="u" access="read">
49 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
50 </property>
51 <property name="Audit" type="u" access="read">
52 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
53 </property>
54 <property name="Type" type="s" access="read">
55 </property>
56 <!-- <property name="Class" type="s" access="read"> -->
57 <!-- </property> -->
58 <property name="Active" type="b" access="read">
59 </property>
60 <property name="State" type="s" access="read">
61 </property>
62 <property name="IdleHint" type="b" access="read">
63 </property>
64 <property name="IdleSinceHint" type="t" access="read">
65 </property>
66 <property name="IdleSinceHintMonotonic" type="t" access="read">
67 </property>
68 <property name="CanIdle" type="b" access="read">
69 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
70 </property>
71 <property name="CanLock" type="b" access="read">
72 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
73 </property>
74 <property name="LockedHint" type="b" access="read">
75 </property>
76 <method name="Terminate">
77 </method>
78 <method name="Activate">
79 </method>
80 <!-- <method name="Lock"> -->
81 <!-- </method> -->
82 <!-- <method name="Unlock"> -->
83 <!-- </method> -->
84 <method name="SetIdleHint">
85 <arg type="b" name="idle" direction="in"/>
86 </method>
87 <method name="SetLockedHint">
88 <arg type="b" name="locked" direction="in"/>
89 </method>
90 <method name="Kill">
91 <arg type="s" name="whom" direction="in"/>
92 <arg type="i" name="signal_number" direction="in"/>
93 </method>
94 <method name="TakeControl">
95 <arg type="b" name="force" direction="in"/>
96 </method>
97 <method name="ReleaseControl">
98 </method>
99 <method name="SetType">
100 <arg type="s" name="type" direction="in"/>
101 </method>
102 <!-- <method name="SetClass"> -->
103 <!-- <arg type="s" name="class" direction="in"/> -->
104 <!-- </method> -->
105 <method name="SetDisplay">
106 <arg type="s" name="display" direction="in"/>
107 </method>
108 <method name="SetTTY">
109 <arg type="h" name="tty_fd" direction="in"/>
110 </method>
111 <method name="TakeDevice">
112 <arg type="u" name="major" direction="in"/>
113 <arg type="u" name="minor" direction="in"/>
114 <arg type="h" name="fd" direction="out"/>
115 <arg type="b" name="inactive" direction="out"/>
116 </method>
117 <method name="ReleaseDevice">
118 <arg type="u" name="major" direction="in"/>
119 <arg type="u" name="minor" direction="in"/>
120 </method>
121 <method name="PauseDeviceComplete">
122 <arg type="u" name="major" direction="in"/>
123 <arg type="u" name="minor" direction="in"/>
124 </method>
125 <method name="SetBrightness">
126 <arg type="s" name="subsystem" direction="in"/>
127 <arg type="s" name="name" direction="in"/>
128 <arg type="u" name="brightness" direction="in"/>
129 </method>
130 <signal name="PauseDevice">
131 <arg type="u" name="major"/>
132 <arg type="u" name="minor"/>
133 <arg type="s" name="type"/>
134 </signal>
135 <signal name="ResumeDevice">
136 <arg type="u" name="major"/>
137 <arg type="u" name="minor"/>
138 <arg type="h" name="fd"/>
139 </signal>
140 <signal name="Lock">
141 </signal>
142 <signal name="Unlock">
143 </signal>
144 </interface>
145</node>
146
diff --git a/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.systemd1.Manager.xml b/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.systemd1.Manager.xml
new file mode 100644
index 00000000..b4f84a13
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell-plugins/org.freedesktop.systemd1.Manager.xml
@@ -0,0 +1,817 @@
1<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
2"https://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
3<node>
4 <interface name="org.freedesktop.systemd1.Manager">
5 <property name="Version" type="s" access="read">
6 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
7 </property>
8 <property name="Features" type="s" access="read">
9 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
10 </property>
11 <property name="Virtualization" type="s" access="read">
12 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
13 </property>
14 <property name="ConfidentialVirtualization" type="s" access="read">
15 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
16 </property>
17 <property name="Architecture" type="s" access="read">
18 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
19 </property>
20 <property name="Tainted" type="s" access="read">
21 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
22 </property>
23 <property name="FirmwareTimestamp" type="t" access="read">
24 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
25 </property>
26 <property name="FirmwareTimestampMonotonic" type="t" access="read">
27 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
28 </property>
29 <property name="LoaderTimestamp" type="t" access="read">
30 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
31 </property>
32 <property name="LoaderTimestampMonotonic" type="t" access="read">
33 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
34 </property>
35 <property name="KernelTimestamp" type="t" access="read">
36 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
37 </property>
38 <property name="KernelTimestampMonotonic" type="t" access="read">
39 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
40 </property>
41 <property name="InitRDTimestamp" type="t" access="read">
42 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
43 </property>
44 <property name="InitRDTimestampMonotonic" type="t" access="read">
45 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
46 </property>
47 <property name="UserspaceTimestamp" type="t" access="read">
48 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
49 </property>
50 <property name="UserspaceTimestampMonotonic" type="t" access="read">
51 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
52 </property>
53 <property name="FinishTimestamp" type="t" access="read">
54 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
55 </property>
56 <property name="FinishTimestampMonotonic" type="t" access="read">
57 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
58 </property>
59 <property name="ShutdownStartTimestamp" type="t" access="read">
60 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
61 </property>
62 <property name="ShutdownStartTimestampMonotonic" type="t" access="read">
63 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
64 </property>
65 <property name="SecurityStartTimestamp" type="t" access="read">
66 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
67 </property>
68 <property name="SecurityStartTimestampMonotonic" type="t" access="read">
69 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
70 </property>
71 <property name="SecurityFinishTimestamp" type="t" access="read">
72 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
73 </property>
74 <property name="SecurityFinishTimestampMonotonic" type="t" access="read">
75 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
76 </property>
77 <property name="GeneratorsStartTimestamp" type="t" access="read">
78 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
79 </property>
80 <property name="GeneratorsStartTimestampMonotonic" type="t" access="read">
81 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
82 </property>
83 <property name="GeneratorsFinishTimestamp" type="t" access="read">
84 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
85 </property>
86 <property name="GeneratorsFinishTimestampMonotonic" type="t" access="read">
87 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
88 </property>
89 <property name="UnitsLoadStartTimestamp" type="t" access="read">
90 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
91 </property>
92 <property name="UnitsLoadStartTimestampMonotonic" type="t" access="read">
93 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
94 </property>
95 <property name="UnitsLoadFinishTimestamp" type="t" access="read">
96 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
97 </property>
98 <property name="UnitsLoadFinishTimestampMonotonic" type="t" access="read">
99 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
100 </property>
101 <property name="UnitsLoadTimestamp" type="t" access="read">
102 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
103 </property>
104 <property name="UnitsLoadTimestampMonotonic" type="t" access="read">
105 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
106 </property>
107 <property name="InitRDSecurityStartTimestamp" type="t" access="read">
108 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
109 </property>
110 <property name="InitRDSecurityStartTimestampMonotonic" type="t" access="read">
111 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
112 </property>
113 <property name="InitRDSecurityFinishTimestamp" type="t" access="read">
114 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
115 </property>
116 <property name="InitRDSecurityFinishTimestampMonotonic" type="t" access="read">
117 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
118 </property>
119 <property name="InitRDGeneratorsStartTimestamp" type="t" access="read">
120 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
121 </property>
122 <property name="InitRDGeneratorsStartTimestampMonotonic" type="t" access="read">
123 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
124 </property>
125 <property name="InitRDGeneratorsFinishTimestamp" type="t" access="read">
126 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
127 </property>
128 <property name="InitRDGeneratorsFinishTimestampMonotonic" type="t" access="read">
129 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
130 </property>
131 <property name="InitRDUnitsLoadStartTimestamp" type="t" access="read">
132 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
133 </property>
134 <property name="InitRDUnitsLoadStartTimestampMonotonic" type="t" access="read">
135 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
136 </property>
137 <property name="InitRDUnitsLoadFinishTimestamp" type="t" access="read">
138 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
139 </property>
140 <property name="InitRDUnitsLoadFinishTimestampMonotonic" type="t" access="read">
141 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
142 </property>
143 <property name="LogLevel" type="s" access="readwrite">
144 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
145 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
146 </property>
147 <property name="LogTarget" type="s" access="readwrite">
148 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
149 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
150 </property>
151 <property name="NNames" type="u" access="read">
152 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
153 </property>
154 <property name="NFailedUnits" type="u" access="read">
155 </property>
156 <property name="NJobs" type="u" access="read">
157 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
158 </property>
159 <property name="NInstalledJobs" type="u" access="read">
160 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
161 </property>
162 <property name="NFailedJobs" type="u" access="read">
163 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
164 </property>
165 <property name="Progress" type="d" access="read">
166 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
167 </property>
168 <property name="Environment" type="as" access="read">
169 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
170 </property>
171 <property name="ConfirmSpawn" type="b" access="read">
172 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
173 </property>
174 <property name="ShowStatus" type="b" access="read">
175 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
176 </property>
177 <property name="UnitPath" type="as" access="read">
178 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
179 </property>
180 <property name="DefaultStandardOutput" type="s" access="read">
181 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
182 </property>
183 <property name="DefaultStandardError" type="s" access="read">
184 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
185 </property>
186 <property name="WatchdogDevice" type="s" access="read">
187 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
188 </property>
189 <property name="WatchdogLastPingTimestamp" type="t" access="read">
190 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
191 </property>
192 <property name="WatchdogLastPingTimestampMonotonic" type="t" access="read">
193 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
194 </property>
195 <property name="RuntimeWatchdogUSec" type="t" access="readwrite">
196 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
197 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
198 </property>
199 <property name="RuntimeWatchdogPreUSec" type="t" access="readwrite">
200 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
201 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
202 </property>
203 <property name="RuntimeWatchdogPreGovernor" type="s" access="readwrite">
204 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
205 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
206 </property>
207 <property name="RebootWatchdogUSec" type="t" access="readwrite">
208 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
209 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
210 </property>
211 <property name="KExecWatchdogUSec" type="t" access="readwrite">
212 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
213 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
214 </property>
215 <property name="ServiceWatchdogs" type="b" access="readwrite">
216 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
217 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
218 </property>
219 <property name="ControlGroup" type="s" access="read">
220 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
221 </property>
222 <property name="SystemState" type="s" access="read">
223 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
224 </property>
225 <property name="ExitCode" type="y" access="read">
226 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
227 </property>
228 <property name="DefaultTimerAccuracyUSec" type="t" access="read">
229 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
230 </property>
231 <property name="DefaultTimeoutStartUSec" type="t" access="read">
232 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
233 </property>
234 <property name="DefaultTimeoutStopUSec" type="t" access="read">
235 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
236 </property>
237 <property name="DefaultTimeoutAbortUSec" type="t" access="read">
238 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
239 </property>
240 <property name="DefaultDeviceTimeoutUSec" type="t" access="read">
241 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
242 </property>
243 <property name="DefaultRestartUSec" type="t" access="read">
244 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
245 </property>
246 <property name="DefaultStartLimitIntervalUSec" type="t" access="read">
247 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
248 </property>
249 <property name="DefaultStartLimitBurst" type="u" access="read">
250 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
251 </property>
252 <property name="DefaultCPUAccounting" type="b" access="read">
253 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
254 </property>
255 <property name="DefaultBlockIOAccounting" type="b" access="read">
256 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
257 </property>
258 <property name="DefaultIOAccounting" type="b" access="read">
259 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
260 </property>
261 <property name="DefaultIPAccounting" type="b" access="read">
262 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
263 </property>
264 <property name="DefaultMemoryAccounting" type="b" access="read">
265 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
266 </property>
267 <property name="DefaultTasksAccounting" type="b" access="read">
268 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
269 </property>
270 <property name="DefaultLimitCPU" type="t" access="read">
271 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
272 </property>
273 <property name="DefaultLimitCPUSoft" type="t" access="read">
274 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
275 </property>
276 <property name="DefaultLimitFSIZE" type="t" access="read">
277 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
278 </property>
279 <property name="DefaultLimitFSIZESoft" type="t" access="read">
280 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
281 </property>
282 <property name="DefaultLimitDATA" type="t" access="read">
283 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
284 </property>
285 <property name="DefaultLimitDATASoft" type="t" access="read">
286 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
287 </property>
288 <property name="DefaultLimitSTACK" type="t" access="read">
289 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
290 </property>
291 <property name="DefaultLimitSTACKSoft" type="t" access="read">
292 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
293 </property>
294 <property name="DefaultLimitCORE" type="t" access="read">
295 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
296 </property>
297 <property name="DefaultLimitCORESoft" type="t" access="read">
298 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
299 </property>
300 <property name="DefaultLimitRSS" type="t" access="read">
301 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
302 </property>
303 <property name="DefaultLimitRSSSoft" type="t" access="read">
304 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
305 </property>
306 <property name="DefaultLimitNOFILE" type="t" access="read">
307 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
308 </property>
309 <property name="DefaultLimitNOFILESoft" type="t" access="read">
310 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
311 </property>
312 <property name="DefaultLimitAS" type="t" access="read">
313 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
314 </property>
315 <property name="DefaultLimitASSoft" type="t" access="read">
316 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
317 </property>
318 <property name="DefaultLimitNPROC" type="t" access="read">
319 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
320 </property>
321 <property name="DefaultLimitNPROCSoft" type="t" access="read">
322 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
323 </property>
324 <property name="DefaultLimitMEMLOCK" type="t" access="read">
325 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
326 </property>
327 <property name="DefaultLimitMEMLOCKSoft" type="t" access="read">
328 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
329 </property>
330 <property name="DefaultLimitLOCKS" type="t" access="read">
331 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
332 </property>
333 <property name="DefaultLimitLOCKSSoft" type="t" access="read">
334 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
335 </property>
336 <property name="DefaultLimitSIGPENDING" type="t" access="read">
337 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
338 </property>
339 <property name="DefaultLimitSIGPENDINGSoft" type="t" access="read">
340 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
341 </property>
342 <property name="DefaultLimitMSGQUEUE" type="t" access="read">
343 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
344 </property>
345 <property name="DefaultLimitMSGQUEUESoft" type="t" access="read">
346 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
347 </property>
348 <property name="DefaultLimitNICE" type="t" access="read">
349 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
350 </property>
351 <property name="DefaultLimitNICESoft" type="t" access="read">
352 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
353 </property>
354 <property name="DefaultLimitRTPRIO" type="t" access="read">
355 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
356 </property>
357 <property name="DefaultLimitRTPRIOSoft" type="t" access="read">
358 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
359 </property>
360 <property name="DefaultLimitRTTIME" type="t" access="read">
361 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
362 </property>
363 <property name="DefaultLimitRTTIMESoft" type="t" access="read">
364 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
365 </property>
366 <property name="DefaultTasksMax" type="t" access="read">
367 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
368 </property>
369 <property name="DefaultMemoryPressureThresholdUSec" type="t" access="read">
370 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
371 </property>
372 <property name="DefaultMemoryPressureWatch" type="s" access="read">
373 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
374 </property>
375 <property name="TimerSlackNSec" type="t" access="read">
376 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
377 </property>
378 <property name="DefaultOOMPolicy" type="s" access="read">
379 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
380 </property>
381 <property name="DefaultOOMScoreAdjust" type="i" access="read">
382 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
383 </property>
384 <property name="CtrlAltDelBurstAction" type="s" access="read">
385 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
386 </property>
387 <property name="SoftRebootsCount" type="u" access="read">
388 <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="const"/>
389 </property>
390 <method name="GetUnit">
391 <arg type="s" name="name" direction="in"/>
392 <arg type="o" name="unit" direction="out"/>
393 </method>
394 <method name="GetUnitByPID">
395 <arg type="u" name="pid" direction="in"/>
396 <arg type="o" name="unit" direction="out"/>
397 </method>
398 <method name="GetUnitByInvocationID">
399 <arg type="ay" name="invocation_id" direction="in"/>
400 <arg type="o" name="unit" direction="out"/>
401 </method>
402 <method name="GetUnitByControlGroup">
403 <arg type="s" name="cgroup" direction="in"/>
404 <arg type="o" name="unit" direction="out"/>
405 </method>
406 <method name="GetUnitByPIDFD">
407 <arg type="h" name="pidfd" direction="in"/>
408 <arg type="o" name="unit" direction="out"/>
409 <arg type="s" name="unit_id" direction="out"/>
410 <arg type="ay" name="invocation_id" direction="out"/>
411 </method>
412 <method name="LoadUnit">
413 <arg type="s" name="name" direction="in"/>
414 <arg type="o" name="unit" direction="out"/>
415 </method>
416 <method name="StartUnit">
417 <arg type="s" name="name" direction="in"/>
418 <arg type="s" name="mode" direction="in"/>
419 <arg type="o" name="job" direction="out"/>
420 </method>
421 <method name="StartUnitWithFlags">
422 <arg type="s" name="name" direction="in"/>
423 <arg type="s" name="mode" direction="in"/>
424 <arg type="t" name="flags" direction="in"/>
425 <arg type="o" name="job" direction="out"/>
426 </method>
427 <method name="StartUnitReplace">
428 <arg type="s" name="old_unit" direction="in"/>
429 <arg type="s" name="new_unit" direction="in"/>
430 <arg type="s" name="mode" direction="in"/>
431 <arg type="o" name="job" direction="out"/>
432 </method>
433 <method name="StopUnit">
434 <arg type="s" name="name" direction="in"/>
435 <arg type="s" name="mode" direction="in"/>
436 <arg type="o" name="job" direction="out"/>
437 </method>
438 <method name="ReloadUnit">
439 <arg type="s" name="name" direction="in"/>
440 <arg type="s" name="mode" direction="in"/>
441 <arg type="o" name="job" direction="out"/>
442 </method>
443 <method name="RestartUnit">
444 <arg type="s" name="name" direction="in"/>
445 <arg type="s" name="mode" direction="in"/>
446 <arg type="o" name="job" direction="out"/>
447 </method>
448 <method name="TryRestartUnit">
449 <arg type="s" name="name" direction="in"/>
450 <arg type="s" name="mode" direction="in"/>
451 <arg type="o" name="job" direction="out"/>
452 </method>
453 <method name="ReloadOrRestartUnit">
454 <arg type="s" name="name" direction="in"/>
455 <arg type="s" name="mode" direction="in"/>
456 <arg type="o" name="job" direction="out"/>
457 </method>
458 <method name="ReloadOrTryRestartUnit">
459 <arg type="s" name="name" direction="in"/>
460 <arg type="s" name="mode" direction="in"/>
461 <arg type="o" name="job" direction="out"/>
462 </method>
463 <!-- <method name="EnqueueUnitJob"> -->
464 <!-- <arg type="s" name="name" direction="in"/> -->
465 <!-- <arg type="s" name="job_type" direction="in"/> -->
466 <!-- <arg type="s" name="job_mode" direction="in"/> -->
467 <!-- <arg type="u" name="job_id" direction="out"/> -->
468 <!-- <arg type="o" name="job_path" direction="out"/> -->
469 <!-- <arg type="s" name="unit_id" direction="out"/> -->
470 <!-- <arg type="o" name="unit_path" direction="out"/> -->
471 <!-- <arg type="s" name="job_type" direction="out"/> -->
472 <!-- <arg type="a(uosos)" name="affected_jobs" direction="out"/> -->
473 <!-- </method> -->
474 <method name="KillUnit">
475 <arg type="s" name="name" direction="in"/>
476 <arg type="s" name="whom" direction="in"/>
477 <arg type="i" name="signal" direction="in"/>
478 </method>
479 <method name="QueueSignalUnit">
480 <arg type="s" name="name" direction="in"/>
481 <arg type="s" name="whom" direction="in"/>
482 <arg type="i" name="signal" direction="in"/>
483 <arg type="i" name="value" direction="in"/>
484 </method>
485 <method name="CleanUnit">
486 <arg type="s" name="name" direction="in"/>
487 <arg type="as" name="mask" direction="in"/>
488 </method>
489 <method name="FreezeUnit">
490 <arg type="s" name="name" direction="in"/>
491 </method>
492 <method name="ThawUnit">
493 <arg type="s" name="name" direction="in"/>
494 </method>
495 <method name="ResetFailedUnit">
496 <arg type="s" name="name" direction="in"/>
497 </method>
498 <!-- <method name="SetUnitProperties"> -->
499 <!-- <arg type="s" name="name" direction="in"/> -->
500 <!-- <arg type="b" name="runtime" direction="in"/> -->
501 <!-- <arg type="a(sv)" name="properties" direction="in"/> -->
502 <!-- </method> -->
503 <method name="BindMountUnit">
504 <arg type="s" name="name" direction="in"/>
505 <arg type="s" name="source" direction="in"/>
506 <arg type="s" name="destination" direction="in"/>
507 <arg type="b" name="read_only" direction="in"/>
508 <arg type="b" name="mkdir" direction="in"/>
509 </method>
510 <!-- <method name="MountImageUnit"> -->
511 <!-- <arg type="s" name="name" direction="in"/> -->
512 <!-- <arg type="s" name="source" direction="in"/> -->
513 <!-- <arg type="s" name="destination" direction="in"/> -->
514 <!-- <arg type="b" name="read_only" direction="in"/> -->
515 <!-- <arg type="b" name="mkdir" direction="in"/> -->
516 <!-- <arg type="a(ss)" name="options" direction="in"/> -->
517 <!-- </method> -->
518 <method name="RefUnit">
519 <arg type="s" name="name" direction="in"/>
520 </method>
521 <method name="UnrefUnit">
522 <arg type="s" name="name" direction="in"/>
523 </method>
524 <!-- <method name="StartTransientUnit"> -->
525 <!-- <arg type="s" name="name" direction="in"/> -->
526 <!-- <arg type="s" name="mode" direction="in"/> -->
527 <!-- <arg type="a(sv)" name="properties" direction="in"/> -->
528 <!-- <arg type="a(sa(sv))" name="aux" direction="in"/> -->
529 <!-- <arg type="o" name="job" direction="out"/> -->
530 <!-- </method> -->
531 <!-- <method name="GetUnitProcesses"> -->
532 <!-- <arg type="s" name="name" direction="in"/> -->
533 <!-- <arg type="a(sus)" name="processes" direction="out"/> -->
534 <!-- </method> -->
535 <!-- <method name="AttachProcessesToUnit"> -->
536 <!-- <arg type="s" name="unit_name" direction="in"/> -->
537 <!-- <arg type="s" name="subcgroup" direction="in"/> -->
538 <!-- <arg type="au" name="pids" direction="in"/> -->
539 <!-- </method> -->
540 <method name="AbandonScope">
541 <arg type="s" name="name" direction="in"/>
542 </method>
543 <method name="GetJob">
544 <arg type="u" name="id" direction="in"/>
545 <arg type="o" name="job" direction="out"/>
546 </method>
547 <!-- <method name="GetJobAfter"> -->
548 <!-- <arg type="u" name="id" direction="in"/> -->
549 <!-- <arg type="a(usssoo)" name="jobs" direction="out"/> -->
550 <!-- </method> -->
551 <!-- <method name="GetJobBefore"> -->
552 <!-- <arg type="u" name="id" direction="in"/> -->
553 <!-- <arg type="a(usssoo)" name="jobs" direction="out"/> -->
554 <!-- </method> -->
555 <method name="CancelJob">
556 <arg type="u" name="id" direction="in"/>
557 </method>
558 <method name="ClearJobs">
559 </method>
560 <method name="ResetFailed">
561 </method>
562 <method name="SetShowStatus">
563 <arg type="s" name="mode" direction="in"/>
564 </method>
565 <!-- <method name="ListUnits"> -->
566 <!-- <arg type="a(ssssssouso)" name="units" direction="out"/> -->
567 <!-- </method> -->
568 <!-- <method name="ListUnitsFiltered"> -->
569 <!-- <arg type="as" name="states" direction="in"/> -->
570 <!-- <arg type="a(ssssssouso)" name="units" direction="out"/> -->
571 <!-- </method> -->
572 <!-- <method name="ListUnitsByPatterns"> -->
573 <!-- <arg type="as" name="states" direction="in"/> -->
574 <!-- <arg type="as" name="patterns" direction="in"/> -->
575 <!-- <arg type="a(ssssssouso)" name="units" direction="out"/> -->
576 <!-- </method> -->
577 <!-- <method name="ListUnitsByNames"> -->
578 <!-- <arg type="as" name="names" direction="in"/> -->
579 <!-- <arg type="a(ssssssouso)" name="units" direction="out"/> -->
580 <!-- </method> -->
581 <!-- <method name="ListJobs"> -->
582 <!-- <arg type="a(usssoo)" name="jobs" direction="out"/> -->
583 <!-- </method> -->
584 <method name="Subscribe">
585 </method>
586 <method name="Unsubscribe">
587 </method>
588 <method name="Dump">
589 <arg type="s" name="output" direction="out"/>
590 </method>
591 <method name="DumpUnitsMatchingPatterns">
592 <arg type="as" name="patterns" direction="in"/>
593 <arg type="s" name="output" direction="out"/>
594 </method>
595 <method name="DumpByFileDescriptor">
596 <arg type="h" name="fd" direction="out"/>
597 </method>
598 <method name="DumpUnitsMatchingPatternsByFileDescriptor">
599 <arg type="as" name="patterns" direction="in"/>
600 <arg type="h" name="fd" direction="out"/>
601 </method>
602 <method name="Reload">
603 </method>
604 <method name="Reexecute">
605 <annotation name="org.freedesktop.DBus.Method.NoReply" value="true"/>
606 </method>
607 <method name="Exit">
608 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
609 </method>
610 <method name="Reboot">
611 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
612 </method>
613 <method name="SoftReboot">
614 <arg type="s" name="new_root" direction="in"/>
615 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
616 </method>
617 <method name="PowerOff">
618 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
619 </method>
620 <method name="Halt">
621 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
622 </method>
623 <method name="KExec">
624 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
625 </method>
626 <method name="SwitchRoot">
627 <arg type="s" name="new_root" direction="in"/>
628 <arg type="s" name="init" direction="in"/>
629 <annotation name="org.freedesktop.systemd1.Privileged" value="true"/>
630 </method>
631 <method name="SetEnvironment">
632 <arg type="as" name="assignments" direction="in"/>
633 </method>
634 <method name="UnsetEnvironment">
635 <arg type="as" name="names" direction="in"/>
636 </method>
637 <method name="UnsetAndSetEnvironment">
638 <arg type="as" name="names" direction="in"/>
639 <arg type="as" name="assignments" direction="in"/>
640 </method>
641 <method name="EnqueueMarkedJobs">
642 <arg type="ao" name="jobs" direction="out"/>
643 </method>
644 <!-- <method name="ListUnitFiles"> -->
645 <!-- <arg type="a(ss)" name="unit_files" direction="out"/> -->
646 <!-- </method> -->
647 <!-- <method name="ListUnitFilesByPatterns"> -->
648 <!-- <arg type="as" name="states" direction="in"/> -->
649 <!-- <arg type="as" name="patterns" direction="in"/> -->
650 <!-- <arg type="a(ss)" name="unit_files" direction="out"/> -->
651 <!-- </method> -->
652 <method name="GetUnitFileState">
653 <arg type="s" name="file" direction="in"/>
654 <arg type="s" name="state" direction="out"/>
655 </method>
656 <!-- <method name="EnableUnitFiles"> -->
657 <!-- <arg type="as" name="files" direction="in"/> -->
658 <!-- <arg type="b" name="runtime" direction="in"/> -->
659 <!-- <arg type="b" name="force" direction="in"/> -->
660 <!-- <arg type="b" name="carries_install_info" direction="out"/> -->
661 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
662 <!-- </method> -->
663 <!-- <method name="DisableUnitFiles"> -->
664 <!-- <arg type="as" name="files" direction="in"/> -->
665 <!-- <arg type="b" name="runtime" direction="in"/> -->
666 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
667 <!-- </method> -->
668 <!-- <method name="EnableUnitFilesWithFlags"> -->
669 <!-- <arg type="as" name="files" direction="in"/> -->
670 <!-- <arg type="t" name="flags" direction="in"/> -->
671 <!-- <arg type="b" name="carries_install_info" direction="out"/> -->
672 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
673 <!-- </method> -->
674 <!-- <method name="DisableUnitFilesWithFlags"> -->
675 <!-- <arg type="as" name="files" direction="in"/> -->
676 <!-- <arg type="t" name="flags" direction="in"/> -->
677 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
678 <!-- </method> -->
679 <!-- <method name="DisableUnitFilesWithFlagsAndInstallInfo"> -->
680 <!-- <arg type="as" name="files" direction="in"/> -->
681 <!-- <arg type="t" name="flags" direction="in"/> -->
682 <!-- <arg type="b" name="carries_install_info" direction="out"/> -->
683 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
684 <!-- </method> -->
685 <!-- <method name="ReenableUnitFiles"> -->
686 <!-- <arg type="as" name="files" direction="in"/> -->
687 <!-- <arg type="b" name="runtime" direction="in"/> -->
688 <!-- <arg type="b" name="force" direction="in"/> -->
689 <!-- <arg type="b" name="carries_install_info" direction="out"/> -->
690 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
691 <!-- </method> -->
692 <!-- <method name="LinkUnitFiles"> -->
693 <!-- <arg type="as" name="files" direction="in"/> -->
694 <!-- <arg type="b" name="runtime" direction="in"/> -->
695 <!-- <arg type="b" name="force" direction="in"/> -->
696 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
697 <!-- </method> -->
698 <!-- <method name="PresetUnitFiles"> -->
699 <!-- <arg type="as" name="files" direction="in"/> -->
700 <!-- <arg type="b" name="runtime" direction="in"/> -->
701 <!-- <arg type="b" name="force" direction="in"/> -->
702 <!-- <arg type="b" name="carries_install_info" direction="out"/> -->
703 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
704 <!-- </method> -->
705 <!-- <method name="PresetUnitFilesWithMode"> -->
706 <!-- <arg type="as" name="files" direction="in"/> -->
707 <!-- <arg type="s" name="mode" direction="in"/> -->
708 <!-- <arg type="b" name="runtime" direction="in"/> -->
709 <!-- <arg type="b" name="force" direction="in"/> -->
710 <!-- <arg type="b" name="carries_install_info" direction="out"/> -->
711 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
712 <!-- </method> -->
713 <!-- <method name="MaskUnitFiles"> -->
714 <!-- <arg type="as" name="files" direction="in"/> -->
715 <!-- <arg type="b" name="runtime" direction="in"/> -->
716 <!-- <arg type="b" name="force" direction="in"/> -->
717 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
718 <!-- </method> -->
719 <!-- <method name="UnmaskUnitFiles"> -->
720 <!-- <arg type="as" name="files" direction="in"/> -->
721 <!-- <arg type="b" name="runtime" direction="in"/> -->
722 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
723 <!-- </method> -->
724 <!-- <method name="RevertUnitFiles"> -->
725 <!-- <arg type="as" name="files" direction="in"/> -->
726 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
727 <!-- </method> -->
728 <!-- <method name="SetDefaultTarget"> -->
729 <!-- <arg type="s" name="name" direction="in"/> -->
730 <!-- <arg type="b" name="force" direction="in"/> -->
731 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
732 <!-- </method> -->
733 <method name="GetDefaultTarget">
734 <arg type="s" name="name" direction="out"/>
735 </method>
736 <!-- <method name="PresetAllUnitFiles"> -->
737 <!-- <arg type="s" name="mode" direction="in"/> -->
738 <!-- <arg type="b" name="runtime" direction="in"/> -->
739 <!-- <arg type="b" name="force" direction="in"/> -->
740 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
741 <!-- </method> -->
742 <!-- <method name="AddDependencyUnitFiles"> -->
743 <!-- <arg type="as" name="files" direction="in"/> -->
744 <!-- <arg type="s" name="target" direction="in"/> -->
745 <!-- <arg type="s" name="type" direction="in"/> -->
746 <!-- <arg type="b" name="runtime" direction="in"/> -->
747 <!-- <arg type="b" name="force" direction="in"/> -->
748 <!-- <arg type="a(sss)" name="changes" direction="out"/> -->
749 <!-- </method> -->
750 <method name="GetUnitFileLinks">
751 <arg type="s" name="name" direction="in"/>
752 <arg type="b" name="runtime" direction="in"/>
753 <arg type="as" name="links" direction="out"/>
754 </method>
755 <method name="SetExitCode">
756 <arg type="y" name="number" direction="in"/>
757 </method>
758 <method name="LookupDynamicUserByName">
759 <arg type="s" name="name" direction="in"/>
760 <arg type="u" name="uid" direction="out"/>
761 </method>
762 <method name="LookupDynamicUserByUID">
763 <arg type="u" name="uid" direction="in"/>
764 <arg type="s" name="name" direction="out"/>
765 </method>
766 <!-- <method name="GetDynamicUsers"> -->
767 <!-- <arg type="a(us)" name="users" direction="out"/> -->
768 <!-- </method> -->
769 <!-- <method name="DumpUnitFileDescriptorStore"> -->
770 <!-- <arg type="s" name="name" direction="in"/> -->
771 <!-- <arg type="a(suuutuusu)" name="entries" direction="out"/> -->
772 <!-- </method> -->
773 <!-- <method name="StartAuxiliaryScope"> -->
774 <!-- <arg type="s" name="name" direction="in"/> -->
775 <!-- <arg type="ah" name="pidfds" direction="in"/> -->
776 <!-- <arg type="t" name="flags" direction="in"/> -->
777 <!-- <arg type="a(sv)" name="properties" direction="in"/> -->
778 <!-- <arg type="o" name="job" direction="out"/> -->
779 <!-- <annotation name="org.freedesktop.DBus.Deprecated" value="true"/> -->
780 <!-- </method> -->
781 <signal name="UnitNew">
782 <arg type="s" name="id"/>
783 <arg type="o" name="unit"/>
784 </signal>
785 <signal name="UnitRemoved">
786 <arg type="s" name="id"/>
787 <arg type="o" name="unit"/>
788 </signal>
789 <signal name="JobNew">
790 <arg type="u" name="id"/>
791 <arg type="o" name="job"/>
792 <arg type="s" name="unit"/>
793 </signal>
794 <signal name="JobRemoved">
795 <arg type="u" name="id"/>
796 <arg type="o" name="job"/>
797 <arg type="s" name="unit"/>
798 <arg type="s" name="result"/>
799 </signal>
800 <signal name="StartupFinished">
801 <arg type="t" name="firmware"/>
802 <arg type="t" name="loader"/>
803 <arg type="t" name="kernel"/>
804 <arg type="t" name="initrd"/>
805 <arg type="t" name="userspace"/>
806 <arg type="t" name="total"/>
807 </signal>
808 <signal name="UnitFilesChanged">
809 </signal>
810 <signal name="Reloading">
811 <arg type="b" name="active"/>
812 </signal>
813 </interface>
814 <node name="unit"/>
815 <node name="job"/>
816</node>
817
diff --git a/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
new file mode 100644
index 00000000..dcc23279
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/ActiveWindowDisplay.qml
@@ -0,0 +1,172 @@
1import QtQuick
2import qs.Services
3import Quickshell
4import Quickshell.Widgets
5
6Item {
7 id: activeWindowDisplay
8
9 required property int maxWidth
10 required property var screen
11
12 property var activeWindow: {
13 let currWindowId = Array.from(NiriService.workspaces).find(ws => {
14 return ws.output === screen.name && ws.is_active;
15 })?.active_window_id;
16
17 return currWindowId ? Array.from(NiriService.windows).find(win => win.id == currWindowId) : null;
18 }
19 property var windowEntry: activeWindow ? DesktopEntries.heuristicLookup(activeWindow.app_id) : null
20
21 anchors.verticalCenter: parent.verticalCenter
22 width: activeWindowDisplayContent.width
23 height: parent.height
24
25 WrapperMouseArea {
26 id: widgetMouseArea
27
28 anchors.fill: parent
29
30 hoverEnabled: true
31
32 Item {
33 anchors.fill: parent
34
35 Row {
36 id: activeWindowDisplayContent
37
38 width: childrenRect.width
39 height: parent.height
40 anchors.verticalCenter: parent.verticalCenter
41 spacing: 8
42
43 IconImage {
44 id: activeWindowIcon
45
46 implicitSize: 14
47
48 anchors.verticalCenter: parent.verticalCenter
49
50 source: {
51 let icon = activeWindowDisplay.windowEntry?.icon
52 if (typeof icon === 'string' || icon instanceof String) {
53 if (icon.includes("?path=")) {
54 const split = icon.split("?path=")
55 if (split.length !== 2)
56 return icon
57 const name = split[0]
58 const path = split[1]
59 const fileName = name.substring(
60 name.lastIndexOf("/") + 1)
61 return `file://${path}/${fileName}`
62 } else
63 icon = Quickshell.iconPath(icon);
64 return icon
65 }
66 return ""
67 }
68 asynchronous: true
69 smooth: true
70 mipmap: true
71 }
72
73 Text {
74 id: windowTitle
75
76 width: Math.min(implicitWidth, activeWindowDisplay.maxWidth - activeWindowIcon.width - activeWindowDisplayContent.spacing)
77
78 property var appAliases: { "Firefox": "Mozilla Firefox", "mpv Media Player": "mpv", "Thunderbird": "Mozilla Thunderbird", "Thunderbird (LMU)": "Mozilla Thunderbird" }
79
80 elide: Text.ElideRight
81 maximumLineCount: 1
82 color: "white"
83 anchors.verticalCenter: parent.verticalCenter
84 text: {
85 if (!activeWindowDisplay.activeWindow)
86 return "";
87
88 var title = activeWindowDisplay.activeWindow.title;
89 var appName = activeWindowDisplay.windowEntry?.name;
90 if (appAliases[appName])
91 appName = appAliases[appName];
92 if (appName && title.endsWith(appName)) {
93 const oldTitle = title;
94 title = title.substring(0, title.length - appName.length);
95 title = title.replace(/\s*(—|-)\s*$/, "");
96 if (!title)
97 title = oldTitle;
98 }
99 return title;
100 }
101 }
102 }
103 }
104 }
105
106 Loader {
107 id: tooltipLoader
108
109 active: false
110
111 Connections {
112 target: widgetMouseArea
113 function onContainsMouseChanged() {
114 if (widgetMouseArea.containsMouse)
115 tooltipLoader.active = true;
116 }
117 }
118
119 PopupWindow {
120 id: tooltip
121
122 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse
123
124 anchor {
125 item: widgetMouseArea
126 edges: Edges.Bottom | Edges.Left
127 }
128 visible: false
129
130 onNextVisibleChanged: hangTimer.restart()
131
132 Timer {
133 id: hangTimer
134 interval: tooltip.visible ? 100 : 500
135 onTriggered: {
136 tooltip.visible = tooltip.nextVisible;
137 if (!tooltip.visible)
138 tooltipLoader.active = false;
139 }
140 }
141
142 implicitWidth: widgetTooltipText.contentWidth + 16
143 implicitHeight: widgetTooltipText.contentHeight + 16
144 color: "black"
145
146 WrapperMouseArea {
147 id: tooltipMouseArea
148
149 hoverEnabled: true
150 enabled: true
151
152 anchors.fill: parent
153
154 Item {
155 anchors.fill: parent
156
157 Text {
158 id: widgetTooltipText
159
160 anchors.centerIn: parent
161
162 font.pointSize: 10
163 font.family: "Fira Mono"
164 color: "white"
165
166 text: JSON.stringify(Object.assign({}, activeWindowDisplay.activeWindow), null, 2)
167 }
168 }
169 }
170 }
171 }
172}
diff --git a/accounts/gkleen@sif/shell/quickshell/Bar.qml b/accounts/gkleen@sif/shell/quickshell/Bar.qml
new file mode 100644
index 00000000..9210066c
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Bar.qml
@@ -0,0 +1,117 @@
1import Quickshell
2import Quickshell.Wayland
3import QtQuick
4
5PanelWindow {
6 id: bar
7
8 required property var modelData
9 screen: modelData
10
11 WlrLayershell.namespace: "bar"
12
13 anchors {
14 top: true
15 left: true
16 right: true
17 }
18 margins {
19 left: 26 + 8
20 right: 26 + 8
21 }
22
23 implicitHeight: 21
24 color: "transparent"
25
26 Rectangle {
27 color: Qt.rgba(0, 0, 0, 0.75)
28 anchors.fill: parent
29 // bottomLeftRadius: 8
30 // bottomRightRadius: 8
31 }
32
33 Row {
34 id: left
35
36 height: parent.height
37 width: childrenRect.width
38 anchors.left: parent.left
39 anchors.leftMargin: 8
40 anchors.verticalCenter: parent.verticalCenter
41 spacing: 8
42
43 WorkspaceSwitcher {
44 screen: bar.screen
45 }
46 }
47
48 Row {
49 id: center
50
51 height: parent.height
52 width: childrenRect.width
53 anchors.centerIn: parent
54 spacing: 5
55
56 ActiveWindowDisplay {
57 screen: bar.screen
58 maxWidth: bar.width - 2*Math.max(left.width, right.width) - 2*8
59 }
60 }
61
62 Row {
63 id: right
64
65 height: parent.height
66 width: childrenRect.width
67 anchors.right: parent.right
68 anchors.rightMargin: 8
69 anchors.verticalCenter: parent.verticalCenter
70 spacing: 0
71
72 // WorktimeWidget { command: "time"; }
73
74 // WorktimeWidget { command: "today"; }
75
76 KeyboardLayout {}
77
78 Item {
79 visible: privacy.visible
80 height: parent.height
81 width: 8 - 4
82 }
83
84 PrivacyWidget {
85 id: privacy
86 }
87
88 Item {
89 visible: privacy.visible
90 height: parent.height
91 width: 8 - 4
92 }
93
94 SystemTray {}
95
96 PipewireWidget {}
97
98 BrightnessWidget {}
99
100 BatteryWidget {}
101
102 WaylandInhibitorWidget {
103 window: bar
104 }
105
106 NotificationInhibitorWidget {}
107
108 LidSwitchInhibitorWidget {}
109
110 Item {
111 height: parent.height
112 width: 8 - 4
113 }
114
115 Clock {}
116 }
117} \ No newline at end of file
diff --git a/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml
new file mode 100644
index 00000000..da17df2a
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/BatteryWidget.qml
@@ -0,0 +1,136 @@
1import QtQuick
2import Quickshell
3import Quickshell.Widgets
4import Quickshell.Services.UPower
5
6Item {
7 id: root
8
9 height: parent.height
10 width: batteryIcon.width + 8
11 anchors.verticalCenter: parent.verticalCenter
12
13 property var batteryDevice: Array.from(UPower.devices.values).find(dev => dev.isLaptopBattery)
14
15 WrapperMouseArea {
16 id: widgetMouseArea
17
18 anchors.fill: parent
19
20 hoverEnabled: true
21
22 Item {
23 anchors.fill: parent
24
25 MaterialDesignIcon {
26 id: batteryIcon
27
28 implicitSize: 14
29 anchors.centerIn: parent
30
31 icon: {
32 if (!root.batteryDevice?.ready)
33 return "battery-unknown";
34
35 if (root.batteryDevice.state == UPowerDeviceState.FullyCharged)
36 return "power-plug-battery";
37
38 const perdec = 10 * Math.max(1, Math.ceil(root.batteryDevice.percentage * 10));
39 if (root.batteryDevice.state == UPowerDeviceState.Charging)
40 return `battery-charging-${perdec}`;
41 if (perdec == 100)
42 return "battery";
43 return `battery-${perdec}`;
44 }
45 color: {
46 if (!root.batteryDevice?.ready)
47 return "#555";
48
49 if (root.batteryDevice.state != UPowerDeviceState.FullyCharged && root.batteryDevice.state != UPowerDeviceState.Charging && root.batteryDevice.timeToEmpty < 20 * 60)
50 return "#f2201f";
51 if (root.batteryDevice.state != UPowerDeviceState.FullyCharged && root.batteryDevice.state != UPowerDeviceState.Charging && root.batteryDevice.timeToEmpty < 40 * 60)
52 return "#f28a21";
53 if (root.batteryDevice.state != UPowerDeviceState.FullyCharged)
54 return "#fff";
55 return "#555";
56 }
57 }
58 }
59 }
60
61 PopupWindow {
62 id: tooltip
63
64 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse
65
66 anchor {
67 item: widgetMouseArea
68 edges: Edges.Bottom | Edges.Left
69 }
70 visible: false
71
72 onNextVisibleChanged: hangTimer.restart()
73
74 Timer {
75 id: hangTimer
76 interval: 100
77 onTriggered: tooltip.visible = tooltip.nextVisible
78 }
79
80 implicitWidth: widgetTooltipText.contentWidth + 16
81 implicitHeight: widgetTooltipText.contentHeight + 16
82 color: "black"
83
84 WrapperMouseArea {
85 id: tooltipMouseArea
86
87 hoverEnabled: true
88 enabled: true
89
90 anchors.fill: parent
91
92 Item {
93 anchors.fill: parent
94
95 Text {
96 id: widgetTooltipText
97
98 anchors.centerIn: parent
99
100 font.pointSize: 10
101 font.family: "Fira Sans"
102 color: "white"
103
104 text: {
105 const stateStr = UPowerDeviceState.toString(root.batteryDevice.state);
106 var outStr = stateStr;
107 if (root.batteryDevice.state != UPowerDeviceState.FullyCharged)
108 outStr += ` ${Math.round(root.batteryDevice.percentage * 100)}%`;
109
110 function formatTime(t) {
111 var res = "";
112 for (const unit of [{ "s": "h", "v": 3600 }, { "s": "m", "v": 60 }, { "s": "s", "v": 1 }]) {
113 if (t < unit.v)
114 continue;
115 res += Math.floor(t / unit.v) + unit.s;
116 t %= unit.v;
117 }
118 return res;
119 }
120 if (root.batteryDevice.timeToEmpty != 0) {
121 const tStr = formatTime(Math.floor(root.batteryDevice.timeToEmpty / 60) * 60);
122 if (tStr)
123 outStr += " " + tStr;
124 } else if (root.batteryDevice.timeToFull != 0) {
125 const tStr = formatTime(Math.ceil(root.batteryDevice.timeToFull / 60) * 60);
126 if (tStr)
127 outStr += " " + tStr;
128 }
129
130 return outStr;
131 }
132 }
133 }
134 }
135 }
136}
diff --git a/accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml b/accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml
new file mode 100644
index 00000000..a432179e
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/BrightnessOSD.qml
@@ -0,0 +1,117 @@
1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Widgets
5import qs.Services
6
7Scope {
8 id: root
9
10 property bool show: false
11 property bool inhibited: true
12
13 Connections {
14 target: Brightness
15
16 function onCurrBrightnessChanged() {
17 root.show = true;
18 hideTimer.restart();
19 }
20 }
21
22 onShowChanged: {
23 if (show)
24 hideTimer.restart();
25 }
26
27 Timer {
28 id: hideTimer
29 interval: 1000
30 onTriggered: root.show = false
31 }
32
33 Timer {
34 id: startInhibit
35 interval: 100
36 running: true
37 onTriggered: {
38 root.show = false;
39 root.inhibited = false;
40 }
41 }
42
43 LazyLoader {
44 active: root.show && !root.inhibited
45
46 Variants {
47 model: Quickshell.screens
48
49 delegate: Scope {
50 id: screenScope
51
52 required property var modelData
53
54 PanelWindow {
55 id: window
56
57 screen: screenScope.modelData
58
59 anchors.top: true
60 margins.top: screen.height / 2 - 50 + 3.5
61 exclusiveZone: 0
62 exclusionMode: ExclusionMode.Ignore
63
64 implicitWidth: 400
65 implicitHeight: 50
66
67 mask: Region {}
68
69 color: "transparent"
70
71 Rectangle {
72 anchors.fill: parent
73 color: Qt.rgba(0, 0, 0, 0.75)
74 }
75
76 RowLayout {
77 id: layout
78
79 anchors.centerIn: parent
80
81 height: 50 - 8*2
82 width: 400 - 8*2
83
84 MaterialDesignIcon {
85 id: volumeIcon
86
87 implicitWidth: parent.height
88 implicitHeight: parent.height
89
90 icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}`
91 }
92
93 Rectangle {
94 Layout.fillWidth: true
95
96 implicitHeight: 10
97
98 color: "#50ffffff"
99
100 Rectangle {
101 anchors {
102 left: parent.left
103 top: parent.top
104 bottom: parent.bottom
105 }
106
107 color: "white"
108
109 implicitWidth: parent.width * Brightness.currBrightness
110 }
111 }
112 }
113 }
114 }
115 }
116 }
117}
diff --git a/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml b/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml
new file mode 100644
index 00000000..3bb5a80e
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/BrightnessWidget.qml
@@ -0,0 +1,84 @@
1import QtQuick
2import Quickshell
3import Quickshell.Widgets
4import qs.Services
5
6Item {
7 height: parent.height
8 width: brightnessIcon.width + 8
9 anchors.verticalCenter: parent.verticalCenter
10
11 WrapperMouseArea {
12 id: widgetMouseArea
13
14 anchors.fill: parent
15
16 hoverEnabled: true
17
18 property real sensitivity: (1 / 50) / 120
19 onWheel: event => Brightness.currBrightness += event.angleDelta.y * sensitivity
20
21 Item {
22 anchors.fill: parent
23
24 MaterialDesignIcon {
25 id: brightnessIcon
26
27 implicitSize: 14
28 anchors.centerIn: parent
29
30 icon: `brightness-${Math.min(7, Math.floor(Brightness.currBrightness * 7) + 1)}`
31 color: "#555"
32 }
33 }
34 }
35
36 PopupWindow {
37 id: tooltip
38
39 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse
40
41 anchor {
42 item: widgetMouseArea
43 edges: Edges.Bottom | Edges.Left
44 }
45 visible: false
46
47 onNextVisibleChanged: hangTimer.restart()
48
49 Timer {
50 id: hangTimer
51 interval: 100
52 onTriggered: tooltip.visible = tooltip.nextVisible
53 }
54
55 implicitWidth: widgetTooltipText.contentWidth + 16
56 implicitHeight: widgetTooltipText.contentHeight + 16
57 color: "black"
58
59 WrapperMouseArea {
60 id: tooltipMouseArea
61
62 hoverEnabled: true
63 enabled: true
64
65 anchors.fill: parent
66
67 Item {
68 anchors.fill: parent
69
70 Text {
71 id: widgetTooltipText
72
73 anchors.centerIn: parent
74
75 font.pointSize: 10
76 font.family: "Fira Sans"
77 color: "white"
78
79 text: `${Math.round(Brightness.currBrightness * 100)}%`
80 }
81 }
82 }
83 }
84}
diff --git a/accounts/gkleen@sif/shell/quickshell/Clock.qml b/accounts/gkleen@sif/shell/quickshell/Clock.qml
new file mode 100644
index 00000000..b7004528
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Clock.qml
@@ -0,0 +1,295 @@
1import QtQml
2import QtQuick
3import Quickshell
4import Custom as Custom
5import QtQuick.Controls
6import QtQuick.Layouts
7import Quickshell.Widgets
8
9Item {
10 id: clockItem
11
12 property bool calendarPopup: true
13
14 width: clock.contentWidth
15 height: parent.height
16 anchors.verticalCenter: parent.verticalCenter
17
18 WrapperMouseArea {
19 id: clockMouseArea
20
21 anchors.fill: parent
22 hoverEnabled: true
23 enabled: clockItem.calendarPopup
24
25 Item {
26 anchors.fill: parent
27
28 Text {
29 id: clock
30 color: "white"
31
32 anchors.verticalCenter: parent.verticalCenter
33
34 Custom.Chrono {
35 id: chrono
36
37 onDateChanged: clock.text = format("W{0:%V-%u} {0:%F} {0:%H:%M:%S%Ez}")
38 }
39
40 font.pointSize: 10
41 font.family: "Fira Sans"
42 font.features: { "tnum": 1 }
43 }
44 }
45 }
46
47 Loader {
48 id: tooltipLoader
49
50 active: false
51
52 Connections {
53 target: clockMouseArea
54 function onContainsMouseChanged() {
55 if (clockMouseArea.containsMouse)
56 tooltipLoader.active = true;
57 }
58 }
59
60 sourceComponent: PopupWindow {
61 id: tooltip
62
63 property bool nextVisible: clockMouseArea.containsMouse || tooltipMouseArea.containsMouse
64
65 anchor {
66 item: clockMouseArea
67 edges: Edges.Bottom | Edges.Left
68 }
69 visible: false
70
71 onNextVisibleChanged: hangTimer.restart()
72
73 Timer {
74 id: hangTimer
75 interval: 100
76 onTriggered: {
77 tooltip.visible = tooltip.nextVisible;
78 if (!tooltip.visible)
79 tooltipLoader.active = false;
80 }
81 }
82
83 implicitWidth: tooltipLayout.childrenRect.width + 16
84 implicitHeight: tooltipLayout.childrenRect.height + 16
85 color: "black"
86
87 onVisibleChanged: {
88 yearCalendar.year = chrono.date.getFullYear();
89 yearCalendar.angleRem = 0;
90 }
91
92 WrapperMouseArea {
93 id: tooltipMouseArea
94
95 hoverEnabled: true
96 enabled: true
97
98 onWheel: event => yearCalendar.scrollYear(event)
99
100 anchors.fill: parent
101
102 Item {
103 id: clockTooltipContent
104
105 anchors.fill: parent
106
107 ColumnLayout {
108 id: tooltipLayout
109
110 anchors {
111 left: parent.left
112 top: parent.top
113 leftMargin: 8
114 topMargin: 8
115 }
116
117 Text {
118 id: yearLabel
119
120 horizontalAlignment: Text.AlignHCenter
121
122 font.pointSize: 14
123 font.family: "Fira Sans"
124 font.features: { "tnum": 1 }
125 color: "white"
126
127 text: yearCalendar.year
128
129 Layout.fillWidth: true
130 Layout.bottomMargin: 8
131 }
132
133 GridLayout {
134 property int year: chrono.date.getFullYear()
135
136 id: yearCalendar
137
138 columns: 3
139 columnSpacing: 16
140 rowSpacing: 16
141
142 Layout.alignment: Qt.AlignHCenter
143 Layout.fillWidth: false
144
145 property real angleRem: 0
146 property real sensitivity: 1 / 120
147
148 function scrollYear(event) {
149 angleRem += event.angleDelta.y;
150 const d = Math.round(angleRem * sensitivity);
151 yearCalendar.year += d;
152 angleRem -= d / sensitivity;
153 }
154
155 Connections {
156 target: clockMouseArea
157 function onWheel(event) { yearCalendar.scrollYear(event); }
158 }
159
160 Repeater {
161 model: 12
162
163 GridLayout {
164 columns: 2
165
166 required property int index
167 property int month: index
168
169 id: monthCalendar
170
171 Layout.alignment: Qt.AlignTop | Qt.AlignRight
172 Layout.fillWidth: false
173
174 Text {
175 Layout.column: 1
176 Layout.fillWidth: true
177
178 horizontalAlignment: Text.AlignHCenter
179
180 font.pointSize: 10
181 font.family: "Fira Sans"
182
183 text: new Date(yearCalendar.year, monthCalendar.month, 1).toLocaleString(Qt.locale("en_DK"), "MMMM")
184
185 color: "#ffead3"
186 }
187
188 DayOfWeekRow {
189 locale: grid.locale
190
191 Layout.row: 1
192 Layout.column: 1
193 Layout.fillWidth: true
194
195 delegate: WrapperItem {
196 required property string shortName
197
198 width: dowLabel.contentWidth + 6
199
200 Text {
201 id: dowLabel
202
203 anchors.fill: parent
204
205 font.pointSize: 10
206 font.family: "Fira Sans"
207
208 text: parent.shortName
209 color: "#ffcc66"
210
211 horizontalAlignment: Text.AlignHCenter
212 verticalAlignment: Text.AlignVCenter
213 }
214 }
215 }
216
217 WeekNumberColumn {
218 month: grid.month
219 year: grid.year
220 locale: grid.locale
221
222 Layout.fillHeight: true
223
224 delegate: Text {
225 required property int weekNumber
226
227 opacity: {
228 const simple = new Date(weekNumber == 1 && monthCalendar.month == 12 ? yearCalendar.year + 1 : yearCalendar.year, 0, 1 + (weekNumber - 1) * 7);
229 const dayOfWeek = simple.getDay();
230 const isoWeekStart = simple;
231
232 isoWeekStart.setDate(simple.getDate() - dayOfWeek + 1);
233 if (dayOfWeek > 4) {
234 isoWeekStart.setDate(isoWeekStart.getDate() + 7);
235 }
236
237 for (let i = 0; i < 7; i++) {
238 const dayInWeek = new Date(isoWeekStart);
239 dayInWeek.setDate(dayInWeek.getDate() + i);
240 if (dayInWeek.getMonth() == monthCalendar.month)
241 return 1;
242 }
243
244 return 0;
245 }
246
247 font.pointSize: 10
248 font.family: "Fira Sans"
249 font.features: { "tnum": 1 }
250
251 text: weekNumber
252 color: "#99ffdd"
253
254 horizontalAlignment: Text.AlignRight
255 verticalAlignment: Text.AlignVCenter
256 }
257 }
258
259 MonthGrid {
260 id: grid
261
262 year: yearCalendar.year
263 month: monthCalendar.month
264 locale: Qt.locale("en_DK")
265
266 Layout.fillWidth: true
267 Layout.fillHeight: true
268
269 delegate: Text {
270 required property var model
271
272 opacity: model.month === monthCalendar.month ? 1 : 0
273
274 font.pointSize: 10
275 font.family: "Fira Sans"
276 font.features: { "tnum": 1 }
277
278 property bool today: chrono.date.getFullYear() == model.year && chrono.date.getMonth() == model.month && chrono.date.getDate() == model.day
279
280 text: model.day
281 color: today ? "#ff6699" : "white"
282
283 horizontalAlignment: Text.AlignRight
284 verticalAlignment: Text.AlignVCenter
285 }
286 }
287 }
288 }
289 }
290 }
291 }
292 }
293 }
294 }
295}
diff --git a/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml
new file mode 100644
index 00000000..46302e54
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/KeyboardLayout.qml
@@ -0,0 +1,112 @@
1import Quickshell
2import QtQuick
3import qs.Services
4import Quickshell.Widgets
5
6Item {
7 width: kbdLabel.contentWidth + 8
8 height: parent.height
9 anchors.verticalCenter: parent.verticalCenter
10
11 WrapperMouseArea {
12 id: kbdMouseArea
13
14 anchors.fill: parent
15
16 hoverEnabled: true
17 cursorShape: Qt.PointingHandCursor
18 enabled: true
19 onClicked: {
20 NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": "Next" } } }, _ => {})
21 }
22 onWheel: event => {
23 NiriService.sendCommand({ "Action": { "SwitchLayout": { "layout": event.angleDelta > 0 ? "Next" : "Prev" } } }, _ => {})
24 }
25
26 Rectangle {
27 id: kbdWidget
28
29 property var keyboardAbbrev: { "English (programmer Dvorak)": "dvp", "English (US)": "us" }
30
31 anchors.fill: parent
32 color: {
33 if (kbdMouseArea.containsMouse) {
34 return "#33808080";
35 }
36 return "transparent";
37 }
38
39 Text {
40 id: kbdLabel
41
42 font.pointSize: 10
43 font.family: "Fira Sans"
44 color: {
45 if (NiriService.keyboardLayouts?.current_idx === 0)
46 return "#555";
47 return "white";
48 }
49 anchors.centerIn: parent
50
51 text: {
52 const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx];
53 if (!currentLayout)
54 return "";
55 return kbdWidget.keyboardAbbrev[currentLayout] ? kbdWidget.keyboardAbbrev[currentLayout] : currentLayout;
56 }
57 }
58 }
59 }
60
61 PopupWindow {
62 id: tooltip
63
64 property bool nextVisible: kbdMouseArea.containsMouse || tooltipMouseArea.containsMouse
65
66 anchor {
67 item: kbdMouseArea
68 edges: Edges.Bottom | Edges.Left
69 }
70 visible: false
71
72 onNextVisibleChanged: hangTimer.restart()
73
74 Timer {
75 id: hangTimer
76 interval: 100
77 onTriggered: tooltip.visible = tooltip.nextVisible
78 }
79
80 implicitWidth: kbdTooltipText.contentWidth + 16
81 implicitHeight: kbdTooltipText.contentHeight + 16
82 color: "black"
83
84 WrapperMouseArea {
85 id: tooltipMouseArea
86
87 hoverEnabled: true
88 enabled: true
89
90 anchors.fill: parent
91
92 Item {
93 anchors.fill: parent
94
95 Text {
96 id: kbdTooltipText
97
98 anchors.centerIn: parent
99
100 font.pointSize: 10
101 font.family: "Fira Sans"
102 color: "white"
103
104 text: {
105 const currentLayout = NiriService.keyboardLayouts?.names?.[NiriService.keyboardLayouts.current_idx];
106 return currentLayout || "";
107 }
108 }
109 }
110 }
111 }
112}
diff --git a/accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml
new file mode 100644
index 00000000..8410dcda
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/LidSwitchInhibitorWidget.qml
@@ -0,0 +1,47 @@
1import Quickshell
2import QtQuick
3import Quickshell.Widgets
4import qs.Services
5
6Item {
7 id: root
8
9 width: icon.width + 8
10 height: parent.height
11 anchors.verticalCenter: parent.verticalCenter
12
13 WrapperMouseArea {
14 id: widgetMouseArea
15
16 anchors.fill: parent
17
18 hoverEnabled: true
19 cursorShape: Qt.PointingHandCursor
20
21 onClicked: InhibitorState.lidSwitchInhibited = !InhibitorState.lidSwitchInhibited
22
23 Rectangle {
24 anchors.fill: parent
25 color: {
26 if (widgetMouseArea.containsMouse) {
27 return "#33808080";
28 }
29 return "transparent";
30 }
31
32 Item {
33 anchors.fill: parent
34
35 MaterialDesignIcon {
36 id: icon
37
38 implicitSize: 14
39 anchors.centerIn: parent
40
41 icon: InhibitorState.lidSwitchInhibited ? "laptop-off" : "laptop"
42 color: InhibitorState.lidSwitchInhibited ? "#f28a21" : "#555"
43 }
44 }
45 }
46 }
47}
diff --git a/accounts/gkleen@sif/shell/quickshell/LockSurface.qml b/accounts/gkleen@sif/shell/quickshell/LockSurface.qml
new file mode 100644
index 00000000..f4f8f0cd
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/LockSurface.qml
@@ -0,0 +1,227 @@
1import Quickshell.Widgets
2import QtQuick.Effects
3import QtQuick.Layouts
4import QtQuick
5import QtQuick.Controls
6import QtQuick.Controls.Fusion
7import qs.Services
8import QtQml
9
10Item {
11 id: lockSurface
12
13 property var screen
14 property list<var> messages: []
15 property bool responseRequired: false
16 property bool responseVisible: false
17 property string currentText: ""
18 property bool authRunning: false
19
20 signal response(string responseText)
21
22 anchors.fill: parent
23
24 Item {
25 id: background
26
27 anchors.fill: parent
28
29 property Img current: one
30 property string source: selector.selected
31
32 WallpaperSelector {
33 id: selector
34 seed: lockSurface.screen?.name || ""
35 }
36
37 onSourceChanged: {
38 if (!source)
39 current = null;
40 else if (current === one)
41 two.update()
42 else
43 one.update()
44 }
45
46 Img { id: one }
47 Img { id: two }
48
49 component Img: Item {
50 id: img
51
52 property string source
53
54 function update() {
55 source = background.source || ""
56 }
57
58 anchors.fill: parent
59
60 Image {
61 id: imageSource
62
63 source: img.source
64 sourceSize: Qt.size(parent.width, parent.height)
65 fillMode: Image.PreserveAspectCrop
66 smooth: true
67 visible: false
68 asynchronous: true
69 cache: false
70
71 onStatusChanged: {
72 if (status === Image.Ready) {
73 background.current = img
74 }
75 }
76 }
77
78 MultiEffect {
79 id: imageEffect
80
81 source: imageSource
82 anchors.fill: parent
83 blurEnabled: true
84 blur: 1
85 blurMax: 64
86 blurMultiplier: 2
87
88 opacity: 0
89
90 states: State {
91 name: "visible"
92 when: background.current === img
93
94 PropertyChanges {
95 imageEffect.opacity: 1
96 }
97 StateChangeScript {
98 name: "unloadOther"
99 script: {
100 if (img === one)
101 two.source = ""
102 if (img === two)
103 one.source = ""
104 }
105 }
106 }
107
108 transitions: Transition {
109 SequentialAnimation {
110 NumberAnimation {
111 target: imageEffect
112 properties: "opacity"
113 duration: {
114 if (img === one && two.source == "" || img === two && one.source == "")
115 return 0;
116 return 5000;
117 }
118 easing.type: Easing.OutCubic
119 }
120 ScriptAction {
121 scriptName: "unloadOther"
122 }
123 }
124 }
125 }
126 }
127 }
128
129 Item {
130 anchors {
131 top: lockSurface.top
132 left: lockSurface.left
133 right: lockSurface.right
134 }
135
136 implicitWidth: lockSurface.width
137 implicitHeight: 21
138
139 Rectangle {
140 anchors.fill: parent
141 color: Qt.rgba(0, 0, 0, 0.75)
142 }
143
144 Clock {
145 anchors.centerIn: parent
146 calendarPopup: false
147 }
148 }
149
150 WrapperRectangle {
151 id: unlockUi
152
153 Keys.onPressed: event => {
154 if (!lockSurface.authRunning) {
155 event.accepted = true;
156 lockSurface.authRunning = true;
157 }
158 }
159 focus: !passwordBox.visible
160
161 visible: lockSurface.authRunning
162
163 color: Qt.rgba(0, 0, 0, 0.75)
164 margin: 8
165
166 anchors.centerIn: parent
167
168 ColumnLayout {
169 spacing: 4
170
171 BusyIndicator {
172 visible: running
173 running: !Array.from(lockSurface.messages).length && !lockSurface.responseRequired
174 }
175
176 Repeater {
177 model: lockSurface.messages
178
179 Text {
180 required property var modelData
181
182 font.pointSize: 10
183 font.family: "Fira Sans"
184 color: modelData.error ? "#f28a21" : "#ffffff"
185
186 text: String(modelData.text).trim()
187
188 Layout.fillWidth: true
189 horizontalAlignment: Text.AlignHCenter
190 }
191 }
192
193 TextField {
194 id: passwordBox
195
196 visible: lockSurface.responseRequired
197 echoMode: lockSurface.responseVisible ? TextInput.Normal : TextInput.Password
198 inputMethodHints: Qt.ImhSensitiveData
199
200 onTextChanged: lockSurface.currentText = passwordBox.text
201 onAccepted: {
202 passwordBox.readOnly = true;
203 lockSurface.response(lockSurface.currentText);
204 }
205
206 Connections {
207 target: lockSurface
208 function onCurrentTextChanged() {
209 passwordBox.text = lockSurface.currentText
210 }
211 }
212 Connections {
213 target: lockSurface
214 function onResponseRequiredChanged() {
215 if (lockSurface.responseRequired)
216 passwordBox.readOnly = false;
217 passwordBox.focus = true;
218 passwordBox.selectAll();
219 }
220 }
221
222 Layout.topMargin: 4
223 Layout.fillWidth: true
224 }
225 }
226 }
227}
diff --git a/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
new file mode 100644
index 00000000..996fd41b
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Lockscreen.qml
@@ -0,0 +1,132 @@
1import Quickshell
2import Quickshell.Wayland
3import Quickshell.Io
4import Quickshell.Services.Pam
5import Quickshell.Services.Mpris
6import Custom as Custom
7import qs.Services
8import QtQml
9
10Scope {
11 id: lockscreen
12
13 property string currentText: ""
14
15 PamContext {
16 id: pam
17
18 property list<var> messages: []
19
20 config: "quickshell"
21 onCompleted: result => {
22 if (result === PamResult.Success) {
23 lock.locked = false;
24 }
25 }
26 onPamMessage: {
27 messages = Array.from(messages).concat([{ "text": pam.message, "error": pam.messageIsError }])
28 }
29 onActiveChanged: {
30 messages = [];
31 }
32 }
33
34 IpcHandler {
35 target: "Lockscreen"
36
37 function setLocked(locked: bool): void { lock.locked = locked; }
38 function getLocked(): bool { return lock.locked; }
39 }
40
41 Connections {
42 target: Custom.Systemd
43 function onSleep(before: bool) {
44 console.log(`received prepare for sleep ${before}`);
45 if (before)
46 lock.locked = true;
47 }
48 function onLock() { lock.locked = true; }
49 function onUnlock() { lock.locked = false; }
50 }
51
52 IdleMonitor {
53 id: idleMonitor
54 enabled: !lock.secure
55 timeout: 600
56 respectInhibitors: true
57
58 onIsIdleChanged: {
59 if (idleMonitor.isIdle)
60 lock.locked = true;
61 }
62 }
63
64 Custom.SystemdInhibitor {
65 enabled: !lock.secure
66
67 what: Custom.SystemdInhibitorParams.Sleep
68 who: "quickshell"
69 why: "Lock session"
70 mode: Custom.SystemdInhibitorParams.Delay
71 }
72
73 Binding {
74 target: NotificationManager
75 property: "lockscreenActive"
76 value: lock.locked
77 }
78
79 WlSessionLock {
80 id: lock
81
82 onLockStateChanged: {
83 if (!locked && pam.active)
84 pam.abort();
85
86 if (locked) {
87 NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {});
88 Custom.KeePassXC.lockAllDatabases();
89 Array.from(MprisProxy.players).forEach(player => {
90 if (player.canPause && player.isPlaying)
91 player.pause();
92 });
93 // Custom.Systemd.stopUserUnit("gpg-agent.service", "replace");
94 GpgAgent.reloadAgent();
95 }
96 }
97 Component.onCompleted: { (_ => {})(MprisProxy.players); }
98
99 onSecureStateChanged: Custom.Systemd.lockedHint = lock.secure
100
101 WlSessionLockSurface {
102 id: lockSurface
103
104 color: "black"
105
106 LockSurface {
107 id: surfaceContent
108
109 onResponse: responseText => pam.respond(responseText)
110 onAuthRunningChanged: {
111 if (authRunning)
112 pam.start();
113 }
114 Connections {
115 target: pam
116 function onMessagesChanged() { surfaceContent.messages = pam.messages; }
117 function onResponseRequiredChanged() { surfaceContent.responseRequired = pam.responseRequired; }
118 function onActiveChanged() { surfaceContent.authRunning = pam.active; }
119 }
120 onCurrentTextChanged: lockscreen.currentText = currentText
121 Connections {
122 target: lockscreen
123 function onCurrentTextChanged() { surfaceContent.currentText = lockscreen.currentText; }
124 }
125 Connections {
126 target: lockSurface
127 function onScreenChanged() { surfaceContent.screen = lockSurface.screen; }
128 }
129 }
130 }
131 }
132}
diff --git a/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
new file mode 100644
index 00000000..155a009e
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/MaterialDesignIcon.qml
@@ -0,0 +1,35 @@
1import QtQuick
2import QtQuick.Effects
3
4Item {
5 id: root
6
7 required property string icon
8 property color color: "white"
9
10 property real implicitSize: 0
11
12 readonly property real actualSize: Math.min(root.width, root.height)
13
14 implicitWidth: root.implicitSize
15 implicitHeight: root.implicitSize
16
17 Image {
18 id: sourceImage
19 source: "file://" + @mdi@ + "/svg/" + root.icon + ".svg"
20 anchors.fill: parent
21 fillMode: Image.PreserveAspectFit
22
23 sourceSize.width: root.actualSize
24 sourceSize.height: root.actualSize
25
26 layer.enabled: true
27 layer.effect: MultiEffect {
28 id: effect
29
30 brightness: 1
31 colorization: 1
32 colorizationColor: root.color
33 }
34 }
35}
diff --git a/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml b/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml
new file mode 100644
index 00000000..beff205c
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/NiriIdle.qml
@@ -0,0 +1,30 @@
1import QtQml
2import Quickshell
3import Quickshell.Wayland
4import qs.Services
5import Custom as Custom
6
7Scope {
8 IdleMonitor {
9 id: idleMonitor30
10 timeout: 30
11
12 onIsIdleChanged: Custom.Systemd.setIdleHint(idleMonitor30.isIdle)
13 }
14 IdleMonitor {
15 id: idleMonitor540
16 timeout: 540
17
18 onIsIdleChanged: {
19 if (idleMonitor540.isIdle)
20 NiriService.sendCommand({ "Action": { "PowerOffMonitors": {} } }, _ => {});
21 }
22 }
23 Connections {
24 target: Custom.Systemd
25 function onSleep(before: bool) {
26 if (!before)
27 NiriService.sendCommand({ "Action": { "PowerOnMonitors": {} } }, _ => {});
28 }
29 }
30}
diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml
new file mode 100644
index 00000000..cc0e49b1
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/NotificationDisplay.qml
@@ -0,0 +1,340 @@
1import QtQml
2import QtQml.Models
3import QtQuick
4import Quickshell
5import Quickshell.Widgets
6import Quickshell.Wayland
7import qs.Services
8import QtQuick.Layouts
9import Quickshell.Services.Notifications
10
11Scope {
12 id: root
13
14 property var activeScreen: Array.from(Quickshell.screens).find(screen => screen.name === Array.from(NiriService.workspaces).find(ws => ws.is_focused)?.output) ?? null
15
16 Instantiator {
17 id: notifsRepeater
18
19 model: ScriptModel {
20 values: NotificationManager.groups
21 }
22
23 delegate: PanelWindow {
24 id: notifWindow
25
26 visible: NotificationManager.active
27
28 screen: root.activeScreen
29
30 WlrLayershell.namespace: "notifications"
31
32 required property var modelData
33 required property var index
34
35 property int activeIx: modelData.length - 1
36 onModelDataChanged: {
37 notifWindow.activeIx = modelData.length - 1;
38 }
39
40 property color textColor: {
41 if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Low)
42 return "#ff999999";
43 return "white";
44 }
45 property color backgroundColor: {
46 if (notifWindow.modelData?.[notifWindow.activeIx]?.urgency == NotificationUrgency.Critical)
47 return "#dd900000";
48 return "black";
49 }
50
51 anchors {
52 right: true
53 top: true
54 }
55
56 readonly property real spaceAbove: {
57 var res = 0;
58 for (let i = 0; i < notifWindow.index; i++) {
59 (_ => {})(notifsRepeater.objectAt(i).modelData);
60 res += notifsRepeater.objectAt(i).height + 8;
61 }
62 return res;
63 }
64
65 margins {
66 right: 26 + 8
67 top: 8 + spaceAbove
68 }
69
70 color: "transparent"
71
72 implicitHeight: Math.max(notifCount.visible ? notifCount.contentHeight : 0, notifSummary.contentHeight) + (notifBody.visible ? 8 + notifBody.contentHeight : 0) + (notifActions.visible ? 8 + notifActions.height : 0) + (notifTime.visible ? 8 + notifTime.contentHeight : 0) + 16
73 implicitWidth: 400
74
75 WrapperMouseArea {
76 enabled: true
77
78 anchors.fill: parent
79
80 cursorShape: Qt.PointingHandCursor
81
82 onClicked: {
83 for (const notif of notifWindow.modelData)
84 notif.dismiss();
85 }
86
87 property real angleRem: 0
88 property real sensitivity: 1 / 120
89 onWheel: event => {
90 angleRem += event.angleDelta.y;
91 const d = Math.round(angleRem * sensitivity);
92 angleRem -= d / sensitivity;
93 notifWindow.activeIx = ((notifWindow.modelData?.length ?? 1) + notifWindow.activeIx - d) % (notifWindow.modelData?.length ?? 1);
94 }
95
96 Rectangle {
97 color: notifWindow.backgroundColor
98 anchors.fill: parent
99 border {
100 color: Qt.hsla(195/360, 1, 0.45, 1)
101 width: 2
102 }
103
104 GridLayout {
105 id: notifLayout
106
107 width: 400 - 16
108 anchors.fill: parent
109 anchors.margins: 8
110 columnSpacing: 8
111 rowSpacing: 8
112
113 columns: notifImage.visible ? 3 : 2
114 rows: {
115 var res = 1;
116 if (notifBody.visible)
117 res += 1;
118 if (notifActions.visible)
119 res += 1;
120 if (notifTime.visible)
121 res += 1;
122 return res;
123 }
124
125 Text {
126 id: notifCount
127
128 visible: notifWindow.modelData?.length > 1 ?? false
129 text: `${notifWindow.activeIx + 1}/${notifWindow.modelData?.length ?? ""}`
130
131 font.pointSize: 10
132 font.family: "Fira Sans"
133 font.bold: true
134 font.features: { "tnum": 1 }
135 color: notifWindow.textColor
136 maximumLineCount: 1
137
138 Layout.fillWidth: false
139 Layout.row: 0
140 Layout.column: notifImage.visible ? 1 : 0
141 }
142
143 Text {
144 id: notifSummary
145
146 text: notifWindow.modelData?.[notifWindow.activeIx]?.summary ?? ""
147
148 font.pointSize: 10
149 font.family: "Fira Sans"
150 font.italic: true
151 color: notifWindow.textColor
152 maximumLineCount: 1
153 elide: Text.ElideRight
154
155 Layout.fillWidth: true
156 Layout.row: 0
157 Layout.column: (notifCount.visible ? 1 : 0) + (notifImage.visible ? 1 : 0)
158 Layout.columnSpan: notifCount.visible ? 1 : 2
159 }
160
161 Image {
162 id: notifImage
163
164 visible: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? false
165
166 onStatusChanged: {
167 if (notifImage.status == Image.Error)
168 notifImage.visible = false;
169 }
170
171 source: (notifWindow.modelData?.[notifWindow.activeIx]?.image || notifWindow.modelData?.[notifWindow.activeIx]?.appIcon) ?? ""
172 fillMode: Image.PreserveAspectFit
173 asynchronous: true
174 smooth: true
175 mipmap: true
176
177 Layout.maximumWidth: 50
178 Layout.column: 0
179 Layout.row: 0
180 Layout.fillHeight: true
181 Layout.rowSpan: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0)
182 }
183
184 Text {
185 id: notifBody
186
187 visible: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? false
188 text: notifWindow.modelData?.[notifWindow.activeIx]?.body ?? ""
189 textFormat: Text.RichText
190 wrapMode: Text.Wrap
191
192 font.pointSize: 10
193 font.family: "Fira Sans"
194 color: notifWindow.textColor
195
196 Layout.fillWidth: true
197 Layout.row: 1
198 Layout.column: notifImage.visible ? 1 : 0
199 Layout.columnSpan: notifCount.visible ? 2 : 1
200 }
201
202 Text {
203 id: notifTime
204
205 Connections {
206 target: NotificationManager.clock
207 function onDateChanged() {
208 notifTime.text = NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime);
209 }
210 }
211
212 visible: notifTime.text && notifTime.text !== "now"
213 text: NotificationManager.formatTime(notifWindow.modelData?.[notifWindow.activeIx]?.receivedTime)
214
215 font.pointSize: 8
216 font.family: "Fira Sans"
217 font.italic: true
218 color: "#555"
219 maximumLineCount: 1
220 horizontalAlignment: Text.AlignRight
221
222 Layout.fillWidth: true
223 Layout.row: notifBody.visible ? 2 : 1
224 Layout.column: notifImage.visible ? 1 : 0
225 Layout.columnSpan: 2
226 }
227
228 RowLayout {
229 id: notifActions
230
231 visible: notifWindow.modelData?.[notifWindow.activeIx]?.actions.length > 0 ?? false
232
233 spacing: 8
234 uniformCellSizes: true
235
236 width: 400 - 16
237 Layout.row: 1 + (notifBody.visible ? 1 : 0) + (notifTime.visible ? 1 : 0)
238 Layout.column: 0
239 Layout.columnSpan: 2 + (notifImage.visible ? 1 : 0)
240
241 Repeater {
242 model: ScriptModel {
243 values: notifWindow.modelData?.[notifWindow.activeIx]?.actions
244 }
245
246 delegate: WrapperMouseArea {
247 id: actionMouseArea
248
249 required property var modelData
250
251 height: actionLabelWrapper.implicitHeight
252 Layout.fillWidth: true
253 Layout.horizontalStretchFactor: 1
254
255 hoverEnabled: true
256 cursorShape: Qt.PointingHandCursor
257
258 onClicked: actionMouseArea.modelData?.invoke()
259
260 Rectangle {
261 anchors.fill: parent
262
263 color: actionMouseArea.containsMouse ? "#20ffffff" : "transparent"
264
265 border {
266 width: 2
267 color: "#20ffffff"
268 }
269
270 WrapperItem {
271 id: actionLabelWrapper
272
273 margin: 8
274 anchors.centerIn: parent
275
276 RowLayout {
277 id: actionLabelLayout
278
279 spacing: 8
280
281 IconImage {
282 id: actionIcon
283
284 visible: notifWindow.modelData?.[notifWindow.activeIx]?.hasActionIcons
285
286 onStatusChanged: {
287 if (actionIcon.status == Image.Error)
288 actionIcon.visible = false;
289 }
290
291 implicitSize: 16
292 source: {
293 if (!actionIcon.visible)
294 return "";
295
296 let icon = actionMouseArea.modelData?.identifier ?? ""
297 if (icon.includes("?path=")) {
298 const split = icon.split("?path=")
299 if (split.length !== 2)
300 return icon
301 const name = split[0]
302 const path = split[1]
303 const fileName = name.substring(
304 name.lastIndexOf("/") + 1)
305 return `file://${path}/${fileName}`
306 }
307 return icon
308 }
309 asynchronous: true
310 smooth: true
311 mipmap: true
312
313 Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
314 }
315
316 Text {
317 id: actionLabel
318
319 visible: actionMouseArea.modelData?.text ?? false
320
321 text: actionMouseArea.modelData?.text ?? ""
322
323 font.pointSize: 10
324 font.family: "Fira Sans"
325 color: notifWindow.textColor
326 maximumLineCount: 1
327 elide: Text.ElideRight
328 }
329 }
330 }
331 }
332 }
333 }
334 }
335 }
336 }
337 }
338 }
339 }
340}
diff --git a/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml
new file mode 100644
index 00000000..b58467b3
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/NotificationInhibitorWidget.qml
@@ -0,0 +1,266 @@
1import Quickshell
2import QtQuick
3import Quickshell.Widgets
4import qs.Services
5import QtQuick.Controls
6import QtQuick.Layouts
7import QtQuick.Shapes
8
9Item {
10 id: root
11
12 width: icon.width + 8
13 height: parent.height
14 anchors.verticalCenter: parent.verticalCenter
15
16 WrapperMouseArea {
17 id: widgetMouseArea
18
19 anchors.fill: parent
20
21 hoverEnabled: true
22 cursorShape: Qt.PointingHandCursor
23
24 onClicked: NotificationManager.displayInhibited = !NotificationManager.displayInhibited
25
26 Rectangle {
27 anchors.fill: parent
28 color: {
29 if (widgetMouseArea.containsMouse) {
30 return "#33808080";
31 }
32 return "transparent";
33 }
34
35 Item {
36 anchors.fill: parent
37
38 MaterialDesignIcon {
39 id: icon
40
41 implicitSize: 14
42 anchors.centerIn: parent
43
44 icon: NotificationManager.active ? "message" : "message-off"
45 color: {
46 if (!NotificationManager.active && !NotificationManager.displayInhibited)
47 return "#f28a21";
48 if (NotificationManager.displayInhibited)
49 return "white";
50 return "#555";
51 }
52 }
53 }
54 }
55 }
56
57 Loader {
58 id: tooltipLoader
59
60 active: false
61
62 Connections {
63 target: widgetMouseArea
64 function onContainsMouseChanged() {
65 if (widgetMouseArea.containsMouse)
66 tooltipLoader.active = true;
67 }
68 }
69
70 sourceComponent: PopupWindow {
71 id: tooltip
72
73 property bool nextVisible: NotificationManager.active && (widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse)
74
75 anchor {
76 item: widgetMouseArea
77 edges: Edges.Bottom | Edges.Left
78 }
79 visible: false
80
81 onNextVisibleChanged: hangTimer.restart()
82
83 Timer {
84 id: hangTimer
85 interval: tooltip.visible ? 100 : 500
86 onTriggered: {
87 tooltip.visible = tooltip.nextVisible;
88 if (!tooltip.visible)
89 tooltipLoader.active = false;
90 }
91 }
92
93 implicitWidth: 400
94 implicitHeight: Math.min(tooltip.screen.height * 0.66, Math.max(100, scroll.contentHeight + 16))
95 color: "black"
96
97 WrapperMouseArea {
98 id: tooltipMouseArea
99
100 hoverEnabled: true
101 enabled: true
102
103 anchors.fill: parent
104
105 WrapperItem {
106 margin: 8
107
108 ScrollView {
109 id: scroll
110
111 contentWidth: availableWidth
112 // ScrollBar.vertical.policy: ScrollBar.AlwaysOn
113
114 ColumnLayout {
115 id: historyLayout
116 anchors {
117 left: parent.left
118 right: parent.right
119 }
120
121 spacing: 8
122
123 Repeater {
124 model: ScriptModel {
125 values: [...NotificationManager.history].reverse().map(o => o.notification)
126 }
127
128 delegate: GridLayout {
129 id: notif
130
131 Layout.fillWidth: true
132 Layout.preferredHeight: notifSummary.contentHeight + (notifBody.visible ? notifBody.contentHeight + 8 : 0) + (notifSep.visible ? notifSep.height + 8 : 0) + notifTime.contentHeight + 8
133
134 required property var modelData
135 required property int index
136
137 columnSpacing: 8
138 rowSpacing: 8
139
140 columns: notifImage.visible ? 2 : 1
141 rows: {
142 var res = 2;
143 if (notifBody.visible)
144 res += 1;
145 if (notifSep.visible)
146 res += 1;
147 return res;
148 }
149
150 Shape {
151 id: notifSep
152
153 visible: notif.index != 0
154
155 height: 2
156 width: 400 - 32
157
158 ShapePath {
159 strokeWidth: 2
160 strokeColor: "#20ffffff"
161 startX: 0; startY: 0;
162 PathLine { x: 400 - 32; y: 0; }
163 }
164
165 Layout.row: 0
166 Layout.column: 0
167 Layout.columnSpan: notifImage.visible ? 2 : 1
168 Layout.alignment: Qt.AlignHCenter
169 }
170
171 Text {
172 id: notifSummary
173
174 text: notif.modelData?.summary ?? ""
175
176 font.pointSize: 10
177 font.family: "Fira Sans"
178 font.italic: true
179 color: "white"
180 maximumLineCount: 1
181 elide: Text.ElideRight
182
183 Layout.fillWidth: true
184 Layout.row: notifSep.visible ? 1 : 0
185 Layout.column: notifImage.visible ? 1 : 0
186 }
187
188 Image {
189 id: notifImage
190
191 visible: (notif.modelData?.image || notif.modelData?.appIcon) ?? false
192
193 onStatusChanged: {
194 if (notifImage.status == Image.Error)
195 notifImage.visible = false;
196 }
197
198 source: (notif.modelData?.image || notif.modelData?.appIcon) ?? ""
199 fillMode: Image.PreserveAspectFit
200 asynchronous: true
201 smooth: true
202 mipmap: true
203
204 Layout.maximumWidth: 50
205 Layout.column: 0
206 Layout.row: notifSep.visible ? 1 : 0
207 Layout.fillHeight: true
208 Layout.rowSpan: notifBody.visible ? 3 : 2
209 }
210
211 Text {
212 id: notifBody
213
214 visible: notif.modelData?.body ?? false
215 text: notif.modelData?.body ?? ""
216 textFormat: Text.RichText
217 wrapMode: Text.Wrap
218
219 font.pointSize: 10
220 font.family: "Fira Sans"
221 color: "white"
222
223 Layout.fillWidth: true
224 Layout.row: notifSep.visible ? 2 : 1
225 Layout.column: notifImage.visible ? 1 : 0
226 }
227
228 Text {
229 id: notifTime
230
231 Connections {
232 target: NotificationManager.clock
233 function onDateChanged() {
234 notifTime.text = NotificationManager.formatTime(notif.modelData?.receivedTime);
235 }
236 }
237
238 text: NotificationManager.formatTime(notif.modelData?.receivedTime)
239
240 font.pointSize: 8
241 font.family: "Fira Sans"
242 font.italic: true
243 color: "#555"
244 maximumLineCount: 1
245 horizontalAlignment: Text.AlignRight
246
247 Layout.fillWidth: true
248 Layout.row: {
249 var res = 1;
250 if (notifSep.visible)
251 res += 1;
252 if (notifBody.visible)
253 res += 1;
254 return res;
255 }
256 Layout.column: notifImage.visible ? 1 : 0
257 }
258 }
259 }
260 }
261 }
262 }
263 }
264 }
265 }
266}
diff --git a/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
new file mode 100644
index 00000000..9c6b65a4
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/PipewireWidget.qml
@@ -0,0 +1,483 @@
1import QtQuick
2import QtQuick.Layouts
3import QtQuick.Controls.Fusion
4import Quickshell
5import Quickshell.Services.Pipewire
6import Quickshell.Widgets
7
8Item {
9 height: parent.height
10 width: volumeIcon.width + 8
11 anchors.verticalCenter: parent.verticalCenter
12
13 PwObjectTracker {
14 objects: [Pipewire.defaultAudioSink]
15 }
16
17 WrapperMouseArea {
18 id: widgetMouseArea
19
20 anchors.fill: parent
21 hoverEnabled: true
22 cursorShape: Qt.PointingHandCursor
23
24 onClicked: {
25 if (!Pipewire.defaultAudioSink)
26 return;
27 Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted;
28 }
29
30 property real sensitivity: (1 / 40) / 120
31 onWheel: event => {
32 if (!Pipewire.defaultAudioSink)
33 return;
34 Pipewire.defaultAudioSink.audio.volume += event.angleDelta.y * sensitivity;
35 }
36
37 Rectangle {
38 id: volumeWidget
39
40 anchors.fill: parent
41 color: {
42 if (widgetMouseArea.containsMouse)
43 return "#33808080";
44 return "transparent";
45 }
46
47 Item {
48 anchors.fill: parent
49
50 MaterialDesignIcon {
51 id: volumeIcon
52
53 implicitSize: 14
54 anchors.centerIn: parent
55
56 icon: {
57 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
58 return "volume-off";
59 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
60 return "volume-low";
61 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
62 return "volume-medium";
63 return "volume-high";
64 }
65 color: "#555"
66 }
67 }
68 }
69 }
70
71 Loader {
72 id: tooltipLoader
73
74 active: false
75
76 Connections {
77 target: widgetMouseArea
78 function onContainsMouseChanged() {
79 if (widgetMouseArea.containsMouse)
80 tooltipLoader.active = true;
81 }
82 }
83
84 PwObjectTracker {
85 objects: Pipewire.devices
86 }
87 PwObjectTracker {
88 objects: Pipewire.nodes
89 }
90
91 sourceComponent: PopupWindow {
92 id: tooltip
93
94 property bool openPopup: false
95 property bool nextVisible: widgetMouseArea.containsMouse || tooltipMouseArea.containsMouse || openPopup
96
97 anchor {
98 item: widgetMouseArea
99 edges: Edges.Bottom | Edges.Left
100 }
101 visible: false
102
103 onNextVisibleChanged: hangTimer.restart()
104
105 Timer {
106 id: hangTimer
107 interval: 100
108 onTriggered: {
109 tooltip.visible = tooltip.nextVisible;
110 if (!tooltip.visible)
111 tooltipLoader.active = false;
112 }
113 }
114
115 implicitWidth: tooltipContent.width
116 implicitHeight: tooltipContent.height
117 color: "transparent"
118
119 Rectangle {
120 width: tooltip.width
121 height: tooltipLayout.childrenRect.height + 16
122 color: "black"
123 }
124
125 WrapperItem {
126 id: tooltipContent
127
128 bottomMargin: Math.max(0, 200 - tooltipLayout.implicitHeight)
129
130 WrapperMouseArea {
131 id: tooltipMouseArea
132
133 hoverEnabled: true
134 enabled: true
135
136 WrapperItem {
137 margin: 8
138 bottomMargin: 8
139
140 GridLayout {
141 id: tooltipLayout
142
143 columns: 4
144
145 Repeater {
146 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
147
148 Item {
149 id: descItem
150
151 required property var modelData
152 required property int index
153
154 Layout.column: 0
155 Layout.row: index
156
157 implicitWidth: descText.contentWidth
158 implicitHeight: descText.contentHeight
159
160 Text {
161 id: descText
162
163 color: "white"
164 font.pointSize: 10
165 font.family: "Fira Sans"
166
167 text: descItem.modelData.description
168 }
169 }
170 }
171
172 Repeater {
173 id: defaultSinkRepeater
174
175 model: {
176 Array.from(Pipewire.devices.values)
177 .filter(dev => dev.type == "Audio/Device")
178 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSink && node.device?.id == device.id ));
179 }
180
181 Item {
182 id: defaultSinkItem
183
184 required property var modelData
185 required property int index
186
187 visible: Boolean(modelData)
188
189 PwObjectTracker {
190 objects: [defaultSinkItem.modelData]
191 }
192
193 Layout.column: 1
194 Layout.row: index
195
196 Layout.fillHeight: true
197
198 implicitWidth: 16 + 8
199
200 WrapperMouseArea {
201 id: defaultSinkMouseArea
202
203 anchors.fill: parent
204 hoverEnabled: true
205 cursorShape: Qt.PointingHandCursor
206
207 onClicked: {
208 Pipewire.preferredDefaultAudioSink = defaultSinkItem.modelData
209 }
210
211 onWheel: event => scrollVolume(event);
212 property real sensitivity: (1 / 40) / 120
213 function scrollVolume(event) {
214 defaultSinkItem.modelData.audio.volume += event.angleDelta.y * sensitivity;
215 }
216
217 Rectangle {
218 id: defaultSinkWidget
219
220 anchors.fill: parent
221 color: {
222 if (defaultSinkMouseArea.containsMouse)
223 return "#33808080";
224 return "transparent";
225 }
226
227 MaterialDesignIcon {
228 width: 16
229 height: 16
230 anchors.centerIn: parent
231
232 icon: {
233 if (defaultSinkItem.modelData?.id == Pipewire.defaultAudioSink?.id)
234 return "speaker";
235 return "speaker-off";
236 }
237 color: icon == "speaker" ? "white" : "#555"
238 }
239 }
240 }
241
242 PopupWindow {
243 id: volumeTooltip
244
245 property bool nextVisible: defaultSinkMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse
246
247 anchor {
248 item: defaultSinkMouseArea
249 edges: Edges.Bottom | Edges.Left
250 }
251 visible: false
252
253 onNextVisibleChanged: volumeHangTimer.restart()
254
255 onVisibleChanged: tooltip.openPopup = volumeTooltip.visible
256
257 Timer {
258 id: volumeHangTimer
259 interval: 100
260 onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible
261 }
262
263 implicitWidth: volumeTooltipText.contentWidth + 16
264 implicitHeight: volumeTooltipText.contentHeight + 16
265 color: "black"
266
267 WrapperMouseArea {
268 id: volumeTooltipMouseArea
269
270 hoverEnabled: true
271 enabled: true
272
273 onWheel: event => defaultSinkMouseArea.scrollVolume(event);
274
275 anchors.fill: parent
276
277 Item {
278 anchors.fill: parent
279
280 Text {
281 id: volumeTooltipText
282
283 anchors.centerIn: parent
284
285 font.pointSize: 10
286 font.family: "Fira Sans"
287 color: "white"
288
289 text: `${Math.round(defaultSinkItem.modelData?.audio?.volume * 100)}%`
290 }
291 }
292 }
293 }
294 }
295 }
296
297 Repeater {
298 id: defaultSourceRepeater
299
300 model: {
301 Array.from(Pipewire.devices.values)
302 .filter(dev => dev.type == "Audio/Device")
303 .map(device => Array.from(Pipewire.nodes.values).find(node => node.type == PwNodeType.AudioSource && node.device?.id == device.id ));
304 }
305
306 Item {
307 id: defaultSourceItem
308
309 required property var modelData
310 required property int index
311
312 visible: Boolean(modelData)
313
314 PwObjectTracker {
315 objects: [defaultSourceItem.modelData]
316 }
317
318 Layout.column: 2
319 Layout.row: index
320
321 Layout.fillHeight: true
322
323 implicitWidth: 16 + 8
324
325 WrapperMouseArea {
326 id: defaultSourceMouseArea
327
328 anchors.fill: parent
329 hoverEnabled: true
330 cursorShape: Qt.PointingHandCursor
331
332 onClicked: {
333 Pipewire.preferredDefaultAudioSource = defaultSourceItem.modelData
334 }
335
336 onWheel: event => scrollVolume(event);
337 property real sensitivity: (1 / 40) / 120
338 function scrollVolume(event) {
339 defaultSourceItem.modelData.audio.volume += event.angleDelta.y * sensitivity;
340 }
341
342 Rectangle {
343 id: defaultSourceWidget
344
345 anchors.fill: parent
346 color: {
347 if (defaultSourceMouseArea.containsMouse)
348 return "#33808080";
349 return "transparent";
350 }
351
352 MaterialDesignIcon {
353 width: 16
354 height: 16
355 anchors.centerIn: parent
356
357 icon: {
358 if (defaultSourceItem.modelData?.id == Pipewire.defaultAudioSource?.id)
359 return "microphone";
360 return "microphone-off";
361 }
362 color: icon == "microphone" ? "white" : "#555"
363 }
364 }
365 }
366
367 PopupWindow {
368 id: volumeTooltip
369
370 property bool nextVisible: defaultSourceMouseArea.containsMouse || volumeTooltipMouseArea.containsMouse
371
372 anchor {
373 item: defaultSourceMouseArea
374 edges: Edges.Bottom | Edges.Left
375 }
376 visible: false
377
378 onNextVisibleChanged: volumeHangTimer.restart()
379
380 onVisibleChanged: tooltip.openPopup = volumeTooltip.visible
381
382 Timer {
383 id: volumeHangTimer
384 interval: 100
385 onTriggered: volumeTooltip.visible = volumeTooltip.nextVisible
386 }
387
388 implicitWidth: volumeTooltipText.contentWidth + 16
389 implicitHeight: volumeTooltipText.contentHeight + 16
390 color: "black"
391
392 WrapperMouseArea {
393 id: volumeTooltipMouseArea
394
395 hoverEnabled: true
396 enabled: true
397
398 onWheel: event => defaultSourceMouseArea.scrollVolume(event);
399
400 anchors.fill: parent
401
402 Item {
403 anchors.fill: parent
404
405 Text {
406 id: volumeTooltipText
407
408 anchors.centerIn: parent
409
410 font.pointSize: 10
411 font.family: "Fira Sans"
412 color: "white"
413
414 text: `${Math.round(defaultSourceItem.modelData?.audio?.volume * 100)}%`
415 }
416 }
417 }
418 }
419 }
420 }
421
422 Repeater {
423 id: profileRepeater
424
425 model: Array.from(Pipewire.devices.values).filter(dev => dev.type == "Audio/Device")
426
427 Item {
428 id: profileItem
429
430 required property var modelData
431 required property int index
432
433 PwObjectTracker {
434 objects: [profileItem.modelData]
435 }
436
437 Layout.column: 3
438 Layout.row: index
439
440 Layout.fillWidth: true
441
442 implicitWidth: Math.max(profileBox.implicitWidth, 300)
443 implicitHeight: profileBox.height
444
445 ComboBox {
446 id: profileBox
447
448 model: profileItem.modelData.profiles
449
450 textRole: "description"
451 valueRole: "index"
452 onActivated: profileItem.modelData.setProfile(currentValue)
453
454 anchors.fill: parent
455
456 implicitContentWidthPolicy: ComboBox.WidestText
457
458 Connections {
459 target: profileItem.modelData
460 function onCurrentProfileChanged() {
461 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
462 }
463 }
464 Component.onCompleted: {
465 profileBox.currentIndex = Array.from(profileItem.modelData.profiles).findIndex(profile => profile.index == profileItem.modelData.currentProfile);
466 }
467
468 Connections {
469 target: profileBox.popup
470 function onVisibleChanged() {
471 tooltip.openPopup = profileBox.popup.visible
472 }
473 }
474 }
475 }
476 }
477 }
478 }
479 }
480 }
481 }
482 }
483}
diff --git a/accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml b/accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml
new file mode 100644
index 00000000..d7ffadfe
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/PrivacyWidget.qml
@@ -0,0 +1,49 @@
1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Widgets
5import qs.Services
6
7Item {
8 height: parent.height
9 width: layout.childrenRect.width
10 anchors.verticalCenter: parent.verticalCenter
11
12 visible: Array.from(Privacy.activeItems).length > 0
13
14 RowLayout {
15 id: layout
16
17 anchors.fill: parent
18
19 spacing: 8
20
21 Repeater {
22 model: Privacy.activeItems
23
24 Item {
25 id: privacyItem
26
27 required property var modelData;
28
29 height: parent.height
30 width: icon.width
31
32 MaterialDesignIcon {
33 id: icon
34
35 implicitSize: 14
36 anchors.centerIn: parent
37
38 icon: {
39 if (privacyItem.modelData == Privacy.Item.Microphone)
40 return "microphone";
41 if (privacyItem.modelData == Privacy.Item.Screensharing)
42 return "monitor-share";
43 }
44 color: "#f2201f"
45 }
46 }
47 }
48 }
49}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml b/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml
new file mode 100644
index 00000000..8318df50
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/Brightness.qml
@@ -0,0 +1,75 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Io
6import Custom as Custom
7
8Singleton {
9 id: root
10
11 property string subsystem: "backlight"
12 property string device: "intel_backlight"
13
14 property real currBrightness
15 property real exponent: 4
16
17 function calcCurrBrightness() {
18 if (!currFile.loaded || !maxFile.loaded)
19 return undefined;
20 const curr = Number(currFile.text());
21 const max = Number(maxFile.text());
22 const val = Math.pow(curr / max, 1 / root.exponent);
23 return val;
24 }
25
26 Connections {
27 target: currFile
28 function onLoaded() {
29 const b = root.calcCurrBrightness();
30 if (typeof b !== 'undefined')
31 root.currBrightness = b;
32 }
33 }
34 Connections {
35 target: maxFile
36 function onLoaded() {
37 const b = root.calcCurrBrightness();
38 if (typeof b !== 'undefined')
39 root.currBrightness = b;
40 }
41 }
42
43 onCurrBrightnessChanged: {
44 root.currBrightness = Math.max(0, Math.min(1, root.currBrightness));
45
46 const prev = root.calcCurrBrightness();
47 if (typeof prev === 'undefined' || Math.abs(root.currBrightness - prev) < 0.01)
48 return;
49
50 const max = Number(maxFile.text());
51 const actual = Number(currFile.text());
52 let curr = Math.max(0, Math.min(max, Math.pow(root.currBrightness, root.exponent) * max));
53 if (Math.round(curr) == actual && curr < actual)
54 curr = Math.max(0, actual - 1);
55 else if (Math.round(curr) == actual && curr > actual)
56 curr = Math.min(max, actual + 1);
57 // root.currBrightness = Math.pow(curr / max, 1 / root.exponent);
58 Custom.Systemd.setBrightness(root.subsystem, root.device, Math.round(curr));
59 }
60
61 FileView {
62 id: currFile
63 path: `/sys/class/${root.subsystem}/${root.device}/brightness`
64 blockAllReads: true
65 watchChanges: true
66 onFileChanged: reload()
67 }
68 FileView {
69 id: maxFile
70 path: `/sys/class/${root.subsystem}/${root.device}/max_brightness`
71 blockAllReads: true
72 watchChanges: true
73 onFileChanged: reload()
74 }
75}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml b/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml
new file mode 100644
index 00000000..3de69535
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/GpgAgent.qml
@@ -0,0 +1,18 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Io
5
6Singleton {
7 id: root
8
9 Socket {
10 id: agentSocket
11 connected: true
12 path: `${Quickshell.env("XDG_RUNTIME_DIR")}/gnupg/S.gpg-agent`
13 }
14
15 function reloadAgent() {
16 agentSocket.write("RELOADAGENT\n")
17 }
18}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/InhibitorState.qml b/accounts/gkleen@sif/shell/quickshell/Services/InhibitorState.qml
new file mode 100644
index 00000000..fe48fd7f
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/InhibitorState.qml
@@ -0,0 +1,22 @@
1pragma Singleton
2
3import Quickshell
4import Custom as Custom
5
6Singleton {
7 id: inhibitorState
8
9 property bool waylandIdleInhibited: false
10 property alias lidSwitchInhibited: lidSwitchInhibitor.enabled
11
12 Custom.SystemdInhibitor {
13 id: lidSwitchInhibitor
14
15 enabled: false
16
17 what: Custom.SystemdInhibitorParams.HandleLidSwitch
18 who: "quickshell"
19 why: "User request"
20 mode: Custom.SystemdInhibitorParams.BlockWeak
21 }
22}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/MprisProxy.qml b/accounts/gkleen@sif/shell/quickshell/Services/MprisProxy.qml
new file mode 100644
index 00000000..e3ab9755
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/MprisProxy.qml
@@ -0,0 +1,8 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Services.Mpris
5
6Scope {
7 property list<var> players: Mpris.players.values
8}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
new file mode 100644
index 00000000..cce614eb
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/NiriService.qml
@@ -0,0 +1,194 @@
1pragma Singleton
2
3import Quickshell
4import Quickshell.Io
5import QtQuick
6
7Singleton {
8 id: root
9
10 property var workspaces: []
11 property var outputs: {}
12 property var keyboardLayouts: {}
13 property var windows: []
14 readonly property string socketPath: Quickshell.env("NIRI_SOCKET")
15
16 function refreshOutputs() {
17 commandSocket.sendCommand("Outputs", data => {
18 outputs = data.Ok.Outputs;
19 });
20 }
21
22 function sendCommand(command, callback) {
23 commandSocket.sendCommand(command, callback);
24 }
25
26 Socket {
27 id: eventStreamSocket
28 path: root.socketPath
29 connected: true
30
31 property bool acked: false
32
33 onConnectionStateChanged: {
34 if (connected) {
35 acked = false;
36 write('"EventStream"\n');
37 }
38 }
39
40 parser: SplitParser {
41 onRead: line => {
42 try {
43 const event = JSON.parse(line)
44
45 // console.log(JSON.stringify(event))
46
47 if (event.WorkspacesChanged) {
48 root.workspaces = event.WorkspacesChanged.workspaces
49 root.refreshOutputs();
50 } else if (event.WorkspaceActivated)
51 eventWorkspaceActivated(event.WorkspaceActivated);
52 else if (event.WorkspaceUrgencyChanged)
53 eventWorkspaceUrgencyChanged(event.WorkspaceUrgencyChanged);
54 else if (event.WorkspaceActiveWindowChanged)
55 eventWorkspaceActiveWindowChanged(event.WorkspaceActiveWindowChanged);
56 else if (event.KeyboardLayoutsChanged)
57 root.keyboardLayouts = event.KeyboardLayoutsChanged.keyboard_layouts;
58 else if (event.KeyboardLayoutSwitched)
59 root.keyboardLayouts = Object.assign({}, root.keyboardLayouts, {"current_idx": event.KeyboardLayoutSwitched.idx });
60 else if (event.WindowsChanged)
61 root.windows = event.WindowsChanged.windows
62 else if (event.WindowOpenedOrChanged)
63 eventWindowOpenedOrChanged(event.WindowOpenedOrChanged);
64 else if (event.WindowClosed)
65 eventWindowClosed(event.WindowClosed);
66 else if (event.WindowFocusChanged)
67 eventWindowFocusChanged(event.WindowFocusChanged);
68 else if (event.WindowUrgencyChanged)
69 eventWindowUrgencyChanged(event.WindowUrgencyChanged);
70 else if (event.WindowLayoutsChanged)
71 eventWindowLayoutsChanged(event.WindowLayoutsChanged);
72 else if (event.Ok && !eventStreamSocket.acked) { eventStreamSocket.acked = true; }
73 else if (event.OverviewOpenedOrClosed) {}
74 else if (event.ConfigLoaded) {}
75 else
76 console.log(JSON.stringify(event));
77 } catch (e) {
78 console.warn("NiriService: Failed to parse event:", line, e)
79 }
80 }
81 }
82 }
83
84 Socket {
85 id: commandSocket
86 path: root.socketPath
87 connected: true
88
89 property var awaitingAnswer: null
90 property var cmdQueue: []
91
92 parser: SplitParser {
93 onRead: line => {
94 if (commandSocket.awaitingAnswer === null)
95 return;
96
97 try {
98 const response = JSON.parse(line);
99 commandSocket.awaitingAnswer.callback(response);
100 commandSocket.awaitingAnswer = null;
101 } catch (e) {
102 console.warn("NiriService: Failed to parse response:", line, e)
103 }
104 commandSocket._handleQueue();
105 }
106 }
107
108 onCmdQueueChanged: {
109 _handleQueue();
110 }
111 onAwaitingAnswerChanged: {
112 _handleQueue();
113 }
114
115 function _handleQueue() {
116 if (cmdQueue.length <= 0 || awaitingAnswer !== null)
117 return;
118
119 let localQueue = Array.from(cmdQueue);
120 awaitingAnswer = localQueue.shift();
121 cmdQueue = localQueue;
122 write(JSON.stringify(awaitingAnswer.command) + '\n');
123 }
124
125 function sendCommand(command, callback) {
126 cmdQueue = Array.from(cmdQueue).concat([{ "command": command, "callback": callback }])
127 }
128 }
129
130 function eventWorkspaceActivated(data) {
131 let relevant_output = null;
132 Array.from(root.workspaces).forEach(ws => {
133 if (data.id === ws.id)
134 relevant_output = ws.output;
135 });
136 root.workspaces = Array.from(root.workspaces).map(ws => {
137 if (data.focused)
138 ws.is_focused = false;
139 if (ws.output === relevant_output)
140 ws.is_active = false;
141 if (data.id === ws.id) {
142 ws.is_active = true;
143 ws.is_focused = data.focused;
144 }
145 return ws;
146 });
147 }
148 function eventWorkspaceUrgencyChanged(data) {
149 root.workspaces = Array.from(root.workspaces).map(ws => {
150 if (data.id == ws.id)
151 ws.is_urgent = data.urgent;
152 return ws;
153 });
154 }
155 function eventWorkspaceActiveWindowChanged(data) {
156 root.workspaces = Array.from(root.workspaces).map(ws => {
157 if (data.workspace_id === ws.id)
158 ws.active_window_id = data.active_window_id;
159 return ws;
160 });
161 }
162 function eventWindowOpenedOrChanged(data) {
163 root.windows = Array.from(root.windows).map(win => {
164 if (data.window.is_focused)
165 win.is_focused = false;
166 return win;
167 }).filter(win => win.id !== data.window.id).concat([data.window]);
168 }
169 function eventWindowClosed(data) {
170 root.windows = Array.from(root.windows).filter(win => win.id !== data.id);
171 }
172 function eventWindowFocusChanged(data) {
173 root.windows = Array.from(root.windows).map(win => {
174 win.is_focused = win.id === data.id;
175 return win;
176 });
177 }
178 function eventWindowUrgencyChanged(data) {
179 root.windows = Array.from(root.windows).map(win => {
180 if (win.id === data.id)
181 win.is_urgent = data.urgent;
182 return win;
183 });
184 }
185 function eventWindowLayoutsChanged(data) {
186 root.windows = Array.from(root.windows).map(win => {
187 Array.from(data.changes).forEach(change => {
188 if (win.id === change[0])
189 win.layout = change[1];
190 });
191 return win;
192 });
193 }
194}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml
new file mode 100644
index 00000000..f02d1695
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/NotificationManager.qml
@@ -0,0 +1,162 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Services.Notifications
6
7Singleton {
8 id: root
9
10 readonly property bool active: !root.lockscreenActive && !root.displayInhibited
11 property bool lockscreenActive: false
12 property bool displayInhibited: false
13 property alias trackedNotifications: server.trackedNotifications
14 readonly property var groups: {
15 function matchesGroupKey(notif, groupKey) {
16 var matches = true;
17 for (const prop in groupKey.test) {
18 if (notif[prop] !== groupKey.test[prop]) {
19 matches = false;
20 break;
21 }
22 }
23 return matches;
24 }
25
26 var groups = new Map();
27 var notifs = new Array();
28 for (const [ix, notif] of server.trackedNotifications.values.entries()) {
29 var didGroup = false;
30 for (const groupKey of root.groupKeys) {
31 if (!matchesGroupKey(notif, groupKey))
32 continue;
33
34 const key = JSON.stringify({
35 "key": groupKey,
36 "values": Object.assign({}, ...(Array.from(groupKey["group-by"]).map(prop => {
37 var res = {};
38 res[prop] = notif[prop];
39 return res;
40 })))
41 });
42 if (!groups.has(key))
43 groups.set(key, new Array());
44 groups.get(key).push({ "ix": ix, "notif": notif });
45 didGroup = true;
46 break;
47 }
48
49 if (!didGroup)
50 notifs.push([{ "ix": ix, "notif": notif }]);
51 }
52 notifs.push(...groups.values());
53 notifs.sort((as, bs) => Math.min(...(as.map(o => o.ix))) - Math.min(...(bs.map(o => o.ix))));
54 return notifs.map(ns => ns.map(n => n.notif));
55 }
56
57 property var groupKeys: [
58 { "test": { "appName": "Element" }, "group-by": [ "summary" ] }
59 ];
60
61 property int historyLimit: 100
62 property var history: []
63
64 Component {
65 id: expirationTimer
66
67 QtObject {
68 id: timer
69
70 required property QtObject parent
71 required property int expirationTime
72
73 property list<QtObject> data: [
74 Timer {
75 running: root.active && !timer.expired
76 interval: timer.expirationTime
77 onTriggered: {
78 timer.parent.expirationTimer.destroy();
79 timer.parent.expirationTimer = null;
80 timer.parent.expire();
81 }
82 }
83 ]
84 }
85 }
86
87 Component {
88 id: notificationLock
89
90 RetainableLock {}
91 }
92
93 readonly property SystemClock clock: SystemClock {
94 precision: SystemClock.Minutes
95 }
96
97 function formatTime(time) {
98 const now = root.clock.date;
99 const diff = now - time;
100 const minutes = Math.ceil(diff / 60000);
101 const hours = Math.floor(minutes / 60);
102
103 if (hours < 1) {
104 if (minutes < 1)
105 return "now";
106 if (minutes == 1)
107 return "1 minute";
108 return `${minutes} minutes`;
109 }
110
111 const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate())
112 const timeDate = new Date(time.getFullYear(), time.getMonth(), time.getDate())
113 const days = Math.floor((nowDate - timeDate) / (1000 * 86400))
114
115 const timeStr = time.toLocaleTimeString(Qt.locale(), "HH:mm");
116
117 if (days === 0)
118 return timeStr;
119 if (days === 1)
120 return `yesterday ${timeStr}`;
121
122 const dateStr = time.toLocaleTimeString(Qt.locale(), "YYYY-MM-DD");
123 return `${dateStr} ${timeStr}`;
124 }
125
126 NotificationServer {
127 id: server
128
129 bodySupported: true
130 actionsSupported: true
131 actionIconsSupported: true
132 imageSupported: true
133 bodyMarkupSupported: true
134 bodyImagesSupported: true
135
136 onNotification: notification => {
137 var timeout = notification.expireTimeout * 1000;
138 if (notification.appName == "poweralertd")
139 timeout = 2000;
140 if (timeout > 0) {
141 Object.defineProperty(notification, "expirationTimer", { configurable: true, enumerable: true, writable: true });
142 notification.expirationTimer = expirationTimer.createObject(notification, { parent: notification, expirationTime: timeout });
143 }
144 Object.defineProperty(notification, "receivedTime", { configurable: true, enumerable: true, writable: true });
145 notification.receivedTime = root.clock.date;
146 notification.closed.connect((reason) => server.onNotificationClosed(notification, reason));
147 notification.tracked = true;
148 }
149
150 function onNotificationClosed(notification, reason) {
151 while (root.history.length >= root.historyLimit) {
152 root.history[0].lock.locked = false;
153 root.history.shift();
154 }
155
156 root.history.push({
157 lock: notificationLock.createObject(root, { locked: true, object: notification }),
158 notification: notification
159 });
160 }
161 }
162}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml b/accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml
new file mode 100644
index 00000000..9c813e49
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/Privacy.qml
@@ -0,0 +1,63 @@
1pragma Singleton
2
3import QtQml
4import Quickshell
5import Quickshell.Services.Pipewire
6
7Singleton {
8 id: root
9
10 PwObjectTracker {
11 objects: Pipewire.nodes.values
12 }
13
14 enum Item {
15 Microphone,
16 Screensharing
17 }
18
19 readonly property list<var> activeItems: {
20 var items = [];
21 if (microphoneActive)
22 items.push(Privacy.Item.Microphone);
23 if (screensharingActive)
24 items.push(Privacy.Item.Screensharing);
25 return items;
26 }
27
28 readonly property bool microphoneActive: {
29 if (!Pipewire.ready || !Pipewire.nodes?.values) {
30 return false
31 }
32
33 for (const node of Pipewire.nodes.values) {
34 if (!node || (node.type & PwNodeType.AudioInStream) != PwNodeType.AudioInStream)
35 continue;
36
37 if (node.properties?.["stream.monitor"] === "true")
38 continue;
39
40 if (node.audio?.muted)
41 continue;
42
43 return true;
44 }
45
46 return false;
47 }
48
49 readonly property bool screensharingActive: {
50 if (!Pipewire.ready || !Pipewire.nodes?.values) {
51 return false
52 }
53
54 for (const node of Pipewire.nodes.values) {
55 if (!node || (node.type & PwNodeType.VideoInStream) != PwNodeType.VideoInStream)
56 continue;
57
58 return true;
59 }
60
61 return false;
62 }
63}
diff --git a/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml b/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml
new file mode 100644
index 00000000..3c524955
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/Services/WallpaperSelector.qml
@@ -0,0 +1,8 @@
1import Custom as Custom
2
3Custom.FileSelector {
4 id: root
5
6 directory: @wallpapers@
7 epoch: 72000000
8}
diff --git a/accounts/gkleen@sif/shell/quickshell/SystemTray.qml b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
new file mode 100644
index 00000000..f7b4ed96
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/SystemTray.qml
@@ -0,0 +1,201 @@
1import QtQuick
2import QtQuick.Effects
3import Quickshell
4import Quickshell.Widgets
5import Quickshell.Services.SystemTray
6
7Item {
8 anchors.verticalCenter: parent.verticalCenter
9 width: systemTrayRow.childrenRect.width
10 height: parent.height
11 clip: true
12
13 Row {
14 id: systemTrayRow
15 anchors.centerIn: parent
16 width: childrenRect.width
17 height: parent.height
18 spacing: 0
19
20 Repeater {
21 model: ScriptModel {
22 values: {
23 var trayItems = Array.from(SystemTray.items.values).filter(item => item.status !== Status.Passive);
24 trayItems.sort((a, b) => a.category !== b.category ? b.category - a.category : a.id.localeCompare(b.id))
25 return trayItems;
26 }
27 }
28
29 delegate: Item {
30 id: trayItemWrapper
31
32 required property var modelData
33 required property int index
34
35 property var trayItem: modelData
36 property string iconSource: {
37 let icon = trayItem && trayItem.icon
38 if (typeof icon === 'string' || icon instanceof String) {
39 if (icon.includes("?path=")) {
40 const split = icon.split("?path=")
41 if (split.length !== 2)
42 return icon
43 const name = split[0]
44 const path = split[1]
45 const fileName = name.substring(
46 name.lastIndexOf("/") + 1)
47 return `file://${path}/${fileName}`
48 }
49 return icon
50 }
51 return ""
52 }
53
54 width: icon.width + 6
55 height: parent.height
56 anchors.verticalCenter: parent.verticalCenter
57
58 WrapperMouseArea {
59 id: trayItemArea
60
61 anchors.fill: parent
62 acceptedButtons: Qt.LeftButton | Qt.RightButton
63 hoverEnabled: true
64 cursorShape: trayItem.onlyMenu ? Qt.ArrowCursor : Qt.PointingHandCursor
65 onClicked: mouse => {
66 if (!trayItem)
67 return
68
69 if (mouse.button === Qt.LeftButton
70 && !trayItem.onlyMenu) {
71 trayItem.activate()
72 return
73 }
74
75 if (trayItem.hasMenu) {
76 var globalPos = mapToGlobal(0, 0)
77 var currentScreen = screen || Screen
78 var screenX = currentScreen.x || 0
79 var relativeX = globalPos.x - screenX
80 menuAnchor.menu = trayItem.menu
81 menuAnchor.anchor.window = bar
82 menuAnchor.anchor.rect = Qt.rect(
83 relativeX,
84 21,
85 parent.width, 1)
86 menuAnchor.open()
87 }
88 }
89
90 Rectangle {
91 anchors.fill: parent
92 color: {
93 if (trayItemArea.containsMouse)
94 return "#33808080";
95 return "transparent";
96 }
97
98 Item {
99 anchors.fill: parent
100
101 layer.enabled: true
102 layer.effect: MultiEffect {
103 colorization: 1
104 colorizationColor: "#555"
105 }
106
107 IconImage {
108 id: icon
109
110 anchors.centerIn: parent
111 implicitSize: 16
112 source: trayItemWrapper.iconSource
113 asynchronous: true
114 smooth: true
115 mipmap: true
116
117 layer.enabled: true
118 layer.effect: MultiEffect {
119 id: effect
120
121 brightness: 1
122 }
123 }
124 }
125 }
126 }
127
128 PopupWindow {
129 id: tooltip
130
131 property bool nextVisible: (trayItem.tooltipTitle || trayItem.tooltipDescription) && (trayItemArea.containsMouse || tooltipMouseArea.containsMouse) && !menuAnchor.visible
132
133 anchor {
134 item: trayItemArea
135 edges: Edges.Bottom
136 }
137
138 visible: false
139 onNextVisibleChanged: hangTimer.restart()
140
141 Timer {
142 id: hangTimer
143 interval: 100
144 onTriggered: tooltip.visible = tooltip.nextVisible
145 }
146
147 color: "black"
148
149 implicitWidth: Math.max(tooltipTitle.contentWidth, tooltipDescription.contentWidth) + 16
150 implicitHeight: (trayItem.tooltipTitle ? tooltipTitle.contentHeight : 0) + (trayItem.tooltipDescription ? tooltipDescription.contentHeight : 0) + 16
151
152 WrapperMouseArea {
153 id: tooltipMouseArea
154
155 hoverEnabled: true
156 enabled: true
157
158 margin: 4
159
160 anchors.fill: parent
161
162 Item {
163 anchors.fill: parent
164
165 Column {
166 anchors.centerIn: parent
167 Text {
168 id: tooltipTitle
169
170 enabled: trayItem.tooltipTitle
171
172 font.pointSize: 10
173 font.family: "Fira Sans"
174 font.bold: true
175 color: "white"
176
177 text: trayItem.tooltipTitle
178 }
179
180 Text {
181 id: tooltipDescription
182
183 enabled: trayItem.tooltipDescription
184
185 font.pointSize: 10
186 font.family: "Fira Sans"
187 color: "white"
188
189 text: trayItem.tooltipDescription
190 }
191 }
192 }
193 }
194 }
195 }
196 }
197 }
198 QsMenuAnchor {
199 id: menuAnchor
200 }
201}
diff --git a/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml
new file mode 100644
index 00000000..05a40dbc
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/UnixIPC.qml
@@ -0,0 +1,97 @@
1import Quickshell
2import Quickshell.Io
3import Quickshell.Services.Pipewire
4import Quickshell.Services.Mpris
5import qs.Services
6import Custom as Custom
7import QtQml
8
9Scope {
10 id: root
11
12 SocketServer {
13 active: true
14 path: `${Quickshell.env("XDG_RUNTIME_DIR")}/shell.sock`
15 handler: Socket {
16 parser: SplitParser {
17 onRead: line => {
18 const command = (() => {
19 try {
20 return JSON.parse(line);
21 } catch (e) {
22 console.warn("UnixIPC: Failed to parse command:", line, e);
23 }
24 })();
25 if (!command)
26 return;
27
28 if (command.Volume)
29 root.onCommandVolume(command.Volume);
30 else if (command.Brightness)
31 root.onCommandBrightness(command.Brightness);
32 else if (command.LockSession)
33 Custom.Systemd.lockSession();
34 else if (command.Suspend)
35 Custom.Systemd.suspend();
36 else if (command.Hibernate)
37 Custom.Systemd.hibernate();
38 else if (command.Mpris)
39 root.onCommandMpris(command.Mpris);
40 else if (command.Notifications)
41 root.onCommandNotifications(command.Notifications);
42 else
43 console.warn("UnixIPC: Command not handled:", JSON.stringify(command));
44 }
45 }
46
47 onError: e => {
48 if (e == 1)
49 return;
50 console.warn("QLocalSocket::LocalSocketError", e);
51 }
52 }
53 }
54
55 PwObjectTracker {
56 objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ]
57 }
58 function onCommandVolume(command) {
59 if (command.muted === "toggle")
60 Pipewire.defaultAudioSink.audio.muted = !Pipewire.defaultAudioSink.audio.muted;
61 if (command.volume === "up")
62 Pipewire.defaultAudioSink.audio.volume += 0.02;
63 if (command.volume === "down")
64 Pipewire.defaultAudioSink.audio.volume -= 0.02;
65
66 if (command["mic-muted"] === "toggle")
67 Pipewire.defaultAudioSource.audio.muted = !Pipewire.defaultAudioSource.audio.muted;
68 }
69
70 function onCommandBrightness(command) {
71 if (command === "up")
72 Brightness.currBrightness += 0.02
73 if (command === "down")
74 Brightness.currBrightness -= 0.02
75 }
76
77 function onCommandMpris(command) {
78 if (command.PauseAll)
79 Array.from(MprisProxy.players).forEach(player => {
80 if (player.canPause && player.isPlaying)
81 player.pause();
82 });
83 }
84 Component.onCompleted: { (_ => {})(MprisProxy.players); }
85
86 function onCommandNotifications(command) {
87 if (command.DismissGroup && NotificationManager.active) {
88 if (NotificationManager.groups.length > 0)
89 for (const notif of [...NotificationManager.groups[0]])
90 notif.dismiss();
91 }
92 if (command.DismissAll && NotificationManager.active) {
93 for (const notif of [...NotificationManager.trackedNotifications.values])
94 notif.dismiss();
95 }
96 }
97}
diff --git a/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
new file mode 100644
index 00000000..653f4763
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/VolumeOSD.qml
@@ -0,0 +1,163 @@
1import QtQuick
2import QtQuick.Layouts
3import Quickshell
4import Quickshell.Services.Pipewire
5import Quickshell.Widgets
6
7Scope {
8 id: root
9
10 property string show: ""
11 property bool inhibited: true
12
13 PwObjectTracker {
14 objects: [ Pipewire.defaultAudioSink, Pipewire.defaultAudioSource ]
15 }
16
17 Connections {
18 enabled: Pipewire.defaultAudioSink
19 target: Pipewire.defaultAudioSink?.audio
20
21 function onVolumeChanged() {
22 root.show = "sink";
23 hideTimer.restart();
24 }
25 function onMutedChanged() {
26 root.show = "sink";
27 hideTimer.restart();
28 }
29 }
30
31 Connections {
32 enabled: Pipewire.defaultAudioSource
33 target: Pipewire.defaultAudioSource?.audio
34
35 function onVolumeChanged() {
36 root.show = "source";
37 hideTimer.restart();
38 }
39 function onMutedChanged() {
40 root.show = "source";
41 hideTimer.restart();
42 }
43 }
44
45 onShowChanged: {
46 if (show)
47 hideTimer.restart();
48 }
49
50 Timer {
51 id: hideTimer
52 interval: 1000
53 onTriggered: root.show = ""
54 }
55
56 Timer {
57 id: startInhibit
58 interval: 100
59 running: true
60 onTriggered: {
61 root.show = "";
62 root.inhibited = false;
63 }
64 }
65
66 LazyLoader {
67 active: root.show && !root.inhibited
68
69 Variants {
70 model: Quickshell.screens
71
72 delegate: Scope {
73 id: screenScope
74
75 required property var modelData
76
77 PanelWindow {
78 id: window
79
80 screen: screenScope.modelData
81
82 anchors.top: true
83 margins.top: screen.height / 2 - 50 + 3.5
84 exclusiveZone: 0
85 exclusionMode: ExclusionMode.Ignore
86
87 implicitWidth: 400
88 implicitHeight: 50
89
90 mask: Region {}
91
92 color: "transparent"
93
94 Rectangle {
95 anchors.fill: parent
96 color: Qt.rgba(0, 0, 0, 0.75)
97 }
98
99 RowLayout {
100 id: layout
101
102 anchors.centerIn: parent
103
104 height: 50 - 8*2
105 width: 400 - 8*2
106
107 MaterialDesignIcon {
108 id: volumeIcon
109
110 implicitWidth: parent.height
111 implicitHeight: parent.height
112
113 icon: {
114 if (root.show == "sink") {
115 if (!Pipewire.defaultAudioSink || Pipewire.defaultAudioSink.audio.muted)
116 return "volume-off";
117 if (Pipewire.defaultAudioSink.audio.volume <= 0.33)
118 return "volume-low";
119 if (Pipewire.defaultAudioSink.audio.volume <= 0.67)
120 return "volume-medium";
121 return "volume-high";
122 } else if (root.show == "source") {
123 if (!Pipewire.defaultAudioSource || Pipewire.defaultAudioSource.audio.muted)
124 return "microphone-off";
125 if (Pipewire.defaultAudioSource.audio.volume > 1)
126 return "microphone-plus";
127 return "microphone";
128 }
129 return "volume-high";
130 }
131 }
132
133 Rectangle {
134 Layout.fillWidth: true
135
136 implicitHeight: 10
137
138 color: "#50ffffff"
139
140 Rectangle {
141 anchors {
142 left: parent.left
143 top: parent.top
144 bottom: parent.bottom
145 }
146
147 color: Pipewire.defaultAudioSink?.audio.muted ? "#70ffffff" : "white"
148
149 implicitWidth: {
150 if (root.show == "sink")
151 return parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0);
152 else if (root.show == "source")
153 return parent.width * Math.min(1, (Pipewire.defaultAudioSource?.audio.volume ?? 0));
154 return 0;
155 }
156 }
157 }
158 }
159 }
160 }
161 }
162 }
163}
diff --git a/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml b/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml
new file mode 100644
index 00000000..4f85a900
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/WallpaperBackground.qml
@@ -0,0 +1,89 @@
1import QtQuick
2import Quickshell
3import qs.Services
4
5Item {
6 id: root
7
8 anchors.fill: parent
9
10 required property string screen
11
12 property Img current: one
13 property string source: selector.selected
14
15 WallpaperSelector {
16 id: selector
17 seed: screen
18 }
19
20 onSourceChanged: {
21 if (!source)
22 current = null;
23 else if (current === one)
24 two.update()
25 else
26 one.update()
27 }
28
29 Img { id: one }
30 Img { id: two }
31
32 component Img: Image {
33 id: img
34
35 function update() {
36 source = root.source || ""
37 }
38
39 anchors.fill: parent
40 fillMode: Image.PreserveAspectCrop
41 smooth: true
42 asynchronous: true
43 cache: false
44
45 opacity: 0
46
47 onStatusChanged: {
48 if (status === Image.Ready) {
49 root.current = this
50 }
51 }
52
53 states: State {
54 name: "visible"
55 when: root.current === img
56
57 PropertyChanges {
58 img.opacity: 1
59 }
60 StateChangeScript {
61 name: "unloadOther"
62 script: {
63 if (img === one)
64 two.source = ""
65 if (img === two)
66 one.source = ""
67 }
68 }
69 }
70
71 transitions: Transition {
72 SequentialAnimation {
73 NumberAnimation {
74 target: img
75 properties: "opacity"
76 duration: {
77 if (img === one && two.source == "" || img === two && one.source == "")
78 return 0;
79 return 5000;
80 }
81 easing.type: Easing.OutCubic
82 }
83 ScriptAction {
84 scriptName: "unloadOther"
85 }
86 }
87 }
88 }
89}
diff --git a/accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml b/accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml
new file mode 100644
index 00000000..0512ff51
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/WaylandInhibitorWidget.qml
@@ -0,0 +1,56 @@
1import Quickshell
2import QtQuick
3import Quickshell.Widgets
4import Quickshell.Wayland
5import qs.Services
6
7Item {
8 id: root
9
10 required property var window
11
12 width: icon.width + 8
13 height: parent.height
14 anchors.verticalCenter: parent.verticalCenter
15
16 IdleInhibitor {
17 id: inhibitor
18 enabled: InhibitorState.waylandIdleInhibited
19 window: root.window
20 }
21
22 WrapperMouseArea {
23 id: widgetMouseArea
24
25 anchors.fill: parent
26
27 hoverEnabled: true
28 cursorShape: Qt.PointingHandCursor
29
30 onClicked: InhibitorState.waylandIdleInhibited = !InhibitorState.waylandIdleInhibited
31
32 Rectangle {
33 anchors.fill: parent
34 color: {
35 if (widgetMouseArea.containsMouse) {
36 return "#33808080";
37 }
38 return "transparent";
39 }
40
41 Item {
42 anchors.fill: parent
43
44 MaterialDesignIcon {
45 id: icon
46
47 implicitSize: 14
48 anchors.centerIn: parent
49
50 icon: inhibitor.enabled ? "eye" : "eye-off"
51 color: inhibitor.enabled ? "white" : "#555"
52 }
53 }
54 }
55 }
56}
diff --git a/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
new file mode 100644
index 00000000..3ae94346
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/WorkspaceSwitcher.qml
@@ -0,0 +1,204 @@
1import Quickshell
2import QtQuick
3import qs.Services
4import Quickshell.Widgets
5import QtQuick.Layouts
6
7Row {
8 id: workspaces
9
10 required property var screen
11
12 property var ignoreWorkspaces: @ignore_workspaces@
13
14 height: parent.height
15 anchors.verticalCenter: parent.verticalCenter
16 spacing: 0
17
18 Repeater {
19 model: ScriptModel {
20 values: {
21 let currWorkspaces = NiriService.workspaces;
22 const ignoreWorkspaces = Array.from(workspaces.ignoreWorkspaces);
23 currWorkspaces = currWorkspaces.filter(ws => ws.output == workspaces.screen.name).filter(ws => ws.is_active || ignoreWorkspaces.every(iws => iws !== ws.name));
24 currWorkspaces.sort((a, b) => {
25 if (NiriService.outputs?.[a.output]?.logical?.x !== NiriService.outputs?.[b.output]?.logical?.x)
26 return NiriService.outputs?.[a.output]?.logical?.x - NiriService.outputs?.[b.output]?.logical?.x
27 if (NiriService.outputs?.[a.output]?.logical?.y !== NiriService.outputs?.[b.output]?.logical?.y)
28 return NiriService.outputs?.[a.output]?.logical?.y - NiriService.outputs?.[b.output]?.logical?.y
29 return a.idx - b.idx;
30 });
31 return currWorkspaces;
32 }
33 }
34
35 Item {
36 id: wsItem
37
38 property var workspaceData: modelData
39
40 width: wsLabel.contentWidth + 8
41 height: parent.height
42 anchors.verticalCenter: parent.verticalCenter
43
44 WrapperMouseArea {
45 id: mouseArea
46
47 anchors.fill: parent
48
49 hoverEnabled: true
50 cursorShape: Qt.PointingHandCursor
51 enabled: true
52 onClicked: {
53 NiriService.sendCommand({ "Action": { "FocusWorkspace": { "reference": { "Id": workspaceData.id } } } }, _ => {});
54 }
55
56 Rectangle {
57 anchors.fill: parent
58
59 color: {
60 if (mouseArea.containsMouse) {
61 return "#33808080";
62 }
63 return "transparent";
64 }
65
66 Text {
67 id: wsLabel
68
69 anchors.centerIn: parent
70
71 font.pointSize: 10
72 font.family: "Fira Sans"
73 color: {
74 if (workspaceData.is_active)
75 return "#23fd00";
76 if (workspaceData.active_window_id === null)
77 return "#555";
78 return "white";
79 }
80
81 text: workspaceData.name ? workspaceData.name : workspaceData.idx
82 }
83 }
84 }
85
86 PopupWindow {
87 id: tooltip
88
89 property bool nextVisible: (mouseArea.containsMouse || tooltipMouseArea.containsMouse) && [...windowsModel.values].length > 0
90
91 anchor {
92 item: mouseArea
93 edges: Edges.Bottom | Edges.Left
94 }
95 visible: false
96
97 onNextVisibleChanged: hangTimer.restart()
98
99 Timer {
100 id: hangTimer
101 interval: 100
102 onTriggered: tooltip.visible = tooltip.nextVisible
103 }
104
105 implicitWidth: tooltipContent.implicitWidth
106 implicitHeight: tooltipContent.implicitHeight
107 color: "black"
108
109 WrapperMouseArea {
110 id: tooltipMouseArea
111
112 hoverEnabled: true
113 enabled: true
114
115 anchors.fill: parent
116
117 WrapperItem {
118 id: tooltipContent
119
120 margin: 0
121
122 ColumnLayout {
123 spacing: 0
124
125 Repeater {
126 model: ScriptModel {
127 id: windowsModel
128
129 values: {
130 let currWindows = Array.from(NiriService.windows).filter(win => win.workspace_id == wsItem.workspaceData.id);
131 currWindows.sort((a, b) => {
132 if (a.is_floating !== b.is_floating)
133 return b.is_floating - a.is_floating;
134 if (a.layout.tile_pos_in_workspace_view?.[0] !== b.layout.tile_pos_in_workspace_view?.[0])
135 return a.layout.tile_pos_in_workspace_view?.[0] - b.layout.tile_pos_in_workspace_view?.[0]
136 if (a.layout.tile_pos_in_workspace_view?.[1] !== b.layout.tile_pos_in_workspace_view?.[1])
137 return a.layout.tile_pos_in_workspace_view?.[1] - b.layout.tile_pos_in_workspace_view?.[1]
138 if (a.layout.pos_in_scrolling_layout?.[0] !== b.layout.pos_in_scrolling_layout?.[0])
139 return a.layout.pos_in_scrolling_layout?.[0] - b.layout.pos_in_scrolling_layout?.[0]
140 if (a.layout.pos_in_scrolling_layout?.[1] !== b.layout.pos_in_scrolling_layout?.[1])
141 return a.layout.pos_in_scrolling_layout?.[1] - b.layout.pos_in_scrolling_layout?.[1]
142 if (a.app_id !== b.app_id)
143 return a.app_id.localeCompare(b.app_id);
144
145 return a.title.localeCompare(b.title);
146 });
147 return currWindows;
148 }
149 }
150
151 WrapperMouseArea {
152 id: windowMouseArea
153
154 required property int index
155 required property var modelData
156 property var windowData: modelData
157
158 hoverEnabled: true
159 cursorShape: Qt.PointingHandCursor
160 enabled: true
161
162 Layout.fillWidth: true
163
164 onClicked: {
165 NiriService.sendCommand({ "Action": { "FocusWindow": { "id": windowData.id } } }, _ => {})
166 }
167
168 WrapperRectangle {
169 color: windowMouseArea.containsMouse ? "#33808080" : "transparent";
170
171 WrapperItem {
172 rightMargin: 8
173 leftMargin: 8
174 topMargin: windowMouseArea.index == 0 ? 8 : 4
175 bottomMargin: windowMouseArea.index == windowsModel.values.length - 1 ? 8 : 4
176
177 Text {
178 id: windowLabel
179
180 font.pointSize: 10
181 font.family: "Fira Sans"
182 color: {
183 if (windowData.is_focused)
184 return "#23fd00";
185 if (NiriService.workspaces.find(ws => ws.id == windowData.workspace_id)?.active_window_id == windowData.id)
186 return "white";
187 return "#555";
188 }
189
190 text: windowData.title
191
192 horizontalAlignment: Text.AlignLeft
193 }
194 }
195 }
196 }
197 }
198 }
199 }
200 }
201 }
202 }
203 }
204}
diff --git a/accounts/gkleen@sif/shell/quickshell/WorktimeWidget.qml b/accounts/gkleen@sif/shell/quickshell/WorktimeWidget.qml
new file mode 100644
index 00000000..04bcc581
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/WorktimeWidget.qml
@@ -0,0 +1,120 @@
1import QtQml
2import Quickshell
3import Quickshell.Io
4import QtQuick
5import Quickshell.Widgets
6
7Item {
8 id: root
9
10 required property string command
11 property var state: null
12
13 height: parent.height
14 width: label.contentWidth + 8
15 anchors.verticalCenter: parent.verticalCenter
16
17 Process {
18 id: process
19 running: true
20 command: [ @worktime@, root.command, "--waybar" ]
21 stdout: StdioCollector {
22 id: processCollector
23 onStreamFinished: {
24 try {
25 root.state = JSON.parse(processCollector.text);
26 } catch (e) {
27 console.warn("Worktime: Failed to parse output:", processCollector.text, e);
28 }
29 }
30 }
31 }
32
33 Timer {
34 running: true
35 interval: 60
36 repeat: true
37 onTriggered: process.running = true
38 }
39
40 WrapperMouseArea {
41 id: mouseArea
42
43 anchors.fill: parent
44
45 enabled: true
46 hoverEnabled: true
47
48 Item {
49 anchors.fill: parent
50
51 Text {
52 id: label
53
54 anchors.centerIn: parent
55
56 visible: root.state?.text ?? false
57 text: root.state?.text ?? ""
58
59 font.pointSize: 10
60 font.family: "Fira Sans"
61 color: {
62 if (root.state?.class == "running")
63 return "white";
64 if (root.state?.class == "over")
65 return "#f28a21";
66 return "#555";
67 }
68 }
69 }
70 }
71
72 PopupWindow {
73 id: tooltip
74
75 property bool nextVisible: Boolean(root.state?.tooltip ?? false) && (mouseArea.containsMouse || tooltipMouseArea.containsMouse)
76
77 anchor {
78 item: mouseArea
79 edges: Edges.Bottom | Edges.Left
80 }
81 visible: false
82
83 onNextVisibleChanged: hangTimer.restart()
84
85 Timer {
86 id: hangTimer
87 interval: 100
88 onTriggered: tooltip.visible = tooltip.nextVisible
89 }
90
91 implicitWidth: tooltipText.contentWidth + 16
92 implicitHeight: tooltipText.contentHeight + 16
93 color: "black"
94
95 WrapperMouseArea {
96 id: tooltipMouseArea
97
98 enabled: true
99 hoverEnabled: true
100
101 anchors.fill: parent
102
103 Item {
104 anchors.fill: parent
105
106 Text {
107 id: tooltipText
108
109 anchors.centerIn: parent
110
111 font.pointSize: 10
112 font.family: "Fira Sans"
113 color: "white"
114
115 text: root.state?.tooltip ?? ""
116 }
117 }
118 }
119 }
120}
diff --git a/accounts/gkleen@sif/shell/quickshell/displaymanager.qml b/accounts/gkleen@sif/shell/quickshell/displaymanager.qml
new file mode 100644
index 00000000..b452c03d
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/displaymanager.qml
@@ -0,0 +1,115 @@
1//@ pragma UseQApplication
2
3import Quickshell
4import Quickshell.Wayland
5import Quickshell.Io
6import Quickshell.Services.Greetd
7import QtQml
8
9
10ShellRoot {
11 id: displaymanager
12
13 settings.watchFiles: false
14
15 property string currentText: ""
16 property string username: @username@
17 property list<string> command: @niri_session@
18 property list<var> messages: []
19 property bool responseRequired: false
20 property bool responseVisible: false
21
22 signal startAuth()
23
24 onStartAuth: {
25 if (Greetd.state !== GreetdState.Inactive)
26 Greetd.cancelSession();
27 displaymanager.messages = [];
28 Greetd.createSession(displaymanager.username);
29 }
30
31 Connections {
32 target: Greetd
33 function onStateChanged() {
34 console.log("greetd state: ", GreetdState.toString(Greetd.state));
35 if (Greetd.state === GreetdState.ReadyToLaunch)
36 Greetd.launch(displaymanager.command);
37 }
38 function onAuthMessage(message: string, error: bool, responseRequired: bool, echoResponse: bool) {
39 displaymanager.responseVisible = echoResponse;
40 displaymanager.responseRequired = responseRequired;
41 displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": error }]);
42 }
43 function onAuthFailure(message: string) {
44 displaymanager.responseRequired = false;
45 displaymanager.messages = Array.from(displaymanager.messages).concat([{ "text": message, "error": true }]);
46 }
47 }
48
49 Component.onCompleted: {
50 if (Greetd.state !== GreetdState.Inactive)
51 Greetd.cancelSession();
52 }
53
54 Variants {
55 model: Quickshell.screens
56
57 delegate: Scope {
58 id: screenScope
59
60 required property var modelData
61
62 PanelWindow {
63 color: "black"
64
65 screen: screenScope.modelData
66
67 WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
68
69 anchors.top: true
70 anchors.bottom: true
71 anchors.left: true
72 anchors.right: true
73
74 LockSurface {
75 id: surfaceContent
76
77 screen: screenScope.modelData
78
79 onCurrentTextChanged: displaymanager.currentText = currentText
80 Connections {
81 target: displaymanager
82 function onCurrentTextChanged() { surfaceContent.currentText = displaymanager.currentText; }
83 function onMessagesChanged() { surfaceContent.messages = Array.from(displaymanager.messages); }
84 function onResponseRequiredChanged() { surfaceContent.responseRequired = displaymanager.responseRequired; }
85 function onResponseVisibleChanged() { surfaceContent.responseVisible = displaymanager.responseVisible; }
86 }
87
88 onResponse: responseText => Greetd.respond(responseText);
89 Connections {
90 target: Greetd
91 function onStateChanged() {
92 if (Greetd.state === GreetdState.Authenticating) {
93 surfaceContent.authRunning = true;
94 } else {
95 surfaceContent.authRunning = false;
96 }
97 }
98 }
99
100 onAuthRunningChanged: {
101 if (surfaceContent.authRunning && Greetd.state !== GreetdState.Authenticating)
102 displaymanager.startAuth();
103 }
104 Component.onCompleted: {
105 surfaceContent.authRunning = Greetd.state === GreetdState.Authenticating
106 surfaceContent.messages = Array.from(displaymanager.messages);
107 surfaceContent.responseVisible = displaymanager.responseVisible;
108 surfaceContent.responseRequired = displaymanager.responseRequired;
109 surfaceContent.currentText = displaymanager.currentText;
110 }
111 }
112 }
113 }
114 }
115}
diff --git a/accounts/gkleen@sif/shell/quickshell/shell.qml b/accounts/gkleen@sif/shell/quickshell/shell.qml
new file mode 100644
index 00000000..fb8b16dc
--- /dev/null
+++ b/accounts/gkleen@sif/shell/quickshell/shell.qml
@@ -0,0 +1,53 @@
1//@ pragma UseQApplication
2
3import Quickshell
4import Quickshell.Wayland
5
6ShellRoot {
7 settings.watchFiles: false
8
9 Variants {
10 model: Quickshell.screens
11
12 delegate: Scope {
13 id: screenScope
14
15 required property var modelData
16
17 PanelWindow {
18 id: bgWindow
19
20 screen: screenScope.modelData
21
22 WlrLayershell.layer: WlrLayer.Background
23 WlrLayershell.namespace: "background"
24 exclusionMode: ExclusionMode.Ignore
25
26 anchors.top: true
27 anchors.bottom: true
28 anchors.left: true
29 anchors.right: true
30
31 color: "black"
32
33 WallpaperBackground {
34 screen: bgWindow.screen.name
35 }
36 }
37
38 Bar {
39 modelData: screenScope.modelData
40 }
41 }
42 }
43
44 Lockscreen {}
45 NiriIdle {}
46
47 VolumeOSD {}
48 BrightnessOSD {}
49
50 NotificationDisplay {}
51
52 UnixIPC {}
53}
diff --git a/accounts/gkleen@sif/ssh-hosts.nix b/accounts/gkleen@sif/ssh-hosts.nix
index 107f1e76..a250509b 100644
--- a/accounts/gkleen@sif/ssh-hosts.nix
+++ b/accounts/gkleen@sif/ssh-hosts.nix
@@ -1,5 +1,12 @@
1{ pkgs, ... }: 1{ lib, pkgs, ... }:
2{ 2let
3 autosshProxyPorts = {
4 "ssh.math.lmu.de" = 8118;
5 "mathw0h" = 8122;
6 "mathw0e" = 8124;
7 };
8 autosshProxy = host: "${lib.getExe pkgs.socat} - SOCKS4A:127.0.0.1:%h:%p,socksport=${toString autosshProxyPorts.${host}}";
9in {
3 "git.ymir" = 10 "git.ymir" =
4 { hostname = "ymir.yggdrasil.li"; 11 { hostname = "ymir.yggdrasil.li";
5 user = "gitolite"; 12 user = "gitolite";
@@ -290,15 +297,15 @@
290 }; 297 };
291 "mathw0d" = 298 "mathw0d" =
292 { hostname = "mathw0d.mathinst.loc"; 299 { hostname = "mathw0d.mathinst.loc";
293 proxyJump = "mathw0h"; 300 proxyCommand = autosshProxy "mathw0h";
294 }; 301 };
295 "mathw0e" = 302 "mathw0e" =
296 { hostname = "mathw0e.mathinst.loc"; 303 { hostname = "mathw0e.mathinst.loc";
297 proxyJump = "mathw0h"; 304 proxyCommand = autosshProxy "mathw0h";
298 }; 305 };
299 "mathw0f" = 306 "mathw0f" =
300 { hostname = "mathw0f.mathinst.loc"; 307 { hostname = "mathw0f.mathinst.loc";
301 proxyJump = "mathw0h"; 308 proxyCommand = autosshProxy "mathw0h";
302 }; 309 };
303 "mathw0g" = 310 "mathw0g" =
304 { hostname = "mathw0g.mathinst.loc"; 311 { hostname = "mathw0g.mathinst.loc";
@@ -306,8 +313,8 @@
306 "mathw0h" = 313 "mathw0h" =
307 { hostname = "mathw0h.mathinst.loc"; 314 { hostname = "mathw0h.mathinst.loc";
308 }; 315 };
309 "proxy.mathw0g" = 316 "proxy.ssh.math.lmu.de" =
310 { hostname = "mathw0g.mathinst.loc"; 317 { hostname = "ssh.math.lmu.de";
311 extraOptions = { 318 extraOptions = {
312 ControlPath = "none"; 319 ControlPath = "none";
313 ExitOnForwardFailure = "yes"; 320 ExitOnForwardFailure = "yes";
@@ -317,7 +324,17 @@
317 }; 324 };
318 "proxy.mathw0h" = 325 "proxy.mathw0h" =
319 { hostname = "mathw0h.mathinst.loc"; 326 { hostname = "mathw0h.mathinst.loc";
320 proxyJump = "proxy.mathw0g"; 327 proxyCommand = autosshProxy "ssh.math.lmu.de";
328 extraOptions = {
329 ControlPath = "none";
330 ExitOnForwardFailure = "yes";
331 ServerAliveCountMax = "15";
332 ServerAliveInterval = "2";
333 };
334 };
335 "proxy.mathw0e" =
336 { hostname = "mathw0e.mathinst.loc";
337 proxyCommand = autosshProxy "mathw0h";
321 extraOptions = { 338 extraOptions = {
322 ControlPath = "none"; 339 ControlPath = "none";
323 ExitOnForwardFailure = "yes"; 340 ExitOnForwardFailure = "yes";
@@ -327,7 +344,7 @@
327 }; 344 };
328 "vrt-kvm06" = 345 "vrt-kvm06" =
329 { hostname = "vrt-kvm06"; 346 { hostname = "vrt-kvm06";
330 proxyJump = "mathw0e"; 347 proxyCommand = autosshProxy "mathw0e";
331 user = "root"; 348 user = "root";
332 extraOptions = { 349 extraOptions = {
333 PasswordAuthentication = "yes"; 350 PasswordAuthentication = "yes";
@@ -336,7 +353,7 @@
336 }; 353 };
337 "vrt-kvm05" = 354 "vrt-kvm05" =
338 { hostname = "vrt-kvm05"; 355 { hostname = "vrt-kvm05";
339 proxyJump = "mathw0e"; 356 proxyCommand = autosshProxy "mathw0e";
340 user = "root"; 357 user = "root";
341 extraOptions = { 358 extraOptions = {
342 PasswordAuthentication = "yes"; 359 PasswordAuthentication = "yes";
@@ -345,7 +362,7 @@
345 }; 362 };
346 "vrt-kvm04" = 363 "vrt-kvm04" =
347 { hostname = "vrt-kvm04"; 364 { hostname = "vrt-kvm04";
348 proxyJump = "mathw0e"; 365 proxyCommand = autosshProxy "mathw0e";
349 user = "root"; 366 user = "root";
350 extraOptions = { 367 extraOptions = {
351 PasswordAuthentication = "yes"; 368 PasswordAuthentication = "yes";
@@ -354,7 +371,7 @@
354 }; 371 };
355 "vrt-kvm02" = 372 "vrt-kvm02" =
356 { hostname = "vrt-kvm02"; 373 { hostname = "vrt-kvm02";
357 proxyJump = "mathw0e"; 374 proxyCommand = autosshProxy "mathw0e";
358 user = "root"; 375 user = "root";
359 extraOptions = { 376 extraOptions = {
360 PasswordAuthentication = "yes"; 377 PasswordAuthentication = "yes";
@@ -363,7 +380,7 @@
363 }; 380 };
364 "vrt-kvm03" = 381 "vrt-kvm03" =
365 { hostname = "vrt-kvm03"; 382 { hostname = "vrt-kvm03";
366 proxyJump = "mathw0e"; 383 proxyCommand = autosshProxy "mathw0e";
367 user = "root"; 384 user = "root";
368 extraOptions = { 385 extraOptions = {
369 PasswordAuthentication = "yes"; 386 PasswordAuthentication = "yes";
@@ -372,7 +389,7 @@
372 }; 389 };
373 "vrt-kvm01" = 390 "vrt-kvm01" =
374 { hostname = "vrt-kvm01"; 391 { hostname = "vrt-kvm01";
375 proxyJump = "mathw0e"; 392 proxyCommand = autosshProxy "mathw0e";
376 user = "root"; 393 user = "root";
377 extraOptions = { 394 extraOptions = {
378 PasswordAuthentication = "yes"; 395 PasswordAuthentication = "yes";
@@ -381,39 +398,44 @@
381 }; 398 };
382 "tts-www01" = 399 "tts-www01" =
383 { hostname = "tts-www01.mathinst.loc"; 400 { hostname = "tts-www01.mathinst.loc";
384 proxyJump = "mathw0h"; 401 proxyCommand = autosshProxy "mathw0h";
385 user = "root"; 402 user = "root";
386 }; 403 };
387 "vpn-wg01" = 404 "vpn-wg01" =
388 { hostname = "vpn-wg01.mathinst.loc"; 405 { hostname = "vpn-wg01.mathinst.loc";
389 proxyJump = "mathw0h"; 406 proxyCommand = autosshProxy "mathw0h";
390 user = "root"; 407 user = "root";
391 }; 408 };
392 "repo-apt01" = 409 "repo-apt01" =
393 { hostname = "repo-apt01.mathinst.loc"; 410 { hostname = "repo-apt01.mathinst.loc";
394 proxyJump = "mathw0h"; 411 proxyCommand = autosshProxy "mathw0h";
395 user = "root"; 412 user = "root";
396 }; 413 };
397 "ldap-lmumr01" = 414 "ldap-lmumr01" =
398 { hostname = "ldap-lmumr01.mathinst.loc"; 415 { hostname = "ldap-lmumr01.mathinst.loc";
399 proxyJump = "mathw0h"; 416 proxyCommand = autosshProxy "mathw0h";
400 user = "root"; 417 user = "root";
401 }; 418 };
402 "mail-mi01" = 419 "mail-mi01" =
403 { hostname = "mail-mi01.mathinst.loc"; 420 { hostname = "mail-mi01.mathinst.loc";
404 proxyJump = "mathw0h"; 421 proxyCommand = autosshProxy "mathw0h";
405 }; 422 };
406 "mail-www02" = 423 "mail-www02" =
407 { hostname = "mail-www02.mathinst.loc"; 424 { hostname = "mail-www02.mathinst.loc";
408 proxyJump = "mathw0h"; 425 proxyCommand = autosshProxy "mathw0h";
409 }; 426 };
410 "dpl-fai01" = 427 "dpl-fai01" =
411 { hostname = "dpl-fai01.mathinst.loc"; 428 { hostname = "dpl-fai01.mathinst.loc";
412 user = "root"; 429 user = "root";
413 }; 430 };
431 "dpl-fai02" =
432 { hostname = "dpl-fai02.mathinst.loc";
433 user = "root";
434 proxyJump = "mgmt01";
435 };
414 "math05" = 436 "math05" =
415 { hostname = "math05.mathinst.loc"; 437 { hostname = "math05.mathinst.loc";
416 proxyJump = "mathw0h"; 438 proxyCommand = autosshProxy "mathw0h";
417 extraOptions.KexAlgorithms = "+diffie-hellman-group1-sha1"; 439 extraOptions.KexAlgorithms = "+diffie-hellman-group1-sha1";
418 }; 440 };
419 "switch01" = 441 "switch01" =
@@ -439,20 +461,20 @@
439 }; 461 };
440 "www-mi01" = 462 "www-mi01" =
441 { hostname = "www-mi01.mathinst.loc"; 463 { hostname = "www-mi01.mathinst.loc";
442 proxyJump = "mathw0h"; 464 proxyCommand = autosshProxy "mathw0h";
443 }; 465 };
444 "cip04" = 466 "cip04" =
445 { hostname = "cip04.cipmath.loc"; 467 { hostname = "cip04.cipmath.loc";
446 proxyJump = "mathw0h"; 468 proxyCommand = autosshProxy "mathw0h";
447 }; 469 };
448 "mgmt-cls01" = 470 "mgmt-cls01" =
449 { user = "root"; 471 { user = "root";
450 hostname = "mgmt-cls01.cipmath.loc"; 472 hostname = "mgmt-cls01.cipmath.loc";
451 proxyJump = "ssh.math.lmu.de"; 473 proxyCommand = autosshProxy "ssh.math.lmu.de";
452 }; 474 };
453 "mgmt01" = 475 "mgmt01" =
454 { hostname = "mgmt01.mathinst.loc"; 476 { hostname = "mgmt01.mathinst.loc";
455 proxyJump = "mathw0h"; 477 proxyCommand = autosshProxy "mathw0h";
456 user = "root"; 478 user = "root";
457 }; 479 };
458 "ssh-lb01" = 480 "ssh-lb01" =
@@ -471,17 +493,17 @@
471 "rdlx02" = { hostname = "rdlx02.mathinst.loc"; proxyJump = "mgmt01"; }; 493 "rdlx02" = { hostname = "rdlx02.mathinst.loc"; proxyJump = "mgmt01"; };
472 "math0d" = 494 "math0d" =
473 { hostname = "math0d.mathinst.loc"; 495 { hostname = "math0d.mathinst.loc";
474 proxyJump = "mathw0h"; 496 proxyCommand = autosshProxy "mathw0h";
475 }; 497 };
476 "dhcp01" = 498 "dhcp01" =
477 { hostname = "dhcp01.mathinst.loc"; 499 { hostname = "dhcp01.mathinst.loc";
478 user = "root"; 500 user = "root";
479 proxyJump = "mathw0h"; 501 proxyCommand = autosshProxy "mathw0h";
480 }; 502 };
481 "dhcp02" = 503 "dhcp02" =
482 { hostname = "dhcp02.mathinst.loc"; 504 { hostname = "dhcp02.mathinst.loc";
483 user = "root"; 505 user = "root";
484 proxyJump = "mathw0h"; 506 proxyCommand = autosshProxy "mathw0h";
485 }; 507 };
486 "cc-gpu-l01" = 508 "cc-gpu-l01" =
487 { hostname = "cc-gpu-l01.mathinst.loc"; 509 { hostname = "cc-gpu-l01.mathinst.loc";
@@ -546,7 +568,7 @@
546 user = "root"; 568 user = "root";
547 }; 569 };
548 "nas*" = 570 "nas*" =
549 { proxyJump = "mathw0e"; 571 { proxyCommand = autosshProxy "mathw0e";
550 user = "admin"; 572 user = "admin";
551 extraOptions = { 573 extraOptions = {
552 PasswordAuthentication = "yes"; 574 PasswordAuthentication = "yes";
@@ -554,9 +576,4 @@
554 HostKeyAlgorithms = "+ecdsa-sha2-nistp256"; 576 HostKeyAlgorithms = "+ecdsa-sha2-nistp256";
555 }; 577 };
556 }; 578 };
557 "game01" =
558 { hostname = "game01.yggdrasil.li";
559 user = "factorio";
560 identityFile = "~/.ssh/gkleen@sif.midgard.yggdrasil";
561 };
562} 579}
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 33bf7ef2..e601b49c 100644
--- a/accounts/gkleen@sif/systemd.nix
+++ b/accounts/gkleen@sif/systemd.nix
@@ -6,7 +6,7 @@ let
6 cfg = config.home-manager.users.${userName}; 6 cfg = config.home-manager.users.${userName};
7 7
8 autossh-socks-script = pkgs.writeScript "autossh" '' 8 autossh-socks-script = pkgs.writeScript "autossh" ''
9 #!${pkgs.zsh}/bin/zsh -xe 9 #!${lib.getExe pkgs.zsh} -xe
10 10
11 host="''${1%:*}" 11 host="''${1%:*}"
12 port="''${1#*:}" 12 port="''${1#*:}"
@@ -15,31 +15,29 @@ let
15 cmd=() 15 cmd=()
16 16
17 if [[ -n "''${SSHPASS_SECRET}" ]]; then 17 if [[ -n "''${SSHPASS_SECRET}" ]]; then
18 cmd+=(${pkgs.sshpassSecret}/bin/sshpass-secret) 18 cmd+=(${lib.getExe' pkgs.sshpassSecret "sshpass-secret"})
19 cmd+=("''${(@s/:/)SSHPASS_SECRET}") 19 cmd+=("''${(@s/:/)SSHPASS_SECRET}")
20 cmd+=(--) 20 cmd+=(--)
21 fi 21 fi
22 22
23 cmd+=(${pkgs.openssh}/bin/ssh -vN -D localhost:''${port} "''${host}") 23 cmd+=(${lib.getExe' pkgs.openssh "ssh"} -vN -D 127.0.0.1:''${port} "''${host}")
24 24
25 ( exec -a "''${cmd[1]}" -- ''${cmd} ) & 25 ( exec -a "''${cmd[1]}" -- ''${cmd} ) &
26 pid=$! 26 pid=$!
27 27
28 newpid="" 28 newpid=""
29 i=200 29 i=200
30 while ! newpid=$(${pkgs.lsof}/bin/lsof -Pi @localhost:"''${port}" -sTCP:LISTEN -t); do 30 while ! newpid=$(${lib.getExe pkgs.lsof} -Pi @localhost:"''${port}" -sTCP:LISTEN -t); do
31 if ! kill -0 "''${pid}"; then 31 if ! kill -0 "''${pid}"; then
32 wait "''${pid}" 32 wait "''${pid}"
33 exit $? 33 exit $?
34 fi 34 fi
35 [[ "''${i}" -gt 0 ]] || exit 1 35 [[ "''${i}" -gt 0 ]] || exit 1
36 i=$((''${i} - 1)) 36 i=$((''${i} - 1))
37 ${pkgs.coreutils}/bin/sleep 0.1 37 ${lib.getExe' pkgs.coreutils "sleep"} 0.1
38 done 38 done
39 39
40 ${config.systemd.package}/bin/systemd-notify --ready 40 ${lib.getExe' config.systemd.package "systemd-notify"} --pid=''${newpid} --ready
41
42 wait "''${pid}" "''${newpid}"
43 ''; 41 '';
44in { 42in {
45 tmpfiles.rules = [ 43 tmpfiles.rules = [
@@ -48,11 +46,11 @@ in {
48 ]; 46 ];
49 47
50 services = { 48 services = {
51 sync-keepass = { 49 "sync-keepass@" = {
52 Service = { 50 Service = {
53 Type = "oneshot"; 51 Type = "oneshot";
54 WorkingDirectory = "~"; 52 WorkingDirectory = "~";
55 ExecStart = toString (pkgs.writers.writePython3 "sync-keepass" { 53 ExecStart = "${pkgs.writers.writePython3 "sync-keepass" {
56 libraries = with pkgs.python3Packages; [ python-dateutil ]; 54 libraries = with pkgs.python3Packages; [ python-dateutil ];
57 } '' 55 } ''
58 import json 56 import json
@@ -61,13 +59,13 @@ in {
61 from datetime import datetime 59 from datetime import datetime
62 from dateutil.tz import tzlocal 60 from dateutil.tz import tzlocal
63 from dateutil.parser import isoparse 61 from dateutil.parser import isoparse
64 from sys import stderr 62 from sys import stderr, argv
65 63
66 64
67 remote_fs = 'surtr' 65 remote_fs = 'surtr' if argv[1] == 'store.kdbx' else 'mathcloud'
68 remote_file = 'store.kdbx' 66 remote_file = argv[1]
69 target_file = expanduser('~/store.kdbx') 67 target_file = expanduser(f'~/{argv[1]}')
70 meta_file = expanduser('~/.store.kdbx.json') 68 meta_file = expanduser(f'~/.{argv[1]}.json')
71 69
72 upload_time = None 70 upload_time = None
73 our_last_upload_time = None 71 our_last_upload_time = None
@@ -117,22 +115,14 @@ in {
117 do_upload() 115 do_upload()
118 elif upload_time is not None and (mod_time is None or upload_time > mod_time) and (our_last_upload_time is None or upload_time > our_last_upload_time): # noqa: E501 116 elif upload_time is not None and (mod_time is None or upload_time > mod_time) and (our_last_upload_time is None or upload_time > our_last_upload_time): # noqa: E501
119 do_download() 117 do_download()
120 ''); 118 ''} \"%I\"";
121 Environment = [ "RCLONE_PASSWORD_COMMAND=\"${pkgs.coreutils}/bin/cat ${config.sops.secrets.gkleen-rclone.path}\"" "PATH=${pkgs.rclone}/bin" ]; 119 Environment = [ "RCLONE_PASSWORD_COMMAND=\"${pkgs.coreutils}/bin/cat ${config.sops.secrets.gkleen-rclone.path}\"" "PATH=${pkgs.rclone}/bin" ];
122 }; 120 };
123 }; 121 };
124 emacs = { 122 emacs = {
125 Unit = { 123 Unit = {
126 After = ["graphical-session-pre.target"]; 124 After = [ "graphical-session.target" ];
127 }; 125 BindsTo = [ "graphical-session.target" ];
128 };
129 dunst = {
130 Service = {
131 ExecStart = lib.mkForce "${cfg.services.dunst.package}/bin/dunst";
132 Restart = "always";
133 };
134 Install = {
135 WantedBy = ["graphical-session.target"];
136 }; 126 };
137 }; 127 };
138 keepassxc = { 128 keepassxc = {
@@ -144,8 +134,8 @@ in {
144 Environment = [ "QT_QPA_PLATFORM=wayland" ]; 134 Environment = [ "QT_QPA_PLATFORM=wayland" ];
145 }; 135 };
146 Unit = { 136 Unit = {
147 Requires = ["graphical-session-pre.target"]; 137 After = [ "graphical-session.target" ];
148 After = ["graphical-session-pre.target"]; 138 BindsTo = [ "graphical-session.target" ];
149 }; 139 };
150 }; 140 };
151 mpris-proxy = { 141 mpris-proxy = {
@@ -154,7 +144,7 @@ in {
154 Service.ExecStart = "${pkgs.bluez}/bin/mpris-proxy"; 144 Service.ExecStart = "${pkgs.bluez}/bin/mpris-proxy";
155 Install.WantedBy = [ "default.target" ]; 145 Install.WantedBy = [ "default.target" ];
156 }; 146 };
157 "autossh-socks@proxy.mathw0h:8119" = { 147 "autossh-socks@proxy.ssh.math.lmu.de:8119" = {
158 Service = { 148 Service = {
159 Type = "notify"; 149 Type = "notify";
160 NotifyAccess = "all"; 150 NotifyAccess = "all";
@@ -162,7 +152,7 @@ in {
162 Restart = "always"; 152 Restart = "always";
163 RestartSec = "23s"; 153 RestartSec = "23s";
164 ExecStart = "${autossh-socks-script} \"%I\""; 154 ExecStart = "${autossh-socks-script} \"%I\"";
165 Environment = [ "SSHPASS_SECRET=gkleen@mathw0g.math.lmu.de" ]; 155 Environment = [ "SSHPASS_SECRET=gkleen@ssh.math.lmu.de" ];
166 }; 156 };
167 Unit = { 157 Unit = {
168 StopWhenUnneeded = true; 158 StopWhenUnneeded = true;
@@ -183,44 +173,58 @@ in {
183 StopWhenUnneeded = true; 173 StopWhenUnneeded = true;
184 }; 174 };
185 }; 175 };
186 swayidle = { 176 "autossh-socks@proxy.mathw0h:8123" = {
187 Service = { 177 Service = {
188 RuntimeDirectory = "swayidle"; 178 Type = "notify";
189 }; 179 NotifyAccess = "all";
190 }; 180 WorkingDirectory = "~";
191 psi-notify = { 181 Restart = "always";
192 Install = { 182 RestartSec = "23s";
193 WantedBy = ["graphical-session.target"]; 183 ExecStart = "${autossh-socks-script} \"%I\"";
184 Environment = [ "SSHPASS_SECRET=gkleen@mathw0h.mathinst.loc" ];
194 }; 185 };
195 Unit = { 186 Unit = {
196 Requires = ["graphical-session-pre.target"]; 187 StopWhenUnneeded = true;
197 After = ["graphical-session-pre.target"]; 188 StartLimitInterval = "180s";
189 StartLimitBurst = 7;
198 }; 190 };
191 };
192 "autossh-socks@proxy.mathw0e:8125" = {
199 Service = { 193 Service = {
200 ExecStart = lib.getExe pkgs.psi-notify;
201 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
202 Type = "notify"; 194 Type = "notify";
195 NotifyAccess = "all";
196 WorkingDirectory = "~";
203 Restart = "always"; 197 Restart = "always";
204 WatchdogSec = "2s"; 198 RestartSec = "23s";
199 ExecStart = "${autossh-socks-script} \"%I\"";
200 Environment = [ "SSHPASS_SECRET=gkleen@mathw0e.mathinst.loc" ];
201 };
202 Unit = {
203 StopWhenUnneeded = true;
204 StartLimitInterval = "180s";
205 StartLimitBurst = 7;
205 }; 206 };
206 }; 207 };
207 polkit-gnome-authentication-agent-1 = { 208 psi-notify = {
208 Install = { 209 Install = {
209 WantedBy = ["graphical-session.target"]; 210 WantedBy = ["graphical-session.target"];
210 }; 211 };
211 Unit = { 212 Unit = {
212 PartOf = ["graphical-session.target"]; 213 After = [ "graphical-session.target" ];
213 Requires = ["graphical-session-pre.target"]; 214 PartOf = [ "graphical-session.target" ];
214 After = ["graphical-session-pre.target"];
215 }; 215 };
216 Service = { 216 Service = {
217 ExecStart = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1"; 217 ExecStart = lib.getExe pkgs.psi-notify;
218 Restart = "on-failure"; 218 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
219 Type = "notify";
220 Restart = "always";
221 WatchdogSec = "2s";
219 }; 222 };
220 }; 223 };
221 gtklock = { 224 gtklock = {
222 Unit = { 225 Unit = {
223 Requisite = ["graphical-session.target"]; 226 Requisite = ["graphical-session.target"];
227 After = [ "graphical-session.target" ];
224 PartOf = ["graphical-session.target"]; 228 PartOf = ["graphical-session.target"];
225 }; 229 };
226 Service = { 230 Service = {
@@ -228,53 +232,55 @@ in {
228 RuntimeDirectory = "gtklock"; 232 RuntimeDirectory = "gtklock";
229 CacheDirectory = "gtklock"; 233 CacheDirectory = "gtklock";
230 ExecStartPre = [ 234 ExecStartPre = [
231 "${pkgs.libsForQt5.qt5.qttools.bin}/bin/qdbus org.keepassxc.KeePassXC.MainWindow /keepassxc org.keepassxc.KeePassXC.MainWindow.lockAllDatabases" 235 "-${lib.getExe' pkgs.libsForQt5.qt5.qttools.bin "qdbus"} org.keepassxc.KeePassXC.MainWindow /keepassxc org.keepassxc.KeePassXC.MainWindow.lockAllDatabases"
232 "${config.systemd.package}/bin/systemctl --user stop gpg-agent.service" 236 "-${lib.getExe' config.systemd.package "systemctl"} --user stop gpg-agent.service"
233 (pkgs.writeShellScript "generate-css" '' 237 "-${lib.getExe pkgs.playerctl} -a pause"
234 set -x 238 "-${lib.getExe (pkgs.writeShellApplication {
235 export PATH="${lib.makeBinPath [cfg.programs.wpaperd.package pkgs.jq pkgs.coreutils pkgs.imagemagick pkgs.findutils]}:$PATH" 239 name = "generate-css";
240 runtimeInputs = with pkgs; [cfg.services.wpaperd.package jq coreutils imagemagick findutils];
241 text = ''
242 declare -A monitors
243 monitors=()
244 while IFS= read -r entry; do
245 path=$(jq -r ".path" <<<"$entry")
246 [[ -z "$path" || ! -f "$path" ]] && continue
247 blurred_path="$CACHE_DIRECTORY"/"$(b2sum -l 128 <<<"$path" | cut -d' ' -f1)"."''${path##*.}"
248 monitor=$(jq -r ".display" <<<"$entry")
249 if [[ ! -f "$blurred_path" ]]; then
250 mkdir -p "$(dirname "$blurred_path")"
251 magick "$path" -filter Gaussian -resize 6.25% -define filter:sigma=2.5 -resize 1600% "$blurred_path" &
252 fi
253 monitors+=([$monitor]="$blurred_path")
254 done < <(wpaperctl all-wallpapers -j | jq -c ".[]")
255 # wait
236 256
237 declare -A monitors 257 cp --no-preserve=mode ${pkgs.writeText "gtklock.css" ''
238 monitors=() 258 #window-box {
239 while IFS= read -r entry; do 259 padding: 64px;
240 path=$(jq -r ".path" <<<"$entry") 260 /* border: 1px solid black; */
241 [[ -z "$path" || ! -f "$path" ]] && continue 261 border-radius: 4px;
242 blurred_path="$CACHE_DIRECTORY"/"$(b2sum -l 128 <<<"$path" | cut -d' ' -f1)"."''${path##*.}" 262 box-shadow: rgba(0, 0, 0, 0.8) 0px 4px 12px;
243 monitor=$(jq -r ".display" <<<"$entry") 263 /* background-color: white; */
244 if [[ ! -f "$blurred_path" ]]; then 264 background-color: rgba(0, 0, 0, 0.5);
245 mkdir -p "$(dirname "$blurred_path")" 265 }
246 magick "$path" -filter Gaussian -resize 6.25% -define filter:sigma=2.5 -resize 1600% "$blurred_path" & 266 ''} "$RUNTIME_DIRECTORY"/style.css
247 fi 267 for monitor in "''${!monitors[@]}"; do
248 monitors+=([$monitor]="$blurred_path") 268 cat >>"$RUNTIME_DIRECTORY"/style.css <<EOF
249 done < <(wpaperctl all-wallpapers -j | jq -c ".[]") 269 window#''${monitor} {
250 wait 270 background-image: url("''${monitors[$monitor]}");
251 271 background-repeat: no-repeat;
252 cp --no-preserve=mode ${pkgs.writeText "gtklock.css" '' 272 background-size: 100% 100%;
253 #window-box { 273 background-origin: content-box;
254 padding: 64px;
255 /* border: 1px solid black; */
256 border-radius: 4px;
257 box-shadow: rgba(0, 0, 0, 0.8) 0px 4px 12px;
258 /* background-color: white; */
259 background-color: rgba(0, 0, 0, 0.5);
260 } 274 }
261 ''} "$RUNTIME_DIRECTORY"/style.css 275 EOF
262 for monitor in "''${!monitors[@]}"; do 276 done
263 cat >>"$RUNTIME_DIRECTORY"/style.css <<EOF 277 '';
264 window#''${monitor} { 278 })}"
265 background-image: url("''${monitors[$monitor]}");
266 background-repeat: no-repeat;
267 background-size: 100% 100%;
268 background-origin: content-box;
269 }
270 EOF
271 done
272 '')
273 ]; 279 ];
274 NotifyAccess = "all"; 280 NotifyAccess = "all";
275 ExecStart = ''${lib.getExe pkgs.gtklock} -s "''${RUNTIME_DIRECTORY}/style.css" -L ${pkgs.writeShellScript "after-lock" '' 281 ExecStart = ''${lib.getExe pkgs.gtklock} -s "''${RUNTIME_DIRECTORY}/style.css" -L ${pkgs.writeShellScript "after-lock" ''
276 ${cfg.wayland.windowManager.hyprland.package}/bin/hyprctl dispatch dpms off 282 ${lib.getExe cfg.programs.niri.package} msg action power-off-monitors
277 ${config.systemd.package}/bin/systemd-notify --ready 283 ${lib.getExe' config.systemd.package "systemd-notify"} --ready
278 ''}''; 284 ''}'';
279 }; 285 };
280 }; 286 };
@@ -322,15 +328,60 @@ in {
322 ExecStopPost = "${pkgs.coreutils}/bin/rm -rfv \"$CACHE_DIRECTORY\""; 328 ExecStopPost = "${pkgs.coreutils}/bin/rm -rfv \"$CACHE_DIRECTORY\"";
323 }; 329 };
324 }; 330 };
331 # wpaperd = {
332 # Install = {
333 # WantedBy = ["graphical-session.target"];
334 # };
335 # Unit = {
336 # After = [ "graphical-session.target" ];
337 # PartOf = [ "graphical-session.target" ];
338 # };
339 # Service = {
340 # ExecStart = lib.getExe cfg.services.wpaperd.package;
341 # Type = "simple";
342 # Restart = "always";
343 # RestartSec = "2s";
344 # };
345 # };
346 xembed-sni-proxy = {
347 Unit = {
348 PartOf = lib.mkForce ["tray.target"];
349 };
350 };
351 poweralertd = {
352 Unit = {
353 After = ["graphical-session.target"];
354 };
355 };
356 network-manager-applet = {
357 Unit = {
358 PartOf = lib.mkForce ["tray.target"];
359 };
360 };
361 udiskie = {
362 Unit = {
363 PartOf = lib.mkForce ["tray.target"];
364 };
365 };
366 blueman-applet = {
367 Unit = {
368 PartOf = lib.mkForce ["tray.target"];
369 };
370 Install = {
371 WantedBy = lib.mkForce ["tray.target"];
372 };
373 };
325 } // listToAttrs (map ({host, port}: nameValuePair "proxy-to-autossh-socks@${toString port}" { 374 } // listToAttrs (map ({host, port}: nameValuePair "proxy-to-autossh-socks@${toString port}" {
326 Unit = { 375 Unit = {
327 Requires = ["autossh-socks@${host}:${toString (port + 1)}.service" "proxy-to-autossh-socks@${toString port}.socket"]; 376 BindsTo = ["autossh-socks@${host}:${toString (port + 1)}.service" "proxy-to-autossh-socks@${toString port}.socket"];
328 After = ["autossh-socks@${host}:${toString (port + 1)}.service" "proxy-to-autossh-socks@${toString port}.socket"]; 377 After = ["autossh-socks@${host}:${toString (port + 1)}.service" "proxy-to-autossh-socks@${toString port}.socket"];
329 }; 378 };
330 Service = { 379 Service = {
331 ExecStart = "${config.systemd.package}/lib/systemd/systemd-socket-proxyd --exit-idle-time=10s localhost:${toString (port + 1)}"; 380 ExecStart = "${config.systemd.package}/lib/systemd/systemd-socket-proxyd --exit-idle-time=60s 127.0.0.1:${toString (port + 1)}";
381 Restart = "always";
382 RestartSec = "23s";
332 }; 383 };
333 }) [{ host = "proxy.mathw0h"; port = 8118; } { host = "proxy.vidhar"; port = 8120; }]); 384 }) [{ host = "proxy.ssh.math.lmu.de"; port = 8118; } { host = "proxy.vidhar"; port = 8120; } { host = "proxy.mathw0h"; port = 8122; } { host = "proxy.mathw0e"; port = 8124; }]);
334 sockets = listToAttrs (map (port: nameValuePair "proxy-to-autossh-socks@${toString port}" { 385 sockets = listToAttrs (map (port: nameValuePair "proxy-to-autossh-socks@${toString port}" {
335 Socket = { 386 Socket = {
336 ListenStream = "%I"; 387 ListenStream = "%I";
@@ -338,7 +389,7 @@ in {
338 Install = { 389 Install = {
339 WantedBy = ["default.target"]; 390 WantedBy = ["default.target"];
340 }; 391 };
341 }) [8118 8120]) // { 392 }) [8118 8120 8122 8124]) // {
342 "yt-dlp" = { 393 "yt-dlp" = {
343 Socket = { 394 Socket = {
344 SocketMode = "0600"; 395 SocketMode = "0600";
@@ -352,7 +403,7 @@ in {
352 }; 403 };
353 }; 404 };
354 timers = { 405 timers = {
355 sync-keepass = { 406 "sync-keepass@store.kdbx" = {
356 Timer = { 407 Timer = {
357 OnActiveSec = "1m"; 408 OnActiveSec = "1m";
358 OnUnitActiveSec = "1m"; 409 OnUnitActiveSec = "1m";
@@ -362,6 +413,16 @@ in {
362 WantedBy = ["default.target"]; 413 WantedBy = ["default.target"];
363 }; 414 };
364 }; 415 };
416 "sync-keepass@rz.kdbx" = {
417 Timer = {
418 OnActiveSec = "1d";
419 OnUnitActiveSec = "1d";
420 };
421
422 Install = {
423 WantedBy = ["default.target"];
424 };
425 };
365 }; 426 };
366 targets = { 427 targets = {
367 graphical-session = { 428 graphical-session = {
@@ -372,6 +433,9 @@ in {
372 }; 433 };
373 tray = { 434 tray = {
374 Unit = { 435 Unit = {
436 PartOf = [ "graphical-session.target" ];
437 # Requires = [ "waybar.service" ];
438 After = [ "graphical-session.target" ]; # "waybar.service" ];
375 Wants = ["blueman-applet.service" "udiskie.service" "network-manager-applet.service"]; 439 Wants = ["blueman-applet.service" "udiskie.service" "network-manager-applet.service"];
376 }; 440 };
377 }; 441 };
diff --git a/accounts/gkleen@sif/taffybar/default.nix b/accounts/gkleen@sif/taffybar/default.nix
deleted file mode 100644
index 98366d8f..00000000
--- a/accounts/gkleen@sif/taffybar/default.nix
+++ /dev/null
@@ -1,2 +0,0 @@
1{ haskellPackages ? (import <nixpkgs> {}).haskellPackages }:
2haskellPackages.callCabal2nix "gkleen-sif-taffybar" ./. {}
diff --git a/accounts/gkleen@sif/taffybar/gkleen-sif-taffybar.cabal b/accounts/gkleen@sif/taffybar/gkleen-sif-taffybar.cabal
deleted file mode 100644
index e32cb473..00000000
--- a/accounts/gkleen@sif/taffybar/gkleen-sif-taffybar.cabal
+++ /dev/null
@@ -1,32 +0,0 @@
1name: gkleen-sif-taffybar
2version: 0.0.0
3build-type: Simple
4cabal-version: >=1.10
5
6data-files: taffybar.css
7
8executable taffybar
9 hs-source-dirs: src
10 main-is: taffybar.hs
11 ghc-options: -threaded -rtsopts -with-rtsopts=-N -O2 -Wall
12 build-depends: base
13 , containers
14 , directory
15 , filepath
16 , gtk3
17 , taffybar
18 , X11>=1.8
19 , transformers
20 , gi-gtk
21 , time, time-locale-compat
22 , text
23 , HStringTemplate
24 , gtk-sni-tray
25 , hslogger
26 other-modules: Paths_gkleen_sif_taffybar
27 , System.Taffybar.Widget.Clock
28 , System.Taffybar.Widget.TooltipBattery
29 default-language: Haskell2010
30 default-extensions: ScopedTypeVariables
31 , LambdaCase
32 , NamedFieldPuns \ No newline at end of file
diff --git a/accounts/gkleen@sif/taffybar/src/System/Taffybar/Widget/Clock.hs b/accounts/gkleen@sif/taffybar/src/System/Taffybar/Widget/Clock.hs
deleted file mode 100644
index e8dc480f..00000000
--- a/accounts/gkleen@sif/taffybar/src/System/Taffybar/Widget/Clock.hs
+++ /dev/null
@@ -1,111 +0,0 @@
1{-# LANGUAGE OverloadedStrings #-}
2module System.Taffybar.Widget.Clock
3 ( textClockNew
4 , textClockNewWith
5 , defaultClockConfig
6 , ClockConfig(..)
7 , ClockUpdateStrategy(..)
8 ) where
9
10import Control.Monad.IO.Class
11import Data.Maybe
12import qualified Data.Text as T
13import qualified Data.Time.Clock as Clock
14import Data.Time.Format
15import Data.Time.LocalTime
16import qualified Data.Time.Locale.Compat as L
17import GI.Gtk
18import System.Taffybar.Widget.Generic.PollingLabel
19
20type ClockFormat = L.TimeLocale -> ZonedTime -> T.Text
21
22-- | Create the widget. I recommend passing @Nothing@ for the TimeLocale
23-- parameter. The format string can include Pango markup
24-- (<http://developer.gnome.org/pango/stable/PangoMarkupFormat.html>).
25textClockNew ::
26 MonadIO m => Maybe L.TimeLocale -> ClockFormat -> Double -> m GI.Gtk.Widget
27textClockNew userLocale format interval =
28 textClockNewWith cfg
29 where
30 cfg = defaultClockConfig { clockTimeLocale = userLocale
31 , clockFormat = format
32 , clockUpdateStrategy = ConstantInterval interval
33 }
34
35data ClockUpdateStrategy
36 = ConstantInterval Double
37 | RoundedTargetInterval Int Double
38 deriving (Eq, Ord, Show)
39
40data ClockConfig = ClockConfig
41 { clockTimeZone :: Maybe TimeZone
42 , clockTimeLocale :: Maybe L.TimeLocale
43 , clockFormat :: ClockFormat
44 , clockUpdateStrategy :: ClockUpdateStrategy
45 }
46
47-- | A clock configuration that defaults to the current locale
48defaultClockConfig :: ClockConfig
49defaultClockConfig = ClockConfig
50 { clockTimeZone = Nothing
51 , clockTimeLocale = Nothing
52 , clockFormat = \locale zonedTime -> T.pack $ formatTime locale "%a %b %_d %r" zonedTime
53 , clockUpdateStrategy = RoundedTargetInterval 5 0.0
54 }
55
56systemGetTZ :: IO TimeZone
57systemGetTZ = getCurrentTimeZone
58
59-- | A configurable text-based clock widget. It currently allows for
60-- a configurable time zone through the 'ClockConfig'.
61--
62-- See also 'textClockNew'.
63textClockNewWith :: MonadIO m => ClockConfig -> m Widget
64textClockNewWith ClockConfig
65 { clockTimeZone = userZone
66 , clockTimeLocale = userLocale
67 , clockFormat = format
68 , clockUpdateStrategy = updateStrategy
69 } = liftIO $ do
70 let getTZ = maybe systemGetTZ return userZone
71 locale = fromMaybe L.defaultTimeLocale userLocale
72
73 let getUserZonedTime =
74 utcToZonedTime <$> getTZ <*> Clock.getCurrentTime
75
76 doTimeFormat = format locale
77
78 getRoundedTimeAndNextTarget = do
79 zonedTime <- getUserZonedTime
80 return $ case updateStrategy of
81 ConstantInterval interval ->
82 (doTimeFormat zonedTime, Nothing, interval)
83 RoundedTargetInterval roundSeconds offset ->
84 let roundSecondsDiffTime = fromIntegral roundSeconds
85 addTheRound = addLocalTime roundSecondsDiffTime
86 localTime = zonedTimeToLocalTime zonedTime
87 ourLocalTimeOfDay = localTimeOfDay localTime
88 seconds = round $ todSec ourLocalTimeOfDay
89 secondsFactor = seconds `div` roundSeconds
90 displaySeconds = secondsFactor * roundSeconds
91 baseLocalTimeOfDay =
92 ourLocalTimeOfDay { todSec = fromIntegral displaySeconds }
93 ourLocalTime =
94 localTime { localTimeOfDay = baseLocalTimeOfDay }
95 roundedLocalTime =
96 if seconds `mod` roundSeconds > roundSeconds `div` 2
97 then addTheRound ourLocalTime
98 else ourLocalTime
99 roundedZonedTime =
100 zonedTime { zonedTimeToLocalTime = roundedLocalTime }
101 nextTarget = addTheRound ourLocalTime
102 amountToWait = realToFrac $ diffLocalTime nextTarget localTime
103 in (doTimeFormat roundedZonedTime, Nothing, amountToWait - offset)
104
105 label <- pollingLabelWithVariableDelay getRoundedTimeAndNextTarget
106 ebox <- eventBoxNew
107 containerAdd ebox label
108 eventBoxSetVisibleWindow ebox False
109 widgetShowAll ebox
110 toWidget ebox
111
diff --git a/accounts/gkleen@sif/taffybar/src/System/Taffybar/Widget/TooltipBattery.hs b/accounts/gkleen@sif/taffybar/src/System/Taffybar/Widget/TooltipBattery.hs
deleted file mode 100644
index 9dc52774..00000000
--- a/accounts/gkleen@sif/taffybar/src/System/Taffybar/Widget/TooltipBattery.hs
+++ /dev/null
@@ -1,101 +0,0 @@
1{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE ScopedTypeVariables #-}
3module System.Taffybar.Widget.TooltipBattery ( batteryIconTooltipNew ) where
4
5import Control.Applicative
6import Control.Monad
7import Control.Monad.IO.Class
8import Control.Monad.Trans.Reader
9import Data.Int (Int64)
10import qualified Data.Text as T
11import GI.Gtk
12import Prelude
13import StatusNotifier.Tray (scalePixbufToSize)
14import System.Taffybar.Context
15import System.Taffybar.Information.Battery
16import System.Taffybar.Util
17import System.Taffybar.Widget.Generic.AutoSizeImage
18import System.Taffybar.Widget.Generic.ChannelWidget
19import Text.Printf
20import Text.StringTemplate
21import Data.Function ((&))
22
23-- | Just the battery info that will be used for display (this makes combining
24-- several easier).
25data BatteryWidgetInfo = BWI
26 { seconds :: Maybe Int64
27 , percent :: Double
28 , status :: String
29 , rate :: Maybe Double
30 } deriving (Eq, Show)
31
32-- | Format a duration expressed as seconds to hours and minutes
33formatDuration :: Int64 -> String
34formatDuration secs = let minutes, hours, minutes' :: Int64
35 minutes = secs `div` 60
36 (hours, minutes') = minutes `divMod` 60
37 in printf "%02d:%02d" hours minutes'
38
39getBatteryWidgetInfo :: BatteryInfo -> BatteryWidgetInfo
40getBatteryWidgetInfo info =
41 let battPctNum :: Double
42 battPctNum = batteryPercentage info
43 battTime :: Maybe Int64
44 battTime =
45 case batteryState info of
46 BatteryStateCharging -> Just $ batteryTimeToFull info
47 BatteryStateDischarging -> Just $ batteryTimeToEmpty info
48 _ -> Nothing
49 battStatus :: String
50 battStatus =
51 case batteryState info of
52 BatteryStateCharging -> "↑"
53 BatteryStateDischarging -> "↓"
54 BatteryStateEmpty -> "⤓"
55 BatteryStateFullyCharged -> "⤒"
56 _ -> "?"
57 battRate :: Maybe Double
58 battRate | rawRate < 0.1 = Nothing
59 | otherwise = Just rawRate
60 where rawRate = batteryEnergyRate info
61 in BWI{ seconds = battTime, percent = battPctNum, status = battStatus, rate = battRate }
62
63-- | Given (maybe summarized) battery info and format: provides the string to display
64formatBattInfo :: BatteryWidgetInfo -> String -> T.Text
65formatBattInfo info fmt =
66 let tpl = newSTMP fmt
67 tpl' = tpl
68 & setManyAttrib [ ("percentage", printf "%.0f" $ percent info)
69 , ("status", status info)
70 ]
71 & setManyAttrib [ ("time", formatDuration <$> seconds info)
72 , ("rate", printf "%.0f" <$> rate info)
73 ]
74 in render tpl'
75
76themeLoadFlags :: [IconLookupFlags]
77themeLoadFlags = [IconLookupFlagsGenericFallback, IconLookupFlagsUseBuiltin]
78
79batteryIconTooltipNew :: String -> TaffyIO Widget
80batteryIconTooltipNew format = do
81 DisplayBatteryChanVar (chan, _) <- setupDisplayBatteryChanVar ["IconName", "State", "Percentage", "TimeToFull", "TimeToEmpty", "EnergyRate"]
82 ctx <- ask
83 liftIO $ do
84 image <- imageNew
85 styleCtx <- widgetGetStyleContext =<< toWidget image
86 defaultTheme <- iconThemeGetDefault
87 let getCurrentBatteryIconNameStringTooltip = do
88 info <- runReaderT getDisplayBatteryInfo ctx
89 let iconNameString = T.pack $ batteryIconName info
90 tooltip = formatBattInfo (getBatteryWidgetInfo info) format
91 return (iconNameString, tooltip)
92 extractPixbuf info =
93 fst <$> iconInfoLoadSymbolicForContext info styleCtx
94 setIconForSize size = do
95 (name, tooltip) <- getCurrentBatteryIconNameStringTooltip
96 widgetSetTooltipMarkup image $ Just tooltip
97 iconThemeLookupIcon defaultTheme name size themeLoadFlags >>=
98 traverse extractPixbuf >>=
99 traverse (scalePixbufToSize size OrientationHorizontal)
100 updateImage <- autoSizeImage image setIconForSize OrientationHorizontal
101 toWidget =<< channelWidgetNew image chan (const $ postGUIASync updateImage)
diff --git a/accounts/gkleen@sif/taffybar/src/taffybar.hs b/accounts/gkleen@sif/taffybar/src/taffybar.hs
deleted file mode 100644
index 67ee942d..00000000
--- a/accounts/gkleen@sif/taffybar/src/taffybar.hs
+++ /dev/null
@@ -1,89 +0,0 @@
1{-# LANGUAGE OverloadedStrings #-}
2
3module Main where
4
5import System.Taffybar (startTaffybar)
6import System.Taffybar.Context (TaffybarConfig(..))
7import System.Taffybar.Hooks
8import System.Taffybar.SimpleConfig hiding (SimpleTaffyConfig(cssPaths))
9import System.Taffybar.Widget
10import qualified System.Taffybar.Widget.Clock as MyClock
11import System.Taffybar.Widget.TooltipBattery
12
13import Data.Time.Format
14import Data.Time.LocalTime
15import Data.Time.Calendar.WeekDate
16
17import qualified Data.Text as T
18
19import Control.Exception (SomeException, try)
20import Control.Monad.Trans.Reader (mapReaderT)
21
22import Paths_gkleen_sif_taffybar
23
24import System.Log.Logger
25
26
27main :: IO ()
28main = do
29 logger <- getLogger "System.Taffybar"
30 saveGlobalLogger $ setLevel INFO logger
31
32 myCssPath <- getDataFileName "taffybar.css"
33 startTaffybar taffybarConfig{ cssPaths = pure myCssPath }
34
35
36taffybarConfig :: TaffybarConfig
37taffybarConfig =
38 let myWorkspacesConfig =
39 defaultWorkspacesConfig
40 { maxIcons = Just 0
41 , widgetGap = 7
42 , showWorkspaceFn = \case
43 -- Workspace{ workspaceState = Empty } -> False
44 Workspace{ workspaceName } | workspaceName == "NSP" -> False
45 _other -> True
46 , getWindowIconPixbuf = \i d -> either (\(_ :: SomeException) -> Nothing) id <$> mapReaderT try (defaultGetWindowIconPixbuf i d)
47 , urgentWorkspaceState = True
48 }
49 workspaces = workspacesNew myWorkspacesConfig
50 clock = MyClock.textClockNewWith MyClock.defaultClockConfig
51 { MyClock.clockUpdateStrategy = MyClock.RoundedTargetInterval 1 0.0
52 , MyClock.clockFormat = \tl zt@ZonedTime{ zonedTimeToLocalTime = LocalTime{ localDay } }
53 -> let date = formatTime tl "%Y-%m-%d" localDay
54 weekdate = "W" <> show2 woy <> "-" <> show dow
55 where (_, woy, dow) = toWeekDate localDay
56 show2 :: Int -> String
57 show2 x = replicate (2 - length s) '0' ++ s
58 where s = show x
59 time = formatTime tl "%H:%M:%S%Ez" zt
60 in T.intercalate " " $ map T.pack [weekdate, date, time]
61 }
62 layout = layoutNew defaultLayoutConfig
63 windowsW = windowsNew defaultWindowsConfig
64 { getMenuLabel = truncatedGetMenuLabel 80
65 , getActiveLabel = truncatedGetActiveLabel 80
66 }
67 worktime = commandRunnerNew 60 "worktime" [] "worktime"
68 worktimeToday = commandRunnerNew 60 "worktime" ["today"] "worktime today"
69 -- See https://github.com/taffybar/gtk-sni-tray#statusnotifierwatcher
70 -- for a better way to set up the sni tray
71 -- tray = sniTrayThatStartsWatcherEvenThoughThisIsABadWayToDoIt
72 tray = sniTrayNew
73 myConfig = defaultSimpleTaffyConfig
74 { startWidgets =
75 workspaces : map (>>= buildContentsBox) [ layout, windowsW ]
76 , endWidgets = map (>>= buildContentsBox) $ reverse
77 -- , mpris2New
78 [ worktime, worktimeToday
79 , clock
80 , tray
81 , batteryIconTooltipNew "$status$ $percentage$%$if(time)$$if(rate)$ ($rate$W $time$)$else$ ($time$)$endif$$elseif(rate)$ ($rate$W)$endif$"
82 ]
83 , barPosition = Top
84 , barPadding = 2
85 , barHeight = ExactSize 28
86 , widgetSpacing = 10
87 }
88 in withBatteryRefresh $ withLogServer $
89 withToggleServer $ toTaffyConfig myConfig
diff --git a/accounts/gkleen@sif/taffybar/taffybar.css b/accounts/gkleen@sif/taffybar/taffybar.css
deleted file mode 100644
index 7a297465..00000000
--- a/accounts/gkleen@sif/taffybar/taffybar.css
+++ /dev/null
@@ -1,146 +0,0 @@
1@define-color transparent rgba(0.0, 0.0, 0.0, 0.0);
2@define-color white #808080;
3@define-color gray #202020;
4@define-color green #008000;
5@define-color yellow #808000;
6@define-color blue #000080;
7@define-color red #800000;
8@define-color black #000000;
9/* @define-color taffy-blue #0c7cd5; */
10@define-color taffy-blue @blue;
11
12@define-color active-window-color @white;
13@define-color urgent-window-color @taffy-blue;
14@define-color font-color @white;
15@define-color menu-background-color @black;
16@define-color menu-font-color @white;
17
18/* Top level styling */
19
20.taffy-window * {
21 /*
22 This removes any existing styling from UI elements. Taffybar will not
23 cohere with your gtk theme.
24 */
25 all: unset;
26
27 font-family: "Fira Sans", sans-serif;
28 font-size: 21px;
29 color: @font-color;
30}
31
32.taffy-box {
33 /* border-radius: 10px; */
34 background-color: @black;
35}
36
37.inner-pad {
38 /* padding-bottom: 5px; */
39 /* padding-top: 5px; */
40 padding-left: 2px;
41 padding-right: 2px;
42}
43
44.contents {
45 /* padding-bottom: 4px; */
46 /* padding-top: 4px; */
47 padding-right: 2px;
48 padding-left: 2px;
49 transition: background-color .5s;
50 border-radius: 5px;
51}
52
53/* Workspaces styling */
54
55.workspace-label {
56 padding-right: 3px;
57 padding-left: 2px;
58 font-size: 21px;
59}
60
61.workspace-label.active {
62 color: @green;
63}
64.workspace-label.visible {
65 color: @yellow;
66}
67.workspace-label.empty {
68 color: @gray;
69}
70.workspace-label.urgent {
71 color: @red;
72}
73
74.active .contents {
75 background-color: rgba(0.0, 0.0, 0.0, 0.5);
76}
77
78.visible .contents {
79 background-color: rgba(0.0, 0.0, 0.0, 0.2);
80}
81
82.window-icon-container {
83 transition: opacity .5s, box-shadow .5s;
84 opacity: 1;
85}
86
87/* This gives space for the box-shadow (they look like underlines) that follow.
88 This will actually affect all widgets, (not just the workspace icons), but
89 that is what we want since we want the icons to look the same. */
90.auto-size-image, .sni-tray {
91 padding-top: 3px;
92 padding-bottom: 3px;
93}
94
95.window-icon-container.active {
96 box-shadow: inset 0 -3px @white;
97}
98
99.window-icon-container.urgent {
100 box-shadow: inset 0 -3px @urgent-window-color;
101}
102
103.window-icon-container.inactive .window-icon {
104 padding: 0px;
105}
106
107.window-icon-container.minimized .window-icon {
108 opacity: .3;
109}
110
111.window-icon {
112 opacity: 1;
113 transition: opacity .5s;
114}
115
116/* Button styling */
117
118button {
119 background-color: @transparent;
120 border-width: 0px;
121 border-radius: 0px;
122}
123
124button:checked, button:hover .Contents:hover {
125 box-shadow: inset 0 -3px @taffy-blue;
126}
127
128/* Menu styling */
129
130/* The ".taffy-window" prefixed selectors are needed because if they aren't present,
131 the top level .Taffybar selector takes precedence */
132.taffy-window menuitem *, menuitem * {
133 color: @menu-font-color;
134}
135
136.taffy-window menuitem, menuitem {
137 background-color: @menu-background-color;
138}
139
140.taffy-window menuitem:hover, menuitem:hover {
141 background-color: @taffy-blue;
142}
143
144.taffy-window menuitem:hover > label, menuitem:hover > label {
145 color: @white;
146}
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/pdf2pdf.nix b/accounts/gkleen@sif/utils/pdf2pdf.nix
new file mode 100644
index 00000000..9f4cbc3e
--- /dev/null
+++ b/accounts/gkleen@sif/utils/pdf2pdf.nix
@@ -0,0 +1,8 @@
1pkgs@{ lib, resholve, zsh, ghostscript_headless, ... }:
2
3resholve.writeScriptBin "pdf2pdf" {
4 inputs = with pkgs; [ghostscript_headless];
5 interpreter = lib.getExe zsh;
6} ''
7 exec gs -dPDFSETTINGS=/prepress -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dSAFER -dPreserveAnnots=false "-sOutputFile=''${2}" "''${1}"
8''
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/xmonad/.gitignore b/accounts/gkleen@sif/xmonad/.gitignore
deleted file mode 100644
index c11891cd..00000000
--- a/accounts/gkleen@sif/xmonad/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
1**/#*#
2**/.stack-work/
3/stack.yaml.lock
4/*.cabal
diff --git a/accounts/gkleen@sif/xmonad/default.nix b/accounts/gkleen@sif/xmonad/default.nix
deleted file mode 100644
index 8790c12f..00000000
--- a/accounts/gkleen@sif/xmonad/default.nix
+++ /dev/null
@@ -1,7 +0,0 @@
1argumentPackages@{ ... }:
2
3let
4 # defaultPackages = (import ./stackage.nix {});
5 # haskellPackages = defaultPackages // argumentPackages;
6 haskellPackages = argumentPackages;
7in haskellPackages.callPackage ./xmonad-yggdrasil.nix {}
diff --git a/accounts/gkleen@sif/xmonad/lib/XMonad/Mpv.hs b/accounts/gkleen@sif/xmonad/lib/XMonad/Mpv.hs
deleted file mode 100644
index e6accdcc..00000000
--- a/accounts/gkleen@sif/xmonad/lib/XMonad/Mpv.hs
+++ /dev/null
@@ -1,127 +0,0 @@
1{-# LANGUAGE DeriveGeneric, OverloadedLists, OverloadedStrings, ViewPatterns, ExistentialQuantification, MultiWayIf #-}
2
3module XMonad.Mpv
4 ( MpvCommand(..), MpvResponse(..), MpvException(..)
5 , mpv
6 , mpvDir
7 , mpvAll, mpvOne
8 , mpvResponse
9 ) where
10
11import Data.Aeson
12
13import Data.Monoid
14
15import Network.Socket hiding (recv)
16import Network.Socket.ByteString
17
18import qualified Data.ByteString as BS
19import qualified Data.ByteString.Char8 as CBS
20import qualified Data.ByteString.Lazy as LBS
21
22import GHC.Generics (Generic)
23import Data.Typeable (Typeable)
24import Data.String (IsString(..))
25
26import Control.Exception
27
28import System.IO.Temp (getCanonicalTemporaryDirectory)
29
30import Control.Monad
31import Control.Exception (bracket)
32import Control.Monad.IO.Class (MonadIO(..))
33
34import System.FilePath
35import System.Directory (getDirectoryContents)
36
37import Data.List
38import Data.Either
39import Data.Maybe
40
41import Debug.Trace
42
43
44data MpvCommand
45 = forall a. ToJSON a => MpvSetProperty String a
46 | MpvGetProperty String
47data MpvResponse
48 = MpvError String
49 | MpvSuccess (Maybe Value)
50 deriving (Read, Show, Generic, Eq)
51data MpvException = MpvException String
52 | MpvNoValue
53 | MpvNoParse String
54 deriving (Generic, Typeable, Read, Show)
55instance Exception MpvException
56
57
58instance ToJSON MpvCommand where
59 toJSON (MpvSetProperty name val) = Array ["set_property", fromString name, toJSON val]
60 toJSON (MpvGetProperty name) = Array ["get_property", fromString name]
61
62instance FromJSON MpvResponse where
63 parseJSON = withObject "response object" $ \obj -> do
64 mval <- obj .:? "data"
65 err <- obj .: "error"
66
67 let ret
68 | err == "success" = MpvSuccess mval
69 | otherwise = MpvError err
70
71 return ret
72
73mpvSocket :: FilePath -> (Socket -> IO a) -> IO a
74mpvSocket sockPath = withSocketsDo . bracket mkSock close
75 where
76 mkSock = do
77 sock <- socket AF_UNIX Stream defaultProtocol
78 connect sock $ SockAddrUnix (traceId sockPath)
79 return sock
80
81mpvResponse :: FromJSON v => MpvResponse -> IO v
82mpvResponse (MpvError str) = throwIO $ MpvException str
83mpvResponse (MpvSuccess Nothing) = throwIO MpvNoValue
84mpvResponse (MpvSuccess (Just v)) = case fromJSON v of
85 Success v' -> return v'
86 Error str -> throwIO $ MpvNoParse str
87
88mpv :: FilePath -> MpvCommand -> IO MpvResponse
89mpv sockPath cmd = mpvSocket sockPath $ \sock -> do
90 let message = (`BS.append` "\n") . LBS.toStrict . encode $ Object [("command", toJSON cmd)]
91 traceIO $ show message
92 sendAll sock message
93 let recvAll = do
94 prefix <- recv sock 4096
95 if
96 | (prefix', rest) <- CBS.break (== '\n') prefix
97 , not (BS.null rest) -> return prefix'
98 | BS.null prefix -> return prefix
99 | otherwise -> BS.append prefix <$> recvAll
100 response <- recvAll
101 traceIO $ show response
102 either (ioError . userError) return . traceShowId $ eitherDecodeStrict' response
103
104mpvDir :: Exception e => FilePath -> (FilePath -> [(FilePath, Either e MpvResponse)] -> Maybe MpvCommand) -> IO [(FilePath, Either e MpvResponse)]
105mpvDir dir step = do
106 socks <- filter (".sock" `isSuffixOf`) <$> getDirectoryContents dir
107 go [] socks
108 where
109 go acc [] = return acc
110 go acc (sock:socks)
111 | Just cmd <- step sock acc = do
112 res <- try $ mpv (dir </> sock) cmd
113 go ((sock, res) : acc) socks
114 | otherwise =
115 go acc socks
116
117mpvAll :: FilePath -> MpvCommand -> IO [MpvResponse]
118mpvAll dir cmd = do
119 results <- map snd <$> (mpvDir dir (\_ _ -> Just cmd) :: IO [(FilePath, Either SomeException MpvResponse)])
120 mapM (either throwIO return) results
121
122mpvOne :: FilePath -> MpvCommand -> IO (Maybe MpvResponse)
123mpvOne dir cmd = listToMaybe . snd . partitionEithers . map snd <$> (mpvDir dir step :: IO [(FilePath, Either SomeException MpvResponse)])
124 where
125 step _ results
126 | any (isRight . snd) results = Nothing
127 | otherwise = Just cmd
diff --git a/accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MyPass.hs b/accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MyPass.hs
deleted file mode 100644
index 1caefae5..00000000
--- a/accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MyPass.hs
+++ /dev/null
@@ -1,94 +0,0 @@
1module XMonad.Prompt.MyPass
2 (
3 -- * Usages
4 -- $usages
5 mkPassPrompt
6 ) where
7
8import Control.Monad (liftM)
9import XMonad.Core
10import XMonad.Prompt ( XPrompt
11 , showXPrompt
12 , commandToComplete
13 , nextCompletion
14 , getNextCompletion
15 , XPConfig
16 , mkXPrompt
17 , searchPredicate)
18import System.Directory (getHomeDirectory)
19import System.FilePath (takeExtension, dropExtension, combine)
20import System.Posix.Env (getEnv)
21import XMonad.Util.Run (runProcessWithInput)
22
23-- $usages
24-- You can use this module with the following in your @~\/.xmonad\/xmonad.hs@:
25--
26-- > import XMonad.Prompt.Pass
27--
28-- Then add a keybinding for 'passPrompt', 'passGeneratePrompt' or 'passRemovePrompt':
29--
30-- > , ((modMask x , xK_p) , passPrompt xpconfig)
31-- > , ((modMask x .|. controlMask, xK_p) , passGeneratePrompt xpconfig)
32-- > , ((modMask x .|. controlMask .|. shiftMask, xK_p), passRemovePrompt xpconfig)
33--
34-- For detailed instructions on:
35--
36-- - editing your key bindings, see "XMonad.Doc.Extending#Editing_key_bindings".
37--
38-- - how to setup the password storage, see <http://git.zx2c4.com/password-store/about/>
39--
40
41type Predicate = String -> String -> Bool
42
43getPassCompl :: [String] -> Predicate -> String -> IO [String]
44getPassCompl compls p s
45 | length s <= minL
46 , all ((> minL) . length) compls = return []
47 | otherwise = do return $ filter (p s) compls
48 where
49 minL = 3
50
51type PromptLabel = String
52
53data Pass = Pass PromptLabel
54
55instance XPrompt Pass where
56 showXPrompt (Pass prompt) = prompt ++ ": "
57 commandToComplete _ c = c
58 nextCompletion _ = getNextCompletion
59
60-- | Default password store folder in $HOME/.password-store
61--
62passwordStoreFolderDefault :: String -> String
63passwordStoreFolderDefault home = combine home ".password-store"
64
65-- | Compute the password store's location.
66-- Use the PASSWORD_STORE_DIR environment variable to set the password store.
67-- If empty, return the password store located in user's home.
68--
69passwordStoreFolder :: IO String
70passwordStoreFolder =
71 getEnv "PASSWORD_STORE_DIR" >>= computePasswordStoreDir
72 where computePasswordStoreDir Nothing = liftM passwordStoreFolderDefault getHomeDirectory
73 computePasswordStoreDir (Just storeDir) = return storeDir
74
75-- | A pass prompt factory
76--
77mkPassPrompt :: PromptLabel -> (String -> X ()) -> XPConfig -> X ()
78mkPassPrompt promptLabel passwordFunction xpconfig = do
79 passwords <- io (passwordStoreFolder >>= getPasswords)
80 mkXPrompt (Pass promptLabel) xpconfig (getPassCompl passwords $ searchPredicate xpconfig) passwordFunction
81
82-- | Retrieve the list of passwords from the password storage 'passwordStoreDir
83getPasswords :: FilePath -> IO [String]
84getPasswords passwordStoreDir = do
85 files <- runProcessWithInput "find" [
86 passwordStoreDir,
87 "-type", "f",
88 "-name", "*.gpg",
89 "-printf", "%P\n"] []
90 return $ map removeGpgExtension $ lines files
91
92removeGpgExtension :: String -> String
93removeGpgExtension file | takeExtension file == ".gpg" = dropExtension file
94 | otherwise = file
diff --git a/accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MyShell.hs b/accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MyShell.hs
deleted file mode 100644
index c268f87d..00000000
--- a/accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MyShell.hs
+++ /dev/null
@@ -1,105 +0,0 @@
1module XMonad.Prompt.MyShell
2 ( Shell (..)
3 , shellPrompt
4 , prompt
5 , safePrompt
6 , unsafePrompt
7 , getCommands
8 , getShellCompl
9 , split
10 ) where
11
12import Codec.Binary.UTF8.String (encodeString)
13import Control.Exception as E
14import Control.Monad (forM)
15import Data.List (isPrefixOf)
16import System.Directory (doesDirectoryExist, getDirectoryContents)
17import System.Environment (getEnv)
18import System.Posix.Files (getFileStatus, isDirectory)
19
20import XMonad hiding (config)
21import XMonad.Prompt
22import XMonad.Util.Run
23
24econst :: Monad m => a -> IOException -> m a
25econst = const . return
26
27data Shell = Shell String
28
29instance XPrompt Shell where
30 showXPrompt (Shell q) = q
31 completionToCommand _ = escape
32
33shellPrompt :: String -> XPConfig -> X ()
34shellPrompt q c = do
35 cmds <- io getCommands
36 mkXPrompt (Shell q) c (getShellCompl cmds) spawn
37
38{- $spawns
39 See safe and unsafeSpawn in "XMonad.Util.Run".
40 prompt is an alias for safePrompt;
41 safePrompt and unsafePrompt work on the same principles, but will use
42 XPrompt to interactively query the user for input; the appearance is
43 set by passing an XPConfig as the second argument. The first argument
44 is the program to be run with the interactive input.
45 You would use these like this:
46
47 > , ((modm, xK_b), safePrompt "firefox" greenXPConfig)
48 > , ((modm .|. shiftMask, xK_c), prompt ("xterm" ++ " -e") greenXPConfig)
49
50 Note that you want to use safePrompt for Firefox input, as Firefox
51 wants URLs, and unsafePrompt for the XTerm example because this allows
52 you to easily start a terminal executing an arbitrary command, like
53 'top'. -}
54
55prompt, unsafePrompt, safePrompt :: String -> FilePath -> XPConfig -> X ()
56prompt = unsafePrompt
57safePrompt q c config = mkXPrompt (Shell q) config (getShellCompl [c]) run
58 where run = safeSpawn c . return
59unsafePrompt q c config = mkXPrompt (Shell q) config (getShellCompl [c]) run
60 where run a = unsafeSpawn $ c ++ " " ++ a
61
62getShellCompl :: [String] -> String -> IO [String]
63getShellCompl cmds s | s == "" || last s == ' ' = return []
64 | otherwise = do
65 f <- fmap lines $ runProcessWithInput "bash" [] ("compgen -A file -- "
66 ++ s ++ "\n")
67 files <- case f of
68 [x] -> do fs <- getFileStatus (encodeString x)
69 if isDirectory fs then return [x ++ "/"]
70 else return [x]
71 _ -> return f
72 return . uniqSort $ files ++ commandCompletionFunction cmds s
73
74commandCompletionFunction :: [String] -> String -> [String]
75commandCompletionFunction cmds str | '/' `elem` str = []
76 | otherwise = filter (isPrefixOf str) cmds
77
78getCommands :: IO [String]
79getCommands = do
80 p <- getEnv "PATH" `E.catch` econst []
81 let ds = filter (/= "") $ split ':' p
82 es <- forM ds $ \d -> do
83 exists <- doesDirectoryExist d
84 if exists
85 then getDirectoryContents d
86 else return []
87 return . uniqSort . filter ((/= '.') . head) . concat $ es
88
89split :: Eq a => a -> [a] -> [[a]]
90split _ [] = []
91split e l =
92 f : split e (rest ls)
93 where
94 (f,ls) = span (/=e) l
95 rest s | s == [] = []
96 | otherwise = tail s
97
98escape :: String -> String
99escape [] = ""
100escape (x:xs)
101 | isSpecialChar x = '\\' : x : escape xs
102 | otherwise = x : escape xs
103
104isSpecialChar :: Char -> Bool
105isSpecialChar = flip elem " &\\@\"'#?$*()[]{};"
diff --git a/accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MySsh.hs b/accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MySsh.hs
deleted file mode 100644
index 998c533e..00000000
--- a/accounts/gkleen@sif/xmonad/lib/XMonad/Prompt/MySsh.hs
+++ /dev/null
@@ -1,246 +0,0 @@
1module XMonad.Prompt.MySsh
2 ( -- * Usage
3 -- $usage
4 sshPrompt,
5 Ssh,
6 Override (..),
7 mkOverride,
8 Conn (..),
9 moshCmd,
10 moshCmd',
11 sshCmd,
12 inTmux,
13 withEnv
14 ) where
15
16import XMonad
17import XMonad.Util.Run
18import XMonad.Prompt
19
20import System.Directory
21import System.Environment
22import qualified Control.Exception as E
23
24import Control.Monad
25import Data.Maybe
26
27import Text.Parsec.String
28import Text.Parsec
29import Data.Char (isSpace)
30
31econst :: Monad m => a -> E.IOException -> m a
32econst = const . return
33
34-- $usage
35-- 1. In your @~\/.xmonad\/xmonad.hs@:
36--
37-- > import XMonad.Prompt
38-- > import XMonad.Prompt.Ssh
39--
40-- 2. In your keybindings add something like:
41--
42-- > , ((modm .|. controlMask, xK_s), sshPrompt defaultXPConfig)
43--
44-- Keep in mind, that if you want to use the completion you have to
45-- disable the "HashKnownHosts" option in your ssh_config
46--
47-- For detailed instruction on editing the key binding see
48-- "XMonad.Doc.Extending#Editing_key_bindings".
49
50data Override = Override
51 { oUser :: Maybe String
52 , oHost :: String
53 , oPort :: Maybe Int
54 , oCommand :: Conn -> String
55 }
56
57mkOverride = Override { oUser = Nothing, oHost = "", oPort = Nothing, oCommand = sshCmd }
58sshCmd c = concat
59 [ "ssh -t "
60 , if isJust $ cUser c then (fromJust $ cUser c) ++ "@" else ""
61 , cHost c
62 , if isJust $ cPort c then " -p " ++ (show $ fromJust $ cPort c) else ""
63 , " -- "
64 , cCommand c
65 ]
66moshCmd c = concat
67 [ "mosh "
68 , if isJust $ cUser c then (fromJust $ cUser c) ++ "@" else ""
69 , cHost c
70 , if isJust $ cPort c then " --ssh=\"ssh -p " ++ (show $ fromJust $ cPort c) ++ "\"" else ""
71 , " -- "
72 , cCommand c
73 ]
74moshCmd' p c = concat
75 [ "mosh "
76 , "--server=" ++ p ++ " "
77 , if isJust $ cUser c then (fromJust $ cUser c) ++ "@" else ""
78 , cHost c
79 , if isJust $ cPort c then " --ssh=\"ssh -p " ++ (show $ fromJust $ cPort c) ++ "\"" else ""
80 , " -- "
81 , cCommand c
82 ]
83inTmux Nothing c
84 | null $ cCommand c = c { cCommand = "tmux new-session" }
85 | otherwise = c { cCommand = "tmux new-session \"" ++ (cCommand c) ++ "\"" }
86inTmux (Just h) c
87 | null $ cCommand c = c { cCommand = "tmux new-session -As " <> h }
88 | otherwise = c { cCommand = "tmux new-session \"" ++ (cCommand c) ++ "\"" }
89withEnv :: [(String, String)] -> Conn -> Conn
90withEnv envs c = c { cCommand = "env" ++ (concat $ map (\(n, v) -> ' ' : (n ++ "=" ++ v)) envs) ++ " " ++ (cCommand c) }
91
92data Conn = Conn
93 { cUser :: Maybe String
94 , cHost :: String
95 , cPort :: Maybe Int
96 , cCommand :: String
97 } deriving (Eq, Show, Read)
98
99data Ssh = Ssh
100
101instance XPrompt Ssh where
102 showXPrompt Ssh = "SSH to: "
103 commandToComplete _ c = c
104 nextCompletion _ = getNextCompletion
105
106toConn :: String -> Maybe Conn
107toConn = toConn' . parse connParser "(unknown)"
108toConn' :: Either ParseError Conn -> Maybe Conn
109toConn' (Left _) = Nothing
110toConn' (Right a) = Just a
111
112connParser :: Parser Conn
113connParser = do
114 spaces
115 user' <- optionMaybe $ try $ do
116 str <- many1 $ satisfy (\c -> (not $ isSpace c) && (c /= '@'))
117 char '@'
118 return str
119 host' <- many1 $ satisfy (not . isSpace)
120 port' <- optionMaybe $ try $ do
121 space
122 string "-p"
123 spaces
124 int <- many1 digit
125 (space >> return ()) <|> eof
126 return $ (read int :: Int)
127 spaces
128 command' <- many anyChar
129 eof
130 return $ Conn
131 { cHost = host'
132 , cUser = user'
133 , cPort = port'
134 , cCommand = command'
135 }
136
137sshPrompt :: [Override] -> XPConfig -> X ()
138sshPrompt o c = do
139 sc <- io sshComplList
140 mkXPrompt Ssh c (mkComplFunFromList c sc) $ ssh o
141
142ssh :: [Override] -> String -> X ()
143ssh overrides str = do
144 let cmd = applyOverrides overrides str
145 liftIO $ putStr "SSH Command: "
146 liftIO $ putStrLn cmd
147 runInTerm "" cmd
148
149applyOverrides :: [Override] -> String -> String
150applyOverrides [] str = "ssh " ++ str
151applyOverrides (o:os) str = case (applyOverride o str) of
152 Just str -> str
153 Nothing -> applyOverrides os str
154
155applyOverride :: Override -> String -> Maybe String
156applyOverride o str = let
157 conn = toConn str
158 in
159 if isNothing conn then Nothing else
160 case (fromJust conn) `matches` o of
161 True -> Just $ (oCommand o) (fromJust conn)
162 False -> Nothing
163
164matches :: Conn -> Override -> Bool
165a `matches` b = and
166 [ justBool (cUser a) (oUser b) (==)
167 , (cHost a) == (oHost b)
168 , justBool (cPort a) (oPort b) (==)
169 ]
170
171justBool :: Eq a => Maybe a -> Maybe a -> (a -> a -> Bool) -> Bool
172justBool Nothing _ _ = True
173justBool _ Nothing _ = True
174justBool (Just a) (Just b) match = a `match` b
175
176sshComplList :: IO [String]
177sshComplList = uniqSort `fmap` liftM2 (++) sshComplListLocal sshComplListGlobal
178
179sshComplListLocal :: IO [String]
180sshComplListLocal = do
181 h <- getEnv "HOME"
182 s1 <- sshComplListFile $ h ++ "/.ssh/known_hosts"
183 s2 <- sshComplListConf $ h ++ "/.ssh/config"
184 return $ s1 ++ s2
185
186sshComplListGlobal :: IO [String]
187sshComplListGlobal = do
188 env <- getEnv "SSH_KNOWN_HOSTS" `E.catch` econst "/nonexistent"
189 fs <- mapM fileExists [ env
190 , "/usr/local/etc/ssh/ssh_known_hosts"
191 , "/usr/local/etc/ssh_known_hosts"
192 , "/etc/ssh/ssh_known_hosts"
193 , "/etc/ssh_known_hosts"
194 ]
195 case catMaybes fs of
196 [] -> return []
197 (f:_) -> sshComplListFile' f
198
199sshComplListFile :: String -> IO [String]
200sshComplListFile kh = do
201 f <- doesFileExist kh
202 if f then sshComplListFile' kh
203 else return []
204
205sshComplListFile' :: String -> IO [String]
206sshComplListFile' kh = do
207 l <- readFile kh
208 return $ map (getWithPort . takeWhile (/= ',') . concat . take 1 . words)
209 $ filter nonComment
210 $ lines l
211
212sshComplListConf :: String -> IO [String]
213sshComplListConf kh = do
214 f <- doesFileExist kh
215 if f then sshComplListConf' kh
216 else return []
217
218sshComplListConf' :: String -> IO [String]
219sshComplListConf' kh = do
220 l <- readFile kh
221 return $ map (!!1)
222 $ filter isHost
223 $ map words
224 $ lines l
225 where
226 isHost ws = take 1 ws == ["Host"] && length ws > 1
227
228fileExists :: String -> IO (Maybe String)
229fileExists kh = do
230 f <- doesFileExist kh
231 if f then return $ Just kh
232 else return Nothing
233
234nonComment :: String -> Bool
235nonComment [] = False
236nonComment ('#':_) = False
237nonComment ('|':_) = False -- hashed, undecodeable
238nonComment _ = True
239
240getWithPort :: String -> String
241getWithPort ('[':str) = host ++ " -p " ++ port
242 where (host,p) = break (==']') str
243 port = case p of
244 ']':':':x -> x
245 _ -> "22"
246getWithPort str = str
diff --git a/accounts/gkleen@sif/xmonad/package.yaml b/accounts/gkleen@sif/xmonad/package.yaml
deleted file mode 100644
index f65137af..00000000
--- a/accounts/gkleen@sif/xmonad/package.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
1name: xmonad-yggdrasil
2
3executables:
4 xmonad:
5 dependencies:
6 - base
7 - xmonad
8 - xmonad-contrib
9 - aeson
10 - bytestring
11 - text
12 - temporary
13 - filepath
14 - directory
15 - network
16 - unix
17 - utf8-string
18 - parsec
19 - process
20 - mtl
21 - X11
22 - transformers
23 - containers
24 - hostname
25 - libnotify
26 - taffybar
27
28 main: xmonad.hs
29 source-dirs:
30 - .
31 - lib
diff --git a/accounts/gkleen@sif/xmonad/stack.nix b/accounts/gkleen@sif/xmonad/stack.nix
deleted file mode 100644
index 17a49e04..00000000
--- a/accounts/gkleen@sif/xmonad/stack.nix
+++ /dev/null
@@ -1,17 +0,0 @@
1{ ghc, nixpkgs ? import ./nixpkgs.nix {} }:
2
3let
4 haskellPackages = import ./stackage.nix { inherit nixpkgs; };
5 inherit (nixpkgs {}) pkgs;
6in pkgs.haskell.lib.buildStackProject {
7 inherit ghc;
8 inherit (haskellPackages) stack;
9 name = "stackenv";
10 buildInputs = (with pkgs;
11 [ xorg.libX11 xorg.libXrandr xorg.libXinerama xorg.libXScrnSaver xorg.libXext xorg.libXft
12 cairo
13 glib
14 ]) ++ (with haskellPackages;
15 [
16 ]);
17}
diff --git a/accounts/gkleen@sif/xmonad/stack.yaml b/accounts/gkleen@sif/xmonad/stack.yaml
deleted file mode 100644
index b8ed1147..00000000
--- a/accounts/gkleen@sif/xmonad/stack.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
1nix:
2 enable: true
3 shell-file: stack.nix
4
5resolver: lts-13.21
6
7packages:
8 - .
9
10extra-deps: []
diff --git a/accounts/gkleen@sif/xmonad/xmonad-yggdrasil.nix b/accounts/gkleen@sif/xmonad/xmonad-yggdrasil.nix
deleted file mode 100644
index 7c853619..00000000
--- a/accounts/gkleen@sif/xmonad/xmonad-yggdrasil.nix
+++ /dev/null
@@ -1,21 +0,0 @@
1{ mkDerivation, aeson, base, bytestring, containers, directory
2, filepath, hostname, hpack, mtl, network, parsec, process, lib
3, temporary, transformers, unix, utf8-string, X11, xmonad
4, xmonad-contrib, libnotify, taffybar
5}:
6mkDerivation {
7 pname = "xmonad-yggdrasil";
8 version = "0.0.0";
9 src = ./.;
10 isLibrary = false;
11 isExecutable = true;
12 libraryToolDepends = [ hpack ];
13 executableHaskellDepends = [
14 aeson base bytestring containers directory filepath hostname mtl
15 network parsec process temporary transformers unix utf8-string X11
16 xmonad xmonad-contrib libnotify taffybar
17 ];
18 preConfigure = "hpack";
19 license = "unknown";
20 hydraPlatforms = lib.platforms.none;
21}
diff --git a/accounts/gkleen@sif/xmonad/xmonad.hs b/accounts/gkleen@sif/xmonad/xmonad.hs
deleted file mode 100644
index a44d3bb7..00000000
--- a/accounts/gkleen@sif/xmonad/xmonad.hs
+++ /dev/null
@@ -1,939 +0,0 @@
1{-# LANGUAGE TupleSections, ViewPatterns, OverloadedStrings, FlexibleInstances, UndecidableInstances, MultiWayIf, NumDecimals #-}
2
3import XMonad
4import XMonad.Hooks.DynamicLog
5import XMonad.Hooks.ManageDocks
6import XMonad.Util.Run hiding (proc)
7import XMonad.Util.Loggers
8import XMonad.Util.EZConfig(additionalKeys)
9import System.IO
10import System.IO.Error
11import System.Environment
12import Data.Map (Map)
13import qualified Data.Map as Map
14import qualified XMonad.StackSet as W
15import System.Exit
16import Control.Monad.State (get)
17-- import XMonad.Layout.Spiral
18import Data.Ratio
19import Data.List
20import Data.Char
21import Data.Maybe (fromMaybe, listToMaybe, maybeToList, catMaybes, isJust)
22import XMonad.Layout.Tabbed
23import XMonad.Prompt
24import XMonad.Prompt.Input
25import XMonad.Util.Scratchpad
26import XMonad.Util.NamedScratchpad
27import XMonad.Util.Ungrab
28import Control.Monad (sequence, liftM, liftM2, join, void)
29import XMonad.Util.WorkspaceCompare
30import XMonad.Layout.NoBorders
31import XMonad.Layout.PerWorkspace
32import XMonad.Layout.SimplestFloat
33import XMonad.Layout.Renamed
34import XMonad.Layout.Reflect
35import XMonad.Layout.OnHost
36import XMonad.Layout.Combo
37import XMonad.Layout.ComboP
38import XMonad.Layout.Column
39import XMonad.Layout.TwoPane
40import XMonad.Layout.IfMax
41import XMonad.Layout.LayoutBuilder
42import XMonad.Layout.WindowNavigation
43import XMonad.Layout.Dwindle
44import XMonad.Layout.TrackFloating
45import System.Process
46import System.Directory (removeFile)
47import System.Posix.Files
48import System.FilePath ((</>))
49import Control.Concurrent
50import System.Posix.Process (getProcessID)
51import System.IO.Error
52import System.IO
53import XMonad.Hooks.ManageHelpers hiding (CW)
54import XMonad.Hooks.UrgencyHook as U
55import XMonad.Hooks.EwmhDesktops
56import XMonad.StackSet (RationalRect (..))
57import Control.Monad (when, filterM, (<=<))
58import Graphics.X11.ExtraTypes.XF86
59import XMonad.Util.Cursor
60import XMonad.Actions.Warp
61import XMonad.Actions.FloatKeys
62import XMonad.Util.SpawnOnce
63import System.Directory
64import System.FilePath
65import XMonad.Actions.CopyWindow
66import XMonad.Hooks.ServerMode
67import XMonad.Actions.Commands
68import XMonad.Actions.CycleWS
69import XMonad.Actions.RotSlaves
70import XMonad.Actions.UpdatePointer
71import XMonad.Prompt.Window
72import Data.IORef
73import Data.Monoid
74import Data.String
75import qualified XMonad.Actions.PhysicalScreens as P
76
77import XMonad.Layout.IM
78
79import System.Taffybar.Support.PagerHints (pagerHints)
80
81import XMonad.Prompt.MyShell
82import XMonad.Prompt.MyPass
83import XMonad.Prompt.MySsh
84
85import XMonad.Mpv
86
87import Network.HostName
88
89import Control.Applicative ((<$>))
90
91import Libnotify as Notify hiding (appName)
92import qualified Libnotify as Notify (appName)
93import Libnotify (Notification)
94-- import System.Information.Battery
95
96import Data.Int (Int32)
97
98import System.Posix.Process
99import System.Posix.Signals
100import System.Posix.IO as Posix
101import Control.Exception
102
103import System.IO.Unsafe
104
105import Control.Monad.Trans.Class
106import Control.Monad.Trans.Maybe
107
108import Data.Fixed (Micro)
109
110import qualified Data.Text as Text
111import Data.Ord (comparing)
112import Debug.Trace
113
114instance MonadIO m => IsString (m ()) where
115 fromString = spawn
116
117type KeyMap = Map (ButtonMask, KeySym) (X ())
118
119data Host = Host
120 { hName :: HostName
121 , hManageHook :: ManageHook
122 , hWsp :: Integer -> WorkspaceId
123 , hCoWsp :: String -> Maybe WorkspaceId
124 , hKeysMod :: XConfig Layout -> (KeyMap -> KeyMap)
125 , hScreens :: [P.PhysicalScreen]
126 , hKbLayouts :: [(String, Maybe String)]
127 , hCmds :: X [(String, X ())]
128 , hKeyUpKeys :: XConfig Layout -> KeyMap
129 }
130
131defaultHost = Host { hName = "unkown"
132 , hManageHook = composeOne [manageScratchTerm]
133 , hWsp = show
134 , hCoWsp = const Nothing
135 , hKeysMod = const id
136 , hScreens = [0,1..]
137 , hKbLayouts = [ ("us", Just "dvp")
138 , ("us", Nothing)
139 , ("de", Nothing)
140 ]
141 , hCmds = return []
142 , hKeyUpKeys = const Map.empty
143 }
144
145browser :: String
146browser = "env MOZ_USE_XINPUT2=1 firefox"
147
148gray, darkGray, red, green :: String
149gray = "#808080"
150darkGray = "#202020"
151red = "#800000"
152green = "#008000"
153
154hostFromName :: HostName -> Host
155hostFromName h@("vali") = defaultHost { hName = h
156 , hManageHook = composeOne $ catMaybes [ Just manageScratchTerm
157 , assign "web" $ className =? ".dwb-wrapped"
158 , assign "web" $ className =? "Chromium"
159 , assign "work" $ className =? "Emacs"
160 , assign "media" $ className =? "mpv"
161 ]
162 , hWsp = hWsp
163 , hCoWsp = hCoWsp
164 , hKeysMod = \conf -> Map.union $ (Map.fromList $ join $ map (spawnBindings conf) [ (xK_d, ["chromium", "chromium $(xclip -o)"])
165 , (xK_e, ["emacsclient -c"])
166 ])
167 `Map.union`
168 ( Map.fromList [ ((XMonad.modMask conf .|. controlMask, xK_Return), scratchpadSpawnActionCustom $ (XMonad.terminal conf) ++ " -name scratchpad -title scratchpad -e tmux new-session -D -s scratch")
169 ] )
170 , hScreens = hScreens defaultHost
171 }
172 where
173 workspaceNames = Map.fromList [ (2, "web")
174 , (3, "work")
175 , (10, "media")
176 ]
177 hWsp = wspFromMap workspaceNames
178 hCoWsp = coWspFromMap workspaceNames
179 assign wsp test = (\wsp -> test -?> doShift wsp) <$> hCoWsp wsp
180hostFromName h
181 | h `elem` ["hel", "sif"] = defaultHost { hName = h
182 , hManageHook = namedScratchpadManageHook scratchpads <+> composeOne (catMaybes
183 [ assign "mpv" $ className =? "mpv"
184 , assign "mpv" $ stringProperty "WM_WINDOW_ROLE" =? "presentation"
185 , assign "read" $ stringProperty "WM_WINDOW_ROLE" =? "presenter"
186 , assign "mpv" $ className =? "factorio"
187 , assign "mpv" $ resource =? "twitch"
188 , assign "web" $ className =? "chromium-browser"
189 , assign "web" $ className =? "Google-chrome"
190 , assign "work" $ (appName =? "Devtools" <&&> className =? "firefox")
191 , assign "work" $ className =? "Postman"
192 , assign "web" $ (appName =? "Navigator" <&&> className =? "firefox")
193 , assign "comm" $ (className =? "Emacs" <&&> title =? "Mail")
194 , assign "comm" $ className =? "Zulip"
195 , assign "comm" $ className =? "Element"
196 , assign "comm" $ className =? "Rocket.Chat"
197 , assign "comm" $ className =? "Discord"
198 , assign "comm" $ className =? "Rainbow"
199 , assign "media" $ resource =? "media"
200 , assign "monitor" $ className =? "Grafana"
201 , assign "monitor" $ className =? "Virt-viewer"
202 , assign "monitor" $ resource =? "htop"
203 , assign "monitor" $ resource =? "monitor"
204 , assign "monitor" $ className =? "xfreerdp"
205 , assign "monitor" $ className =? "org.remmina.Remmina"
206 , Just $ resource =? "htop" -?> centerFloat
207 , Just $ (className =? "Scp-dbus-service.py") -?> centerFloat
208 , Just $ resource =? "log" -?> centerFloat
209 , assign "work" $ className =? "Alacritty"
210 , Just $ (appName =? "Edit with Emacs FRAME") -?> centerFloat
211 , assign' ["work", "uni"] $ (className =? "Emacs" <&&> appName /=? "Edit with Emacs FRAME")
212 , assign' ["work", "uni"] $ className =? "jetbrains-idea-ce"
213 , assign "read" $ className =? "llpp"
214 , assign "read" $ className =? "Evince"
215 , assign "read" $ className =? "Zathura"
216 , assign "read" $ className =? "MuPDF"
217 , assign "read" $ className =? "Xournal"
218 , assign "read" $ appName =? "libreoffice"
219 , assign "read" $ appName =? "com-trollworks-gcs-app-GCS"
220 , assign "read" $ appName =? "Tux.py"
221 , assign "read" $ className =? "Gnucash"
222 , assign "comm" $ className =? "Skype"
223 , assign "comm" $ className =? "Daily"
224 , assign "comm" $ className =? "Pidgin"
225 , assign "comm" $ className =? "Thunderbird"
226 , assign "comm" $ className =? "Slack"
227 , Just $ (resource =? "xvkbd") -?> doRectFloat $ RationalRect (1 % 8) (3 % 8) (6 % 8) (4 % 8)
228 , Just $ (stringProperty "_NET_WM_WINDOW_TYPE" =? "_NET_WM_WINDOW_TYPE_DIALOG") -?> doFloat
229 , Just $ (className =? "Dunst") -?> doFloat
230 , Just $ (className =? "Xmessage") -?> doCenterFloat
231 , Just $ (className =? "Nm-openconnect-auth-dialog") -?> centerFloat
232 , Just $ (className =? "Pinentry") -?> doCenterFloat
233 , Just $ (className =? "pinentry") -?> doCenterFloat
234 , Just $ (stringProperty "WM_WINDOW_ROLE" =? "GtkFileChooseDialog") -?> centerFloatSmall
235 , Just $ (className =? "Nvidia-settings") -?> doCenterFloat
236 , Just $ fmap ("Minetest" `isInfixOf`) title -?> doIgnore
237 , Just $ fmap ("Automachef" `isInfixOf`) title -?> doIgnore
238 , assign "call" $ className =? "zoom"
239 ])
240 , hWsp = hWsp
241 , hCoWsp = hCoWsp
242 , hKeysMod = \conf -> Map.union $ (Map.fromList $ join $ map (spawnBindings conf) [ (xK_e, ["emacsclient -c"])
243 , (xK_d, [fromString browser, "google-chrome" {- , "notmuch-links" -}])
244 , (xK_c, [ inputPrompt xPConfigMonospace "dc" ?+ dc ])
245 , (xK_g, ["pidgin"])
246 , (xK_s, ["skype"])
247 -- , (xK_p, [mkPassPrompt "Type password" pwType xPConfig, mkPassPrompt "Show password" pwShow xPConfig, mkPassPrompt "Copy password" pwClip xPConfig])
248 , (xK_w, ["sudo rewacom"])
249 , (xK_y, [ "tmux new-window -dt media /var/media/link.hs $(xclip -o)"
250 , "tmux new-window -dt media /var/media/download.hs $(xclip -o)"
251 , "tmux new-window -dt media /var/media/download.hs $(xclip -o -selection clipboard)"
252 ])
253 , (xK_l, [ "tmux new-window -dt media mpv $(xclip -o)"
254 , "tmux new-window -dt media mpv $(xclip -o -selection clipboard)"
255 , "alacritty --class media -e tmuxp load /var/media"
256 ])
257 {- , (xK_m, [ "emacsclient -c -F \"'(title . \\\"Mail\\\")\" -e '(notmuch)'"
258 , "emacsclient -c -F \"'(title . \\\"Mail\\\")\" -e '(notmuch-mua-new-mail)'"
259 , "emacsclient -c -F \"'(title . \\\"Mail\\\")\" -e \"(browse-url-mail \"$(xclip -o)\")\""
260 ]) -}
261 , (xK_Return, ["keynav start,windowzoom", "keynav start"])
262 , (xK_t, [inputPrompt xPConfigMonospace "fuzzytime timer" ?+ fuzzytime, fuzzytime "unset", work_fuzzytime])
263 , (xK_a, [inputPrompt xPConfigMonospace "adjmix" ?+ adjmix])
264 , (xK_s, [ inputPromptWithCompl xPConfigMonospace "start synergy" synergyCompl ?+ synergyStart
265 , inputPromptWithCompl xPConfigMonospace "stop synergy" synergyCompl ?+ synergyStop
266 ])
267 , (xK_h, [ "alacritty --class htop -e htop"
268 , "alacritty --class log -e journalctl -xef"
269 ])
270 , (xK_x, [ "autorandr -c"
271 , "autorandr -fl def"
272 ])
273 , (xK_z, [ "zulip -- --force-device-scale-factor=2"
274 ])
275 ])
276 `Map.union`
277 ( Map.fromList [ ((XMonad.modMask conf .|. controlMask, xK_Return), namedScratchpadAction scratchpads "term")
278 , ((XMonad.modMask conf .|. controlMask, xK_a), namedScratchpadAction scratchpads "pavucontrol")
279 , ((XMonad.modMask conf .|. controlMask, xK_o), namedScratchpadAction scratchpads "easyeffects")
280 , ((XMonad.modMask conf .|. controlMask .|. shiftMask, xK_o), namedScratchpadAction scratchpads "helvum")
281 , ((XMonad.modMask conf .|. controlMask, xK_w), namedScratchpadAction scratchpads "alarms")
282 , ((XMonad.modMask conf .|. controlMask, xK_b), namedScratchpadAction scratchpads "blueman")
283 , ((XMonad.modMask conf .|. controlMask, xK_p), namedScratchpadAction scratchpads "keepassxc")
284 , ((XMonad.modMask conf .|. controlMask, xK_t), namedScratchpadAction scratchpads "toggl")
285 , ((XMonad.modMask conf .|. controlMask, xK_e), namedScratchpadAction scratchpads "emacs")
286 , ((XMonad.modMask conf .|. controlMask, xK_m), namedScratchpadAction scratchpads "calendar")
287 , ((XMonad.modMask conf .|. controlMask, xK_f), namedScratchpadAction scratchpads "music")
288 , ((XMonad.modMask conf .|. mod1Mask, xK_Up), rotate U)
289 , ((XMonad.modMask conf .|. mod1Mask, xK_Down), rotate D)
290 , ((XMonad.modMask conf .|. mod1Mask, xK_Left), rotate L)
291 , ((XMonad.modMask conf .|. mod1Mask, xK_Right), rotate R)
292 , ((controlMask, xK_space ), "dunstctl close" )
293 , ((controlMask .|. shiftMask, xK_space ), "dunstctl close-all" )
294 , ((controlMask, xK_period), "dunstctl context" )
295 , ((controlMask, xK_comma ), "dunstctl history-pop")
296 -- , ((XMonad.modMask conf .|. shiftMask, xK_a), startMute "hel")
297 ] )
298 , hKeyUpKeys = \conf -> Map.fromList [ -- ((XMonad.modMask conf .|. shiftMask, xK_a), stopMute "hel")
299 ]
300 , hScreens = hScreens defaultHost
301 , hCmds = return [ ("prev-workspace", prevWS)
302 , ("next-workspace", nextWS)
303 , ("prev-window", rotAllDown)
304 , ("next-window", rotAllUp)
305 , ("banish", banishScreen LowerRight)
306 , ("update-gpg-tty", safeSpawn "gpg-connect-agent" ["UPDATESTARTUPTTY", "/bye"])
307 , ("rescreen", rescreen)
308 , ("repanel", do
309 spawn "nm-applet"
310 spawn "blueman-applet"
311 spawn "pasystray"
312 spawn "kdeconnect-indicator"
313 spawn "dunst -print"
314 spawn "udiskie"
315 spawn "autocutsel -s PRIMARY"
316 spawn "autocutsel -s CLIPBOARD"
317 )
318 , ("pause", mediaMpv $ MpvSetProperty "pause" True)
319 , ("unpause", mediaMpv $ MpvSetProperty "pause" False)
320 , ("exit", io $ exitWith ExitSuccess)
321 ]
322 }
323 where
324 withGdkScale act = void . xfork $ setEnv "GDK_SCALE" "2" >> act
325 workspaceNames = Map.fromList [ (1, "comm")
326 , (2, "web")
327 , (3, "work")
328 , (4, "read")
329 , (5, "monitor")
330 , (6, "uni")
331 , (8, "call")
332 , (9, "media")
333 , (10, "mpv")
334 ]
335 scratchpads = [ NS "term" "alacritty --class scratchpad --title scratchpad -e tmux new-session -AD -s scratch" (resource =? "scratchpad") centerFloat
336 , NS "pavucontrol" "pavucontrol" (resource =? "pavucontrol") centerFloat
337 , NS "helvum" "helvum" (resource =? "helvum") centerFloat
338 , NS "easyeffects" "easyeffects" (resource =? "easyeffects") centerFloat
339 , NS "alarms" "alarm-clock-applet" (className =? "Alarm-clock-applet" <&&> title =? "Alarms") centerFloat
340 , NS "blueman" "blueman-manager" (className =? ".blueman-manager-wrapped") centerFloat
341 , NS "keepassxc" "keepassxc" (className =? "KeePassXC") centerFloat
342 , NS "toggl" "toggldesktop" (className =? "Toggl Desktop") centerFloat
343 , NS "calendar" "minetime -- --force-device-scale-factor=1.6" (className =? "MineTime") centerFloat
344 , NS "emacs" "emacsclient -c -F \"'(title . \\\"Scratchpad\\\")\"" (className =? "Emacs" <&&> title =? "Scratchpad") centerFloat
345 , NS "music" "ytmdesktop" (className =? "youtube-music-desktop-app") centerFloat
346 ]
347 centerFloat = customFloating $ RationalRect (1 % 16) (1 % 16) (7 % 8) (7 % 8)
348 centerFloatSmall = customFloating $ RationalRect (1 % 4) (1 % 4) (1 % 2) (1 % 2)
349 hWsp = wspFromMap workspaceNames
350 hCoWsp = coWspFromMap workspaceNames
351 assign wsp test = (\wsp -> test -?> doShift wsp) <$> hCoWsp wsp
352 assign' :: [String] -> Query Bool -> Maybe MaybeManageHook
353 assign' wsps test = do
354 wsIds <- mapM hCoWsp wsps
355 return $ test -?> go wsIds
356 where
357 go :: [WorkspaceId] -> ManageHook
358 go wsps = do
359 visWsps <- liftX $ (\wset -> W.tag . W.workspace <$> W.current wset : W.visible wset) <$> gets windowset
360 case (filter (`elem` visWsps) wsps, wsps) of
361 (wsp : _, _) -> doShift wsp
362 (_, wsp : _) -> doShift wsp
363 ([], []) -> return mempty
364 rotate rot = do
365 safeSpawn "xrandr" ["--output", "eDP-1", "--rotate", xrandrDir]
366 mapM_ rotTouch touchscreens
367 where
368 xrandrDir = case rot of
369 U -> "normal"
370 L -> "left"
371 R -> "right"
372 D -> "inverted"
373 matrix = case rot of
374 U -> [ [ 1, 0, 0]
375 , [ 0, 1, 0]
376 , [ 0, 0, 1]
377 ]
378 L -> [ [ 0, -1, 1]
379 , [ 1, 0, 0]
380 , [ 0, 0, 1]
381 ]
382 R -> [ [ 0, 1, 0]
383 , [-1, 0, 1]
384 , [ 0, 0, 1]
385 ]
386 D -> [ [-1, 0, 1]
387 , [ 0, -1, 1]
388 , [ 0, 0, 1]
389 ]
390 touchscreens = [ "Wacom Co.,Ltd. Pen and multitouch sensor Finger touch"
391 , "Wacom Co.,Ltd. Pen and multitouch sensor Pen stylus"
392 , "Wacom Co.,Ltd. Pen and multitouch sensor Pen eraser"
393 ]
394 rotTouch screen = do
395 safeSpawn "xinput" $ ["set-prop", screen, "Coordinate Transformation Matrix"] ++ map (\n -> show n ++ ",") (concat matrix)
396 safeSpawn "xinput" ["map-to-output", screen, "eDP-1"]
397 withPw f label = io . void . forkProcess $ do
398 uninstallSignalHandlers
399 void $ createSession
400 (dropWhileEnd isSpace -> pw) <- readCreateProcess (proc "pass" ["show", label]) ""
401 void $ f pw
402 pwType :: String -> X ()
403 pwType = withPw $ readCreateProcess (proc "xdotool" ["type", "--clearmodifiers", "--file", "-"])
404 pwClip label = safeSpawn "pass" ["show", "--clip", label]
405 pwShow :: String -> X ()
406 pwShow = withPw $ \pw -> do
407 xmessage <- fromMaybe "xmessage" <$> liftIO (lookupEnv "XMONAD_XMESSAGE")
408 readCreateProcess (proc xmessage ["-file", "-"]) pw
409 fuzzytime str = safeSpawn "fuzzytime" $ "timer" : words str
410 work_fuzzytime = io . void . forkProcess $ do
411 readCreateProcess (proc "worktime" []) "" >>= safeSpawn "fuzzytime" . ("timer" : ) . pure
412 adjmix str = safeSpawn "adjmix" $ words str
413 dc expr = void . xfork $ do
414 result <- readProcess "dc" [] $ expr ++ "f"
415 let
416 (first : rest) = filter (not . null) $ lines result
417 notification = Notify.summary first <> Notify.body (unlines rest) <> Notify.timeout Infinite <> Notify.urgency Normal <> Notify.appName "dc"
418 void $ Notify.display notification
419 synergyCompl = mkComplFunFromList' xPConfigMonospace ["mathw86"]
420 synergyStart host = safeSpawn "systemctl" ["--user", "start", "synergy-rtunnel@" ++ host ++ ".service"]
421 synergyStop host = safeSpawn "systemctl" ["--user", "stop", "synergy-rtunnel@" ++ host ++ ".service"]
422
423hostFromName _ = defaultHost
424
425-- muteRef :: IORef (Maybe (String, Notification))
426-- {-# NOINLINE muteRef #-}
427-- muteRef = unsafePerformIO $ newIORef Nothing
428
429-- startMute, stopMute :: String -> X ()
430-- startMute sink = liftIO $ do
431-- muted <- isJust <$> readIORef muteRef
432-- when (not muted) $ do
433-- let
434-- notification = Notify.summary "Muted" <> Notify.timeout Infinite <> Notify.urgency Normal
435-- level = "0.0dB"
436-- -- level <- runProcessWithInput "ssh" ["bragi", "cat", "/dev/shm/mix/" ++ sink ++ "/level"] ""
437-- -- callProcess "ssh" ["bragi", "adjmix", "-t", sink, "-o", "0"]
438-- hPutStrLn stderr "Mute"
439-- writeIORef muteRef . Just . (level, ) =<< Notify.display notification
440-- stopMute sink = liftIO $ do
441-- let
442-- unmute (Just (level, notification)) = do
443-- hPutStrLn stderr "Unmute"
444-- -- callProcess "ssh" ["bragi", "adjmix", "-t", sink, "-o", level]
445-- Notify.close notification
446-- unmute Nothing = return ()
447-- muted <- isJust <$> readIORef muteRef
448-- when muted . join . atomicModifyIORef muteRef $ (Nothing, ) . unmute
449
450wspFromMap workspaceNames = \i -> case Map.lookup i workspaceNames of
451 Just str -> show i ++ " " ++ str
452 Nothing -> show i
453
454coWspFromMap workspaceNames = \str -> case filter ((== str) . snd) $ Map.toList workspaceNames of
455 [] -> Nothing
456 [(i, _)] -> Just $ wspFromMap workspaceNames i
457 _ -> Nothing
458
459spawnModifiers = [0, controlMask, shiftMask .|. controlMask]
460spawnBindings :: XConfig layout -> (KeySym, [X ()]) -> [((KeyMask, KeySym), X ())]
461spawnBindings conf (k, cmds) = zipWith (\m cmd -> ((modm .|. mod1Mask .|. m, k), cmd)) spawnModifiers cmds
462 where
463 modm = XMonad.modMask conf
464
465manageScratchTerm = (resource =? "scratchpad" <||> resource =? "keysetup") -?> doRectFloat $ RationalRect (1 % 16) (1 % 16) (7 % 8) (7 % 8)
466
467tabbedLayout t = renamed [Replace "Tabbed"] $ reflectHoriz $ t CustomShrink $ tabbedTheme
468tabbedLayoutHoriz t = renamed [Replace "Tabbed Horiz"] $ reflectVert $ t CustomShrink $ tabbedTheme
469tabbedTheme = def
470 { activeColor = "black"
471 , inactiveColor = "black"
472 , urgentColor = "black"
473 , activeBorderColor = gray
474 , inactiveBorderColor = darkGray
475 , urgentBorderColor = red
476 , activeTextColor = gray
477 , inactiveTextColor = gray
478 , urgentTextColor = gray
479 , decoHeight = 32
480 , fontName = "xft:Fira Sans:pixelsize=21"
481 }
482
483main :: IO ()
484main = do
485 arguments <- either (const []) id <$> tryIOError getArgs
486 case arguments of
487 ["--command", s] -> do
488 d <- openDisplay ""
489 rw <- rootWindow d $ defaultScreen d
490 a <- internAtom d "XMONAD_COMMAND" False
491 m <- internAtom d s False
492 allocaXEvent $ \e -> do
493 setEventType e clientMessage
494 setClientMessageEvent e rw a 32 m currentTime
495 sendEvent d rw False structureNotifyMask e
496 sync d False
497 _ -> do
498 -- batteryMon <- xfork $ monitorBattery Nothing Nothing
499 hostname <- getHostName
500 let
501 host = hostFromName hostname
502 setEnv "HOST" hostname
503 let myConfig = withHostUrgency . ewmhFullscreen . ewmh . pagerHints $ docks def
504 { manageHook = hManageHook host
505 , terminal = "alacritty"
506 , layoutHook = smartBorders . avoidStruts $ windowNavigation layout'
507 , logHook = do
508 dynamicLogString xmobarPP' >>= writeProps
509 updatePointer (99 % 100, 98 % 100) (0, 0)
510 , modMask = mod4Mask
511 , keys = \conf -> hKeysMod host conf $ myKeys' conf host
512 , workspaces = take (length numKeys) $ map wsp [1..]
513 , startupHook = setDefaultCursor xC_left_ptr
514 , normalBorderColor = darkGray
515 , focusedBorderColor = gray
516 , handleEventHook = serverModeEventHookCmd' (hCmds host) <+> keyUpEventHook
517 }
518 writeProps str = do
519 let encodeCChar = map $ fromIntegral . fromEnum
520 atoms = [ "_XMONAD_WORKSPACES"
521 , "_XMONAD_LAYOUT"
522 , "_XMONAD_TITLE"
523 ]
524 (flip mapM_) (zip atoms (lines str)) $ \(atom', content) -> do
525 ustring <- getAtom "UTF8_STRING"
526 atom <- getAtom atom'
527 withDisplay $ \dpy -> io $ do
528 root <- rootWindow dpy $ defaultScreen dpy
529 changeProperty8 dpy root atom ustring propModeReplace $ encodeCChar content
530 sync dpy True
531 wsp = hWsp host
532 -- We can´t define per-host layout modifiers because we lack dependent types
533 layout' = onHost "skadhi" ( onWorkspace (wsp 1) (Full ||| withIM (1%5) (Title "Buddy List") tabbedLayout') $
534 onWorkspace (wsp 10) Full $
535 onWorkspace (wsp 2) (Full ||| tabbedLayout') $
536 onWorkspace (wsp 5) tabbedLayout' $
537 onWorkspace (wsp 8) (withIM (1%5) (Title "Friends") tabbedLayout') $
538 defaultLayouts
539 ) $
540 onHost "vali" ( onWorkspace (wsp 2) (Full ||| tabbedLayout' ||| combineTwo (TwoPane 0.01 0.57) Full tabbedLayout') $
541 onWorkspace (wsp 3) workLayouts $
542 defaultLayouts
543 ) $
544 onHost "hel" ( onWorkspace (wsp 1) (withIM (1 % 8) (Title "Buddy List") $ trackFloating tabbedLayout') $
545 onWorkspace (wsp 2) (tabbedLayout''' ||| Dwindle R CW 1 (5 % 100)) $
546 onWorkspace (wsp 3) workLayouts $
547 onWorkspace (wsp 6) workLayouts $
548 onWorkspace (wsp 4) (tabbedLayout' ||| tabbedLayoutHoriz' ||| Dwindle R CW 1 (5 % 100)) $
549 onWorkspace (wsp 5) (tabbedLayout''' ||| Dwindle R CW 1 (5 % 100)) $
550 onWorkspace (wsp 10) (tabbedLayout''' ||| combineTwoP (TwoPane (1 % 100) (3 % 4)) tabbedLayout''' tabbedLayout''' (ClassName "mpv") ||| Dwindle R CW 1 (5 % 100)) $
551 defaultLayouts
552 ) $
553 onHost "sif" ( onWorkspace (wsp 1) (withIM (1 % 8) (Title "Buddy List") $ trackFloating tabbedLayout') $
554 onWorkspace (wsp 2) (tabbedLayout''' ||| Dwindle R CW 1 (5 % 100)) $
555 onWorkspace (wsp 3) workLayouts $
556 onWorkspace (wsp 6) workLayouts $
557 onWorkspace (wsp 4) (tabbedLayout' ||| tabbedLayoutHoriz' ||| Dwindle R CW 1 (5 % 100)) $
558 onWorkspace (wsp 5) (tabbedLayout''' ||| Dwindle R CW 1 (5 % 100)) $
559 onWorkspace (wsp 8) tabbedLayout''' $
560 onWorkspace (wsp 10) (tabbedLayout''' ||| combineTwoP (TwoPane (1 % 100) (3 % 4)) tabbedLayout''' tabbedLayout''' (ClassName "mpv") ||| Dwindle R CW 1 (5 % 100)) $
561 defaultLayouts
562 ) $
563 defaultLayouts
564 -- tabbedLayout''' = renamed [Replace "Tabbed'"] $ IfMax 1 (noBorders Full) (tabbedLayout tabbedBottomAlways)
565 tabbedLayout''' = tabbedLayout tabbedBottom
566 tabbedLayout' = tabbedLayout tabbedBottomAlways
567 tabbedLayoutHoriz' = tabbedLayoutHoriz tabbedLeftAlways
568 defaultLayouts = {- spiralWithDir East CW (1 % 2) -} Dwindle R CW 1 (5 % 100) ||| tabbedLayout' ||| Full
569 -- workLayouts = {- spiralWithDir East CW (1 % 2) -} Dwindle R CW (2 % 1) (5 % 100) ||| tabbedLayout' ||| Full
570 workLayouts = tabbedLayout' ||| (renamed [Replace "Combined"] $ combineTwoP (TwoPane (1 % 100) (1891 % 2560)) tabbedLayout''' (Column 1.6) (ClassName "Postman" `Or` ClassName "Emacs" `Or` ClassName "jetbrains-idea-ce" `Or` (Resource "Devtools" `And` ClassName "Firefox"))) ||| Full ||| Dwindle R CW 1 (5 % 100)
571 sqrtTwo = approxRational (sqrt 2) (1 / 2560)
572 xmobarPP' = xmobarPP { ppTitle = shorten 80
573 , ppSort = (liftM2 (.)) getSortByIndex $ return scratchpadFilterOutWorkspace
574 , ppUrgent = wrap "(" ")" . xmobarColor "#800000" ""
575 , ppHiddenNoWindows = xmobarColor "#202020" "" . wrap "(" ")"
576 , ppVisible = wrap "(" ")" . xmobarColor "#808000" ""
577 , ppCurrent = wrap "(" ")" . xmobarColor "#008000" ""
578 , ppHidden = wrap "(" ")"
579 , ppWsSep = " "
580 , ppSep = "\n"
581 }
582 withHostUrgency = case hostname of
583 "sif" -> withUrgencyHookC urgencyHook' $ def { suppressWhen = U.Never, remindWhen = Every 2 }
584 _ -> id
585 urgencyHook' window = do
586 let blinkLight = (lightHigh >> threadDelay 0.5e6) `finally` lightLow
587 where
588 lightHigh =
589 writeFile "/sys/class/leds/input0::capslock/brightness" =<< readFile "/sys/class/leds/input0::capslock/max_brightness"
590 lightLow = writeFile "/sys/class/leds/input0::capslock/brightness" "0"
591 runQuery ((resource =? "comm" <||> resource =? "Pidgin" <||> className =? "Gajim" <||> className =? "Skype" <||> className =? "Thunderbird") --> void (xfork blinkLight)) window
592 urgencyHook (BorderUrgencyHook { urgencyBorderColor = red }) window
593 shutdown :: SomeException -> IO a
594 shutdown e = do
595 let pids = [ -- batteryMon
596 ]
597 mapM_ (signalProcess sigTERM) pids
598 mapM_ (getProcessStatus False False) pids
599 throw e
600 keyUpEventHook :: Event -> X All
601 keyUpEventHook event = handle event >> return (All True)
602 where
603 handle (KeyEvent { ev_event_type = t, ev_state = m, ev_keycode = code })
604 | t == keyRelease = withDisplay $ \dpy -> do
605 s <- io $ keycodeToKeysym dpy code 0
606 mClean <- cleanMask m
607 ks <- asks $ hKeyUpKeys host . config
608 userCodeDef () $ whenJust (Map.lookup (mClean, s) ks) id
609 | otherwise = return ()
610 handle _ = return ()
611 handle shutdown $ launch myConfig =<< getDirectories
612
613secs :: Int -> Int
614secs = (* 1000000)
615
616-- monitorBattery :: Maybe BatteryContext -> Maybe Notification -> IO ()
617-- monitorBattery Nothing n = do
618-- ctx <- batteryContextNew
619-- case ctx of
620-- Nothing -> threadDelay (secs 10) >> monitorBattery Nothing n
621-- Just _ -> monitorBattery ctx n
622-- monitorBattery ctx@(Just ctx') n = do
623-- batInfo <- getBatteryInfo ctx'
624-- case batInfo of
625-- Nothing -> threadDelay (secs 1) >> monitorBattery ctx n
626-- Just batInfo -> do
627-- let n'
628-- | batteryState batInfo == BatteryStateDischarging
629-- , timeLeft <= 1200
630-- , timeLeft > 0 = Just $ summary "Discharging" <> hint "value" percentage <> urgency u <> body (duz timeLeft ++ "left")
631-- | otherwise = Nothing
632-- u
633-- | timeLeft <= 600 = Critical
634-- | timeLeft <= 1800 = Normal
635-- | otherwise = Low
636-- timeLeft = batteryTimeToEmpty batInfo
637-- percentage :: Int32
638-- percentage = round $ batteryPercentage batInfo
639-- ts = [("s", 60), ("m", 60), ("h", 24), ("d", 365), ("y", 1)]
640-- duz ms = ss
641-- where (ss, _) = foldl (\(ss, x) (s, y) -> ((if rem x y > 0 then show (rem x y) ++ s ++ " " else "") ++ ss , quot x y)) ("", ms) ts
642-- case n' of
643-- Just n' -> Notify.display (maybe mempty reuse n <> Notify.appName "monitorBattery" <> n') >>= (\n -> threadDelay (secs 2) >> monitorBattery ctx (Just n))
644-- Nothing -> threadDelay (secs 30) >> monitorBattery ctx n
645
646disableTouchpad, disableTrackpoint, enableTrackpoint, enableTouchpad :: X ()
647enableTouchpad = safeSpawn "xinput" ["enable", "SynPS/2 Synaptics TouchPad"]
648disableTouchpad = safeSpawn "xinput" ["disable", "SynPS/2 Synaptics TouchPad"]
649enableTrackpoint = safeSpawn "xinput" ["enable", "TPPS/2 IBM TrackPoint"]
650disableTrackpoint = safeSpawn "xinput" ["disable", "TPPS/2 IBM TrackPoint"]
651
652isDisabled :: String -> X Bool
653isDisabled str = do
654 out <- runProcessWithInput "xinput" ["list", str] ""
655 return $ "disabled" `isInfixOf` out
656
657
658spawnKeychain :: X ()
659spawnKeychain = do
660 home <- liftIO getHomeDirectory
661 let keys = (map ((home </>) . (".ssh/" ++)) ["id", "id-rsa"]) ++ ["6B13AA67"]
662 liftIO (maybe (return ()) (setEnv "SSH_ASKPASS") =<< findAskpass)
663 safeSpawn "keychain" . (["--agents", "gpg,ssh"] ++)=<< liftIO (filterM doesFileExist keys)
664 where
665 findAskpass = filter `liftM` readFile "/etc/zshrc"
666 filter = listToMaybe . catMaybes . map (stripPrefix "export SSH_ASKPASS=") . lines
667
668assimilateKeychain :: X ()
669assimilateKeychain = liftIO $ assimilateKeychain' >> return ()
670assimilateKeychain' = tryIOError $ do
671 -- pid <- getProcessID
672 -- tmpDir <- lookupEnv "TMPDIR"
673 -- let tmpDir' = fromMaybe "/tmp" tmpDir
674 -- tmpFile = tmpDir' </> "xmonad-keychain" ++ (show pid) ++ ".env"
675 env <- runProcessWithInput "sh" ["-c", "eval $(keychain --eval --noask --agents gpg,ssh); env"] "" -- > " ++ tmpFile] ""
676 -- env <- readFile tmpFile
677 let envVars = Map.fromList $ map (\(k, v) -> (k, tail' v)) $ map (span (/= '=')) $ envLines
678 envVars' = Map.filterWithKey (\k _ -> k `elem` transfer) envVars
679 transfer = ["SSH_AUTH_SOCK", "SSH_AGENT_PID", "GPG_AGENT_INFO"]
680 envLines = filter (elem '=') $ lines env :: [String]
681 sequence $ map (\(k, c) -> setEnv k c) $ Map.toList envVars'
682 -- removeFile tmpFile
683 where
684 tail' [] = []
685 tail' (x:xs) = xs
686
687
688numKeys = [xK_parenleft, xK_parenright, xK_braceright, xK_plus, xK_braceleft, xK_bracketright, xK_bracketleft, xK_exclam, xK_equal, xK_asterisk]
689
690instance Shrinker CustomShrink where
691 shrinkIt _ "" = [""]
692 shrinkIt s cs
693 | length cs >= 4 = cs : shrinkIt s ((reverse . drop 4 . reverse $ cs) ++ "...")
694 | otherwise = cs : shrinkIt s (init cs)
695
696xPConfig, xPConfigMonospace :: XPConfig
697xPConfig = def
698 { font = "xft:Fira Sans:pixelsize=21"
699 , height = 32
700 , bgColor = "black"
701 , fgColor = gray
702 , fgHLight = green
703 , bgHLight = "black"
704 , borderColor = gray
705 , searchPredicate = (\needle haystack -> all (`isInfixOf` map toLower haystack) . map (map toLower) $ words needle)
706 , position = Top
707 }
708xPConfigMonospace = xPConfig { font = "xft:Fira Code:pixelsize=21" }
709
710sshOverrides host = map (\h -> mkOverride { oHost = h, oCommand = moshCmd . inTmux host} )
711 [ "odin"
712 , "ymir"
713 , "surtr"
714 , "vidhar"
715 , "srv02.uniworx.de"
716 ]
717 ++
718 map (\h -> mkOverride { oHost = h, oCommand = moshCmd' "/run/current-system/sw/bin/mosh-server" . withEnv [("TERM", "xterm")] . inTmux host} )
719 [ "bragi", "bragi.asgard.yggdrasil"
720 ]
721 ++
722 map (\h -> mkOverride { oHost = h, oCommand = sshCmd . inTmux host } )
723 [ "uni2work-dev1", "srv01.uniworx.de"
724 ]
725 ++
726 map (\h -> mkOverride { oHost = h, oCommand = sshCmd . withEnv [("TERM", "xterm")] . inTmux host } )
727 [ "remote.cip.ifi.lmu.de"
728 , "uniworx3", "uniworx4", "uniworx5", "uniworxdb2"
729 , "testworx"
730 ]
731
732backlight :: (Rational -> Rational) -> X ()
733backlight f = void . xfork . liftIO $ do
734 [ _device
735 , _class
736 , read . Text.unpack -> currentBright
737 , _currentPercentage
738 , read . Text.unpack -> maximumBright
739 ] <- Text.splitOn "," . Text.pack <$> readProcess "brightnessctl" ["-m"] ""
740 let current = currentBright % maximumBright
741 new' = f current * fromIntegral maximumBright
742 new :: Integer
743 new | floor new' < 0 = 0
744 | ceiling new' > maximumBright = maximumBright
745 | new' >= maximumBright % 2 = ceiling new'
746 | otherwise = floor new'
747 callProcess "brightnessctl" ["-m", "s", show new]
748
749cycleThrough :: [Rational] -> (Rational -> Rational)
750cycleThrough opts current = fromMaybe currentOpt $ listToMaybe next'
751 where currentOpt = minimumBy (comparing $ abs . subtract current) opts
752 (_, _ : next') = break (== currentOpt) opts
753
754cycleKbLayout :: [(String, Maybe String)] -> X ()
755cycleKbLayout [] = return ()
756cycleKbLayout layouts = liftIO $ do
757 next <- (getNext . extract) `liftM` runProcessWithInput "setxkbmap" ["-query"] ""
758 let
759 args = case next of
760 (l, Just v) -> [l, v]
761 (l, Nothing) -> [l]
762 safeSpawn "setxkbmap" args
763 where
764 extract :: String -> Maybe (String, Maybe String)
765 extract str = listToMaybe $ do
766 ["layout:", l] <- str'
767 [(l, Just v) | ["variant:", v] <- str'] ++ pure (l, Nothing)
768 where
769 str' = map words $ lines str
770 getNext :: Maybe (String, Maybe String) -> (String, Maybe String)
771 getNext = maybe (head layouts) getNext'
772 getNext' x = case elemIndex x layouts of
773 Nothing -> getNext Nothing
774 Just i -> layouts !! ((i + 1) `mod` length layouts)
775
776mpvAll' :: MpvCommand -> IO [MpvResponse]
777mpvAll' = mpvAll "/var/media/.mpv-ipc"
778
779mpvOne' :: MpvCommand -> IO (Maybe MpvResponse)
780mpvOne' = mpvOne "/var/media/.mpv-ipc"
781
782mediaMpv :: MpvCommand -> X ()
783mediaMpv cmd = void . xfork $ print =<< mpvAll' cmd
784
785mediaMpvTogglePause :: X ()
786mediaMpvTogglePause = void . xfork $ do
787 paused <- mapM mpvResponse <=< mpvAll' $ MpvGetProperty "pause"
788 if
789 | and paused -> print <=< mpvAll' $ MpvSetProperty "pause" False
790 | otherwise -> print <=< mpvOne' $ MpvSetProperty "pause" True
791
792myKeys' conf host = Map.fromList $
793 -- launch a terminal
794 [ ((modm, xK_Return), spawn $ (XMonad.terminal conf) ++ " -e tmux")
795 , ((modm .|. shiftMask, xK_Return), spawn $ XMonad.terminal conf)
796
797 -- launch dmenu
798 --, ((modm, xK_d ), spawn "exe=`dmenu_path | dmenu` && eval \"exec $exe\"")
799 , ((modm, xK_d ), shellPrompt "Run: " xPConfigMonospace)
800 , ((modm .|. shiftMask, xK_d ), prompt "Run in Terminal: " ("alacritty" ++ " -e") xPConfigMonospace)
801 , ((modm, xK_at ), sshPrompt (sshOverrides . Just $ hName host) xPConfigMonospace)
802
803 -- close focused window
804 , ((modm .|. shiftMask, xK_q ), kill)
805 , ((modm .|. controlMask .|. shiftMask, xK_q ), spawn "xkill")
806
807 -- Rotate through the available layout algorithms
808 , ((modm, xK_space ), sendMessage NextLayout)
809
810 -- Reset the layouts on the current workspace to default
811 , ((modm .|. controlMask, xK_r ), (setLayout $ XMonad.layoutHook conf) >> refresh)
812
813 -- Resize viewed windows to the correct size
814 , ((modm, xK_r ), refresh)
815
816 -- Move focus to the next window
817 , ((modm, xK_t ), windows W.focusDown)
818
819 -- Move focus to the previous window
820 , ((modm, xK_n ), windows W.focusUp )
821
822 -- Move focus to the master window
823 , ((modm, xK_m ), windows W.focusMaster )
824
825 -- Swap the focused window and the master window
826 , ((modm .|. shiftMask, xK_m ), windows W.swapMaster)
827
828 -- Swap the focused window with the next window
829 , ((modm .|. shiftMask, xK_t ), windows W.swapDown )
830
831 -- Swap the focused window with the previous window
832 , ((modm .|. shiftMask, xK_n ), windows W.swapUp )
833
834 -- Swap the focused window with the previous window
835 , ((modm .|. shiftMask .|. controlMask, xK_m), sendMessage SwapWindow)
836
837 , ((modm, xK_Right), sendMessage $ Go R)
838 , ((modm, xK_Left ), sendMessage $ Go L)
839 , ((modm, xK_Up ), sendMessage $ Go U)
840 , ((modm, xK_Down ), sendMessage $ Go D)
841 , ((modm .|. shiftMask , xK_Right), sendMessage $ Move R)
842 , ((modm .|. shiftMask , xK_Left ), sendMessage $ Move L)
843 , ((modm .|. shiftMask , xK_Up ), sendMessage $ Move U)
844 , ((modm .|. shiftMask , xK_Down ), sendMessage $ Move D)
845 -- , ((modm .|. controlMask, xK_Right), withFocused $ keysMoveWindow (10, 0))
846 -- , ((modm .|. controlMask, xK_Left ), withFocused $ keysMoveWindow (-10, 0))
847 -- , ((modm .|. controlMask, xK_Up ), withFocused $ keysMoveWindow (0, -10))
848 -- , ((modm .|. controlMask, xK_Down ), withFocused $ keysMoveWindow (0, 10))
849 -- Shrink the master area
850 , ((modm, xK_h ), sendMessage Shrink)
851
852 -- Expand the master area
853 , ((modm, xK_s ), sendMessage Expand)
854
855 -- Push window back into tiling
856 , ((modm .|. shiftMask, xK_space ), withFocused $ windows . W.sink)
857 , ((modm, xK_BackSpace), focusUrgent)
858 , ((modm .|. shiftMask, xK_BackSpace), clearUrgents)
859
860 -- Increment the number of windows in the master area
861 , ((modm , xK_comma ), sendMessage (IncMasterN 1))
862
863 -- Deincrement the number of windows in the master area
864 , ((modm , xK_period), sendMessage (IncMasterN (-1)))
865
866 , ((0, xF86XK_AudioRaiseVolume), safeSpawn "pamixer" ["-i", "2"])
867 , ((0, xF86XK_AudioLowerVolume), safeSpawn "pamixer" ["-d", "2"])
868 , ((0, xF86XK_AudioMute), safeSpawn "pamixer" ["-t"])
869 , ((0, xF86XK_AudioPause), mediaMpv $ MpvSetProperty "pause" False)
870 , ((0, {-xF86XK_AudioMicMute-} 269025202), safeSpawn "pulseaudio-ctl" ["mute-input"])
871 , ((0, xF86XK_AudioPlay), mediaMpvTogglePause)
872 , ((0, xK_Print), do
873 home <- liftIO getHomeDirectory
874 unGrab
875 safeSpawn "scrot" ["-s", "-F", home </> "screenshots" </> "%Y-%m-%dT%H:%M:%S.png", "-e", "xclip -selection clipboard -t image/png -i $f"]
876 )
877 , ((modm .|. mod1Mask, xK_space), mediaMpvTogglePause)
878
879 -- , ((0, xF86XK_MonBrightnessDown), backlight . cycleThrough $ reverse brCycle)
880 -- , ((0, xF86XK_MonBrightnessUp ), backlight $ cycleThrough brCycle)
881 , ((modm .|. shiftMask , xK_b), backlight . cycleThrough $ reverse brCycle)
882 , ((modm .|. shiftMask .|. controlMask, xK_b), backlight $ cycleThrough brCycle)
883
884 , ((modm , xK_Escape), cycleKbLayout (hKbLayouts host))
885 , ((modm .|. controlMask, xK_Escape), safeSpawn "setxkbmap" $ fst (head $ hKbLayouts host) : maybeToList (snd . head $ hKbLayouts host))
886
887 -- Toggle the status bar gap
888 -- Use this binding with avoidStruts from Hooks.ManageDocks.
889 -- See also the statusBar function from Hooks.DynamicLog.
890 --
891 , ((modm , xK_b ), sendMessage ToggleStruts)
892
893 , ((modm .|. shiftMask, xK_p ), safeSpawn "playerctl" ["-a", "pause"])
894
895 -- Quit xmonad
896 , ((modm .|. shiftMask, xK_e ), io (exitWith ExitSuccess))
897
898 -- Restart xmonad
899 -- , ((modm .|. shiftMask .|. controlMask, xK_r ), void . xfork $ recompile False >>= flip when (safeSpawn "xmonad" ["--restart"]))
900 , ((modm .|. shiftMask, xK_r ), void . liftIO $ executeFile "xmonad" True [] Nothing)
901 , ((modm .|. shiftMask, xK_l ), void . xfork $ do
902 sessId <- getEnv "XDG_SESSION_ID"
903 safeSpawn "loginctl" ["lock-session", sessId]
904 )
905 , ((modm .|. shiftMask, xK_s ), safeSpawn "systemctl" ["suspend"])
906 , ((modm .|. shiftMask, xK_h ), inputPromptWithCompl xPConfigMonospace "systemctl" powerActCompl ?+ powerAct)
907 , ((modm, xK_v ), windows copyToAll) -- @@ Make focused window always visible
908 , ((modm .|. shiftMask, xK_v ), killAllOtherCopies) -- @@ Toggle window state back
909 , ((modm .|. shiftMask, xK_g ), windowPrompt xPConfig Goto wsWindows)
910 , ((modm , xK_g ), windowPrompt xPConfig Bring allWindows)
911 ]
912 ++
913
914 --
915 -- mod-[1..9], Switch to workspace N
916 --
917 -- mod-[1..9], Switch to workspace N
918 -- mod-shift-[1..9], Move client to workspace N
919 --
920 [((m .|. modm, k), windows $ f i)
921 | (i, k) <- zip (XMonad.workspaces conf) $ numKeys
922 , (f, m) <- [(W.greedyView, 0), (W.shift, shiftMask)]
923 ]
924 ++
925 [((m .|. modm .|. controlMask, k), void . runMaybeT $
926 MaybeT (P.getScreen def i) >>= MaybeT . screenWorkspace >>= lift . windows . f
927 )
928 | (i, k) <- zip (hScreens host) [xK_g, xK_c, xK_r, xK_l]
929 , (f, m) <- [(W.view, 0), (W.shift, shiftMask)]
930 ]
931 where
932 modm = XMonad.modMask conf
933
934 brCycle = [0, 1 % 500, 1 % 250, 1 % 100, 1 % 10, 1 % 4, 1 % 2, 3 % 4, 1]
935
936 powerActWords = ["poweroff", "reboot", "hibernate", "suspend"]
937 powerActCompl = mkComplFunFromList' xPConfigMonospace powerActWords
938 powerAct act | act `elem` powerActWords = safeSpawn "systemctl" $ pure act
939 | otherwise = return ()
diff --git a/accounts/gkleen@sif/zshrc b/accounts/gkleen@sif/zshrc
index e3f675a1..702990c3 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,23 +90,23 @@ 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 -SfL -o ${archiveFile} ${templateArchive}
89 97
90 templateArchive=${archiveFile} 98 templateArchive=${archiveFile}
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,15 +123,19 @@ 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 ;;
135 application/x-iso9660-image)
136 7z x ${templateArchive}
137 unpack=false
138 ;;
127 *) 139 *)
128 tar -xvaf ${templateArchive} 140 tar -xvaf ${templateArchive}
129 unpack=false 141 unpack=false
@@ -134,25 +146,21 @@ dir() {
134 fi 146 fi
135 147
136 148
137 ${wormhole} && wormhole receive --accept-file 149 [[ $wormhole = "true" ]] && wormhole receive --accept-file
138 150
139 151
140 if ${quickserve}; then 152 if [[ ${#@} -gt 0 ]]; then
141 quickserve --root . --upload . --show-hidden --tar gz 153 ${@}
142 fi 154 fi
143 155
156 cd $(pwd) # Needed for mounting to work
144 157
145 if [[ ${#@} -eq 0 ]] || ${forceShell}; then 158 if [[ ${miniserve} = "true" ]]; then
146 if [[ ${#@} -gt 0 ]]; then 159 miniserve --random-route --hidden --enable-tar-gz --enable-zip . &
147 if [[ -z ${nixShell} ]]; then 160 echo $! > "${miniservePIDFile}"
148 ${@} 161 fi
149 else
150 nix-shell ${nixShell} --run "${@}"
151 fi
152 fi
153
154 cd $(pwd) # Needed for mounting to work
155 162
163 if [[ ${#@} -eq 0 ]] && [[ ${miniserve} != "true" ]] || [[ $forceShell = "true" ]]; then
156 isSingleDir() { 164 isSingleDir() {
157 typeset -a contents 165 typeset -a contents
158 contents=(*(N) .*(N)) 166 contents=(*(N) .*(N))
@@ -166,18 +174,9 @@ dir() {
166 } 174 }
167 while d=$(isSingleDir); do cd ${d}; done 175 while d=$(isSingleDir); do cd ${d}; done
168 176
169 177 zsh
170 if [[ -z ${nixShell} ]]; then 178 elif [[ ${miniserve} == "true" ]]; then
171 zsh 179 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 180 fi
182 ) 181 )
183} 182}
@@ -185,27 +184,30 @@ dir() {
185tmpdir() { 184tmpdir() {
186 cleanup() 185 cleanup()
187 { 186 {
188 cd / 187 cd /
189 unmount() { 188 unmount() {
190 printf "Unmounting %s\n" ${1} >&2 189 printf "Unmounting %s\n" ${1} >&2
191 fusermount -u ${1} || umount ${1} || sudo umount ${1} 190 fusermount -u ${1} || umount ${1} || sudo umount ${1}
192 } 191 }
193 192
194 if mountpoint -q -- ${dir}; then 193 if [[ -n ${dir} ]]; then
195 unmount ${dir} || return $? 194 if mountpoint -q -- ${dir}; then
196 else 195 unmount ${dir} || return $?
197 while read -d $'\0' subDir; do 196 else
198 mountpoint -q -- ${subDir} || continue 197 while read -d $'\0' subDir; do
199 unmount ${subDir} || return $? 198 mountpoint -q -- ${subDir} || continue
200 done <<<$(find ${dir} -xdev -type d -print0 | sort -zr) 199 unmount ${subDir} || return $?
201 fi 200 done <<<$(find ${dir} -xdev -type d -print0 | sort -zr)
202 201 fi
203 rm -rfv --one-file-system -- ${dir} 202
203 rm -rfv --one-file-system -- ${dir}
204 dir=""
205 fi
204 } 206 }
205 207
206 local tmpdir="" 208 local tmpdir=""
207 209
208 while getopts ':t:a:s:Sd:ir:wqg:p:' arg; do 210 while getopts ':t:a:d:ir:wg:p:m' arg; do
209 case $arg in 211 case $arg in
210 "t") tmpdir="=${OPTARG}" ;; 212 "t") tmpdir="=${OPTARG}" ;;
211 "?"|":") printf "Invalid option: %s\n" $arg >&2; exit 2 ;; 213 "?"|":") printf "Invalid option: %s\n" $arg >&2; exit 2 ;;
@@ -213,6 +215,8 @@ tmpdir() {
213 done 215 done
214 216
215 ( 217 (
218 set -o localoptions -o localtraps
219 trap 'return 1' INT TERM
216 trap cleanup EXIT 220 trap cleanup EXIT
217 221
218 222
@@ -231,17 +235,7 @@ clock() {
231} 235}
232 236
233public-ip() { 237public-ip() {
234 curl -s -H 'Accept: application/json' $@ ifconfig.co | jq -r '.ip' 238 curl -sSf -H 'Accept: application/json' $@ ifconfig.co | jq -r '.ip'
235}
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} 239}
246 240
247swap() { 241swap() {
@@ -271,14 +265,6 @@ l() {
271 ls --long --binary --git --time-style=iso --header $@ 265 ls --long --binary --git --time-style=iso --header $@
272} 266}
273 267
274re() {
275 systemctl --restart $@
276}
277
278ure() {
279 systemctl --user --restart $@
280}
281
282ssh-installer() { 268ssh-installer() {
283 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/gkleen@sif.midgard.yggdrasil $@ 269 ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o IdentityFile=~/.ssh/gkleen@sif.midgard.yggdrasil $@
284} 270}
@@ -306,20 +292,7 @@ done < <(find ~/projects ~/uni -regextype posix-extended -maxdepth 2 -type d -re
306 sed -zr 's|(.*/([0-9]{2}[ws])/(.+))|\1 \2 \3|' | \ 292 sed -zr 's|(.*/([0-9]{2}[ws])/(.+))|\1 \2 \3|' | \
307 sort -z -r -k2 | sort -z -s -k3 | uniq -z -f 2) 293 sort -z -r -k2 | sort -z -s -k3 | uniq -z -f 2)
308 294
309alias '..'='cd ..'
310alias rzadm=$'tmpdir -i sh -c \'mkdir adm; sshfs gkleen@mgmt01:/adm adm\'' 295alias rzadm=$'tmpdir -i sh -c \'mkdir adm; sshfs gkleen@mgmt01:/adm adm\''
311alias mathcloud=$'tmpdir -i rclone mount --daemon mathcloud:// .' 296alias mathcloud=$'tmpdir -i rclone mount --daemon mathcloud:// .'
312alias -g L='| less'
313alias -g S='&> /dev/null'
314alias -g G='| grep'
315alias -g B='&> /dev/null &'
316alias -g BB='&> /dev/null &!'
317 297
318export DEFAULT_USER=gkleen 298export DEFAULT_USER=gkleen
319
320bindkey -e
321bindkey ';5C' emacs-forward-word
322bindkey ';5D' emacs-backward-word
323bindkey '^[[1;5C' emacs-forward-word
324bindkey '^[[1;5D' emacs-backward-word
325bindkey '^H' backward-kill-word
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 56b571ca..b99d27a9 100644
--- a/flake.lock
+++ b/flake.lock
@@ -6,22 +6,28 @@
6 "nixpkgs": [ 6 "nixpkgs": [
7 "nixpkgs" 7 "nixpkgs"
8 ], 8 ],
9 "poetry2nix": [ 9 "pre-commit-hooks-nix": "pre-commit-hooks-nix",
10 "poetry2nix" 10 "pyproject-build-systems": [
11 "pyproject-build-systems"
12 ],
13 "pyproject-nix": [
14 "pyproject-nix"
11 ], 15 ],
12 "pre-commit-hooks-nix": "pre-commit-hooks-nix" 16 "uv2nix": [
17 "uv2nix"
18 ]
13 }, 19 },
14 "locked": { 20 "locked": {
15 "lastModified": 1723124245, 21 "lastModified": 1749560907,
16 "narHash": "sha256-ThDq7vOXo6G4+C5FHqUc64CeX2c5n36tln4ZlDao6s4=", 22 "narHash": "sha256-zvAxxnJ5dcnjbuog0W6UvlthD1dLm5t4ZQI25jzNDW4=",
17 "owner": "gkleen", 23 "owner": "gkleen",
18 "repo": "backup-utils", 24 "repo": "backup-utils",
19 "rev": "74e65090de63fc99f056098677cc490754e2708f", 25 "rev": "8ed6a1d7c2e337cb2e35ed68f6d852f0d1049908",
20 "type": "gitlab" 26 "type": "gitlab"
21 }, 27 },
22 "original": { 28 "original": {
23 "owner": "gkleen", 29 "owner": "gkleen",
24 "ref": "v0.1.6", 30 "ref": "v0.1.7",
25 "repo": "backup-utils", 31 "repo": "backup-utils",
26 "type": "gitlab" 32 "type": "gitlab"
27 } 33 }
@@ -33,26 +39,45 @@
33 "nixpkgs": [ 39 "nixpkgs": [
34 "nixpkgs" 40 "nixpkgs"
35 ], 41 ],
36 "poetry2nix": [ 42 "pre-commit-hooks-nix": "pre-commit-hooks-nix_2",
37 "poetry2nix" 43 "pyproject-build-systems": "pyproject-build-systems",
44 "pyproject-nix": [
45 "pyproject-nix"
38 ], 46 ],
39 "pre-commit-hooks-nix": "pre-commit-hooks-nix_2" 47 "uv2nix": [
48 "uv2nix"
49 ]
40 }, 50 },
41 "locked": { 51 "locked": {
42 "lastModified": 1734281899, 52 "lastModified": 1750599403,
43 "narHash": "sha256-9QdIl3sjHY4Xij9KrBUkW1KpLB+jyxlI12UHPitlawI=", 53 "narHash": "sha256-MLQ7CISl00w1xq88TL2wukNq3ukzID4u7BVT4okbUik=",
44 "owner": "gkleen", 54 "owner": "gkleen",
45 "repo": "ca", 55 "repo": "ca",
46 "rev": "1e4ee9d25a5282ef7bc6774072229784fa0036f3", 56 "rev": "505a29233ada969b2eca76d616b0d7a8767dfb71",
47 "type": "gitlab" 57 "type": "gitlab"
48 }, 58 },
49 "original": { 59 "original": {
50 "owner": "gkleen", 60 "owner": "gkleen",
51 "ref": "v3.1.3", 61 "ref": "v3.1.5",
52 "repo": "ca", 62 "repo": "ca",
53 "type": "gitlab" 63 "type": "gitlab"
54 } 64 }
55 }, 65 },
66 "crane": {
67 "locked": {
68 "lastModified": 1731098351,
69 "narHash": "sha256-HQkYvKvaLQqNa10KEFGgWHfMAbWBfFp+4cAgkut+NNE=",
70 "owner": "ipetkov",
71 "repo": "crane",
72 "rev": "ef80ead953c1b28316cc3f8613904edc2eb90c28",
73 "type": "github"
74 },
75 "original": {
76 "owner": "ipetkov",
77 "repo": "crane",
78 "type": "github"
79 }
80 },
56 "deploy-rs": { 81 "deploy-rs": {
57 "inputs": { 82 "inputs": {
58 "flake-compat": [ 83 "flake-compat": [
@@ -66,11 +91,11 @@
66 ] 91 ]
67 }, 92 },
68 "locked": { 93 "locked": {
69 "lastModified": 1727447169, 94 "lastModified": 1749105467,
70 "narHash": "sha256-3KyjMPUKHkiWhwR91J1YchF6zb6gvckCAY1jOE+ne0U=", 95 "narHash": "sha256-hXh76y/wDl15almBcqvjryB50B0BaiXJKk20f314RoE=",
71 "owner": "serokell", 96 "owner": "serokell",
72 "repo": "deploy-rs", 97 "repo": "deploy-rs",
73 "rev": "aa07eb05537d4cd025e2310397a6adcedfe72c76", 98 "rev": "6bc76b872374845ba9d645a2f012b764fecd765f",
74 "type": "github" 99 "type": "github"
75 }, 100 },
76 "original": { 101 "original": {
@@ -115,11 +140,11 @@
115 "flake-compat_3": { 140 "flake-compat_3": {
116 "flake": false, 141 "flake": false,
117 "locked": { 142 "locked": {
118 "lastModified": 1733328505, 143 "lastModified": 1747046372,
119 "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 144 "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
120 "owner": "edolstra", 145 "owner": "edolstra",
121 "repo": "flake-compat", 146 "repo": "flake-compat",
122 "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 147 "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
123 "type": "github" 148 "type": "github"
124 }, 149 },
125 "original": { 150 "original": {
@@ -132,6 +157,22 @@
132 "flake-compat_4": { 157 "flake-compat_4": {
133 "flake": false, 158 "flake": false,
134 "locked": { 159 "locked": {
160 "lastModified": 1696426674,
161 "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
162 "owner": "edolstra",
163 "repo": "flake-compat",
164 "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
165 "type": "github"
166 },
167 "original": {
168 "owner": "edolstra",
169 "repo": "flake-compat",
170 "type": "github"
171 }
172 },
173 "flake-compat_5": {
174 "flake": false,
175 "locked": {
135 "lastModified": 1673956053, 176 "lastModified": 1673956053,
136 "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 177 "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
137 "owner": "edolstra", 178 "owner": "edolstra",
@@ -168,11 +209,11 @@
168 "nixpkgs-lib": "nixpkgs-lib_2" 209 "nixpkgs-lib": "nixpkgs-lib_2"
169 }, 210 },
170 "locked": { 211 "locked": {
171 "lastModified": 1726153070, 212 "lastModified": 1749398372,
172 "narHash": "sha256-HO4zgY0ekfwO5bX0QH/3kJ/h4KvUDFZg8YpkNwIbg1U=", 213 "narHash": "sha256-tYBdgS56eXYaWVW3fsnPQ/nFlgWi/Z2Ymhyu21zVM98=",
173 "owner": "hercules-ci", 214 "owner": "hercules-ci",
174 "repo": "flake-parts", 215 "repo": "flake-parts",
175 "rev": "bcef6817a8b2aa20a5a6dbb19b43e63c5bf8619a", 216 "rev": "9305fe4e5c2a6fcf5ba6a3ff155720fbe4076569",
176 "type": "github" 217 "type": "github"
177 }, 218 },
178 "original": { 219 "original": {
@@ -183,6 +224,27 @@
183 }, 224 },
184 "flake-parts_3": { 225 "flake-parts_3": {
185 "inputs": { 226 "inputs": {
227 "nixpkgs-lib": [
228 "lanzaboote",
229 "nixpkgs"
230 ]
231 },
232 "locked": {
233 "lastModified": 1730504689,
234 "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=",
235 "owner": "hercules-ci",
236 "repo": "flake-parts",
237 "rev": "506278e768c2a08bec68eb62932193e341f55c90",
238 "type": "github"
239 },
240 "original": {
241 "owner": "hercules-ci",
242 "repo": "flake-parts",
243 "type": "github"
244 }
245 },
246 "flake-parts_4": {
247 "inputs": {
186 "nixpkgs-lib": "nixpkgs-lib_3" 248 "nixpkgs-lib": "nixpkgs-lib_3"
187 }, 249 },
188 "locked": { 250 "locked": {
@@ -202,11 +264,11 @@
202 "flake-registry": { 264 "flake-registry": {
203 "flake": false, 265 "flake": false,
204 "locked": { 266 "locked": {
205 "lastModified": 1717415742, 267 "lastModified": 1744623129,
206 "narHash": "sha256-HKvoLGZUsBpjkxWkdtctGYj6RH0bl6vcw0OjTOqyzJk=", 268 "narHash": "sha256-nlQTQrHqM+ywXN0evDXnYEV6z6WWZB5BFQ2TkXsduKw=",
207 "owner": "NixOS", 269 "owner": "NixOS",
208 "repo": "flake-registry", 270 "repo": "flake-registry",
209 "rev": "895a65f8d5acf848136ee8fe8e8f736f0d27df96", 271 "rev": "1322f33d5836ae757d2e6190239252cf8402acf6",
210 "type": "github" 272 "type": "github"
211 }, 273 },
212 "original": { 274 "original": {
@@ -296,6 +358,28 @@
296 "gitignore_3": { 358 "gitignore_3": {
297 "inputs": { 359 "inputs": {
298 "nixpkgs": [ 360 "nixpkgs": [
361 "lanzaboote",
362 "pre-commit-hooks-nix",
363 "nixpkgs"
364 ]
365 },
366 "locked": {
367 "lastModified": 1709087332,
368 "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
369 "owner": "hercules-ci",
370 "repo": "gitignore.nix",
371 "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
372 "type": "github"
373 },
374 "original": {
375 "owner": "hercules-ci",
376 "repo": "gitignore.nix",
377 "type": "github"
378 }
379 },
380 "gitignore_4": {
381 "inputs": {
382 "nixpkgs": [
299 "prometheus-borg-exporter", 383 "prometheus-borg-exporter",
300 "pre-commit-hooks-nix", 384 "pre-commit-hooks-nix",
301 "nixpkgs" 385 "nixpkgs"
@@ -322,11 +406,11 @@
322 ] 406 ]
323 }, 407 },
324 "locked": { 408 "locked": {
325 "lastModified": 1722322032, 409 "lastModified": 1753177987,
326 "narHash": "sha256-pnO44gA8GcJj3oCVeGmypSGLr10+usMbJXochJWdugw=", 410 "narHash": "sha256-PkCc+YTrl0A/H6EV09DCr5yZpvQZ9DkuFXj/NNaEvHs=",
327 "owner": "gkleen", 411 "owner": "gkleen",
328 "repo": "home-manager", 412 "repo": "home-manager",
329 "rev": "55c1d61f06fd331f874178a6028f22be22ee7878", 413 "rev": "b493410fc6e427129a1caee8f50970d152a27daa",
330 "type": "github" 414 "type": "github"
331 }, 415 },
332 "original": { 416 "original": {
@@ -343,27 +427,27 @@
343 ] 427 ]
344 }, 428 },
345 "locked": { 429 "locked": {
346 "lastModified": 1710245356, 430 "lastModified": 1749562430,
347 "narHash": "sha256-8cQGUn+N1dTgklMWMejSLN2q8Oz+7Rnqsfaw2rt3bU4=", 431 "narHash": "sha256-M5MqsIsf+o7yngakVUW4poBGZaghB6sUpw7SsWA55kU=",
348 "owner": "gkleen", 432 "owner": "gkleen",
349 "repo": "home-manager", 433 "repo": "home-manager",
350 "rev": "a14fe0c27d04dfa3d80abe2db743e9a7f4f2a33d", 434 "rev": "dca5a2df9f8a00cc34bea6ead249a8446f5f069e",
351 "type": "github" 435 "type": "github"
352 }, 436 },
353 "original": { 437 "original": {
354 "owner": "gkleen", 438 "owner": "gkleen",
355 "ref": "nixos-late-start-23.11", 439 "ref": "nixos-late-start-25.05",
356 "repo": "home-manager", 440 "repo": "home-manager",
357 "type": "github" 441 "type": "github"
358 } 442 }
359 }, 443 },
360 "impermanence": { 444 "impermanence": {
361 "locked": { 445 "locked": {
362 "lastModified": 1731242966, 446 "lastModified": 1737831083,
363 "narHash": "sha256-B3C3JLbGw0FtLSWCjBxU961gLNv+BOOBC6WvstKLYMw=", 447 "narHash": "sha256-LJggUHbpyeDvNagTUrdhe/pRVp4pnS6wVKALS782gRI=",
364 "owner": "nix-community", 448 "owner": "nix-community",
365 "repo": "impermanence", 449 "repo": "impermanence",
366 "rev": "3ed3f0eaae9fcc0a8331e77e9319c8a4abd8a71a", 450 "rev": "4b3e914cdf97a5b536a889e939fb2fd2b043a170",
367 "type": "github" 451 "type": "github"
368 }, 452 },
369 "original": { 453 "original": {
@@ -373,10 +457,36 @@
373 "type": "github" 457 "type": "github"
374 } 458 }
375 }, 459 },
460 "lanzaboote": {
461 "inputs": {
462 "crane": "crane",
463 "flake-compat": "flake-compat_4",
464 "flake-parts": "flake-parts_3",
465 "nixpkgs": [
466 "nixpkgs"
467 ],
468 "pre-commit-hooks-nix": "pre-commit-hooks-nix_3",
469 "rust-overlay": "rust-overlay"
470 },
471 "locked": {
472 "lastModified": 1737639419,
473 "narHash": "sha256-AEEDktApTEZ5PZXNDkry2YV2k6t0dTgLPEmAZbnigXU=",
474 "owner": "nix-community",
475 "repo": "lanzaboote",
476 "rev": "a65905a09e2c43ff63be8c0e86a93712361f871e",
477 "type": "github"
478 },
479 "original": {
480 "owner": "nix-community",
481 "ref": "v0.4.2",
482 "repo": "lanzaboote",
483 "type": "github"
484 }
485 },
376 "leapseconds": { 486 "leapseconds": {
377 "flake": false, 487 "flake": false,
378 "locked": { 488 "locked": {
379 "narHash": "sha256-5ZaoY/bScQS7EGJRHu6vj9XWhbObmxNEaGugaGU7+lg=", 489 "narHash": "sha256-FJgbafPB48+5sT+7ZB8pajSsfJoISEOoaJ0d/2Ya7o8=",
380 "type": "file", 490 "type": "file",
381 "url": "https://data.iana.org/time-zones/tzdb/leap-seconds.list" 491 "url": "https://data.iana.org/time-zones/tzdb/leap-seconds.list"
382 }, 492 },
@@ -385,6 +495,66 @@
385 "url": "https://data.iana.org/time-zones/tzdb/leap-seconds.list" 495 "url": "https://data.iana.org/time-zones/tzdb/leap-seconds.list"
386 } 496 }
387 }, 497 },
498 "niri-flake": {
499 "inputs": {
500 "niri-stable": "niri-stable",
501 "niri-unstable": "niri-unstable",
502 "nixpkgs": [
503 "nixpkgs"
504 ],
505 "nixpkgs-stable": "nixpkgs-stable_3",
506 "xwayland-satellite-stable": "xwayland-satellite-stable",
507 "xwayland-satellite-unstable": "xwayland-satellite-unstable"
508 },
509 "locked": {
510 "lastModified": 1757437545,
511 "narHash": "sha256-7ssbrFnmSrqtCtOySiu5ncyOBxPrR6p2nhNHrg6D+fo=",
512 "owner": "sodiboo",
513 "repo": "niri-flake",
514 "rev": "ef694b996daeeb8684c0adfaa9b7067a6e709054",
515 "type": "github"
516 },
517 "original": {
518 "owner": "sodiboo",
519 "ref": "main",
520 "repo": "niri-flake",
521 "type": "github"
522 }
523 },
524 "niri-stable": {
525 "flake": false,
526 "locked": {
527 "lastModified": 1756556321,
528 "narHash": "sha256-RLD89dfjN0RVO86C/Mot0T7aduCygPGaYbog566F0Qo=",
529 "owner": "YaLTeR",
530 "repo": "niri",
531 "rev": "01be0e65f4eb91a9cd624ac0b76aaeab765c7294",
532 "type": "github"
533 },
534 "original": {
535 "owner": "YaLTeR",
536 "ref": "v25.08",
537 "repo": "niri",
538 "type": "github"
539 }
540 },
541 "niri-unstable": {
542 "flake": false,
543 "locked": {
544 "lastModified": 1757671534,
545 "narHash": "sha256-7tfypHWNtR+wZS9K9XrvcUwyvZ3h8CxInQ2mVsjUU9A=",
546 "owner": "gkleen",
547 "repo": "niri",
548 "rev": "5e3611a3c5f8c819e5517d0b3f795f161579a0db",
549 "type": "github"
550 },
551 "original": {
552 "owner": "gkleen",
553 "ref": "fix/locked-monitor-control",
554 "repo": "niri",
555 "type": "github"
556 }
557 },
388 "nix-github-actions": { 558 "nix-github-actions": {
389 "inputs": { 559 "inputs": {
390 "nixpkgs": [ 560 "nixpkgs": [
@@ -413,11 +583,11 @@
413 ] 583 ]
414 }, 584 },
415 "locked": { 585 "locked": {
416 "lastModified": 1733629314, 586 "lastModified": 1755404379,
417 "narHash": "sha256-U0vivjQFAwjNDYt49Krevs1murX9hKBFe2Ye0cHpgbU=", 587 "narHash": "sha256-Q6ZxZDBmD/B988Jjbx7/NchxOKIpOKBBrx9Yb0zMzpQ=",
418 "owner": "Mic92", 588 "owner": "Mic92",
419 "repo": "nix-index-database", 589 "repo": "nix-index-database",
420 "rev": "f1e477a7dd11e27e7f98b646349cd66bbabf2fb8", 590 "rev": "ebbc1c05f786ae39bb5e04e57bf2c10c44a649e3",
421 "type": "github" 591 "type": "github"
422 }, 592 },
423 "original": { 593 "original": {
@@ -427,6 +597,27 @@
427 "type": "github" 597 "type": "github"
428 } 598 }
429 }, 599 },
600 "nix-monitored": {
601 "inputs": {
602 "nixpkgs": [
603 "nixpkgs"
604 ]
605 },
606 "locked": {
607 "lastModified": 1745680380,
608 "narHash": "sha256-Z8PknjkmIr/8ZCH+dmc2Pc+UltiOr7/oKg37PXuVvuU=",
609 "owner": "ners",
610 "repo": "nix-monitored",
611 "rev": "60f3baa4701d58eab86c2d1d9c3d7e820074d461",
612 "type": "github"
613 },
614 "original": {
615 "owner": "ners",
616 "ref": "master",
617 "repo": "nix-monitored",
618 "type": "github"
619 }
620 },
430 "nixVirt": { 621 "nixVirt": {
431 "inputs": { 622 "inputs": {
432 "nixpkgs": [ 623 "nixpkgs": [
@@ -434,11 +625,11 @@
434 ] 625 ]
435 }, 626 },
436 "locked": { 627 "locked": {
437 "lastModified": 1732406038, 628 "lastModified": 1748140003,
438 "narHash": "sha256-BYNBN+Rtc/SX6qI7m3nmryufRPn0ZYd40yHDo9VQaNE=", 629 "narHash": "sha256-DNBZmuk1YRM2PmwbHzVdXumRjCUzQkMarg4iI/37rOQ=",
439 "owner": "AshleyYakeley", 630 "owner": "AshleyYakeley",
440 "repo": "NixVirt", 631 "repo": "NixVirt",
441 "rev": "fe3aaa86d4458e4f84348941297f7ba82e2a9f67", 632 "rev": "5dfe108fd859b122f9a96981cb6bc12297653d6c",
442 "type": "github" 633 "type": "github"
443 }, 634 },
444 "original": { 635 "original": {
@@ -449,11 +640,11 @@
449 }, 640 },
450 "nixos-hardware": { 641 "nixos-hardware": {
451 "locked": { 642 "locked": {
452 "lastModified": 1733861262, 643 "lastModified": 1755330281,
453 "narHash": "sha256-+jjPup/ByS0LEVIrBbt7FnGugJgLeG9oc+ivFASYn2U=", 644 "narHash": "sha256-aJHFJWP9AuI8jUGzI77LYcSlkA9wJnOIg4ZqftwNGXA=",
454 "owner": "NixOS", 645 "owner": "NixOS",
455 "repo": "nixos-hardware", 646 "repo": "nixos-hardware",
456 "rev": "cf737e2eba82b603f54f71b10cb8fd09d22ce3f5", 647 "rev": "3dac8a872557e0ca8c083cdcfc2f218d18e113b0",
457 "type": "github" 648 "type": "github"
458 }, 649 },
459 "original": { 650 "original": {
@@ -481,16 +672,16 @@
481 }, 672 },
482 "nixpkgs-eostre": { 673 "nixpkgs-eostre": {
483 "locked": { 674 "locked": {
484 "lastModified": 1701282334, 675 "lastModified": 1748026580,
485 "narHash": "sha256-MxCVrXY6v4QmfTwIysjjaX0XUhqBbxTWWB4HXtDYsdk=", 676 "narHash": "sha256-rWtXrcIzU5wm/C8F9LWvUfBGu5U5E7cFzPYT1pHIJaQ=",
486 "owner": "NixOS", 677 "owner": "NixOS",
487 "repo": "nixpkgs", 678 "repo": "nixpkgs",
488 "rev": "057f9aecfb71c4437d2b27d3323df7f93c010b7e", 679 "rev": "11cb3517b3af6af300dd6c055aeda73c9bf52c48",
489 "type": "github" 680 "type": "github"
490 }, 681 },
491 "original": { 682 "original": {
492 "owner": "NixOS", 683 "owner": "NixOS",
493 "ref": "23.11", 684 "ref": "25.05",
494 "repo": "nixpkgs", 685 "repo": "nixpkgs",
495 "type": "github" 686 "type": "github"
496 } 687 }
@@ -509,14 +700,17 @@
509 }, 700 },
510 "nixpkgs-lib_2": { 701 "nixpkgs-lib_2": {
511 "locked": { 702 "locked": {
512 "lastModified": 1725233747, 703 "lastModified": 1748740939,
513 "narHash": "sha256-Ss8QWLXdr2JCBPcYChJhz4xJm+h/xjl4G0c0XlP6a74=", 704 "narHash": "sha256-rQaysilft1aVMwF14xIdGS3sj1yHlI6oKQNBRTF40cc=",
514 "type": "tarball", 705 "owner": "nix-community",
515 "url": "https://github.com/NixOS/nixpkgs/archive/356624c12086a18f2ea2825fed34523d60ccc4e3.tar.gz" 706 "repo": "nixpkgs.lib",
707 "rev": "656a64127e9d791a334452c6b6606d17539476e2",
708 "type": "github"
516 }, 709 },
517 "original": { 710 "original": {
518 "type": "tarball", 711 "owner": "nix-community",
519 "url": "https://github.com/NixOS/nixpkgs/archive/356624c12086a18f2ea2825fed34523d60ccc4e3.tar.gz" 712 "repo": "nixpkgs.lib",
713 "type": "github"
520 } 714 }
521 }, 715 },
522 "nixpkgs-lib_3": { 716 "nixpkgs-lib_3": {
@@ -571,22 +765,54 @@
571 }, 765 },
572 "nixpkgs-stable_2": { 766 "nixpkgs-stable_2": {
573 "locked": { 767 "locked": {
574 "lastModified": 1717179513, 768 "lastModified": 1730741070,
575 "narHash": "sha256-vboIEwIQojofItm2xGCdZCzW96U85l9nDW3ifMuAIdM=", 769 "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=",
576 "owner": "NixOS", 770 "owner": "NixOS",
577 "repo": "nixpkgs", 771 "repo": "nixpkgs",
578 "rev": "63dacb46bf939521bdc93981b4cbb7ecb58427a0", 772 "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3",
579 "type": "github" 773 "type": "github"
580 }, 774 },
581 "original": { 775 "original": {
582 "owner": "NixOS", 776 "owner": "NixOS",
583 "ref": "24.05", 777 "ref": "nixos-24.05",
584 "repo": "nixpkgs", 778 "repo": "nixpkgs",
585 "type": "github" 779 "type": "github"
586 } 780 }
587 }, 781 },
588 "nixpkgs-stable_3": { 782 "nixpkgs-stable_3": {
589 "locked": { 783 "locked": {
784 "lastModified": 1757408970,
785 "narHash": "sha256-aSgK4BLNFFGvDTNKPeB28lVXYqVn8RdyXDNAvgGq+k0=",
786 "owner": "NixOS",
787 "repo": "nixpkgs",
788 "rev": "d179d77c139e0a3f5c416477f7747e9d6b7ec315",
789 "type": "github"
790 },
791 "original": {
792 "owner": "NixOS",
793 "ref": "nixos-25.05",
794 "repo": "nixpkgs",
795 "type": "github"
796 }
797 },
798 "nixpkgs-stable_4": {
799 "locked": {
800 "lastModified": 1748026580,
801 "narHash": "sha256-rWtXrcIzU5wm/C8F9LWvUfBGu5U5E7cFzPYT1pHIJaQ=",
802 "owner": "NixOS",
803 "repo": "nixpkgs",
804 "rev": "11cb3517b3af6af300dd6c055aeda73c9bf52c48",
805 "type": "github"
806 },
807 "original": {
808 "owner": "NixOS",
809 "ref": "25.05",
810 "repo": "nixpkgs",
811 "type": "github"
812 }
813 },
814 "nixpkgs-stable_5": {
815 "locked": {
590 "lastModified": 1678872516, 816 "lastModified": 1678872516,
591 "narHash": "sha256-/E1YwtMtFAu2KUQKV/1+KFuReYPANM2Rzehk84VxVoc=", 817 "narHash": "sha256-/E1YwtMtFAu2KUQKV/1+KFuReYPANM2Rzehk84VxVoc=",
592 "owner": "NixOS", 818 "owner": "NixOS",
@@ -603,11 +829,11 @@
603 }, 829 },
604 "nixpkgs_2": { 830 "nixpkgs_2": {
605 "locked": { 831 "locked": {
606 "lastModified": 1733759999, 832 "lastModified": 1755615617,
607 "narHash": "sha256-463SNPWmz46iLzJKRzO3Q2b0Aurff3U1n0nYItxq7jU=", 833 "narHash": "sha256-HMwfAJBdrr8wXAkbGhtcby1zGFvs+StOp19xNsbqdOg=",
608 "owner": "NixOS", 834 "owner": "NixOS",
609 "repo": "nixpkgs", 835 "repo": "nixpkgs",
610 "rev": "a73246e2eef4c6ed172979932bc80e1404ba2d56", 836 "rev": "20075955deac2583bb12f07151c2df830ef346b4",
611 "type": "github" 837 "type": "github"
612 }, 838 },
613 "original": { 839 "original": {
@@ -673,11 +899,11 @@
673 "treefmt-nix": "treefmt-nix" 899 "treefmt-nix": "treefmt-nix"
674 }, 900 },
675 "locked": { 901 "locked": {
676 "lastModified": 1731205797, 902 "lastModified": 1743690424,
677 "narHash": "sha256-F7N1mxH1VrkVNHR3JGNMRvp9+98KYO4b832KS8Gl2xI=", 903 "narHash": "sha256-cX98bUuKuihOaRp8dNV1Mq7u6/CQZWTPth2IJPATBXc=",
678 "owner": "nix-community", 904 "owner": "nix-community",
679 "repo": "poetry2nix", 905 "repo": "poetry2nix",
680 "rev": "f554d27c1544d9c56e5f1f8e2b8aff399803674e", 906 "rev": "ce2369db77f45688172384bbeb962bc6c2ea6f94",
681 "type": "github" 907 "type": "github"
682 }, 908 },
683 "original": { 909 "original": {
@@ -715,18 +941,14 @@
715 "nixpkgs": [ 941 "nixpkgs": [
716 "ca-util", 942 "ca-util",
717 "nixpkgs" 943 "nixpkgs"
718 ],
719 "nixpkgs-stable": [
720 "ca-util",
721 "nixpkgs"
722 ] 944 ]
723 }, 945 },
724 "locked": { 946 "locked": {
725 "lastModified": 1726745158, 947 "lastModified": 1749636823,
726 "narHash": "sha256-D5AegvGoEjt4rkKedmxlSEmC+nNLMBPWFxvmYnVLhjk=", 948 "narHash": "sha256-WUaIlOlPLyPgz9be7fqWJA5iG6rHcGRtLERSCfUDne4=",
727 "owner": "cachix", 949 "owner": "cachix",
728 "repo": "pre-commit-hooks.nix", 950 "repo": "pre-commit-hooks.nix",
729 "rev": "4e743a6920eab45e8ba0fbe49dc459f1423a4b74", 951 "rev": "623c56286de5a3193aa38891a6991b28f9bab056",
730 "type": "github" 952 "type": "github"
731 }, 953 },
732 "original": { 954 "original": {
@@ -737,11 +959,38 @@
737 }, 959 },
738 "pre-commit-hooks-nix_3": { 960 "pre-commit-hooks-nix_3": {
739 "inputs": { 961 "inputs": {
740 "flake-compat": "flake-compat_4", 962 "flake-compat": [
741 "flake-utils": "flake-utils_2", 963 "lanzaboote",
964 "flake-compat"
965 ],
742 "gitignore": "gitignore_3", 966 "gitignore": "gitignore_3",
967 "nixpkgs": [
968 "lanzaboote",
969 "nixpkgs"
970 ],
971 "nixpkgs-stable": "nixpkgs-stable_2"
972 },
973 "locked": {
974 "lastModified": 1731363552,
975 "narHash": "sha256-vFta1uHnD29VUY4HJOO/D6p6rxyObnf+InnSMT4jlMU=",
976 "owner": "cachix",
977 "repo": "pre-commit-hooks.nix",
978 "rev": "cd1af27aa85026ac759d5d3fccf650abe7e1bbf0",
979 "type": "github"
980 },
981 "original": {
982 "owner": "cachix",
983 "repo": "pre-commit-hooks.nix",
984 "type": "github"
985 }
986 },
987 "pre-commit-hooks-nix_4": {
988 "inputs": {
989 "flake-compat": "flake-compat_5",
990 "flake-utils": "flake-utils_2",
991 "gitignore": "gitignore_4",
743 "nixpkgs": "nixpkgs_3", 992 "nixpkgs": "nixpkgs_3",
744 "nixpkgs-stable": "nixpkgs-stable_3" 993 "nixpkgs-stable": "nixpkgs-stable_5"
745 }, 994 },
746 "locked": { 995 "locked": {
747 "lastModified": 1685361114, 996 "lastModified": 1685361114,
@@ -759,14 +1008,14 @@
759 }, 1008 },
760 "prometheus-borg-exporter": { 1009 "prometheus-borg-exporter": {
761 "inputs": { 1010 "inputs": {
762 "flake-parts": "flake-parts_3", 1011 "flake-parts": "flake-parts_4",
763 "nixpkgs": [ 1012 "nixpkgs": [
764 "nixpkgs" 1013 "nixpkgs"
765 ], 1014 ],
766 "poetry2nix": [ 1015 "poetry2nix": [
767 "poetry2nix" 1016 "poetry2nix"
768 ], 1017 ],
769 "pre-commit-hooks-nix": "pre-commit-hooks-nix_3" 1018 "pre-commit-hooks-nix": "pre-commit-hooks-nix_4"
770 }, 1019 },
771 "locked": { 1020 "locked": {
772 "lastModified": 1722088088, 1021 "lastModified": 1722088088,
@@ -783,6 +1032,81 @@
783 "type": "gitlab" 1032 "type": "gitlab"
784 } 1033 }
785 }, 1034 },
1035 "pyproject-build-systems": {
1036 "inputs": {
1037 "nixpkgs": [
1038 "ca-util",
1039 "nixpkgs"
1040 ],
1041 "pyproject-nix": [
1042 "ca-util",
1043 "pyproject-nix"
1044 ],
1045 "uv2nix": [
1046 "ca-util",
1047 "uv2nix"
1048 ]
1049 },
1050 "locked": {
1051 "lastModified": 1749519371,
1052 "narHash": "sha256-UJONN7mA2stweZCoRcry2aa1XTTBL0AfUOY84Lmqhos=",
1053 "owner": "pyproject-nix",
1054 "repo": "build-system-pkgs",
1055 "rev": "7c06967eca687f3482624250428cc12f43c92523",
1056 "type": "github"
1057 },
1058 "original": {
1059 "owner": "pyproject-nix",
1060 "repo": "build-system-pkgs",
1061 "type": "github"
1062 }
1063 },
1064 "pyproject-build-systems_2": {
1065 "inputs": {
1066 "nixpkgs": [
1067 "nixpkgs"
1068 ],
1069 "pyproject-nix": [
1070 "pyproject-nix"
1071 ],
1072 "uv2nix": [
1073 "uv2nix"
1074 ]
1075 },
1076 "locked": {
1077 "lastModified": 1755484659,
1078 "narHash": "sha256-2FfbqsaHVQd12XFFUAinIMAuGO3853LONmva1gT3vKw=",
1079 "owner": "pyproject-nix",
1080 "repo": "build-system-pkgs",
1081 "rev": "9778e87c2361810ff15e287ca5895c9da4a0e900",
1082 "type": "github"
1083 },
1084 "original": {
1085 "owner": "pyproject-nix",
1086 "repo": "build-system-pkgs",
1087 "type": "github"
1088 }
1089 },
1090 "pyproject-nix": {
1091 "inputs": {
1092 "nixpkgs": [
1093 "nixpkgs"
1094 ]
1095 },
1096 "locked": {
1097 "lastModified": 1754923840,
1098 "narHash": "sha256-QSKpYg+Ts9HYF155ltlj40iBex39c05cpOF8gjoE2EM=",
1099 "owner": "pyproject-nix",
1100 "repo": "pyproject.nix",
1101 "rev": "023cd4be230eacae52635be09eef100c37ef78da",
1102 "type": "github"
1103 },
1104 "original": {
1105 "owner": "pyproject-nix",
1106 "repo": "pyproject.nix",
1107 "type": "github"
1108 }
1109 },
786 "root": { 1110 "root": {
787 "inputs": { 1111 "inputs": {
788 "backup-utils": "backup-utils", 1112 "backup-utils": "backup-utils",
@@ -794,20 +1118,47 @@
794 "home-manager": "home-manager", 1118 "home-manager": "home-manager",
795 "home-manager-eostre": "home-manager-eostre", 1119 "home-manager-eostre": "home-manager-eostre",
796 "impermanence": "impermanence", 1120 "impermanence": "impermanence",
1121 "lanzaboote": "lanzaboote",
1122 "niri-flake": "niri-flake",
797 "nix-index-database": "nix-index-database", 1123 "nix-index-database": "nix-index-database",
1124 "nix-monitored": "nix-monitored",
798 "nixVirt": "nixVirt", 1125 "nixVirt": "nixVirt",
799 "nixos-hardware": "nixos-hardware", 1126 "nixos-hardware": "nixos-hardware",
800 "nixpkgs": "nixpkgs_2", 1127 "nixpkgs": "nixpkgs_2",
801 "nixpkgs-eostre": "nixpkgs-eostre", 1128 "nixpkgs-eostre": "nixpkgs-eostre",
802 "nixpkgs-pgbackrest": "nixpkgs-pgbackrest", 1129 "nixpkgs-pgbackrest": "nixpkgs-pgbackrest",
803 "nixpkgs-stable": "nixpkgs-stable_2", 1130 "nixpkgs-stable": "nixpkgs-stable_4",
804 "nvfetcher": "nvfetcher", 1131 "nvfetcher": "nvfetcher",
805 "poetry2nix": "poetry2nix", 1132 "poetry2nix": "poetry2nix",
806 "prometheus-borg-exporter": "prometheus-borg-exporter", 1133 "prometheus-borg-exporter": "prometheus-borg-exporter",
1134 "pyproject-build-systems": "pyproject-build-systems_2",
1135 "pyproject-nix": "pyproject-nix",
807 "sops-nix": "sops-nix", 1136 "sops-nix": "sops-nix",
1137 "uv2nix": "uv2nix",
808 "waybar": "waybar" 1138 "waybar": "waybar"
809 } 1139 }
810 }, 1140 },
1141 "rust-overlay": {
1142 "inputs": {
1143 "nixpkgs": [
1144 "lanzaboote",
1145 "nixpkgs"
1146 ]
1147 },
1148 "locked": {
1149 "lastModified": 1731897198,
1150 "narHash": "sha256-Ou7vLETSKwmE/HRQz4cImXXJBr/k9gp4J4z/PF8LzTE=",
1151 "owner": "oxalica",
1152 "repo": "rust-overlay",
1153 "rev": "0be641045af6d8666c11c2c40e45ffc9667839b5",
1154 "type": "github"
1155 },
1156 "original": {
1157 "owner": "oxalica",
1158 "repo": "rust-overlay",
1159 "type": "github"
1160 }
1161 },
811 "sops-nix": { 1162 "sops-nix": {
812 "inputs": { 1163 "inputs": {
813 "nixpkgs": [ 1164 "nixpkgs": [
@@ -815,11 +1166,11 @@
815 ] 1166 ]
816 }, 1167 },
817 "locked": { 1168 "locked": {
818 "lastModified": 1733965552, 1169 "lastModified": 1754988908,
819 "narHash": "sha256-GZ4YtqkfyTjJFVCub5yAFWsHknG1nS/zfk7MuHht4Fs=", 1170 "narHash": "sha256-t+voe2961vCgrzPFtZxha0/kmFSHFobzF00sT8p9h0U=",
820 "owner": "Mic92", 1171 "owner": "Mic92",
821 "repo": "sops-nix", 1172 "repo": "sops-nix",
822 "rev": "2d73fc6ac4eba4b9a83d3cb8275096fbb7ab4004", 1173 "rev": "3223c7a92724b5d804e9988c6b447a0d09017d48",
823 "type": "github" 1174 "type": "github"
824 }, 1175 },
825 "original": { 1176 "original": {
@@ -854,8 +1205,9 @@
854 "type": "github" 1205 "type": "github"
855 }, 1206 },
856 "original": { 1207 "original": {
857 "id": "systems", 1208 "owner": "nix-systems",
858 "type": "indirect" 1209 "repo": "default",
1210 "type": "github"
859 } 1211 }
860 }, 1212 },
861 "treefmt-nix": { 1213 "treefmt-nix": {
@@ -879,6 +1231,29 @@
879 "type": "github" 1231 "type": "github"
880 } 1232 }
881 }, 1233 },
1234 "uv2nix": {
1235 "inputs": {
1236 "nixpkgs": [
1237 "nixpkgs"
1238 ],
1239 "pyproject-nix": [
1240 "pyproject-nix"
1241 ]
1242 },
1243 "locked": {
1244 "lastModified": 1755485731,
1245 "narHash": "sha256-k8kxwVs8Oze6q/jAaRa3RvZbb50I/K0b5uptlsh0HXI=",
1246 "owner": "pyproject-nix",
1247 "repo": "uv2nix",
1248 "rev": "bebbd80bf56110fcd20b425589814af28f1939eb",
1249 "type": "github"
1250 },
1251 "original": {
1252 "owner": "pyproject-nix",
1253 "repo": "uv2nix",
1254 "type": "github"
1255 }
1256 },
882 "waybar": { 1257 "waybar": {
883 "inputs": { 1258 "inputs": {
884 "flake-compat": [ 1259 "flake-compat": [
@@ -889,19 +1264,52 @@
889 ] 1264 ]
890 }, 1265 },
891 "locked": { 1266 "locked": {
892 "lastModified": 1734278650, 1267 "lastModified": 1752562190,
893 "narHash": "sha256-z9FiyHDbKC2nwfd/qsHCxLBEogzQj73zo85lW3zIlzY=", 1268 "narHash": "sha256-zWOMCNe56H2PHUd3rJZ6tklZUZBLgRo85jd9IlK1g9o=",
894 "owner": "gkleen", 1269 "owner": "gkleen",
895 "repo": "Waybar", 1270 "repo": "Waybar",
896 "rev": "5432f9c1697a8d2d3e1264a5ce820d7eac26e2c6", 1271 "rev": "d008cd998369c40f2344a856caf39cdbbd7bd068",
897 "type": "github" 1272 "type": "github"
898 }, 1273 },
899 "original": { 1274 "original": {
900 "owner": "gkleen", 1275 "owner": "gkleen",
901 "ref": "feat/privacy-ignore", 1276 "ref": "feat/niri-urgency",
902 "repo": "Waybar", 1277 "repo": "Waybar",
903 "type": "github" 1278 "type": "github"
904 } 1279 }
1280 },
1281 "xwayland-satellite-stable": {
1282 "flake": false,
1283 "locked": {
1284 "lastModified": 1755491097,
1285 "narHash": "sha256-m+9tUfsmBeF2Gn4HWa6vSITZ4Gz1eA1F5Kh62B0N4oE=",
1286 "owner": "Supreeeme",
1287 "repo": "xwayland-satellite",
1288 "rev": "388d291e82ffbc73be18169d39470f340707edaa",
1289 "type": "github"
1290 },
1291 "original": {
1292 "owner": "Supreeeme",
1293 "ref": "v0.7",
1294 "repo": "xwayland-satellite",
1295 "type": "github"
1296 }
1297 },
1298 "xwayland-satellite-unstable": {
1299 "flake": false,
1300 "locked": {
1301 "lastModified": 1757179758,
1302 "narHash": "sha256-TIvyWzRt1miQj6Cf5Wy8Qz43XIZX7c4vTVwRLAT5S4Y=",
1303 "owner": "Supreeeme",
1304 "repo": "xwayland-satellite",
1305 "rev": "970728d0d9d1eada342bb8860af214b601139e58",
1306 "type": "github"
1307 },
1308 "original": {
1309 "owner": "Supreeeme",
1310 "repo": "xwayland-satellite",
1311 "type": "github"
1312 }
905 } 1313 }
906 }, 1314 },
907 "root": "root", 1315 "root": "root",
diff --git a/flake.nix b/flake.nix
index cd50543e..b93a1f2e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -4,9 +4,11 @@
4 nixConfig = { 4 nixConfig = {
5 extra-substituters = [ 5 extra-substituters = [
6 "https://nix-community.cachix.org" 6 "https://nix-community.cachix.org"
7 "https://niri.cachix.org"
7 ]; 8 ];
8 extra-trusted-public-keys = [ 9 extra-trusted-public-keys = [
9 "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" 10 "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
11 "niri.cachix.org-1:Wv0OmO7PsuocRKzfDoJ3mulSl7Z6oezYhGhR+3W2964="
10 ]; 12 ];
11 }; 13 };
12 14
@@ -27,13 +29,13 @@
27 type = "github"; 29 type = "github";
28 owner = "NixOS"; 30 owner = "NixOS";
29 repo = "nixpkgs"; 31 repo = "nixpkgs";
30 ref = "24.05"; 32 ref = "25.05";
31 }; 33 };
32 nixpkgs-eostre = { 34 nixpkgs-eostre = {
33 type = "github"; 35 type = "github";
34 owner = "NixOS"; 36 owner = "NixOS";
35 repo = "nixpkgs"; 37 repo = "nixpkgs";
36 ref = "23.11"; 38 ref = "25.05";
37 }; 39 };
38 home-manager = { 40 home-manager = {
39 type = "github"; 41 type = "github";
@@ -51,7 +53,7 @@
51 type = "github"; 53 type = "github";
52 owner = "gkleen"; 54 owner = "gkleen";
53 repo = "home-manager"; 55 repo = "home-manager";
54 ref = "nixos-late-start-23.11"; 56 ref = "nixos-late-start-25.05";
55 inputs = { 57 inputs = {
56 nixpkgs.follows = "nixpkgs-eostre"; 58 nixpkgs.follows = "nixpkgs-eostre";
57 }; 59 };
@@ -123,25 +125,43 @@
123 nixpkgs.follows = "nixpkgs"; 125 nixpkgs.follows = "nixpkgs";
124 }; 126 };
125 }; 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 };
126 143
127 ca-util = { 144 ca-util = {
128 type = "gitlab"; 145 type = "gitlab";
129 owner = "gkleen"; 146 owner = "gkleen";
130 repo = "ca"; 147 repo = "ca";
131 ref = "v3.1.3"; 148 ref = "v3.1.5";
132 inputs = { 149 inputs = {
150 pyproject-nix.follows = "pyproject-nix";
151 uv2nix.follows = "uv2nix";
133 nixpkgs.follows = "nixpkgs"; 152 nixpkgs.follows = "nixpkgs";
134 poetry2nix.follows = "poetry2nix";
135 }; 153 };
136 }; 154 };
137 backup-utils = { 155 backup-utils = {
138 type = "gitlab"; 156 type = "gitlab";
139 owner = "gkleen"; 157 owner = "gkleen";
140 repo = "backup-utils"; 158 repo = "backup-utils";
141 ref = "v0.1.6"; 159 ref = "v0.1.7";
142 inputs = { 160 inputs = {
143 nixpkgs.follows = "nixpkgs"; 161 nixpkgs.follows = "nixpkgs";
144 poetry2nix.follows = "poetry2nix"; 162 pyproject-nix.follows = "pyproject-nix";
163 uv2nix.follows = "uv2nix";
164 pyproject-build-systems.follows = "pyproject-build-systems";
145 }; 165 };
146 }; 166 };
147 prometheus-borg-exporter = { 167 prometheus-borg-exporter = {
@@ -170,7 +190,7 @@
170 type = "github"; 190 type = "github";
171 owner = "gkleen"; 191 owner = "gkleen";
172 repo = "Waybar"; 192 repo = "Waybar";
173 ref = "feat/privacy-ignore"; 193 ref = "feat/niri-urgency";
174 inputs = { 194 inputs = {
175 nixpkgs.follows = "nixpkgs"; 195 nixpkgs.follows = "nixpkgs";
176 flake-compat.follows = "flake-compat"; 196 flake-compat.follows = "flake-compat";
@@ -182,9 +202,41 @@
182 repo = "NixVirt"; 202 repo = "NixVirt";
183 inputs.nixpkgs.follows = "nixpkgs"; 203 inputs.nixpkgs.follows = "nixpkgs";
184 }; 204 };
205 niri-flake = {
206 type = "github";
207 owner = "sodiboo";
208 repo = "niri-flake";
209 ref = "main";
210 inputs = {
211 nixpkgs.follows = "nixpkgs";
212 niri-unstable = {
213 type = "github";
214 owner = "gkleen";
215 repo = "niri";
216 ref = "fix/locked-monitor-control";
217 };
218 };
219 };
220 nix-monitored = {
221 type = "github";
222 owner = "ners";
223 repo = "nix-monitored";
224 ref = "master";
225 inputs = {
226 nixpkgs.follows = "nixpkgs";
227 };
228 };
229 lanzaboote = {
230 type = "github";
231 owner = "nix-community";
232 repo = "lanzaboote";
233 ref = "v0.4.2";
234
235 inputs.nixpkgs.follows = "nixpkgs";
236 };
185 }; 237 };
186 238
187 outputs = { self, nixpkgs, home-manager, sops-nix, deploy-rs, nvfetcher, ... }@inputs: 239 outputs = { self, nixpkgs, home-manager, sops-nix, deploy-rs, nvfetcher, niri-flake, ... }@inputs:
188 let 240 let
189 inherit (builtins) attrNames attrValues elemAt toJSON isNull pathExists; 241 inherit (builtins) attrNames attrValues elemAt toJSON isNull pathExists;
190 inherit (nixpkgs) lib; 242 inherit (nixpkgs) lib;
@@ -267,9 +319,10 @@
267 mkAccountModule = dir: path: accountName: 319 mkAccountModule = dir: path: accountName:
268 let 320 let
269 userName = accountUserName accountName; 321 userName = accountUserName accountName;
322 hostName = accountHostName accountName;
270 in overrideModule 323 in overrideModule
271 (import (dir + "/${path}")) 324 (import (dir + "/${path}"))
272 (inputs: inputs // { inherit userName; }) 325 (inputs: inputs // { inherit userName hostName; })
273 (outputs: { _file = dir + "/${path}"; } 326 (outputs: { _file = dir + "/${path}"; }
274 // outputs 327 // outputs
275 // { imports = [self.nixosModules.users.${userName} or ({...}: { imports = defaultUserProfiles userName; })] ++ (outputs.imports or []); }); 328 // { imports = [self.nixosModules.users.${userName} or ({...}: { imports = defaultUserProfiles userName; })] ++ (outputs.imports or []); });
@@ -285,7 +338,7 @@
285 forAllUsers = genAttrs (unique (map accountUserName (attrNames self.nixosModules.accounts))); 338 forAllUsers = genAttrs (unique (map accountUserName (attrNames self.nixosModules.accounts)));
286 339
287 activateNixosConfigurations = forAllSystems (system: _pkgs: filterAttrs (_n: v: v != null) (mapAttrs' (hostName: nixosConfig: nameValuePair "${hostName}-activate" (if system == nixosConfig.config.nixpkgs.system then { type = "app"; program = "${nixosConfig.config.system.build.toplevel}/bin/switch-to-configuration"; } else null)) self.nixosConfigurations)); 340 activateNixosConfigurations = forAllSystems (system: _pkgs: filterAttrs (_n: v: v != null) (mapAttrs' (hostName: nixosConfig: nameValuePair "${hostName}-activate" (if system == nixosConfig.config.nixpkgs.system then { type = "app"; program = "${nixosConfig.config.system.build.toplevel}/bin/switch-to-configuration"; } else null)) self.nixosConfigurations));
288 startVMs = forAllSystems (system: pkgs: mapAttrs' (hostName: nixosConfig: nameValuePair "run-${hostName}-vm" { type = "app"; program = "${nixosConfig.config.system.build.vm}/bin/run-${hostName}-vm"; }) (nixImport rec { dir = ./hosts; _import = mkNixosConfiguration [ { config.virtualisation.host.pkgs = pkgs; } ] dir; })); 341 # startVMs = forAllSystems (system: pkgs: mapAttrs' (hostName: nixosConfig: nameValuePair "run-${hostName}-vm" { type = "app"; program = "${nixosConfig.config.system.build.vm}/bin/run-${hostName}-vm"; }) (nixImport rec { dir = ./hosts; _import = mkNixosConfiguration [ { config.virtualisation.host.pkgs = pkgs; } ] dir; }));
289 activateHomeManagerConfigurations = forAllSystems (system: _pkgs: filterAttrs (_n: v: v != null) (listToAttrs (concatLists (mapAttrsToList (hostName: nixosConfig: mapAttrsToList (userName: userCfg: nameValuePair "${userName}@${hostName}-activate" (if system == nixosConfig.config.nixpkgs.system then { type = "app"; program = "${userCfg.home.activationPackage}/activate"; } else null)) nixosConfig.config.home-manager.users) self.nixosConfigurations)))); 342 activateHomeManagerConfigurations = forAllSystems (system: _pkgs: filterAttrs (_n: v: v != null) (listToAttrs (concatLists (mapAttrsToList (hostName: nixosConfig: mapAttrsToList (userName: userCfg: nameValuePair "${userName}@${hostName}-activate" (if system == nixosConfig.config.nixpkgs.system then { type = "app"; program = "${userCfg.home.activationPackage}/activate"; } else null)) nixosConfig.config.home-manager.users) self.nixosConfigurations))));
290 installerShells = system: pkgs: mapAttrs (installerName: config: pkgs.callPackage ./installer/shell.nix { 343 installerShells = system: pkgs: mapAttrs (installerName: config: pkgs.callPackage ./installer/shell.nix {
291 inherit system installerName config; 344 inherit system installerName config;
@@ -322,18 +375,23 @@
322 nixosConfigurations = installerNixosConfigurations // nixImport rec { dir = ./hosts; _import = mkNixosConfiguration [] dir; }; 375 nixosConfigurations = installerNixosConfigurations // nixImport rec { dir = ./hosts; _import = mkNixosConfiguration [] dir; };
323 376
324 homeModules = nixImport rec { dir = ./home-modules; }; 377 homeModules = nixImport rec { dir = ./home-modules; };
325 homeConfigurations = listToAttrs (concatLists (mapAttrsToList (hostname: nixosConfig: mapAttrsToList (username: configuration: nameValuePair "${username}@${hostname}" { inherit (configuration.home) activationPackage; }) nixosConfig.config.home-manager.users) self.nixosConfigurations)); 378 homeConfigurations = listToAttrs (concatLists (mapAttrsToList (hostname: nixosConfig: mapAttrsToList (username: nameValuePair "${username}@${hostname}") nixosConfig.config.home-manager.users) self.nixosConfigurations));
326 379
327 overlays = mapAttrs (_name: path: mkOverlay path) overlayPaths; 380 overlays = mapAttrs (_name: path: mkOverlay path) overlayPaths;
328 381
329 packages = forAllSystems (system: systemPkgs: nixImport rec { dir = ./tools; _import = _path: name: import "${toString dir}/${name}" ({ inherit system; } // inputs); }); 382 packages = forAllSystems (system: systemPkgs: nixImport rec { dir = ./tools; _import = name: _base: import (dir + "/${name}") ({ inherit system; } // inputs); });
330 383
331 # packages = mapAttrs (_name: filterAttrs (_name: isDerivation)) packages; 384 # packages = mapAttrs (_name: filterAttrs (_name: isDerivation)) packages;
332 # packages' = mapAttrs (_name: filterAttrs (_name: value: !(isDerivation value))) packages; 385 # packages' = mapAttrs (_name: filterAttrs (_name: value: !(isDerivation value))) packages;
333 386
334 legacyPackages = forAllSystems (system: systemPkgs: systemPkgs.override { overlays = attrValues self.overlays; }); 387 legacyPackages = forAllSystems (system: systemPkgs: systemPkgs.override { overlays = attrValues self.overlays; });
335 388
336 apps = foldr recursiveUpdate {} [startVMs activateNixosConfigurations activateHomeManagerConfigurations]; 389 apps = foldr recursiveUpdate {} [
390 #startVMs
391 activateNixosConfigurations activateHomeManagerConfigurations
392 ];
393
394 lib = nixImport rec { dir = ./lib; _import = name: _base: import (dir + "/${name}") inputs; };
337 395
338 devShells = forAllSystems (system: systemPkgs: { default = import ./shell.nix ({ inherit system; } // inputs); } // installerShells system systemPkgs); 396 devShells = forAllSystems (system: systemPkgs: { default = import ./shell.nix ({ inherit system; } // inputs); } // installerShells system systemPkgs);
339 397
@@ -358,10 +416,10 @@
358 # path = activateHomeManager (self.nixosConfigurations.${hostname}.config.nixpkgs.system) usercfg.home; 416 # path = activateHomeManager (self.nixosConfigurations.${hostname}.config.nixpkgs.system) usercfg.home;
359 # }) self.nixosConfigurations.${hostname}.config.home-manager.users); 417 # }) self.nixosConfigurations.${hostname}.config.home-manager.users);
360 }) (nixImport { dir = ./hosts; _import = (_path: name: name); }); 418 }) (nixImport { dir = ./hosts; _import = (_path: name: name); });
361 overrides = if pathExists ./deploy then nixImport { dir = ./deploy; _import = path: _name: import (./deploy + "/${path}") inputs; } else {}; 419 overrides = if pathExists ./deploy then nixImport rec { dir = ./deploy; _import = path: _name: import (dir + "/${path}") inputs; } else {};
362 filterEnabled = attrs: mapAttrs (_n: v: filterAttrs (n: _v: n != "enabled") v) (filterAttrs (_n: v: v.enabled or true) attrs); 420 filterEnabled = attrs: mapAttrs (_n: v: filterAttrs (n: _v: n != "enabled") v) (filterAttrs (_n: v: v.enabled or true) attrs);
363 in mapAttrs (_n: v: if v ? "profiles" then v // { profiles = filterEnabled v.profiles; } else v) (filterEnabled (recursiveUpdate defaults overrides)); 421 in mapAttrs (_n: v: if v ? "profiles" then v // { profiles = filterEnabled v.profiles; } else v) (filterEnabled (recursiveUpdate defaults overrides));
364 422
365 checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib; 423 # checks = builtins.mapAttrs (system: deployLib: deployLib.deployChecks self.deploy) deploy-rs.lib;
366 }; 424 };
367} 425}
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/home-modules/pandoc/default.nix b/home-modules/pandoc/default.nix
new file mode 100644
index 00000000..1d16b621
--- /dev/null
+++ b/home-modules/pandoc/default.nix
@@ -0,0 +1,27 @@
1{ pkgs, lib, config, ... }:
2
3let
4 cfg = config.programs.pandoc;
5in {
6 options.programs.pandoc = {
7 germanAbbreviations = lib.mkEnableOption "importing german abbreviations" // { default = true; };
8 extraAbbreviations = lib.mkOption {
9 type = lib.types.listOf lib.types.str;
10 default = [];
11 };
12 };
13
14 config = lib.mkIf cfg.enable {
15 xdg.dataFile = lib.mkIf (cfg.germanAbbreviations || cfg.extraAbbreviations != []) {
16 "pandoc/abbreviations".source = pkgs.runCommand "pandoc-abbreviations" {
17 buildInputs = [ pkgs.coreutils ];
18 } ''
19 cat \
20 <(${lib.getExe' cfg.finalPackage "pandoc"} --print-default-data-file=abbreviations) \
21 ${lib.optionalString cfg.germanAbbreviations ./german_abbreviations.txt} \
22 ${lib.optionalString (cfg.extraAbbreviations != []) (pkgs.writeText "abbrevs.txt" (lib.concatStringsSep "\n" cfg.extraAbbreviations))} \
23 | sort | uniq >$out
24 '';
25 };
26 };
27}
diff --git a/home-modules/pandoc/german_abbreviations.txt b/home-modules/pandoc/german_abbreviations.txt
new file mode 100644
index 00000000..fa4c9c87
--- /dev/null
+++ b/home-modules/pandoc/german_abbreviations.txt
@@ -0,0 +1,1423 @@
1&c.
2A.
3a.
4a.a.O.
5A.C.A.B.
6a.D.
7a.d.D.
8a.g.O.
9Abb.
10abchas.
11abds.
12Abf.
13Abfr.
14Abg.
15abgek.
16abh.
17Abh.
18Abk.
19ABl.
20Abl.
21Abm.
22abn.
23Abn.
24abr.
25Abr.
26Abs.
27abs.
28Abschn.
29Abst.
30Abt.
31abulg.
32abw.
33abwert.
34abzgl.
35accel.
36accresc.
37Add.
38Adj.
39adj.
40Adr.
41adv.
42Adv.
43adyg.
44ae.
45aengl.
46afghan.
47afr.
48afranz.
49afranzös.
50afries.
51afrik.
52afrk.
53afrs.
54afrz.
55afränk.
56ags.
57ahd.
58Ahd.
59aind.
60air.
61akad.
62Akk.
63akkad.
64akt.
65alb.
66alban.
67alem.
68alemann.
69all.
70allg.
71allj.
72allm.
73alltagsspr.
74alphanum.
75Alt.
76altai.
77altengl.
78altfranz.
79altfranzös.
80altfrz.
81altgr.
82althochdt.
83altis.
84altisländ.
85altpreuß.
86altröm.
87alttest.
88alëut.
89am.
90amer.
91amerik.
92amerikan.
93amhar.
94amt.
95amtl.
96Amtm.
97Amtsbl.
98Amtsdt.
99Amtsspr.
100an.
101anal.
102anat.
103Anat.
104anatom.
105andalus.
106ang.
107angelsächs.
108Angest.
109angest.
110angloamerik.
111anglofrz.
112angloind.
113Anh.
114Ank.
115Ankl.
116Anl.
117anl.
118Anm.
119Anm.d.Red.
120Ann.
121ann.
122annamit.
123anord.
124Anord.
125anschl.
126Anschl.
127Anschr.
128antarkt.
129Anthrop.
130anthrop.
131Anw.
132aobd.
133apl.
134Apostr.
135App.
136Apr.
137apreuß.
138ar.
139arab.
140aragon.
141aram.
142aran.
143architekt.
144archäol.
145arg.
146argent.
147arkt.
148armen.
149Art.
150Art.-Nr.
151Artt.
152as.
153aserbaidsch.
154aslaw.
155assyr.
156astron.
157asächs.
158At.-Gew.
159attr.
160Attr.
161Aufl.
162Aug.
163Ausg.
164ausgen.
165Aussch.
166ausschl.
167Ausspr.
168Ausst.
169austral.
170awar.
171awest.
172Az.
173aztek.
174b.
175B.
176Ba.-Wü.
177bab.
178babyl.
179bair.
180Bakt.
181Bal.
182balt.
183baltoslaw.
184Bankw.
185bas.
186baschk.
187bask.
188Bat.
189bauf.
190Bauw.
191bay.
192bayer.
193bayr.
194BayVBl.
195Bd.
196Bde.
197Bed.
198Begr.
199begr.
200beif.
201beil.
202Beil.
203Bem.
204ben.
205berbersprachl.
206Bergb.
207berlin.
208Berufsbez.
209bes.
210besch.
211Beschl.
212best.
213Best.-Nr.
214Betr.
215betr.
216Betriebswiss.
217Bev.
218Bez.
219bez.
220bezw.
221Bf.
222bfn.
223Bg.
224bgld.
225Bgld.
226Bhf.
227Bib.
228bibl.
229bildl.
230bildungsspr.
231Biol.
232biol.
233Bj.
234bl.
235Bl.
236Blk.
237Bln.
238Bodenk.
239bot.
240Bot.
241Br.-M.
242Br.-Mstr.
243bras.
244bret.
245breton.
246brit.
247Brm.
248brn.
249Bruchz.
250bsd.
251Bsp.
252bsplsw.
253bspw.
254BT-Drs.
255Btl.
256btto.
257Bttr.
258Buchw.
259buddh.
260bulg.
261bulgar.
262burjat.
263burmes.
264Bw.
265byzant.
266Bz.
267bzb.
268bzgl.
269bzw.
270böhm.
271Börsenw.
272C.
273ca.
274Carp.
275Cb.
276cf.
277chakass.
278chald.
279chant.
280chem.
281Chem.
282chilen.
283chin.
284Chr.
285christl.
286chron.
287Chron.
288Co.
289Comp.
290cresc.
291D.
292Dankb.
293dankwtw.
294das.
295dass.
296Dat.
297dbzgl.
298ders.
299des.
300desgl.
301Dez.
302dgl.
303Di.
304dial.
305dichter.
306dies.
307dim.
308Dim.
309Dimin.
310dimin.
311Dipl.
312Dipl.-Bibl.
313Dipl.-Ing.
314Dipl.-Kff.
315Dipl.-Kffr.
316Dipl.-Kfm.
317Dipl.-Kfr.
318Dipl.-Psych.
319Dir.
320Diss.
321Do.
322do.
323Do.-Gge.
324dominikan.
325dor.
326Doz.
327Dr.
328Drchf.
329Drcks.
330Dres.
331Drs.
332Drucks.
333dt.
334Dtl.
335dto.
336Dtzd.
337dz.
338Dz.
339dän.
340E.
341ebd.
342Ed.
343ed.
344ehem.
345eidg.
346eig.
347eigtl.
348Einf.
349einh.
350Einl.
351einschl.
352Einw.
353Eisenb.
354Elektrot.
355elektrotechn.
356em.
357engl.
358entspr.
359erb.
360erf.
361erg.
362Erg.
363erk.
364Erl.
365erm.
366Ers.-D.
367ersch.
368erschl.
369Erschl.
370Erschl.-Geb.
371Erschw.
372erschw.
373Erstauff.
374Erstausg.
375Ertr.
376Erw.
377Erw.-Bldg.
378erwähnw.
379Erzb.
380erzg.
381erzgeb.
382eskim.
383estn.
384etc.
385Etg.
386etrusk.
387etw.
388eur.
389europ.
390ev.
391evang.
392evtl.
393Ew.
394ewen.
395ewenk.
396exkl.
397Expl.
398Ez.
399f.
400F.
401Fa.
402fachspr.
403Fachspr.
404Fag.
405Fam.
406fam.
407Febr.
408fem.
409ff.
410Fig.
411fig.
412finanzmath.
413finn.
414finnougr.
415Flgh.
416fläm.
417Fn.
418fnhd.
419folg.
420Forts.
421Fortstzg.
422Fr.
423fr.
424fragm.
425franz.
426französ.
427Frdf.
428frdl.
429frdsprlg.
430Frfr.
431frfr.
432Frh.
433Frhf.
434Frhr.
435fries.
436friesl.
437Frk.
438Frl.
439Frm.
440frnhd.
441Frspr.
442frstl.
443Frt.
444frtr.
445Frwk.
446frz.
447fränk.
448frühnhd.
449Fs.
450Fsch.
451Fschr.
452Fsm.
453Ftm.
454Fut.
455fut.
456färö.
457förml.
458g.
459Ga.
460gall.
461galloroman.
462Gart.
463gaskogn.
464gbd.
465Gbd.
466Gbf.
467GBl.
468Gbl.
469geb.
470Geb.
471Geb.-T.
472Gebr.
473gebr.
474ged.
475gef.
476geg.
477gegr.
478geh.
479gek.
480gel.
481geleg.
482gem.
483gemeingerm.
484gen.
485Gen.
486geod.
487geogr.
488geograf.
489geograph.
490geol.
491geolog.
492geophys.
493georg.
494gep.
495ger.
496germ.
497Ges.
498ges.
499gesch.
500gespr.
501gest.
502get.
503Gew.
504gew.
505gez.
506Gfsch.
507Gft.
508gg.
509ggb.
510ggbfs.
511ggez.
512ggf.
513ggfls.
514ggfs.
515Ggs.
516ggü.
517Ghzg.
518Ghzgt.
519glchz.
520Gld.
521Glde.
522gldg.
523Gldr.
524Gled.
525gleichbed.
526gleichn.
527gleichz.
528Glfl.
529gls.
530gltd.
531gltg.
532glz.
533gm.
534got.
535gr.
536Gr.
537Gramm.
538grammat.
539graph.
540grch.
541Grchl.
542Grdb.
543Grdf.
544Grdfl.
545Grdg.
546Grdl.
547Grdr.
548grds.
549Grdst.
550griech.
551Grz.
552grönländ.
553Gstb.
554Gt.
555gyn.
556gynäk.
557gäl.
558H.-I.
559H.-Qu.
560hait.
561Handw.
562Hbf.
563hd.
564Hd.-Bibl.
565Hdb.
566hdbr.
567Hdbr.
568hdl.
569Hdl.
570Hdlbg.
571hebr.
572hess.
573hethit.
574Hf.
575Hg.
576hg.
577hindust.
578hinr.
579hins.
580Hinw.
581hist.
582HJber.
583Hkl.
584hl.
585hochd.
586hochspr.
587Hom.
588hor.
589Hpfl.
590hptpl.
591hpts.
592Hptst.
593hptw.
594Hptw.
595HQu.
596Hr.
597HReg.
598Hrn.
599Hrsg.
600hrsg.
601Hs.
602Hs.-Nr.
603hschr.
604Hschr.
605HSt.
606Hubbr.
607Hubr.
608Hw.
609Hyaz.
610hydr.
611hydrol.
612Hzm.
613i.
614I.E.
615i.g.O.
616i.Tr.
617iber.
618ibid.
619ide.
620Ident.
621ident.
622idg.
623ie.
624illyr.
625Imkerspr.
626imp.
627Imp.
628in.
629Ind.
630ind.
631indef.
632indekl.
633indian.
634indiff.
635indir.
636indiv.
637indog.
638indogerm.
639indogerman.
640indoiran.
641indon.
642indones.
643Inf.
644inf.
645Ing.
646Inh.
647inkl.
648inn.
649Ins.
650insb.
651insbes.
652int.
653intern.
654intrans.
655ir.
656iran.
657iron.
658isl.
659islam.
660isländ.
661it.
662ital.
663italien.
664j.
665J.
666Jahrh.
667jakut.
668Jan.
669jap.
670japan.
671jav.
672jem.
673jemen.
674Jg.
675jgdfr.
676Jh.
677Jhd.
678Jhdt.
679Jhg.
680Jhs.
681jidd.
682jmd.
683jmdm.
684jmdn.
685jmds.
686journ.
687jr.
688Jr.
689Jt.
690Jtsd.
691jugendspr.
692jugendsprachl.
693jugoslaw.
694Jul.
695jun.
696Jun.
697jur.
698Juw.
699jägersprachl.
700jährl.
701Jän.
702jüd.
703k.u.k.
704K.Ö.St.V.
705kalm.
706kanad.
707Kap.
708karib.
709kastil.
710katal.
711katalan.
712kath.
713kaufm.
714kaukas.
715kelt.
716Kgr.
717Kh.
718kindersprachl.
719kirchenlat.
720kirchenslaw.
721kirchl.
722kirg.
723Kl.
724klass.
725klass.-lat.
726klimatol.
727kol.
728Komm.
729Konj.
730Konv.
731Kop.
732kop.
733kopt.
734korean.
735Kr.
736kreol.
737kret.
738Krh.
739Krhs.
740Krim.-Ob.-Insp.
741krimgot.
742kriminaltechn.
743Krkhs.
744kroat.
745Krs.
746Ks.
747Kto.
748Kto.-Nr.
749kuban.
750kurd.
751Kurzw.
752Kw.
753l.
754L.-Abg.
755lab.
756LAbg.
757ladin.
758landsch.
759Landw.
760langfr.
761langj.
762langob.
763langobard.
764lapp.
765lat.
766latein.
767latinis.
768lautl.
769lautm.
770lbd.
771lbdg.
772Ldkr.
773led.
774leg.
775lett.
776lfd.
777Lfg.
778Lfm.
779Lfrg.
780Lg.
781lgfr.
782Lgft.
783lgj.
784lig.
785ling.
786lit.
787LL.M.
788lrh.
789lt.
790ltd.
791luth.
792luxemb.
793Lz.
794m.
795M.
796M.-Schr.
797m.a.W.
798ma.
799MA.
800Mag.
801malai.
802marinespr.
803marx.
804mask.
805math.
806Math.
807max.
808Max.
809mazedon.
810mbl.
811Mbl.
812MBl.
813Mbll.
814md.
815mdal.
816mdj.
817mdl.
818mdls.
819Mdt.
820me.
821mech.
822meckl.
823med.
824melanes.
825mengl.
826Merc.
827met.
828meteorol.
829meton.
830mex.
831mexik.
832mfr.
833mfranz.
834mfrk.
835mfrz.
836mfränk.
837mgl.
838Mgl.
839mglw.
840mhd.
841mhdt.
842Mi.
843mi.
844Mia.
845milit.
846Mill.
847min.
848Min.
849mind.
850Mio.
851mir.
852Mitgl.
853mitteld.
854mitteldt.
855mittelhochdt.
856Mittw.
857Mitw.
858mlat.
859Mme.
860Mmes.
861mnd.
862mndd.
863mniederd.
864mnl.
865Mo.
866mod.
867mong.
868Mrd.
869Mrz.
870Mschr.
871Msgr.
872Msp.
873mtl.
874mundartl.
875musik.
876MwSt.
877Myth.
878Mz.
879männl.
880möbl.
881n.
882Nachf.
883nachm.
884nat.
885nationalsoz.
886natsoz.
887Nbf.
888Nbfl.
889Nchf.
890nd.
891ndd.
892ndrl.
893neapolit.
894Neub.
895neunorweg.
896neutest.
897neutr.
898Nfl.
899ngl.
900ngr.
901nhbr.
902nhd.
903nicar.
904niederd.
905niederdt.
906niederl.
907niederld.
908niem.
909niger.
910nihil.
911nl.
912nlat.
913nmtl.
914Nom.
915nord.
916nordamerik.
917nordd.
918norddt.
919nordgerm.
920nordostd.
921nordostdt.
922nordwestd.
923nordwestdt.
924norm.
925norw.
926norweg.
927Nov.
928Nr.
929ntw.
930Ntw.
931Nutzfl.
932nw.
933näml.
934nö.
935nördl.
936o.
937O.K.
938ob.
939Ob.
940Obb.
941obb.
942obd.
943Oberlaus.
944obers.
945obersächs.
946obj.
947od.
948offiz.
949Offz.
950Ofr.
951ofrs.
952Okt.
953op.
954Orch.-Bes.
955org.
956Orig.
957orn.
958orth.
959Ortskl.
960Osch.
961osk.
962osman.
963ostd.
964ostdt.
965ostfr.
966ostfrz.
967ostgerm.
968ostidg.
969ostmdt.
970ostmitteld.
971ostniederd.
972ostpr.
973ostpreuß.
974ostw.
975osö.
976Ouv.
977oz.
978Oz.
979oö.
980OÖ.
981P.
982p.
983P.S.
984pa.
985palästin.
986par.
987parag.
988Paragr.
989Parl.
990Part.
991pass.
992Pat.
993pej.
994pers.
995peruan.
996Pet.
997Pf.
998Pfd.
999Pfg.
1000Pfl.
1001pharm.
1002philos.
1003Philos.
1004phonolog.
1005phryg.
1006Phys.
1007phys.
1008phöniz.
1009Pi.
1010pik.
1011Pkt.
1012Pl.
1013Plur.
1014poet.
1015Pol.
1016pol.
1017polit.
1018poln.
1019polynes.
1020port.
1021portug.
1022Pos.
1023pos.
1024pp.
1025ppa.
1026preuß.
1027Priv.-Doz.
1028Prof.
1029prot.
1030Prot.
1031prov.
1032Prov.
1033prov.-fr.
1034provenz.
1035Proz.
1036Proz.-Bev.
1037präd.
1038prähist.
1039Präs.
1040Psych.
1041psych.
1042Päd.
1043Q.
1044q.v.
1045Qmstr.
1046Qt.
1047qu.
1048Qu.
1049quadr.
1050Quadr.
1051qual.
1052Qual.
1053quant.
1054Quant.
1055Quar.
1056Quart.
1057Quat.
1058quitt.
1059Quitt.
1060Quäst.
1061r.
1062r.-k.
1063Rab.
1064rad.
1065Raff.
1066Rak.
1067Randb.
1068Randbem.
1069rat.
1070Rat.
1071Rb.
1072rd.
1073RdErl.
1074Rdf.
1075refl.
1076Reg.
1077Reg.-Bez.
1078Regt.
1079Rel.
1080rel.
1081relig.
1082Rep.
1083resp.
1084Rg.-Präs.
1085RGBl.
1086rglm.
1087Rgstr.
1088Rgt.
1089Rh.
1090rh.
1091rhein.
1092rheinhess.
1093rhet.
1094rhfrk.
1095Rhj.
1096Rhld.
1097Rhs.
1098Ri.
1099Richtl.
1100rip.
1101rk.
1102roman.
1103rotw.
1104Rr.
1105rrh.
1106Rspr.
1107Rtn.
1108Rtt.
1109rumän.
1110russ.
1111Rvj.
1112rzp.
1113rätorom.
1114röm.
1115röm.-kath.
1116S.
1117s.
1118S.-Wk.
1119Sa.
1120Sachs.
1121san.
1122sanskr.
1123Sat.
1124sat.
1125Sb.
1126Sbd.
1127sc.
1128scherzh.
1129Schill.
1130schles.
1131schott.
1132schr.
1133schriftl.
1134Schussw.
1135schwed.
1136schweiz.
1137Schwg.
1138Schwp.
1139schwäb.
1140scil.
1141Sdp.
1142sek.
1143sem.
1144semit.
1145sen.
1146Sep.
1147Sept.
1148serb.
1149serbokroat.
1150Sg.
1151sibir.
1152Sing.
1153singhal.
1154Sir.
1155sizilian.
1156skand.
1157slaw.
1158slow.
1159slowak.
1160slowen.
1161So.
1162sod.
1163sof.
1164sog.
1165sogen.
1166sogl.
1167soldatenspr.
1168solv.
1169somal.
1170sorb.
1171Sout.
1172soz.
1173soziol.
1174span.
1175spez.
1176sportspr.
1177Spr.
1178sprachwiss.
1179Spvg.
1180Spvgg.
1181spätahd.
1182spätgriech.
1183spätlat.
1184spätmhd.
1185Sr.
1186ssp.
1187St.
1188St.-Nr.
1189staatl.
1190Std.
1191stdl.
1192stellv.
1193Stellv.
1194Stk.
1195Str.
1196str.
1197Stud.
1198stud.
1199subsp.
1200Subst.
1201sumer.
1202svw.
1203Swk.
1204syn.
1205Syn.
1206syr.
1207sächs.
1208südafrik.
1209südd.
1210süddt.
1211südl.
1212südostdt.
1213südwestd.
1214Süßw.
1215Tab.
1216Tabl.
1217Taf.
1218tamil.
1219tatar.
1220techn.
1221Tel.
1222telef.
1223Temp.
1224Terr.
1225tessin.
1226test.
1227Tfx.
1228tgl.
1229Tgt.
1230thrak.
1231thür.
1232thüring.
1233Ti.
1234tib.
1235tirol.
1236Tlr.
1237tochar.
1238trans.
1239tsch.
1240tschech.
1241tschechoslowak.
1242Tsd.
1243tun.
1244Tun.
1245tunes.
1246Tunes.
1247tungus.
1248turkotat.
1249typogr.
1250tägl.
1251türk.
1252u.
1253u.a.
1254Ubr.
1255ue.
1256ugr.
1257ugs.
1258ukrain.
1259umbr.
1260umg.
1261unang.
1262unbefl.
1263Unf.
1264unf.
1265unfol.
1266unfr.
1267ung.
1268ungar.
1269ungebr.
1270ungel.
1271ungen.
1272unges.
1273ungl.
1274Uni-Kl.
1275Univ.
1276unv.
1277unverantw.
1278unverb.
1279unverbr.
1280unverd.
1281unverg.
1282unverh.
1283unverk.
1284unverp.
1285unversch.
1286unverz.
1287unverzgl.
1288unvollst.
1289unvorb.
1290unvors.
1291unzerbr.
1292urgerm.
1293urkdl.
1294urspr.
1295ursprüngl.
1296Urt.
1297usf.
1298USt-IdNr.
1299usw.
1300uvm.
1301v.
1302va.
1303Ver.
1304Verf.
1305Verg.
1306vergl.
1307Vergl.
1308verh.
1309Vers.
1310vers.
1311vert.
1312Vfg.
1313vgbl.
1314vgl.
1315Vgl.
1316vh.
1317viell.
1318vj.
1319Vj.
1320vl.
1321vlat.
1322vlt.
1323vmtl.
1324volkst.
1325Vors.
1326vrstl.
1327vrt.
1328vs.
1329vsl.
1330vt.
1331vulg.
1332vulgärlat.
1333Vwz.
1334vzk.
1335w.
1336W.
1337Wa.
1338wal.
1339wehrtgl.
1340weibl.
1341Weis.
1342weißruss.
1343werkt.
1344westd.
1345westdt.
1346Westf.
1347westfäl.
1348westgerm.
1349westl.
1350westmitteld.
1351westmitteldt.
1352Wf.
1353wf.
1354Wfl.
1355wg.
1356wh.
1357Whg.
1358winzerspr.
1359wirtschaftl.
1360wiss.
1361Wkst.
1362Wkstf.
1363wkts.
1364wld.
1365Wr.
1366Ws.
1367Wtb.
1368Ww.
1369Wwe.
1370Wz.
1371Xerogr.
1372Xerok.
1373Xyl.
1374y.
1375Y.
1376yd.
1377Yd.
1378Yds.
1379yds.
1380Z.
1381z.B.
1382za.
1383Zf.
1384Zgm.
1385zgs.
1386zgst.
1387zgw.
1388Zi.
1389Ziff.
1390zit.
1391Zit.
1392zk.
1393Zk.
1394Zool.
1395zool.
1396Zssg.
1397Zssgn.
1398Ztr.
1399Zub.
1400zur.
1401zus.
1402zw.
1403Zz.
1404zz.
1405zzgl.
1406zzt.
1407ägypt.
1408Ökol.
1409ökol.
1410ökon.
1411ökum.
1412örtl.
1413österr.
1414Österr.
1415östl.
1416übers.
1417übertr.
1418überw.
1419Überw.
1420übl.
1421üblw.
1422Übn.
1423übsch.
diff --git a/home-modules/pandoc/german_abbreviations.txt.gup b/home-modules/pandoc/german_abbreviations.txt.gup
new file mode 100755
index 00000000..abcab1da
--- /dev/null
+++ b/home-modules/pandoc/german_abbreviations.txt.gup
@@ -0,0 +1,35 @@
1#!/usr/bin/env nix
2#!nix shell --impure --expr ``
3#!nix with (import (builtins.getFlake ''nixpkgs'') {});
4#!nix python3.withPackages (ps: with ps; [ requests ])
5#!nix `` --command python3
6
7import requests
8import json
9import sys
10import re
11import subprocess
12
13def wiki_cont(url, params):
14 continue_params = None
15 while True:
16 req_params = params
17 if continue_params is not None:
18 req_params |= continue_params
19 json_data = requests.get(url, req_params).json()
20 if "query" in json_data:
21 yield json_data["query"]
22 if "continue" not in json_data:
23 break
24 else:
25 continue_params = json_data["continue"]
26
27out_re = re.compile(r"[^ ]*[^ 0-9][^ ]*\.")
28
29subprocess.run(["gup", "--always"], check=True)
30
31with open(sys.argv[1], 'w') as out:
32 for query in wiki_cont("https://de.wiktionary.org/w/api.php", {"action": "query", "list": "categorymembers", "cmtitle": "Kategorie:Abkürzung_(Deutsch)", "format": "json"}):
33 for item in map(lambda i: i["title"], query["categorymembers"]):
34 if out_re.fullmatch(item):
35 print(item, file=out)
diff --git a/home-modules/quickshell.nix b/home-modules/quickshell.nix
new file mode 100644
index 00000000..dac7089f
--- /dev/null
+++ b/home-modules/quickshell.nix
@@ -0,0 +1,68 @@
1{ config, pkgs, lib, ... }:
2
3let
4 cfg = config.programs.quickshell;
5in {
6 disabledModules = ["programs/quickshell.nix"];
7
8 options = {
9 programs.quickshell = {
10 enable = lib.mkEnableOption "quickshell";
11 package = lib.mkPackageOption pkgs "quickshell" { nullable = true; };
12 config.src = lib.mkOption {
13 type = lib.types.path;
14 };
15 config.replacements = lib.mkOption {
16 type = lib.types.attrsOf lib.types.str;
17 default = {};
18 };
19 };
20 };
21
22 config = lib.mkIf cfg.enable {
23 home.packages = [ cfg.package ];
24
25 xdg.configFile."quickshell".source = pkgs.stdenvNoCC.mkDerivation {
26 name = "quickshell";
27 preferLocalBuild = true;
28 allowSubstitutes = false;
29 dontUnpack = true;
30 inherit (cfg.config) src;
31 buildPhase = ''
32 runHook preBuild
33
34 while IFS= read -r -d $'\0' file <&3; do
35 [[ -z $file ]] && continue
36
37 mkdir -p "$out"/"$(dirname "$file")"
38 substitute "$src"/"$file" "$out"/"$file" \
39 ${lib.concatStringsSep " " (
40 lib.concatLists (lib.mapAttrsToList (name: value: [
41 "--replace-quiet" (lib.escapeShellArg "@${name}@") (lib.escapeShellArg value)
42 ]) cfg.config.replacements)
43 )}
44 done 3< <(find "$src" -type f -printf '%P\0')
45
46 runHook postBuild
47 '';
48 };
49
50 systemd.user.services.quickshell = {
51 Unit = {
52 Description = "quickshell";
53 Documentation = "https://quickshell.org/docs/v${cfg.package.version}";
54 PartOf = [ "graphical-session.target" ];
55 After = [ "graphical-session-pre.target" ];
56 };
57
58 Service = {
59 ExecStart = lib.getExe cfg.package;
60 Restart = "always";
61 };
62
63 Install = {
64 WantedBy = [ "graphical-session.target" ];
65 };
66 };
67 };
68}
diff --git a/hosts/eostre/default.nix b/hosts/eostre/default.nix
index fd4b15f2..d4113024 100644
--- a/hosts/eostre/default.nix
+++ b/hosts/eostre/default.nix
@@ -37,14 +37,10 @@ with lib;
37 powerManagement.enable = true; 37 powerManagement.enable = true;
38 }; 38 };
39 39
40 opengl.enable = true; 40 graphics.enable = true;
41 }; 41 };
42 42
43 environment.etc."machine-id".text = "f457b21333f1491e916521151ff5d468";
44
45 networking = { 43 networking = {
46 hostId = "f457b213";
47
48 domain = "lan.yggdrasil"; 44 domain = "lan.yggdrasil";
49 search = [ "lan.yggdrasil" "yggdrasil" ]; 45 search = [ "lan.yggdrasil" "yggdrasil" ];
50 46
@@ -83,19 +79,14 @@ with lib;
83 ]; 79 ];
84 }; 80 };
85 81
86 82 services.displayManager.sddm = {
87 services.xserver = {
88 enable = true; 83 enable = true;
89 displayManager.sddm = { 84 wayland.enable = true;
90 enable = true; 85 settings = {
91 settings = { 86 Users.HideUsers = "gkleen";
92 Users.HideUsers = "gkleen";
93 };
94 }; 87 };
95 desktopManager.plasma5.enable = true;
96
97 videoDrivers = [ "nvidia" ];
98 }; 88 };
89 services.desktopManager.plasma6.enable = true;
99 90
100 91
101 services.openssh = { 92 services.openssh = {
diff --git a/hosts/sif/default.nix b/hosts/sif/default.nix
index 7c8da63a..fb2dddc6 100644
--- a/hosts/sif/default.nix
+++ b/hosts/sif/default.nix
@@ -12,11 +12,9 @@ let
12in { 12in {
13 imports = with flake.nixosModules.systemProfiles; [ 13 imports = with flake.nixosModules.systemProfiles; [
14 ./hw.nix 14 ./hw.nix
15 ./mail ./libvirt 15 ./email ./libvirt ./greetd
16 tmpfs-root bcachefs initrd-all-crypto-modules default-locale openssh rebuild-machines 16 tmpfs-root bcachefs initrd-all-crypto-modules default-locale openssh rebuild-machines niri-unstable networkmanager lanzaboote
17 networkmanager
18 flakeInputs.nixos-hardware.nixosModules.lenovo-thinkpad-p1 17 flakeInputs.nixos-hardware.nixosModules.lenovo-thinkpad-p1
19 flakeInputs.impermanence.nixosModules.impermanence
20 flakeInputs.nixVirt.nixosModules.default 18 flakeInputs.nixVirt.nixosModules.default
21 ]; 19 ];
22 20
@@ -34,8 +32,11 @@ in {
34 boot = { 32 boot = {
35 initrd = { 33 initrd = {
36 systemd = { 34 systemd = {
37 enable = false;
38 emergencyAccess = config.users.users.root.hashedPassword; 35 emergencyAccess = config.users.users.root.hashedPassword;
36 extraBin = {
37 "vim" = lib.getExe pkgs.vim;
38 "grep" = lib.getExe pkgs.gnugrep;
39 };
39 }; 40 };
40 luks.devices = { 41 luks.devices = {
41 nvm0 = { device = "/dev/disk/by-uuid/bef17e86-d929-4a60-97cb-6bfa133face7"; bypassWorkqueues = true; }; 42 nvm0 = { device = "/dev/disk/by-uuid/bef17e86-d929-4a60-97cb-6bfa133face7"; bypassWorkqueues = true; };
@@ -49,12 +50,8 @@ in {
49 50
50 blacklistedKernelModules = [ "nouveau" ]; 51 blacklistedKernelModules = [ "nouveau" ];
51 52
52 # Use the systemd-boot EFI boot loader. 53 lanzaboote.configurationLimit = 15;
53 loader = { 54 loader = {
54 systemd-boot = {
55 enable = true;
56 configurationLimit = 15;
57 };
58 efi.canTouchEfiVariables = true; 55 efi.canTouchEfiVariables = true;
59 timeout = null; 56 timeout = null;
60 }; 57 };
@@ -62,16 +59,29 @@ in {
62 plymouth.enable = true; 59 plymouth.enable = true;
63 60
64 kernelPackages = pkgs.linuxPackages_latest; 61 kernelPackages = pkgs.linuxPackages_latest;
65 extraModulePackages = with config.boot.kernelPackages; [ v4l2loopback ];
66 kernelModules = ["v4l2loopback"];
67 kernelPatches = [ 62 kernelPatches = [
68 { name = "edac-config"; 63 { name = "edac-config";
69 patch = null; 64 patch = null;
70 extraConfig = '' 65 structuredExtraConfig = with lib.kernel; {
71 EDAC y 66 EDAC = yes;
72 EDAC_IE31200 y 67 EDAC_IE31200 = yes;
73 ''; 68 };
74 } 69 }
70 { name = "zswap-default";
71 patch = null;
72 structuredExtraConfig = with lib.kernel; {
73 ZSWAP_DEFAULT_ON = yes;
74 ZSWAP_SHRINKER_DEFAULT_ON = yes;
75 };
76 }
77 ];
78 consoleLogLevel = 3;
79 kernelParams = [
80 "quiet"
81 "boot.shell_on_fail"
82 "udev.log_priority=3"
83 "rd.systemd.show_status=auto"
84 "plymouth.use-simpledrm"
75 ]; 85 ];
76 86
77 tmp.useTmpfs = true; 87 tmp.useTmpfs = true;
@@ -94,6 +104,8 @@ in {
94 server ptbtime2.ptb.de prefer iburst nts 104 server ptbtime2.ptb.de prefer iburst nts
95 server ptbtime3.ptb.de prefer iburst nts 105 server ptbtime3.ptb.de prefer iburst nts
96 server ptbtime4.ptb.de prefer iburst nts 106 server ptbtime4.ptb.de prefer iburst nts
107 pool ntppool1.time.nl prefer iburst nts
108 pool ntppool2.time.nl prefer iburst nts
97 109
98 authselectmode require 110 authselectmode require
99 minsources 3 111 minsources 3
@@ -122,40 +134,16 @@ in {
122 rulesetFile = ./ruleset.nft; 134 rulesetFile = ./ruleset.nft;
123 }; 135 };
124 136
125 # firewall = {
126 # enable = true;
127 # allowedTCPPorts = [ 22 # ssh
128 # 8000 # quickserve
129 # ];
130 # };
131
132 # wlanInterfaces = {
133 # wlan0 = {
134 # device = "wlp82s0";
135 # };
136 # };
137
138 # bonds = {
139 # "lan" = {
140 # interfaces = [ "wlan0" "enp0s31f6" "dock0" ];
141 # driverOptions = {
142 # miimon = "1000";
143 # mode = "active-backup";
144 # primary_reselect = "always";
145 # };
146 # };
147 # };
148
149 useDHCP = false; 137 useDHCP = false;
150 useNetworkd = true; 138 useNetworkd = true;
151
152 # interfaces."tinc.yggdrasil" = {
153 # virtual = true;
154 # virtualType = config.services.tinc.networks.yggdrasil.interfaceType;
155 # macAddress = "5c:93:21:c3:61:39";
156 # };
157 }; 139 };
158 140
141 environment.etc."NetworkManager/dnsmasq.d/dnssec.conf" = {
142 text = ''
143 conf-file=${pkgs.dnsmasq}/share/dnsmasq/trust-anchors.conf
144 dnssec
145 '';
146 };
159 environment.etc."NetworkManager/dnsmasq.d/libvirt_dnsmasq.conf" = { 147 environment.etc."NetworkManager/dnsmasq.d/libvirt_dnsmasq.conf" = {
160 text = '' 148 text = ''
161 except-interface=virbr0 149 except-interface=virbr0
@@ -398,19 +386,6 @@ in {
398 ]; 386 ];
399 387
400 services = { 388 services = {
401 uucp = {
402 enable = true;
403 nodeName = "sif";
404 remoteNodes = {
405 "ymir" = {
406 publicKeys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG6KNtsCOl5fsZ4rV7udTulGMphJweLBoKapzerWNoLY root@ymir"];
407 hostnames = ["ymir.yggdrasil.li" "ymir.niflheim.yggdrasil"];
408 };
409 };
410
411 defaultCommands = lib.mkForce [];
412 };
413
414 avahi.enable = true; 389 avahi.enable = true;
415 390
416 fwupd.enable = true; 391 fwupd.enable = true;
@@ -429,8 +404,8 @@ in {
429 404
430 logind = { 405 logind = {
431 lidSwitch = "suspend"; 406 lidSwitch = "suspend";
432 lidSwitchDocked = "lock"; 407 lidSwitchDocked = "ignore";
433 lidSwitchExternalPower = "lock"; 408 lidSwitchExternalPower = "ignore";
434 }; 409 };
435 410
436 atd = { 411 atd = {
@@ -439,7 +414,7 @@ in {
439 }; 414 };
440 415
441 xserver = { 416 xserver = {
442 enable = true; 417 enable = false;
443 418
444 xkb = { 419 xkb = {
445 layout = "us"; 420 layout = "us";
@@ -465,47 +440,13 @@ in {
465 }; 440 };
466 libinput.enable = true; 441 libinput.enable = true;
467 442
468 greetd = { 443 envfs.enable = false;
469 enable = true;
470 # settings.default_session.command = let
471 # cfg = config.programs.regreet;
472 # in pkgs.writeShellScript "greeter" ''
473 # modprobe -r nvidia_drm
474 444
475 # exec ${pkgs.dbus}/bin/dbus-run-session ${lib.getExe pkgs.cage} ${lib.escapeShellArgs cfg.cageArgs} -- ${lib.getExe cfg.package} 445 displayManager.defaultSession = "Niri";
476 # '';
477 };
478 }; 446 };
479 447
480 programs.regreet = {
481 enable = true;
482 theme = {
483 package = pkgs.equilux-theme;
484 name = "Equilux-compact";
485 };
486 iconTheme = {
487 package = pkgs.paper-icon-theme;
488 name = "Paper-Mono-Dark";
489 };
490 font = {
491 package = pkgs.fira;
492 name = "Fira Sans";
493 # size = 6;
494 };
495 cageArgs = [ "-s" "-m" "last" ];
496 settings = {
497 GTK.application_prefer_dark_theme = true;
498 };
499 };
500 programs.hyprland.enable = true;
501
502 systemd.tmpfiles.settings = { 448 systemd.tmpfiles.settings = {
503 "10-localtime"."/etc/localtime".L.argument = "/.bcachefs/etc/localtime"; 449 "10-localtime"."/etc/localtime".L.argument = "/.bcachefs/etc/localtime";
504
505 "10-regreet"."/var/cache/regreet/cache.toml".C.argument = toString ((pkgs.formats.toml {}).generate "cache.toml" {
506 last_user = "gkleen";
507 user_to_last_sess.gkleen = "Hyprland";
508 });
509 }; 450 };
510 451
511 users = { 452 users = {
@@ -614,15 +555,15 @@ in {
614 }; 555 };
615 556
616 nvidia = { 557 nvidia = {
617 open = true; 558 open = false;
618 modesetting.enable = true; 559 modesetting.enable = true;
619 powerManagement.enable = true; 560 powerManagement.enable = true;
620 prime = { 561 # prime = {
621 nvidiaBusId = "PCI:1:0:0"; 562 # nvidiaBusId = "PCI:1:0:0";
622 intelBusId = "PCI:0:2:0"; 563 # intelBusId = "PCI:0:2:0";
623 reverseSync.enable = true; 564 # reverseSync.enable = true;
624 offload.enableOffloadCmd = true; 565 # offload.enableOffloadCmd = true;
625 }; 566 # };
626 }; 567 };
627 568
628 graphics = { 569 graphics = {
@@ -665,25 +606,6 @@ in {
665 606
666 environment.etc."X11/xorg.conf.d/50-wacom.conf".source = lib.mkForce ./wacom.conf; 607 environment.etc."X11/xorg.conf.d/50-wacom.conf".source = lib.mkForce ./wacom.conf;
667 608
668 systemd.services."ac-plugged" = {
669 description = "Inhibit handling of lid-switch and sleep";
670
671 path = with pkgs; [ systemd coreutils ];
672
673 script = ''
674 exec systemd-inhibit --what=handle-lid-switch --why="AC is connected" --mode=block sleep infinity
675 '';
676
677 serviceConfig = {
678 Type = "simple";
679 };
680 };
681
682 services.udev.extraRules = with pkgs; lib.mkAfter ''
683 SUBSYSTEM=="power_supply", ENV{POWER_SUPPLY_ONLINE}=="0", RUN+="${systemd}/bin/systemctl --no-block stop ac-plugged.service"
684 SUBSYSTEM=="power_supply", ENV{POWER_SUPPLY_ONLINE}=="1", RUN+="${systemd}/bin/systemctl --no-block start ac-plugged.service"
685 '';
686
687 systemd.services."nix-daemon".serviceConfig = { 609 systemd.services."nix-daemon".serviceConfig = {
688 MemoryAccounting = true; 610 MemoryAccounting = true;
689 MemoryHigh = "50%"; 611 MemoryHigh = "50%";
@@ -696,6 +618,7 @@ in {
696 618
697 services.dbus.packages = with pkgs; 619 services.dbus.packages = with pkgs;
698 [ dbus dconf 620 [ dbus dconf
621 xdg-desktop-portal-gtk
699 ]; 622 ];
700 623
701 services.udisks2.enable = true; 624 services.udisks2.enable = true;
@@ -704,12 +627,12 @@ in {
704 light.enable = true; 627 light.enable = true;
705 wireshark.enable = true; 628 wireshark.enable = true;
706 dconf.enable = true; 629 dconf.enable = true;
707 }; 630 niri.enable = true;
708 631 fuse.userAllowOther = true;
709 zramSwap = { 632 captive-browser = {
710 enable = true; 633 enable = true;
711 algorithm = "zstd"; 634 interface = "wlp82s0";
712 writebackDevice = "/dev/disk/by-label/swap"; 635 };
713 }; 636 };
714 637
715 services.pcscd.enable = true; 638 services.pcscd.enable = true;
@@ -729,6 +652,16 @@ in {
729 environment.sessionVariables."GTK_USE_PORTAL" = "1"; 652 environment.sessionVariables."GTK_USE_PORTAL" = "1";
730 xdg.portal = { 653 xdg.portal = {
731 enable = true; 654 enable = true;
655 extraPortals = with pkgs; [ xdg-desktop-portal-gtk ];
656 config.niri = {
657 default = ["gnome" "gtk"];
658 "org.freedesktop.impl.portal.FileChooser" = ["gtk"];
659 "org.freedesktop.impl.portal.OpenFile" = ["gtk"];
660 "org.freedesktop.impl.portal.Access" = ["gtk"];
661 "org.freedesktop.impl.portal.Notification" = ["gtk"];
662 "org.freedesktop.impl.portal.Secret" = ["none"];
663 "org.freedesktop.impl.portal.Inhibit" = ["none"];
664 };
732 }; 665 };
733 666
734 environment.persistence."/.bcachefs" = { 667 environment.persistence."/.bcachefs" = {
@@ -736,36 +669,26 @@ in {
736 directories = [ 669 directories = [
737 "/nix" 670 "/nix"
738 "/root" 671 "/root"
672 "/home"
739 "/var/log" 673 "/var/log"
740 "/var/lib/sops-nix" 674 "/var/lib/sops-nix"
741 "/var/lib/nixos" 675 "/var/lib/nixos"
742 "/var/lib/systemd" 676 "/var/lib/systemd"
743 "/home"
744 "/var/lib/chrony" 677 "/var/lib/chrony"
745 "/var/lib/fprint" 678 "/var/lib/fprint"
746 "/var/lib/bluetooth" 679 "/var/lib/bluetooth"
747 "/var/lib/upower" 680 "/var/lib/upower"
748 "/var/lib/postfix" 681 "/var/lib/postfix"
682 "/var/lib/regreet"
749 "/etc/NetworkManager/system-connections" 683 "/etc/NetworkManager/system-connections"
750 { directory = "/var/uucp"; user = "uucp"; group = "uucp"; mode = "0700"; } 684 config.boot.lanzaboote.pkiBundle
751 { directory = "/var/spool/uucp"; user = "uucp"; group = "uucp"; mode = "0750"; }
752 ]; 685 ];
753 files = [ 686 files = [
754 ]; 687 ];
688 timezone = true;
755 }; 689 };
756 690
757 systemd.services.timezone = { 691 security.pam.services.quickshell = {};
758 wantedBy = [ "multi-user.target" ];
759 serviceConfig = {
760 Type = "oneshot";
761 RemainAfterExit = true;
762 ExecStart = "${pkgs.coreutils}/bin/cp -vP /.bcachefs/etc/localtime /etc/localtime";
763 ExecStop = "${pkgs.coreutils}/bin/cp -vP /etc/localtime /.bcachefs/etc/localtime";
764 };
765 };
766 services.tzupdate.enable = true;
767
768 security.pam.services.gtklock = {};
769 692
770 home-manager.sharedModules = [ flakeInputs.nixVirt.homeModules.default ]; 693 home-manager.sharedModules = [ flakeInputs.nixVirt.homeModules.default ];
771 694
diff --git a/hosts/sif/email/default.nix b/hosts/sif/email/default.nix
new file mode 100644
index 00000000..bebf7980
--- /dev/null
+++ b/hosts/sif/email/default.nix
@@ -0,0 +1,111 @@
1{ config, lib, pkgs, ... }:
2{
3 services.postfix = {
4 enable = true;
5 enableSmtp = false;
6 enableSubmission = false;
7 setSendmail = true;
8 # networksStyle = "host";
9 settings.main = {
10 recpipient_delimiter = "+";
11 mydestination = [];
12 myhostname = "sif.midgard.yggdrasil";
13
14 mydomain = "yggdrasil.li";
15
16 local_transport = "error:5.1.1 No local delivery";
17 alias_database = [];
18 alias_maps = [];
19 local_recipient_maps = [];
20
21 inet_interfaces = "loopback-only";
22
23 message_size_limit = 0;
24
25 authorized_submit_users = "inline:{ gkleen= }";
26 authorized_flush_users = "inline:{ gkleen= }";
27 authorized_mailq_users = "inline:{ gkleen= }";
28
29 smtp_generic_maps = "inline:{ root=root+sif }";
30
31 mynetworks = ["127.0.0.0/8" "[::1]/128"];
32 smtpd_client_restrictions = ["permit_mynetworks" "reject"];
33 smtpd_relay_restrictions = ["permit_mynetworks" "reject"];
34
35 sender_dependent_default_transport_maps = ''regexp:${pkgs.writeText "sender_relay" ''
36 /@(cip|stud)\.ifi\.(lmu|uni-muenchen)\.de$/ smtp:smtp.ifi.lmu.de
37 /@ifi\.(lmu|uni-muenchen)\.de$/ smtp:smtpin1.ifi.lmu.de:587
38 /@math(ematik)?\.(lmu|uni-muenchen)\.de$/ smtps:smtp.math.lmu.de:465
39 /@(campus\.)?lmu\.de$/ smtp:postout.lrz.de
40 ''}'';
41 sender_bcc_maps = ''regexp:${pkgs.writeText "sender_bcc" ''
42 /^uni2work(-[^@]*)?@ifi\.lmu\.de$/ uni2work@ifi.lmu.de
43 /@ifi\.lmu\.de$/ gregor.kleen@ifi.lmu.de
44 ''}'';
45 relayhost = ["[surtr.yggdrasil.li]:465"];
46 default_transport = "relay";
47
48 smtp_sasl_auth_enable = true;
49 smtp_sender_dependent_authentication = true;
50 smtp_sasl_tls_security_options = "noanonymous";
51 smtp_sasl_mechanism_filter = ["plain"];
52 smtp_sasl_password_maps = "regexp:/run/credentials/postfix.service/sasl_passwd";
53 smtp_cname_overrides_servername = false;
54 smtp_always_send_ehlo = true;
55 smtp_tls_security_level = "dane";
56
57 smtp_tls_loglevel = "1";
58 smtp_dns_support_level = "dnssec";
59 };
60 settings.master = {
61 submission = {
62 type = "inet";
63 private = false;
64 command = "smtpd";
65 args = [
66 "-o" "syslog_name=postfix/$service_name"
67 ];
68 };
69 smtp = { };
70 smtps = {
71 type = "unix";
72 private = true;
73 privileged = true;
74 chroot = false;
75 command = "smtp";
76 args = [
77 "-o" "smtp_tls_wrappermode=yes"
78 "-o" "smtp_tls_security_level=encrypt"
79 ];
80 };
81 relay = {
82 command = "smtp";
83 args = [
84 "-o" "smtp_fallback_relay="
85 "-o" "smtp_tls_security_level=verify"
86 "-o" "smtp_tls_wrappermode=yes"
87 "-o" "smtp_tls_cert_file=${./relay.crt}"
88 "-o" "smtp_tls_key_file=/run/credentials/postfix.service/relay.key"
89 ];
90 };
91 };
92 };
93
94 systemd.services.postfix = {
95 serviceConfig.LoadCredential = [
96 "sasl_passwd:${config.sops.secrets."postfix-sasl-passwd".path}"
97 "relay.key:${config.sops.secrets."relay-key".path}"
98 ];
99 };
100
101 sops.secrets = {
102 postfix-sasl-passwd = {
103 key = "sasl-passwd";
104 sopsFile = ./secrets.yaml;
105 };
106 relay-key = {
107 format = "binary";
108 sopsFile = ./relay.key;
109 };
110 };
111}
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/greetd/default.nix b/hosts/sif/greetd/default.nix
new file mode 100644
index 00000000..081b6346
--- /dev/null
+++ b/hosts/sif/greetd/default.nix
@@ -0,0 +1,92 @@
1{ config, pkgs, lib, flakeInputs, ... }:
2
3let
4 gkleenConfig = config.home-manager.users."gkleen";
5 toIni = lib.generators.toINI {
6 mkKeyValue =
7 key: value:
8 let
9 value' = if lib.isBool value then lib.boolToString value else toString value;
10 in
11 "${lib.escape [ "=" ] key}=${value'}";
12 };
13 toDconfIni = let
14 gvariant = import (flakeInputs.home-manager + "/modules/lib/gvariant.nix") { inherit lib; };
15 mkIniKeyValue = key: value: "${key}=${toString (gvariant.mkValue value)}";
16 in lib.generators.toINI { mkKeyValue = mkIniKeyValue; };
17in {
18 config = {
19 services.greetd = {
20 enable = true;
21 settings.default_session.command = lib.getExe (pkgs.writeShellApplication {
22 name = "sway";
23 runtimeInputs = [ pkgs.sway pkgs.fontconfig ];
24 runtimeEnv = {
25 XDG_DATA_DIRS = lib.makeSearchPath "share" [
26 pkgs.equilux-theme pkgs.paper-icon-theme pkgs.fira
27 ];
28 QT_PLUGIN_PATH = lib.makeSearchPath (pkgs.qt6.qtbase.qtPluginPrefix) [
29 pkgs.qt6Packages.qtbase
30 ];
31 QML2_IMPORT_PATH = lib.makeSearchPath (pkgs.qt6.qtbase.qtQmlPrefix) [
32 pkgs.qt6Packages.qtbase
33 ];
34 QT_QPA_PLATFORMTHEME = "gtk3";
35 XDG_CONFIG_DIR = pkgs.symlinkJoin {
36 name = "config";
37 paths = [
38 (pkgs.writeTextDir "gtk-3.0/settings.ini" (toIni {
39 Settings = {
40 gtk-font-name = "Fira Sans 10";
41 gtk-theme-name = "Equilux-compact";
42 gtk-icon-theme-name = "Paper-Mono-Dark";
43 };
44 }))
45 ];
46 };
47 # XDG_CACHE_HOME = "/var/cache/greetd/greeter";
48 # XDG_CONFIG_HOME = "/var/cache/greetd/greeter/config";
49 };
50 text = ''
51 exec &>/tmp/sway-$$.log
52
53 unset MANAGERPID SYSTEMD_EXEC_PID
54
55 # ${lib.getExe' pkgs.coreutils "mkdir"} -p ''${XDG_CONFIG_HOME}/dconf
56 ${lib.getExe pkgs.dconf} load / < ${pkgs.writeText "dconf.ini" (toDconfIni {
57 "org/gnome/desktop/interface" = {
58 "color-scheme" = "prefer-dark";
59 "font-name" = "Fira Sans 10";
60 "gtk-theme" = "Equilux-compact";
61 "icon-theme" = "Paper-Mono-Dark";
62 };
63 })}
64
65 exec sway --unsupported-gpu --config ${pkgs.writeText "sway-config" ''
66 exec "${lib.getExe' config.systemd.package "systemctl"} --user import-environment {,WAYLAND_}DISPLAY SWAYSOCK; ${lib.getExe gkleenConfig.programs.quickshell.package} --path ${gkleenConfig.xdg.configFile."quickshell".source}/displaymanager.qml; swaymsg exit"
67
68 input type:keyboard {
69 xkb_layout "us,us"
70 xkb_variant "dvp,"
71 xkb_options "compose:caps,grp:win_space_toggle"
72 }
73
74 output eDP-1 scale 1.5
75 ''}
76 '';
77 });
78 };
79
80 # security.pam.services.greetd.fprintAuth = false;
81
82 systemd.services.greetd.serviceConfig = {
83 ExecStartPre = ''${lib.getExe' pkgs.coreutils "install"} -d -o greeter -g greeter -m 0700 ''${CACHE_DIRECTORY}/greeter'';
84 # CacheDirectory = "greetd";
85 };
86
87 users.users.greeter = {
88 home = "/var/lib/greeter";
89 createHome = true;
90 };
91 };
92}
diff --git a/hosts/sif/hw.nix b/hosts/sif/hw.nix
index fc20ef7c..e567c37d 100644
--- a/hosts/sif/hw.nix
+++ b/hosts/sif/hw.nix
@@ -8,15 +8,28 @@
8 options = [ "fmask=0033" "dmask=0022" ]; 8 options = [ "fmask=0033" "dmask=0022" ];
9 }; 9 };
10 "/.bcachefs" = 10 "/.bcachefs" =
11 { device = "/dev/mapper/sif-nvm0:/dev/mapper/sif-nvm1"; 11 { options = [
12 "x-systemd.requires=/dev/disk/by-id/dm-name-sif-nvm0"
13 "x-systemd.requires=/dev/disk/by-id/dm-name-sif-nvm1"
14 ];
15 device = "/dev/disk/by-uuid/fe7bdaac-d2f3-4535-a635-e2fb97ef3802";
12 fsType = "bcachefs"; 16 fsType = "bcachefs";
13 neededForBoot = true; 17 neededForBoot = true;
14 }; 18 };
15 "/var/lib/sops-nix".neededForBoot = true; 19 "/var/lib/sops-nix".neededForBoot = true;
16 "/var/lib/systemd".neededForBoot = true; 20 "/var/lib/systemd".neededForBoot = true;
17 }; 21 };
18 system.etc.overlay.enable = false; 22 swapDevices = [
19 systemd.sysusers.enable = false; 23 { label = "swap"; }
24 ];
25 # system.etc.overlay.enable = false;
26
27 boot.initrd.systemd.packages = [
28 (pkgs.writeTextDir "/etc/systemd/system/sysroot-.bcachefs.mount.d/block_scan.conf" ''
29 [Mount]
30 Environment=BCACHEFS_BLOCK_SCAN=1
31 '')
32 ];
20 33
21 # boot.initrd.supportedFilesystems.bcachefs = true; 34 # boot.initrd.supportedFilesystems.bcachefs = true;
22 # boot.initrd.systemd.units."dev-sif-nvm0:-dev-sif-nvm1.device".enable = false; 35 # boot.initrd.systemd.units."dev-sif-nvm0:-dev-sif-nvm1.device".enable = false;
diff --git a/hosts/sif/libvirt/default.nix b/hosts/sif/libvirt/default.nix
index d0be7dff..9712d0d9 100644
--- a/hosts/sif/libvirt/default.nix
+++ b/hosts/sif/libvirt/default.nix
@@ -8,6 +8,7 @@ with flakeInputs.nixVirt.lib;
8 qemu.swtpm.enable = true; 8 qemu.swtpm.enable = true;
9 allowedBridges = ["virbr0" "rz-0971" "rz-2403"]; 9 allowedBridges = ["virbr0" "rz-0971" "rz-2403"];
10 }; 10 };
11 virtualisation.spiceUSBRedirection.enable = true;
11 virtualisation.libvirt = { 12 virtualisation.libvirt = {
12 enable = true; 13 enable = true;
13 swtpm.enable = true; 14 swtpm.enable = true;
diff --git a/hosts/sif/mail/default.nix b/hosts/sif/mail/default.nix
deleted file mode 100644
index f36cd599..00000000
--- a/hosts/sif/mail/default.nix
+++ /dev/null
@@ -1,70 +0,0 @@
1{ config, 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 223e1f10..1c66df2b 100644
--- a/hosts/surtr/default.nix
+++ b/hosts/surtr/default.nix
@@ -6,7 +6,8 @@ with lib;
6 imports = with flake.nixosModules.systemProfiles; [ 6 imports = with flake.nixosModules.systemProfiles; [
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 9 ./prometheus ./email ./vpn ./borg.nix ./etebase ./immich.nix
10 ./paperless.nix ./hledger.nix ./audiobookshelf.nix ./kimai.nix
10 ]; 11 ];
11 12
12 config = { 13 config = {
@@ -14,9 +15,6 @@ with lib;
14 system = "x86_64-linux"; 15 system = "x86_64-linux";
15 }; 16 };
16 17
17 networking.hostId = "a64cf4d7";
18 environment.etc."machine-id".text = "a64cf4d793ab0a0ed3892ead609fc0bc";
19
20 boot = { 18 boot = {
21 loader.grub = { 19 loader.grub = {
22 enable = true; 20 enable = true;
@@ -24,12 +22,20 @@ with lib;
24 device = "/dev/vda"; 22 device = "/dev/vda";
25 }; 23 };
26 24
27
28 tmp.useTmpfs = true; 25 tmp.useTmpfs = true;
29 26
30 zfs.devNodes = "/dev"; # /dev/vda2 does not show up in /dev/disk/by-id 27 zfs.devNodes = "/dev"; # /dev/vda2 does not show up in /dev/disk/by-id
31 28
32 kernelModules = ["ptp_kvm"]; 29 kernelModules = ["ptp_kvm"];
30 kernelPatches = [
31 { name = "zswap-default";
32 patch = null;
33 structuredExtraConfig = with lib.kernel; {
34 ZSWAP_DEFAULT_ON = yes;
35 ZSWAP_SHRINKER_DEFAULT_ON = yes;
36 };
37 }
38 ];
33 }; 39 };
34 40
35 fileSystems = { 41 fileSystems = {
@@ -38,6 +44,9 @@ with lib;
38 fsType = "vfat"; 44 fsType = "vfat";
39 }; 45 };
40 }; 46 };
47 swapDevices = [
48 { label = "swap"; }
49 ];
41 50
42 networking = { 51 networking = {
43 hostName = "surtr"; 52 hostName = "surtr";
@@ -163,11 +172,6 @@ with lib;
163 stateful = true; 172 stateful = true;
164 }; 173 };
165 174
166 zramSwap = {
167 enable = true;
168 algorithm = "zstd";
169 };
170
171 systemd.sysusers.enable = false; 175 systemd.sysusers.enable = false;
172 system.etc.overlay.mutable = true; 176 system.etc.overlay.mutable = true;
173 boot.enableContainers = true; 177 boot.enableContainers = true;
diff --git a/hosts/surtr/dns/Gupfile b/hosts/surtr/dns/Gupfile
index ac96f620..70674cce 100644
--- a/hosts/surtr/dns/Gupfile
+++ b/hosts/surtr/dns/Gupfile
@@ -1,2 +1,2 @@
1key.gup: 1key.gup:
2 keys/*.yaml \ No newline at end of file 2 keys/* \ No newline at end of file
diff --git a/hosts/surtr/dns/default.nix b/hosts/surtr/dns/default.nix
index 53df798e..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" "app.etesync.yggdrasil.li"]; 160 acmeDomains = ["surtr.yggdrasil.li" "yggdrasil.li" "etesync.yggdrasil.li" "immich.yggdrasil.li" "app.etesync.yggdrasil.li" "paperless.yggdrasil.li" "hledger.yggdrasil.li" "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/key.gup b/hosts/surtr/dns/key.gup
index 32d4f7d6..5b5058b3 100644
--- a/hosts/surtr/dns/key.gup
+++ b/hosts/surtr/dns/key.gup
@@ -3,4 +3,4 @@
3keyName=${${2:t}%.yaml}_key 3keyName=${${2:t}%.yaml}_key
4 4
5keymgr -t ${keyName} > $1 5keymgr -t ${keyName} > $1
6sops -p '7ED22F4AA7BB55728B643DC5471B7D88E4EF66F8,30D3453B8CD02FE2A3E7C78C0FB536FB87AE8F51' --input-type=binary --output-type=binary -e -i $1 \ No newline at end of file 6sops --input-type=binary --output-type=binary -e -i $1
diff --git a/hosts/surtr/dns/keys/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/hledger.yggdrasil.li_acme b/hosts/surtr/dns/keys/hledger.yggdrasil.li_acme
new file mode 100644
index 00000000..b3f4cfb6
--- /dev/null
+++ b/hosts/surtr/dns/keys/hledger.yggdrasil.li_acme
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:lCj8VYJL9z29FJ154XQtxKQLwwitCRGy4krJ6u8yw2FMzoHprEpFgm33+mFspxSKk/It2G8cfTGMZSeVkYJEHb66HNKHl0A2Fz3hwjpRjh1MZAw0wiZJlnS/LNqoGstQ2PJmTQTW3aJRMoT1GS7q/gSp/3rqySA5EOm0GgUiA3Vi7nGpkBenKDEbQbcIBXRdMOk66BCdiz5XGm/1VLQQLO9oVwY2KBnLaZSISohyGVhbIy7GT2ygoWHHxHn0c5CRVNvGNwesM1gO1NnTFrISLMWSrsDPaAtQ,iv:fa8LFjzqsf2ccfbEe5MOmerb7FzXb4xr24y1GWIMT1Q=,tag:7oQ54DKBb76Pbw1lmEHt+Q==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzcHJLRHh0VkpDNDU3cGZS\nMWFlVWpFemZ2RnpnSllDWU1EWGoyWUxyejNzClBGandzaFI5NXY1bG51Y3VxRk9r\nc29NbXBOaEZDblBuaVowemQydkxBdjgKLS0tICtVb2xkMmh4T0Q0cU4xQnBzZExI\namFRUnRYTWIyQ3RHNUVHWTFrUzhhK1UKqmATNmxlhkxM5PP1U6w7fSYVA8AgIRAt\nJ9WZrTffQfXMdw4RmjWcoVHFH39Fe4SteedxliCCcqjkjgSEB4Rgow==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjWlhpQWI4c0x0ZXRGYVE2\nR1dHZzZud0ZxQWsrREhJWUowWE1zem5FVFI0CkJBUnIwY1FGS3N2VnpuSkZjRllZ\ncVgyeVg4cTVjRitzL0RKb0ZQb3BsOEkKLS0tIGs3SDBkamVBNDhQUlh5dmVVZXJs\nM3VKdlFKc21GcFY0UUtiaHFvYWI4V0kKKuWYEncxe9NT2ZS3X3+l/gT4BQOrdCg8\nj2jGL+Yzy/356GO3PFTn2HHLam6KWDKaYB5TlK/zSohfUt5giQH2Lw==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-19T17:13:51Z",
19 "mac": "ENC[AES256_GCM,data:XsBdMCBjB+YuBMZQrjJ5uZtaYKSqsdWVvm+IEoJflCKPIhPk2rBZ3nY8KngXFbq2fWgsYyTM83kb2trEGIEHUPuERt+mgfCI3bSlylriwgsDWihCjyBecNE+BbdXE0+YcNl8pIwBU4M+3f2StQMH22YamToLJ9i9kfKcBrirDuU=,iv:VTIdBVY3kVBMYWhYUmrP2vZ9rpH90DzF68y1aDf2EAs=,tag:YkL+nw6LNXAceZtx9vgf6A==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/surtr/dns/keys/immich.yggdrasil.li_acme b/hosts/surtr/dns/keys/immich.yggdrasil.li_acme
new file mode 100644
index 00000000..c31234bd
--- /dev/null
+++ b/hosts/surtr/dns/keys/immich.yggdrasil.li_acme
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:1i27jx1E4nn8/iXEN90tnQve0MX0HcXyYZoQfga1djcESDARd7kX78jncqnSdEMJPIQCyq2zmvOwEiAwyofIjSBSW2teoxD1PybSZdyvKwOnwLqpVWxgw6LORUoN5c1y4+WmnQa1SJ0a1WJwZ3cFRa3LP5JPbZzmNCZWEg+yGwVHNMsmrBSrjLRFC1NPIfX69lWGZl5VIMw2/SoSMDcOsSURWIpVSEYe9LNrc4/cKQQC/rmpXBg9ekIa8xd7NQTcDZA42bDIBQxJzqkUNV3JeIrl0C+eQw==,iv:MEDhvUvy0PfcLGim06VXkiIGgkNgaQcYqGhJraaGC6M=,tag:43CdGFe5JXOsMCHrvgl+BA==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBLOU5OL0xxVVE2dzVOYnhP\nMUlhYlpyZGZSamU4QkpObHJLS1NJdjdwcmtjCks0OElnaHZvb3BSNnYySnVPcEUx\nazdNVUpZZGRNOVNBVTVUNUdnaC9DM00KLS0tIHUxU2dHMCt4d3hJbXlSNzBKUk1W\na05uZVRwVlMrbEZ2WEkvUy9PUVhmWXcKemRLnCC2mAkCEbZ3bC+iZmIWQCQsI+ew\ni4mmc/mUkGx8/61SR571NXIKmxSx2U5L2IK1pIKy6G/xMkPq+wDgUQ==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5VnNUV2lRR292ZmpNeU51\nQ2YxcnVnd21ybGdpd0I1QlJuSEQwNGZYTDJrCml0YUdxaWJHTEFNYWVIY01jWGk2\nZEJSQ0VZcVV1bU9VZ3dBbUh3a0N1ekUKLS0tIFhvNnRqcnRUZFNYOVhuWkFrZ3Vk\nMjU0YnNDODJRYk9lVENLUU9KU2dkWm8KHqtuNtC39S4oiQFRhNT2OOUOY9KQvDYW\nvtcdR8MSZDE7jsqgLgGS/8lIc0GBbIwYWghgFsLmn2Bkdh2q/VuO9w==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-01-03T15:31:39Z",
19 "mac": "ENC[AES256_GCM,data:NKUbtcQf2DfALfm9kwiirmwD3slfTh4HNIg8BT/xbySHfwsaFtmlZTkhavBNr+b5snR8opATVXnJPAoykxXq8q4G1yDeitnTw6x9KfwgyZKpbJANMTBZEwK+CnZbqYRas1bFC88+D0yWI1yUnle+NPQ8VUj+KxCiUNuWO80mWhE=,iv:2a7CawUQujcZuR1pmsm9L2KGKcrBRAOxiIWEKCkTCEM=,tag:j8caxyN2fvx2FnWXHkWKcw==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.2"
23 }
24} \ No newline at end of file
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/keys/paperless.yggdrasil.li_acme b/hosts/surtr/dns/keys/paperless.yggdrasil.li_acme
new file mode 100644
index 00000000..bc4640db
--- /dev/null
+++ b/hosts/surtr/dns/keys/paperless.yggdrasil.li_acme
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:wOl8KLHD0H+btq0A3UreyVF9bOXZsiTwWJkVH8GubyIQDyiDC8vQm+dfv0rz8TwcBWYpC4aMIPPflG2HsdYO4rKGQ/nBmWmxhNXjpnyRo8iKM1BGb5bxNe4eVcUVhI60NuRJDRLmtDp+0rYGT/MVYp0/mHBINsQCXWBPDoaN2PI2GSnRag/x0wcL27xgH6NDd8glcdCN5nCAPDvazA3LialkXXv7/cceA5Q/Ee6HGzPP0w212/UvBm07Z5tXnHiy5cTbAGTUBfIqC8n501jtaQhpMh/yzA1R8KwUrw==,iv:bLzsthCaanNikNS2Es4J1++E5lijEbjyW5hU4zzNBcg=,tag:eWfZ3AtcSAGv8jWXzqlAwQ==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBNYWZKOHd2UmxWYVlIamk4\nZ04zQnAwcDdsSDBDWUZnekNva3BzRERyWXljCmNvUU1Fczc2aUN6VGl6NEJ6MGIx\nWHVpeVluWnRnbjNadGxkSmYyNE1rZzQKLS0tIGpkYVZQRDJGS21ZZHdlRk1MMm02\nQ09aanNXSWltNi9QeUNtUVQ2UEZybmMK6/qcNYLMcyKTmtROX+ZsRqDxMXwkXiAV\ndsdsWJ5+zSJuK5SEIh0fqEZ/t4pxnMcr1WieETgLSd+w0sNQS7EKPQ==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByRjdJOHdjamRHVjlZOTNV\nbTBuam5vNERIWTB6T0JkR2pLUnlQN1BGbUJNClhrZ2hPRWZtT3BERFNwdmNEMmVu\nT0dxcjNkNGIvMVJQWENoUmRhTGd6SXcKLS0tIDV6WDd1bks4K1VuVkgybjdMd0w0\nMHBsT3FmOWU0WnJsM2diQm1sTU1ON2MKtf5HZ0S1cLMx98vDKRKamS7aHIJZ0OnA\nzH4VoeVm+PKsOeqVfY+gMHLdaMEWLKYsz3B8bxIoL5pvnCdT1QAN2A==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-13T19:23:42Z",
19 "mac": "ENC[AES256_GCM,data:o7zNTjkohzAouYpJUGqf8DUfYf4/g3GZgc+4cf+PjI0OF8uc1WDCPvliBFe6pf/8QMhV5DFWd2SfszWnpnQhtiIVG/2BEk5sw3P6r/SUbSErakFYHueVQKp+9rdxK6uKcHUYhO46E332AwIxTuvNeHtSBMxx0kAwQPuuD/u3L4A=,iv:aiM0sGyGMk5lfBOpB2bDFCY+UfWwyUNixieww6eOSLs=,tag:MI7xJ7RsyZgQfF1SBVVmcQ==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/surtr/dns/zones/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 d42b4719..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 2024102100 ; 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.
@@ -59,5 +56,3 @@ _infinoted._tcp IN SRV 5 0 6523 ymir.yggdrasil.li.
59_submission._tcp IN SRV 5 0 25 ymir.yggdrasil.li. 56_submission._tcp IN SRV 5 0 25 ymir.yggdrasil.li.
60_imap._tcp IN SRV 5 0 143 ymir.yggdrasil.li. 57_imap._tcp IN SRV 5 0 143 ymir.yggdrasil.li.
61_imaps._tcp IN SRV 5 0 993 ymir.yggdrasil.li. 58_imaps._tcp IN SRV 5 0 993 ymir.yggdrasil.li.
62
63_factorio._udp IN SRV 5 0 34197 game01.yggdrasil.li.
diff --git a/hosts/surtr/dns/zones/li.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 092d23ec..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 2024102100 ; serial 4 2025060700 ; serial
5 10800 ; refresh 5 10800 ; refresh
6 3600 ; retry 6 3600 ; retry
7 604800 ; expire 7 604800 ; expire
@@ -69,12 +69,54 @@ _acme-challenge.app.etesync IN NS ns.yggdrasil.li.
69 69
70app.etesync IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::" 70app.etesync IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
71 71
72immich IN A 202.61.241.61
73immich IN AAAA 2a03:4000:52:ada::
74immich IN MX 0 surtr.yggdrasil.li
75immich IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
76_acme-challenge.immich IN NS ns.yggdrasil.li.
77
78immich IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
79
80paperless IN A 202.61.241.61
81paperless IN AAAA 2a03:4000:52:ada::
82paperless IN MX 0 surtr.yggdrasil.li
83paperless IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
84_acme-challenge.paperless IN NS ns.yggdrasil.li.
85
86paperless IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
87
88hledger IN A 202.61.241.61
89hledger IN AAAA 2a03:4000:52:ada::
90hledger IN MX 0 surtr.yggdrasil.li
91hledger IN TXT "v=spf1 redirect=surtr.yggdrasil.li"
92_acme-challenge.hledger IN NS ns.yggdrasil.li.
93
94hledger IN HTTPS 1 . alpn="h2,h3" ipv4hint="202.61.241.61" ipv6hint="2a03:4000:52:ada::"
95
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
72vidhar IN AAAA 2a03:4000:52:ada:4:1:: 112vidhar IN AAAA 2a03:4000:52:ada:4:1::
73vidhar IN MX 0 ymir.yggdrasil.li 113vidhar IN MX 0 ymir.yggdrasil.li
74vidhar IN TXT "v=spf1 redirect=yggdrasil.li" 114vidhar IN TXT "v=spf1 redirect=yggdrasil.li"
75 115
76mailout IN A 188.68.51.254 116mailout IN A 188.68.51.254
77mailout 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::
78mailout IN MX 0 ymir.yggdrasil.li 120mailout IN MX 0 ymir.yggdrasil.li
79mailout IN TXT "v=spf1 redirect=yggdrasil.li" 121mailout IN TXT "v=spf1 redirect=yggdrasil.li"
80 122
@@ -96,6 +138,3 @@ _infinoted._tcp IN SRV 5 0 6523 ymir.yggdrasil.li.
96_submission._tcp IN SRV 5 0 25 ymir.yggdrasil.li. 138_submission._tcp IN SRV 5 0 25 ymir.yggdrasil.li.
97_imap._tcp IN SRV 5 0 143 ymir.yggdrasil.li. 139_imap._tcp IN SRV 5 0 143 ymir.yggdrasil.li.
98_imaps._tcp IN SRV 5 0 993 ymir.yggdrasil.li. 140_imaps._tcp IN SRV 5 0 993 ymir.yggdrasil.li.
99
100game01 IN A 94.16.107.151
101game01 IN AAAA 2a03:4000:50:13d:34ee:a2ff:fed0:328f
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..45619fb0 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,12 +28,14 @@ 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 and '@' in self.args['sender']:
37 with self.server.db_pool.connection() as conn: 39 with self.server.db_pool.connection() as conn:
38 local, domain = self.args['sender'].split(sep='@', maxsplit=1) 40 local, domain = self.args['sender'].split(sep='@', maxsplit=1)
39 extension = None 41 extension = None
@@ -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 4196a8bc..b4b2b5c8 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
@@ -108,19 +124,20 @@ in {
108 services.postfix = { 124 services.postfix = {
109 enable = true; 125 enable = true;
110 enableSmtp = false; 126 enableSmtp = false;
111 hostname = "surtr.yggdrasil.li";
112 recipientDelimiter = "";
113 setSendmail = true; 127 setSendmail = true;
114 postmasterAlias = ""; rootAlias = ""; extraAliases = ""; 128 postmasterAlias = ""; rootAlias = ""; extraAliases = "";
115 destination = []; 129 settings.main = {
116 sslCert = "/run/credentials/postfix.service/surtr.yggdrasil.li.pem"; 130 recpipient_delimiter = "";
117 sslKey = "/run/credentials/postfix.service/surtr.yggdrasil.li.key.pem"; 131 mydestination = [];
118 networks = []; 132 mynetworks = [];
119 config = let 133 myhostname = "surtr.yggdrasil.li";
120 relay_ccert = "texthash:${pkgs.writeText "relay_ccert" ""}"; 134
121 in {
122 smtpd_tls_security_level = "may"; 135 smtpd_tls_security_level = "may";
123 136
137 smtpd_tls_chain_files = [
138 "/run/credentials/postfix.service/surtr.yggdrasil.li.full.pem"
139 ];
140
124 #the dh params 141 #the dh params
125 smtpd_tls_dh1024_param_file = toString config.security.dhparams.params."postfix-1024".path; 142 smtpd_tls_dh1024_param_file = toString config.security.dhparams.params."postfix-1024".path;
126 smtpd_tls_dh512_param_file = toString config.security.dhparams.params."postfix-512".path; 143 smtpd_tls_dh512_param_file = toString config.security.dhparams.params."postfix-512".path;
@@ -155,21 +172,14 @@ in {
155 172
156 smtp_tls_connection_reuse = true; 173 smtp_tls_connection_reuse = true;
157 174
158 tls_server_sni_maps = ''texthash:${pkgs.writeText "sni" ( 175 tls_server_sni_maps = "inline:{${concatMapStringsSep ", " (domain: "{ ${domain} = /run/credentials/postfix.service/${removePrefix "." domain}.full.pem }") (concatMap (domain: [domain "mailin.${domain}" "mailsub.${domain}" ".${domain}"]) emailDomains)}}";
159 concatMapStringsSep "\n\n" (domain:
160 concatMapStringsSep "\n" (subdomain: "${subdomain} /run/credentials/postfix.service/${removePrefix "." subdomain}.full.pem")
161 [domain "mailin.${domain}" "mailsub.${domain}" ".${domain}"]
162 ) emailDomains
163 )}'';
164 176
165 smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix"; 177 smtp_tls_policy_maps = "socketmap:unix:${config.services.postfix-mta-sts-resolver.settings.path}:postfix";
166 178
167 local_recipient_maps = ""; 179 local_recipient_maps = "";
168 180
169 # 10 GiB 181 message_size_limit = 10 * 1024 * 1024 * 1024;
170 message_size_limit = "10737418240"; 182 mailbox_size_limit = 10 * 1024 * 1024 * 1024;
171 # 10 GiB
172 mailbox_size_limit = "10737418240";
173 183
174 smtpd_delay_reject = true; 184 smtpd_delay_reject = true;
175 smtpd_helo_required = true; 185 smtpd_helo_required = true;
@@ -184,25 +194,26 @@ in {
184 dbname = email 194 dbname = email
185 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s' 195 query = SELECT action FROM virtual_mailbox_access WHERE lookup = '%s'
186 ''}" 196 ''}"
187 "check_ccert_access ${relay_ccert}"
188 "reject_non_fqdn_helo_hostname" 197 "reject_non_fqdn_helo_hostname"
189 "reject_invalid_helo_hostname" 198 "reject_invalid_helo_hostname"
190 "reject_unauth_destination" 199 "reject_unauth_destination"
191 "reject_unknown_recipient_domain" 200 "reject_unknown_recipient_domain"
192 "reject_unverified_recipient" 201 "reject_unverified_recipient"
202 "check_policy_service unix:/run/postfix-internal-policy.sock"
193 ]; 203 ];
194 unverified_recipient_reject_code = "550"; 204 unverified_recipient_reject_code = "550";
195 unverified_recipient_reject_reason = "Recipient address lookup failed"; 205 unverified_recipient_reject_reason = "Recipient address lookup failed";
196 address_verify_map = "internal:address_verify_map"; 206 address_verify_map = "internal:address_verify_map";
197 address_verify_positive_expire_time = "1h"; 207 address_verify_positive_expire_time = "1h";
198 address_verify_positive_refresh_time = "15m"; 208 address_verify_positive_refresh_time = "15m";
199 address_verify_negative_expire_time = "15s"; 209 address_verify_negative_expire_time = "5m";
200 address_verify_negative_refresh_time = "5s"; 210 address_verify_negative_refresh_time = "1m";
201 address_verify_cache_cleanup_interval = "5s"; 211 address_verify_cache_cleanup_interval = "12h";
212 address_verify_poll_count = "\${stress?15}\${stress:30}";
202 address_verify_poll_delay = "1s"; 213 address_verify_poll_delay = "1s";
214 address_verify_sender_ttl = "30045s";
203 215
204 smtpd_relay_restrictions = [ 216 smtpd_relay_restrictions = [
205 "check_ccert_access ${relay_ccert}"
206 "reject_unauth_destination" 217 "reject_unauth_destination"
207 ]; 218 ];
208 219
@@ -213,7 +224,7 @@ in {
213 smtpd_client_event_limit_exceptions = ""; 224 smtpd_client_event_limit_exceptions = "";
214 225
215 milter_default_action = "accept"; 226 milter_default_action = "accept";
216 smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"]; 227 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"]; 228 non_smtpd_milters = [config.services.opendkim.socket "local:/run/rspamd/rspamd-milter.sock"];
218 229
219 alias_maps = ""; 230 alias_maps = "";
@@ -225,6 +236,37 @@ in {
225 bounce_queue_lifetime = "20m"; 236 bounce_queue_lifetime = "20m";
226 delay_warning_time = "10m"; 237 delay_warning_time = "10m";
227 238
239 failure_template_file = toString (pkgs.writeText "failure.cf" ''
240 Charset: us-ascii
241 From: Mail Delivery System <MAILER-DAEMON>
242 Subject: Undelivered Mail Returned to Sender
243 Postmaster-Subject: Postmaster Copy: Undelivered Mail
244
245 This is the mail system at host $myhostname.
246
247 I'm sorry to have to inform you that your message could not
248 be delivered to one or more recipients. It's attached below.
249
250 The mail system
251 '');
252 delay_template_file = toString (pkgs.writeText "delay.cf" ''
253 Charset: us-ascii
254 From: Mail Delivery System <MAILER-DAEMON>
255 Subject: Delayed Mail (still being retried)
256 Postmaster-Subject: Postmaster Warning: Delayed Mail
257
258 This is the mail system at host $myhostname.
259
260 ####################################################################
261 # THIS IS A WARNING ONLY. YOU DO NOT NEED TO RESEND YOUR MESSAGE. #
262 ####################################################################
263
264 Your message could not be delivered for more than $delay_warning_time_minutes minute(s).
265 It will be retried until it is $maximal_queue_lifetime_minutes minute(s) old.
266
267 The mail system
268 '');
269
228 smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" '' 270 smtpd_discard_ehlo_keyword_address_maps = "cidr:${pkgs.writeText "esmtp_access" ''
229 # Allow DSN requests from local subnet only 271 # Allow DSN requests from local subnet only
230 192.168.0.0/16 silent-discard 272 192.168.0.0/16 silent-discard
@@ -235,11 +277,6 @@ in {
235 ::/0 silent-discard, dsn 277 ::/0 silent-discard, dsn
236 ''}"; 278 ''}";
237 279
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" '' 280 virtual_mailbox_domains = ''pgsql:${pkgs.writeText "virtual_mailbox_domains.cf" ''
244 hosts = postgresql:///email 281 hosts = postgresql:///email
245 dbname = email 282 dbname = email
@@ -254,13 +291,26 @@ in {
254 virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp"; 291 virtual_transport = "dvlmtp:unix:/run/dovecot-lmtp";
255 smtputf8_enable = false; 292 smtputf8_enable = false;
256 293
257 authorized_submit_users = "inline:{ root= postfwd= }"; 294 authorized_submit_users = "inline:{ root= postfwd= ${config.services.dovecot2.user}= }";
295 authorized_flush_users = "inline:{ root= }";
296 authorized_mailq_users = "inline:{ root= }";
258 297
259 postscreen_access_list = ""; 298 postscreen_access_list = "";
260 postscreen_denylist_action = "drop"; 299 postscreen_denylist_action = "drop";
261 postscreen_greet_action = "enforce"; 300 postscreen_greet_action = "enforce";
301
302 sender_bcc_maps = ''pgsql:${pkgs.writeText "sender_bcc_maps.cf" ''
303 hosts = postgresql:///email
304 dbname = email
305 query = SELECT value FROM sender_bcc_maps WHERE key = '%s'
306 ''}'';
307 recipient_bcc_maps = ''pgsql:${pkgs.writeText "recipient_bcc_maps.cf" ''
308 hosts = postgresql:///email
309 dbname = email
310 query = SELECT value FROM recipient_bcc_maps WHERE key = '%s'
311 ''}'';
262 }; 312 };
263 masterConfig = { 313 settings.master = {
264 "465" = { 314 "465" = {
265 type = "inet"; 315 type = "inet";
266 private = false; 316 private = false;
@@ -283,7 +333,7 @@ in {
283 hosts = postgresql:///email 333 hosts = postgresql:///email
284 dbname = email 334 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')) 335 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}'' 336 ''},check_policy_service unix:/run/postfix-internal-policy.sock,permit_tls_all_clientcerts,reject}''
287 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject" 337 "-o" "smtpd_relay_restrictions=permit_tls_all_clientcerts,reject"
288 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" 338 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
289 "-o" "unverified_sender_reject_code=550" 339 "-o" "unverified_sender_reject_code=550"
@@ -313,7 +363,7 @@ in {
313 hosts = postgresql:///email 363 hosts = postgresql:///email
314 dbname = email 364 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')) 365 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}'' 366 ''},check_policy_service unix:/run/postfix-internal-policy.sock,permit_sasl_authenticated,reject}''
317 "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject" 367 "-o" "smtpd_relay_restrictions=permit_sasl_authenticated,reject"
318 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}" 368 "-o" "{smtpd_data_restrictions = check_policy_service unix:/run/postfwd3/postfwd3.sock}"
319 "-o" "unverified_sender_reject_code=550" 369 "-o" "unverified_sender_reject_code=550"
@@ -328,7 +378,10 @@ in {
328 maxproc = 0; 378 maxproc = 0;
329 args = [ 379 args = [
330 "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" '' 380 "-o" "header_checks=pcre:${pkgs.writeText "header_checks_submission" ''
381 if /^Received: /
382 !/by surtr\.yggdrasil\.li/ STRIP
331 /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1 383 /^Received: from [^ ]+ \([^ ]+ [^ ]+\)\s+(.*)$/ REPLACE Received: $1
384 endif
332 ''}" 385 ''}"
333 ]; 386 ];
334 }; 387 };
@@ -364,17 +417,19 @@ in {
364 417
365 services.postsrsd = { 418 services.postsrsd = {
366 enable = true; 419 enable = true;
367 domain = "surtr.yggdrasil.li"; 420 domains = [ "surtr.yggdrasil.li" ] ++ concatMap (domain: [".${domain}" domain]) emailDomains;
368 separator = "+"; 421 separator = "+";
369 excludeDomains = [ "surtr.yggdrasil.li" 422 extraConfig = ''
370 ] ++ concatMap (domain: [".${domain}" domain]) emailDomains; 423 socketmap = unix:/run/postsrsd/postsrsd-socketmap.sock
424 milter = unix:/run/postsrsd/postsrsd-milter.sock
425 '';
371 }; 426 };
372 427
373 services.opendkim = { 428 services.opendkim = {
374 enable = true; 429 enable = true;
375 user = "postfix"; group = "postfix"; 430 user = "postfix"; group = "postfix";
376 socket = "local:/run/opendkim/opendkim.sock"; 431 socket = "local:/run/opendkim/opendkim.sock";
377 domains = ''csl:${concatStringsSep "," (["surtr.yggdrasil.li"] ++ emailDomains)}''; 432 domains = ''csl:${concatStringsSep "," (["surtr.yggdrasil.li" "yggdrasil.li" "141.li" "kleen.li" "synapse.li" "praseodym.org"] ++ emailDomains)}'';
378 selector = "surtr"; 433 selector = "surtr";
379 configFile = builtins.toFile "opendkim.conf" '' 434 configFile = builtins.toFile "opendkim.conf" ''
380 Syslog true 435 Syslog true
@@ -478,32 +533,32 @@ in {
478 }; 533 };
479 }; 534 };
480 535
481 users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user "dovecot2" ]; 536 users.groups.${config.services.rspamd.group}.members = [ config.services.postfix.user config.services.dovecot2.user ];
482 537
483 services.redis.servers.rspamd.enable = true; 538 services.redis.servers.rspamd.enable = true;
484 539
485 users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ]; 540 users.groups.${config.services.redis.servers.rspamd.user}.members = [ config.services.rspamd.user ];
486 541
542 environment.systemPackages = with pkgs; [ dovecot_pigeonhole dovecot_fts_xapian ];
487 services.dovecot2 = { 543 services.dovecot2 = {
488 enable = true; 544 enable = true;
489 enablePAM = false; 545 enablePAM = false;
490 sslServerCert = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.pem"; 546 sslServerCert = "/run/credentials/dovecot.service/surtr.yggdrasil.li.pem";
491 sslServerKey = "/run/credentials/dovecot2.service/surtr.yggdrasil.li.key.pem"; 547 sslServerKey = "/run/credentials/dovecot.service/surtr.yggdrasil.li.key.pem";
492 sslCACert = toString ./ca/ca.crt; 548 sslCACert = toString ./ca/ca.crt;
493 mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8:INDEX=/var/lib/dovecot/indices/%u"; 549 mailLocation = "maildir:/var/lib/mail/%u/maildir:UTF-8:INDEX=/var/lib/dovecot/indices/%u";
494 modules = with pkgs; [ dovecot_pigeonhole dovecot_fts_xapian ];
495 mailPlugins.globally.enable = [ "fts" "fts_xapian" ]; 550 mailPlugins.globally.enable = [ "fts" "fts_xapian" ];
496 protocols = [ "lmtp" "sieve" ]; 551 protocols = [ "lmtp" "sieve" ];
497 sieve = { 552 sieve = {
498 extensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation"]; 553 extensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation" "vacation-seconds" "vnd.dovecot.debug"];
499 globalExtensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation"]; 554 globalExtensions = ["copy" "imapsieve" "variables" "imap4flags" "vacation" "vacation-seconds" "vnd.dovecot.debug"];
500 }; 555 };
501 extraConfig = let 556 extraConfig = let
502 dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" '' 557 dovecotSqlConf = pkgs.writeText "dovecot-sql.conf" ''
503 driver = pgsql 558 driver = pgsql
504 connect = dbname=email 559 connect = dbname=email
505 password_query = SELECT (CASE WHEN '%k' = 'valid' AND '%m' = 'EXTERNAL' THEN NULL ELSE "password" END) as password, (CASE WHEN '%k' = 'valid' AND '%m' = 'EXTERNAL' THEN true WHEN password IS NULL THEN true ELSE NULL END) as nopassword, "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n' 560 password_query = SELECT (CASE WHEN '%k' = 'valid' AND '%m' = 'EXTERNAL' THEN NULL ELSE "password" END) as password, (CASE WHEN '%k' = 'valid' AND '%m' = 'EXTERNAL' THEN true WHEN password IS NULL THEN true ELSE NULL END) as nopassword, "user", quota_rule, '${config.services.dovecot2.user}' as uid, '${config.services.dovecot2.group}' as gid FROM imap_user WHERE "user" = '%n'
506 user_query = SELECT "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n' 561 user_query = SELECT "user", quota_rule, '${config.services.dovecot2.user}' as uid, 'dovecot2' as gid FROM imap_user WHERE "user" = '%n'
507 iterate_query = SELECT "user" FROM imap_user 562 iterate_query = SELECT "user" FROM imap_user
508 ''; 563 '';
509 in '' 564 in ''
@@ -511,16 +566,16 @@ in {
511 566
512 mail_plugins = $mail_plugins quota 567 mail_plugins = $mail_plugins quota
513 568
514 first_valid_uid = ${toString config.users.users.dovecot2.uid} 569 first_valid_uid = ${toString config.users.users.${config.services.dovecot2.user}.uid}
515 last_valid_uid = ${toString config.users.users.dovecot2.uid} 570 last_valid_uid = ${toString config.users.users.${config.services.dovecot2.user}.uid}
516 first_valid_gid = ${toString config.users.groups.dovecot2.gid} 571 first_valid_gid = ${toString config.users.groups.${config.services.dovecot2.group}.gid}
517 last_valid_gid = ${toString config.users.groups.dovecot2.gid} 572 last_valid_gid = ${toString config.users.groups.${config.services.dovecot2.group}.gid}
518 573
519 ${concatMapStringsSep "\n\n" (domain: 574 ${concatMapStringsSep "\n\n" (domain:
520 concatMapStringsSep "\n" (subdomain: '' 575 concatMapStringsSep "\n" (subdomain: ''
521 local_name ${subdomain} { 576 local_name ${subdomain} {
522 ssl_cert = </run/credentials/dovecot2.service/${subdomain}.pem 577 ssl_cert = </run/credentials/dovecot.service/${subdomain}.pem
523 ssl_key = </run/credentials/dovecot2.service/${subdomain}.key.pem 578 ssl_key = </run/credentials/dovecot.service/${subdomain}.key.pem
524 } 579 }
525 '') ["imap.${domain}" domain] 580 '') ["imap.${domain}" domain]
526 ) emailDomains} 581 ) emailDomains}
@@ -541,10 +596,10 @@ in {
541 auth_debug = yes 596 auth_debug = yes
542 597
543 service auth { 598 service auth {
544 user = dovecot2 599 user = ${config.services.dovecot2.user}
545 } 600 }
546 service auth-worker { 601 service auth-worker {
547 user = dovecot2 602 user = ${config.services.dovecot2.user}
548 } 603 }
549 604
550 userdb { 605 userdb {
@@ -565,7 +620,7 @@ in {
565 args = ${pkgs.writeText "dovecot-sql.conf" '' 620 args = ${pkgs.writeText "dovecot-sql.conf" ''
566 driver = pgsql 621 driver = pgsql
567 connect = dbname=email 622 connect = dbname=email
568 user_query = SELECT DISTINCT ON (extension IS NULL, local IS NULL) "user", quota_rule, 'dovecot2' as uid, 'dovecot2' as gid FROM lmtp_mapping WHERE CASE WHEN extension IS NOT NULL AND local IS NOT NULL THEN ('%n' :: citext) = local || '+' || extension AND domain = ('%d' :: citext) WHEN local IS NOT NULL THEN (local = ('%n' :: citext) OR ('%n' :: citext) ILIKE local || '+%%') AND domain = ('%d' :: citext) WHEN extension IS NOT NULL THEN ('%n' :: citext) ILIKE '%%+' || extension AND domain = ('%d' :: citext) ELSE domain = ('%d' :: citext) END ORDER BY (extension IS NULL) ASC, (local IS NULL) ASC 623 user_query = SELECT DISTINCT ON (extension IS NULL, local IS NULL) "user", quota_rule, '${config.services.dovecot2.user}' as uid, '${config.services.dovecot2.group}' as gid FROM lmtp_mapping WHERE CASE WHEN extension IS NOT NULL AND local IS NOT NULL THEN ('%n' :: citext) = local || '+' || extension AND domain = ('%d' :: citext) WHEN local IS NOT NULL THEN (local = ('%n' :: citext) OR ('%n' :: citext) ILIKE local || '+%%') AND domain = ('%d' :: citext) WHEN extension IS NOT NULL THEN ('%n' :: citext) ILIKE '%%+' || extension AND domain = ('%d' :: citext) ELSE domain = ('%d' :: citext) END ORDER BY (extension IS NULL) ASC, (local IS NULL) ASC
569 ''} 624 ''}
570 625
571 skip = never 626 skip = never
@@ -635,7 +690,7 @@ in {
635 quota_status_success = DUNNO 690 quota_status_success = DUNNO
636 quota_status_nouser = DUNNO 691 quota_status_nouser = DUNNO
637 quota_grace = 10%% 692 quota_grace = 10%%
638 quota_max_mail_size = ${config.services.postfix.config.message_size_limit} 693 quota_max_mail_size = ${toString config.services.postfix.settings.main.message_size_limit}
639 quota_vsizes = yes 694 quota_vsizes = yes
640 } 695 }
641 696
@@ -673,7 +728,7 @@ in {
673 plugin { 728 plugin {
674 plugin = fts fts_xapian 729 plugin = fts fts_xapian
675 fts = xapian 730 fts = xapian
676 fts_xapian = partial=2 full=20 attachments=1 verbose=1 731 fts_xapian = partial=3 full=20 attachments=1 verbose=1
677 732
678 fts_autoindex = yes 733 fts_autoindex = yes
679 734
@@ -688,12 +743,12 @@ in {
688 743
689 systemd.services.dovecot-fts-xapian-optimize = { 744 systemd.services.dovecot-fts-xapian-optimize = {
690 description = "Optimize dovecot indices for fts_xapian"; 745 description = "Optimize dovecot indices for fts_xapian";
691 requisite = [ "dovecot2.service" ]; 746 requisite = [ "dovecot.service" ];
692 after = [ "dovecot2.service" ]; 747 after = [ "dovecot.service" ];
693 startAt = "*-*-* 22:00:00 Europe/Berlin"; 748 startAt = "*-*-* 22:00:00 Europe/Berlin";
694 serviceConfig = { 749 serviceConfig = {
695 Type = "oneshot"; 750 Type = "oneshot";
696 ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A"; 751 ExecStart = "${getExe' pkgs.dovecot "doveadm"} fts optimize -A";
697 PrivateDevices = true; 752 PrivateDevices = true;
698 PrivateNetwork = true; 753 PrivateNetwork = true;
699 ProtectKernelTunables = true; 754 ProtectKernelTunables = true;
@@ -754,31 +809,29 @@ in {
754 809
755 security.acme.rfc2136Domains = { 810 security.acme.rfc2136Domains = {
756 "surtr.yggdrasil.li" = { 811 "surtr.yggdrasil.li" = {
757 restartUnits = [ "postfix.service" "dovecot2.service" ]; 812 restartUnits = [ "postfix.service" "dovecot.service" ];
758 }; 813 };
759 } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains) 814 } // listToAttrs (map (domain: nameValuePair "spm.${domain}" { restartUnits = ["nginx.service"]; }) spmDomains)
760 // listToAttrs (concatMap (domain: [ 815 // listToAttrs (concatMap (domain: [
761 (nameValuePair domain { restartUnits = ["postfix.service" "dovecot2.service"]; }) 816 (nameValuePair domain { restartUnits = ["postfix.service" "dovecot.service"]; })
762 (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; }) 817 (nameValuePair "mailin.${domain}" { restartUnits = ["postfix.service"]; })
763 (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; }) 818 (nameValuePair "mailsub.${domain}" { restartUnits = ["postfix.service"]; })
764 (nameValuePair "imap.${domain}" { restartUnits = ["dovecot2.service"]; }) 819 (nameValuePair "imap.${domain}" { restartUnits = ["dovecot.service"]; })
765 (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; }) 820 (nameValuePair "mta-sts.${domain}" { restartUnits = ["nginx.service"]; })
766 ]) emailDomains); 821 ]) emailDomains);
767 822
768 systemd.services.postfix = { 823 systemd.services.postfix = {
769 serviceConfig.LoadCredential = [ 824 serviceConfig.LoadCredential = let
770 "surtr.yggdrasil.li.key.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/key.pem" 825 tlsCredential = domain: "${domain}.full.pem:${config.security.acme.certs.${domain}.directory}/full.pem";
771 "surtr.yggdrasil.li.pem:${config.security.acme.certs."surtr.yggdrasil.li".directory}/fullchain.pem" 826 in [
772 ] ++ concatMap (domain: 827 (tlsCredential "surtr.yggdrasil.li")
773 map (subdomain: "${subdomain}.full.pem:${config.security.acme.certs.${subdomain}.directory}/full.pem") 828 ] ++ concatMap (domain: map tlsCredential [domain "mailin.${domain}" "mailsub.${domain}"]) emailDomains;
774 [domain "mailin.${domain}" "mailsub.${domain}"]
775 ) emailDomains;
776 }; 829 };
777 830
778 systemd.services.dovecot2 = { 831 systemd.services.dovecot = {
779 preStart = '' 832 preStart = ''
780 for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do 833 for f in /etc/dovecot/sieve_flag.d/*.sieve /etc/dovecot/sieve_before.d/*.sieve; do
781 ${pkgs.dovecot_pigeonhole}/bin/sievec $f 834 ${getExe' pkgs.dovecot_pigeonhole "sievec"} $f
782 done 835 done
783 ''; 836 '';
784 837
@@ -845,15 +898,16 @@ in {
845 charset utf-8; 898 charset utf-8;
846 source_charset utf-8; 899 source_charset utf-8;
847 ''; 900 '';
848 root = pkgs.runCommand "mta-sts.${domain}" {} '' 901 root = pkgs.writeTextFile {
849 mkdir -p $out/.well-known 902 name = "mta-sts.${domain}";
850 cp ${pkgs.writeText "mta-sts.${domain}.txt" '' 903 destination = "/.well-known/mta-sts.txt";
904 text = ''
851 version: STSv1 905 version: STSv1
852 mode: enforce 906 mode: enforce
853 max_age: 2419200 907 max_age: 2419200
854 mx: mailin.${domain} 908 mx: mailin.${domain}
855 ''} $out/.well-known/mta-sts.txt 909 '';
856 ''; 910 };
857 }; 911 };
858 }) emailDomains); 912 }) emailDomains);
859 }; 913 };
@@ -870,7 +924,7 @@ in {
870 systemd.services.spm = { 924 systemd.services.spm = {
871 serviceConfig = { 925 serviceConfig = {
872 Type = "notify"; 926 Type = "notify";
873 ExecStart = "${pkgs.spm}/bin/spm-server"; 927 ExecStart = getExe' pkgs.spm "spm-server";
874 User = "spm"; 928 User = "spm";
875 Group = "spm"; 929 Group = "spm";
876 930
@@ -928,7 +982,7 @@ in {
928 serviceConfig = { 982 serviceConfig = {
929 Type = "notify"; 983 Type = "notify";
930 984
931 ExecStart = "${ccert-policy-server}/bin/ccert-policy-server"; 985 ExecStart = getExe' ccert-policy-server "ccert-policy-server";
932 986
933 Environment = [ 987 Environment = [
934 "PGDATABASE=email" 988 "PGDATABASE=email"
@@ -961,6 +1015,53 @@ in {
961 }; 1015 };
962 users.groups."postfix-ccert-sender-policy" = {}; 1016 users.groups."postfix-ccert-sender-policy" = {};
963 1017
1018 systemd.sockets."postfix-internal-policy" = {
1019 requiredBy = ["postfix.service"];
1020 wants = ["postfix-internal-policy.service"];
1021 socketConfig = {
1022 ListenStream = "/run/postfix-internal-policy.sock";
1023 };
1024 };
1025 systemd.services."postfix-internal-policy" = {
1026 after = [ "postgresql.service" ];
1027 bindsTo = [ "postgresql.service" ];
1028
1029 serviceConfig = {
1030 Type = "notify";
1031
1032 ExecStart = lib.getExe internal-policy-server;
1033
1034 Environment = [
1035 "PGDATABASE=email"
1036 ];
1037
1038 DynamicUser = false;
1039 User = "postfix-internal-policy";
1040 Group = "postfix-internal-policy";
1041 ProtectSystem = "strict";
1042 SystemCallFilter = "@system-service";
1043 NoNewPrivileges = true;
1044 ProtectKernelTunables = true;
1045 ProtectKernelModules = true;
1046 ProtectKernelLogs = true;
1047 ProtectControlGroups = true;
1048 MemoryDenyWriteExecute = true;
1049 RestrictSUIDSGID = true;
1050 KeyringMode = "private";
1051 ProtectClock = true;
1052 RestrictRealtime = true;
1053 PrivateDevices = true;
1054 PrivateTmp = true;
1055 ProtectHostname = true;
1056 ReadWritePaths = ["/run/postgresql"];
1057 };
1058 };
1059 users.users."postfix-internal-policy" = {
1060 isSystemUser = true;
1061 group = "postfix-internal-policy";
1062 };
1063 users.groups."postfix-internal-policy" = {};
1064
964 services.postfwd = { 1065 services.postfwd = {
965 enable = true; 1066 enable = true;
966 cache = false; 1067 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/hledger.nix b/hosts/surtr/hledger.nix
new file mode 100644
index 00000000..e44933c3
--- /dev/null
+++ b/hosts/surtr/hledger.nix
@@ -0,0 +1,66 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "hledger.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."hledger" = {
13 servers = {
14 "[2a03:4000:52:ada:4:1::]:5000" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "hledger.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/hledger.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/hledger.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/hledger.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://hledger;
35
36 proxy_http_version 1.1;
37 proxy_set_header Upgrade $http_upgrade;
38 proxy_set_header Connection "upgrade";
39
40 proxy_redirect off;
41 proxy_set_header Host $host;
42 proxy_set_header X-Real-IP $remote_addr;
43 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44 proxy_set_header X-Forwarded-Host $server_name;
45 proxy_set_header X-Forwarded-Proto $scheme;
46
47 client_max_body_size 0;
48 proxy_request_buffering off;
49 proxy_buffering off;
50 '';
51 };
52 };
53 };
54 };
55
56 systemd.services.nginx = {
57 serviceConfig = {
58 LoadCredential = [
59 "hledger.yggdrasil.li.key.pem:${config.security.acme.certs."hledger.yggdrasil.li".directory}/key.pem"
60 "hledger.yggdrasil.li.pem:${config.security.acme.certs."hledger.yggdrasil.li".directory}/fullchain.pem"
61 "hledger.yggdrasil.li.chain.pem:${config.security.acme.certs."hledger.yggdrasil.li".directory}/chain.pem"
62 ];
63 };
64 };
65 };
66}
diff --git a/hosts/surtr/http/default.nix b/hosts/surtr/http/default.nix
index f3a7154e..ea527cb5 100644
--- a/hosts/surtr/http/default.nix
+++ b/hosts/surtr/http/default.nix
@@ -13,8 +13,6 @@
13 recommendedTlsSettings = true; 13 recommendedTlsSettings = true;
14 sslDhparam = config.security.dhparams.params.nginx.path; 14 sslDhparam = config.security.dhparams.params.nginx.path;
15 commonHttpConfig = '' 15 commonHttpConfig = ''
16 ssl_ecdh_curve X448:X25519:prime256v1:secp521r1:secp384r1;
17
18 log_format main 16 log_format main
19 '$remote_addr "$remote_user" ' 17 '$remote_addr "$remote_user" '
20 '"$host" "$request" $status $bytes_sent ' 18 '"$host" "$request" $status $bytes_sent '
diff --git a/hosts/surtr/immich.nix b/hosts/surtr/immich.nix
new file mode 100644
index 00000000..61a55e77
--- /dev/null
+++ b/hosts/surtr/immich.nix
@@ -0,0 +1,66 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "immich.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."immich" = {
13 servers = {
14 "[2a03:4000:52:ada:4:1::]:2283" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "immich.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/immich.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/immich.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/immich.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://immich;
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 "immich.yggdrasil.li.key.pem:${config.security.acme.certs."immich.yggdrasil.li".directory}/key.pem"
60 "immich.yggdrasil.li.pem:${config.security.acme.certs."immich.yggdrasil.li".directory}/fullchain.pem"
61 "immich.yggdrasil.li.chain.pem:${config.security.acme.certs."immich.yggdrasil.li".directory}/chain.pem"
62 ];
63 };
64 };
65 };
66}
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/paperless.nix b/hosts/surtr/paperless.nix
new file mode 100644
index 00000000..7bc4397c
--- /dev/null
+++ b/hosts/surtr/paperless.nix
@@ -0,0 +1,66 @@
1{ config, ... }:
2
3{
4 config = {
5 security.acme.rfc2136Domains = {
6 "paperless.yggdrasil.li" = {
7 restartUnits = ["nginx.service"];
8 };
9 };
10
11 services.nginx = {
12 upstreams."paperless" = {
13 servers = {
14 "[2a03:4000:52:ada:4:1::]:28981" = {};
15 };
16 extraConfig = ''
17 keepalive 8;
18 '';
19 };
20 virtualHosts = {
21 "paperless.yggdrasil.li" = {
22 kTLS = true;
23 http3 = true;
24 forceSSL = true;
25 sslCertificate = "/run/credentials/nginx.service/paperless.yggdrasil.li.pem";
26 sslCertificateKey = "/run/credentials/nginx.service/paperless.yggdrasil.li.key.pem";
27 sslTrustedCertificate = "/run/credentials/nginx.service/paperless.yggdrasil.li.chain.pem";
28 extraConfig = ''
29 charset utf-8;
30 '';
31
32 locations = {
33 "/".extraConfig = ''
34 proxy_pass http://paperless;
35
36 proxy_http_version 1.1;
37 proxy_set_header Upgrade $http_upgrade;
38 proxy_set_header Connection "upgrade";
39
40 proxy_redirect off;
41 proxy_set_header Host $host;
42 proxy_set_header X-Real-IP $remote_addr;
43 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
44 proxy_set_header X-Forwarded-Host $server_name;
45 proxy_set_header X-Forwarded-Proto $scheme;
46
47 client_max_body_size 0;
48 proxy_request_buffering off;
49 proxy_buffering off;
50 '';
51 };
52 };
53 };
54 };
55
56 systemd.services.nginx = {
57 serviceConfig = {
58 LoadCredential = [
59 "paperless.yggdrasil.li.key.pem:${config.security.acme.certs."paperless.yggdrasil.li".directory}/key.pem"
60 "paperless.yggdrasil.li.pem:${config.security.acme.certs."paperless.yggdrasil.li".directory}/fullchain.pem"
61 "paperless.yggdrasil.li.chain.pem:${config.security.acme.certs."paperless.yggdrasil.li".directory}/chain.pem"
62 ];
63 };
64 };
65 };
66}
diff --git a/hosts/surtr/postgresql/default.nix b/hosts/surtr/postgresql/default.nix
index 583e4443..3786ea7c 100644
--- a/hosts/surtr/postgresql/default.nix
+++ b/hosts/surtr/postgresql/default.nix
@@ -89,6 +89,10 @@ in {
89 "d /var/spool/pgbackrest 0750 postgres postgres - -" 89 "d /var/spool/pgbackrest 0750 postgres postgres - -"
90 ]; 90 ];
91 91
92 systemd.services.postgresql.serviceConfig = {
93 ReadWritePaths = [ "/var/spool/pgbackrest" "/var/lib/pgbackrest/archive/surtr" ];
94 };
95
92 systemd.services.migrate-postgresql = { 96 systemd.services.migrate-postgresql = {
93 after = [ "postgresql.service" ]; 97 after = [ "postgresql.service" ];
94 bindsTo = [ "postgresql.service" ]; 98 bindsTo = [ "postgresql.service" ];
@@ -276,6 +280,64 @@ in {
276 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;
277 281
278 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;
279 ''} 341 ''}
280 342
281 psql etebase postgres -eXf ${pkgs.writeText "etebase.sql" '' 343 psql etebase postgres -eXf ${pkgs.writeText "etebase.sql" ''
diff --git a/hosts/surtr/tls/default.nix b/hosts/surtr/tls/default.nix
index b1c05888..b25bd2ea 100644
--- a/hosts/surtr/tls/default.nix
+++ b/hosts/surtr/tls/default.nix
@@ -41,7 +41,7 @@ in {
41 41
42 acceptTerms = true; 42 acceptTerms = true;
43 # DNS challenge is slow 43 # DNS challenge is slow
44 preliminarySelfsigned = true; 44 # preliminarySelfsigned = true;
45 defaults = { 45 defaults = {
46 email = "phikeebaogobaegh@141.li"; 46 email = "phikeebaogobaegh@141.li";
47 # We don't like NIST curves and Let's Encrypt doesn't support 47 # We don't like NIST curves and Let's Encrypt doesn't support
diff --git a/hosts/surtr/tls/tsig_key.gup b/hosts/surtr/tls/tsig_key.gup
index 3d81b603..46a3789e 100644
--- a/hosts/surtr/tls/tsig_key.gup
+++ b/hosts/surtr/tls/tsig_key.gup
@@ -1,6 +1,6 @@
1#!/usr/bin/env zsh 1#!/usr/bin/env zsh
2 2
3keyFile=../dns/keys/${2:t}_acme.yaml 3keyFile=../dns/keys/${2:t}_acme
4gup -u $keyFile 4gup -u $keyFile
5sops -d --input-type=binary --output-type=binary ${keyFile} | yq -r '.key[0].secret' > $1 5sops -d --input-type=binary --output-type=binary ${keyFile} | yq -r '.key[0].secret' > $1
6sops -p '7ED22F4AA7BB55728B643DC5471B7D88E4EF66F8,30D3453B8CD02FE2A3E7C78C0FB536FB87AE8F51' --input-type=binary -e -i $1 \ No newline at end of file 6sops --input-type=binary -e -i $1
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/hledger.yggdrasil.li b/hosts/surtr/tls/tsig_keys/hledger.yggdrasil.li
new file mode 100644
index 00000000..ab6cdd68
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/hledger.yggdrasil.li
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:Yd70QIj9DE6a5IN+Mf2M5p95vkRMHRg9BXaM686W7BRtthOw9m54/5FK6JWr,iv:cIOIKinkqFFPgTZdewWVY0h6kM5hGfVzuA4iYNhwK5c=,tag:Ds/oI+TOERbIdcGbI4WoEg==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1NnJ4SmsvSlh3ZlliSm9C\nNG45clh3NEZWZ05jaHhVK0xqTVRFL2wxN1ZrCk5hL2p2ZjhtcDBjTEZscXFTNkY5\nemVZSUUwV2V5cFBTdWo4RWxsM2xROVEKLS0tIDBFSFlkUVJ0ajJEUENlelVFKzVk\ndnJhMURMU2o3WVBKZGNVRXBiNytqUFEKHivcSTYy5D770C0h7RsmLBmkIG9+MDoV\ngJHvfkGzXPKwmDTMKdHbIk+ctI+u0/1jMn/K2Q9OFnOIYxP3gHiFag==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArYUNCQ1U2MXpQdmM4SHR0\nTnJWZlFGTUQ1NjVqZ3Z2MGt5NFpBVFp0b0YwCkdseitkbzI1RkZyL3V5Z1pNMG9R\nVnEzRUxrTDQrS3BiMXB1QUFPeUcxZUkKLS0tIGJFODkwNFY3c0tBLzFBNjFiYjJk\nZW82bTdia2F1NHpNSG1IYmhWb1ZCTHMK9ovFx3+x5PrV4y6+RH5XA5DK2wRPXlAt\ncxxpRZIlmnvhZXIeCYE9yhHFmz3uAn0Oib1RUDblca9FlnF9tyYD6g==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-19T17:13:51Z",
19 "mac": "ENC[AES256_GCM,data:ReRuK9qdZV8AbMzA9Yur0AZW+1RF3aRnfBvsKJkQtXsFdkmJQ4QkRGtL27RmjFdvQ3kXBIyhib7hYA60AJ0amduYrSScY0dtz8AurjyE4f2BGQ9/QeKRBfKXHxLvj4/xWNvS4+PVdGKkKbqIs8isz9n77WQQ3lTHop2K/TjaTuQ=,iv:gUhDK9oeUHdpQ2Fp8mFDIgPFo2JjHE0jjooL7FmvmrE=,tag:2lkwSl3j3oqamdLbM9wbow==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/surtr/tls/tsig_keys/immich.yggdrasil.li b/hosts/surtr/tls/tsig_keys/immich.yggdrasil.li
new file mode 100644
index 00000000..73104cc1
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/immich.yggdrasil.li
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:COfmT91I4+yiPhN3Hi7BTqMHyKhdKtwlzT9vNgTZc7FWTHhfuTtCHQo/rhX0,iv:RDs//AT8peUhKwIRdchCScUr/PlEzyMzQPB90S4k3g4=,tag:Lh4BULmQ6+hC+Ed8s9k0Hw==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGVEQvU29uWnVjMS8xcGp1\nam5hZVN3ejAxaGdpVWNia0VxT0I3dzR4YlV3ClczOGd0ZDJmUDhqb2dEdm5VeUdX\nRlR1WDNYUU9qaTYrRzhYMXBTV1JjV1UKLS0tIHBONU55RWtRSkR6K2NTNFZrUEZj\nbktqY0xBdGtiZWFFV3JUUVZTOC9YV2MKI4Ytz1NZ9+Og0GzIt/bh6L3aJUeR476g\nyRNifW4eOHf4Ne02ElpEoq6woInkxk8Ou/SJVIRmEOhjwm+qbV17gQ==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBVOVNydWcyYjFrZDR2WStu\nWkJQd3VTTGtqVzdLVFR1eFdPZXBtVFBzVXc4ClREQVpKeXlhWlBFQzVFL0VGME5K\nUUhoa3A1YWdvSkZVV1FQcUh5L3RoUmMKLS0tIHE4c3A1OTNXVk9xZHJPZTlSMlQ4\nZnZUTXdjUGZuN3NoSEFSSko1aU5aQUEKHcuI2+9q7DsDwRn7mfwcSyC7AixzCC0e\nhqnGaW0HxmtLeOFuSPLdFMhhockCYGEV/907i/X6EImepWC4cf3bqA==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-01-03T15:31:40Z",
19 "mac": "ENC[AES256_GCM,data:soDFDk35A1ULzOosZNrbhvtG3NPJDpAtLP3xrDtCBxgSGQ0lWrQ0o3MaKaJoDXQv7g/vYghmSwjH+0In0Ib3OWg0WLAlhwTEsiAn1o4JNRu/wF5aqvazOiDzFu7cyWil4Lsphy5eZgtc4IUp75SlCQc71xlNLoxudPpdcSxNLWg=,iv:jBYUZbiWM5gxFA+ZdpxpZIkz3WfgFi59tXFp242/qr8=,tag:H7ihAryZ8be+BbaqXFhRRg==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.2"
23 }
24} \ No newline at end of file
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/tls/tsig_keys/paperless.yggdrasil.li b/hosts/surtr/tls/tsig_keys/paperless.yggdrasil.li
new file mode 100644
index 00000000..b1029931
--- /dev/null
+++ b/hosts/surtr/tls/tsig_keys/paperless.yggdrasil.li
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:D9l0pklD2KDZ4/TXHtXg00MmCnjCVVBG0AK9j5OxxBCyYseCTckp2P/iPOng,iv:DjvuKWPr/jldfk0eZ5+jWHN0RurdruR4Md7AMAPzRQg=,tag:h0c4m3hpATzzb6a7DVmi9w==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3S05aS0ZEcVU2T3BIOG13\nSDJLTUp4OG9ZYVluK2gxbUJDSFRaQ0xnS0YwClFkKzByanJGOWFwbTlISndyU0Rx\nN1FJb3FVaUZOVDBsWEdHTVNGaGNtMVkKLS0tIDI1RmxaMlhVd1FPL1dNdlRGK0Nq\nMFpJZTNnWncrbkV5YS82ZnhGRld0UG8KIuf7bC7GVxaGeR7gwC7kGu/wtBppjq4H\nyDT05CYJf9/EE3K5aJpIOlxyqowRs2SINvIVkyd5ggYkkxCctmGXjQ==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUV3ZqTVJkNWdHeTlKK3Vl\nVTdrdzE2Z3YzVmZxelNFMDNMTUJneHNtNzFRCmQxTU84ekV4bFVLajU0ajB6ZzZK\neEpuQTcyS1o4MW9xTU5nMXVUR0gxTmcKLS0tIGVzZC8xYTc1VkU2RzA2NFQ1K2xz\nLzhPVjBUcytWNGRsdnFob3A4aWljelkKFMlmigcEVzelcEiv6WGya1dsIOJYr7YT\naBHgMttV7zzYHLqvIVJSCz+uw2FqDyqN46twmzFC0HSHeiKbvRrHVw==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-13T19:23:42Z",
19 "mac": "ENC[AES256_GCM,data:0Fcgq0pOZtBBSiK8pUr/jadXMdtbZYFhUbSe+7DQpB8Fo2r8cEoT+Cpcy7tu+l9eXUiDk/tXTBJyMXaW4XWwS/Fe6Zcb95UYaYR1Y6OM9JVPYmwd6QSeC13MwzhYaCDlBiWWq69Zn8grEg7npWo/LS9LK7IEbN7EI8o7QYDI6cw=,iv:C+8ZmVTNWySQ+/6j+YirSwZzoMqXRlgstk47Efxmqps=,tag:Be/6Ve6M/Dcm/6QrbF+JTw==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/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/surtr/zfs.nix b/hosts/surtr/zfs.nix
index 17c5cd32..3795956d 100644
--- a/hosts/surtr/zfs.nix
+++ b/hosts/surtr/zfs.nix
@@ -49,7 +49,7 @@
49 49
50 boot.postBootCommands = '' 50 boot.postBootCommands = ''
51 echo "=== STARTING ZPOOL IMPORT ===" 51 echo "=== STARTING ZPOOL IMPORT ==="
52 ${pkgs.zfs}/bin/zpool import -a -N -d /dev 52 ${pkgs.zfs}/bin/zpool import -a -f -N -d /dev
53 ${pkgs.zfs}/bin/zpool status 53 ${pkgs.zfs}/bin/zpool status
54 ${pkgs.zfs}/bin/zfs mount -a 54 ${pkgs.zfs}/bin/zfs mount -a
55 echo "=== ZPOOL IMPORT COMPLETE ===" 55 echo "=== ZPOOL IMPORT COMPLETE ==="
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..42920069
--- /dev/null
+++ b/hosts/vidhar/audiobookshelf/abs-podcast-autoplaylist-gkleen.toml
@@ -0,0 +1,19 @@
1{
2 "data": "ENC[AES256_GCM,data:7STcG1J3zLHzlHi2LZgSa+pmKlYU6X1eLvjXcx7gvCuHFkXwa/ldrHdzaBXAMrQ+C+DWM4+cyhdal3ypxXueBuBEvH3P7/KofPP2A519sdZo1vDkXCzYRD3Ow74M+hX5ej6hL1mv21HayrRMPnYfH8HUWk1kd/y69EjpjvDHyCpQuw1WG5M4FDUaTd4Vh51QRCSG/Is29dEkvQowhIvNqnAR4KW9PpfjlbUPHauZ6PQLnlJmI9QCcAGJ8hHtp5T+xctTym82GAlXugTJpHnkqcxnGteu9H7UuiDMQ3aKKGYzZCpWkNHrbD1IRhzPzSjVlN2nIX3Ydp8ENNf61EGOrp5hzmV0MhwRHKfz5rE1FRVuyHcwhwBdJzfhzrL9zXZYzn9Zuf9KWwsF+1+58i9u9hPym63r4c1+RbMjNKzHc1/yhhsRDCrTwvMd/Hc6KfUi3H1wW/r02Va2bOCkYrOkWIQpBdx4ThMgkTaHr94DBMqT8n3Fzhc7+uaUsl6I9gMwTn8QCV3g4U7Mp3+/gnQLOdsCTgKr69x1iPfp7jzzbC29MWhIk+2aig+iWRMVT0n4AWIxooc3cYnfOJNtCdJwKuPUl4xNlZ22NK3XQfxBURSw0pi5KyDO1wgTVRoRJMnXZyin3bZ10OU8QKBqLp22FXpG/+mHrm3Y6yLkqfIP5ry+b86Rb7nWJlxDTwTGSoGi8sJzwNb+H9ioG7LjaSmvr7V+2aSq0+72WopAkIFSBq/yIWX3J/rNZuia6jKMY/8bSLDJz3V/YuzeUJ5feUkOS4KW1J1UkaYd6XxZ9Y8szCS/B7Ocz0YiV8fHmOzZKRn8BTmmXUIoyxO07MmwQ1shYfiVCwXjdjq2f3/CZbXNVOPvhGBYOcutUztftu3H9DRDTKrVae1TnztCVSkBSk1o52uSj0r8t6f4l1r7UG1TviOnVLVh1/6iiJVQey0m8G6ugw22zxfhI0Uea/rB70i+2UuoxrmsOfg+TgnvKBuakEBifp4rx0S5wyz05RYXkBLJTIwi2fVGmulqjZuCQNQnI3cfPI+oGcykob9IQqdo/Dwd118hNPVAwdDHyaiGXGh8eu8N2OCLQxlGZDtkVUveaEasJaZ0WWWaK97FUeoNJfUnB7Y3lNjv5u2rzviZyk4=,iv:jT21FNnHod6btDlBa3UflK3au5VmcsABs5OTMXF6oFA=,tag:Oh8cOL+edT5Wp0I1L5+vwg==,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-08-11T07:08:36Z",
15 "mac": "ENC[AES256_GCM,data:ZL/dOz+NC8sr8vPBsux+gFOWxUhQqMSmG1az7udhB0ckmOXtnrPBzMM1gs+5pwXLvfLux0m4xzT87+o87axIECnCq35FSuMjtEBK24OUJXsLG/q/tDv5dfRBy/976dM5W7YkBVX/uc03p8CLKf5w4XYNeRKnSwjLvWGd9runDOU=,iv:9ZIeJ5aDVVPHi3/oHqWkWtEfeivV/nFFyQ1lJWJwMu8=,tag:TfkHaopMa+Z0zk38A6/NTA==,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 42a9e80d..1c60ed22 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 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
@@ -136,7 +136,7 @@ with lib;
136 wantedBy = ["basic.target"]; 136 wantedBy = ["basic.target"];
137 serviceConfig = { 137 serviceConfig = {
138 ExecStart = pkgs.writeShellScript "limit-pstate-start" '' 138 ExecStart = pkgs.writeShellScript "limit-pstate-start" ''
139 echo 60 > /sys/devices/system/cpu/intel_pstate/max_perf_pct 139 echo 40 > /sys/devices/system/cpu/intel_pstate/max_perf_pct
140 ''; 140 '';
141 RemainAfterExit = true; 141 RemainAfterExit = true;
142 ExecStop = pkgs.writeShellScript "limit-pstate-stop" '' 142 ExecStop = pkgs.writeShellScript "limit-pstate-stop" ''
@@ -157,8 +157,6 @@ with lib;
157 recommendedProxySettings = true; 157 recommendedProxySettings = true;
158 recommendedTlsSettings = true; 158 recommendedTlsSettings = true;
159 commonHttpConfig = '' 159 commonHttpConfig = ''
160 ssl_ecdh_curve X25519:prime256v1:secp521r1:secp384r1;
161
162 log_format main 160 log_format main
163 '$remote_addr "$remote_user" ' 161 '$remote_addr "$remote_user" '
164 '"$host" "$request" $status $bytes_sent ' 162 '"$host" "$request" $status $bytes_sent '
diff --git a/hosts/vidhar/hledger/default.nix b/hosts/vidhar/hledger/default.nix
new file mode 100644
index 00000000..ae080f66
--- /dev/null
+++ b/hosts/vidhar/hledger/default.nix
@@ -0,0 +1,83 @@
1{ config, lib, pkgs, ... }:
2{
3 config = {
4 services.hledger-web = {
5 enable = true;
6 allow = "view";
7 stateDir = "/var/lib/hledger";
8 journalFiles = lib.mkForce ["web.journal"];
9 baseUrl = "https://hledger.yggdrasil.li";
10 extraOptions = [
11 "--socket=/run/hledger-web/http.sock"
12 ];
13 };
14 users = {
15 users.hledger.uid = 982;
16 groups.hledger.gid = 979;
17 };
18 systemd.services.hledger-web = {
19 serviceConfig = {
20 UMask = "0002";
21 ReadOnlyPaths = [ config.services.hledger-web.stateDir ];
22 RuntimeDirectory = [ "hledger-web" ];
23 PrivateDevices = true;
24 StateDirectory = "hledger";
25 CapabilityBoundingSet = "";
26 AmbientCapabilities = "";
27 ProtectSystem = "strict";
28 ProtectKernelTunables = true;
29 ProtectKernelModules = true;
30 ProtectControlGroups = true;
31 ProtectClock = true;
32 ProtectHostname = true;
33 ProtectHome = "tmpfs";
34 ProtectKernelLogs = true;
35 ProtectProc = "invisible";
36 ProcSubset = "pid";
37 PrivateNetwork = false;
38 RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX";
39 SystemCallArchitectures = "native";
40 SystemCallFilter = [
41 "@system-service @resources"
42 "~@obsolete @privileged"
43 ];
44 RestrictSUIDSGID = true;
45 RemoveIPC = true;
46 NoNewPrivileges = true;
47 RestrictRealtime = true;
48 RestrictNamespaces = true;
49 LockPersonality = true;
50 PrivateUsers = true;
51 TemporaryFileSystem = [ "/var/lib/hledger/.cache:mode=0750,uid=${toString (config.users.users.hledger.uid)},gid=${toString (config.users.groups.hledger.gid)}" ];
52 };
53 };
54 services.nginx = {
55 upstreams.hledger = {
56 servers = { "unix:/run/hledger-web/http.sock" = {}; };
57 };
58 virtualHosts."hledger.yggdrasil.li" = {
59 listen = [
60 { addr = "[2a03:4000:52:ada:4:1::]"; port = 5000; }
61 ];
62 extraConfig = ''
63 set_real_ip_from 2a03:4000:52:ada:4::;
64 auth_basic "hledger";
65 auth_basic_user_file "/run/credentials/nginx.service/hledger_users";
66 '';
67 locations."/" = {
68 proxyPass = "http://hledger/";
69 proxyWebsockets = true;
70 };
71 };
72 };
73 systemd.services.nginx.serviceConfig = {
74 SupplementaryGroups = [ "hledger" ];
75 LoadCredential = [ "hledger_users:${config.sops.secrets."hledger_users".path}" ];
76 };
77 sops.secrets."hledger_users" = {
78 format = "binary";
79 sopsFile = ./htpasswd;
80 reloadUnits = [ "nginx.service" ];
81 };
82 };
83}
diff --git a/hosts/vidhar/hledger/htpasswd b/hosts/vidhar/hledger/htpasswd
new file mode 100644
index 00000000..016cb525
--- /dev/null
+++ b/hosts/vidhar/hledger/htpasswd
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:9MNDIAc7ePYk3xQDorX2pU8ybJkJb33RKiJxc2DYauXFNQYxtGwCYhZwod7p7fPh3KqZxBNMRoZXr+/RnV+trsqjAcOOjnXTWLbX6nubq/xm+q0BxEjOPn7FvJF9XOblBeupldo+byGh2CMH9qQv5Fov,iv:3Tym+Mfr48OJet3qDFZPg0XjYr4sNQdNdiu0vUxmzbY=,tag:E0sxRY/jeMVlqH6uAYvD/Q==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA3eFBsOEM2ZUNVT2V3LytC\nTUJvUDdKc0VzMyt2cDFKYU03djBjZVFpeVY4CjByMXhPVXRJVjhKQWZvQ2xuOTE3\ncXdJV1lZaHR3cVl0Z0hQaG00M2dGbjQKLS0tIEIzenVxb3cwM3pXTUl1YUZlSlk2\nbDc3VmE5NkEyZ2tRd01OUGZibmhtUlEKxdesIdvzm8s0SmXU5R+tSbmS5Dj24jrb\nEiMERYy1g8GyHR3d2/mU5iOIdsBegSZReUVzomaMT9L7/TmubgOP3g==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age1qffdqvy9arld9zd5a5cylt0n98xhcns5shxhrhwjq5g4qa844ejselaa4l",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBPa2RDZzR6cEFYTFA1QkND\nbndVeHVrMVJ0MWZvRmw5VXRhOHlRYllIRWxRCjU4dks4R25LS1RZMHFnbmpQRVZz\nNXhubkJvZFc2amRwMDVtQlE0NnBKNzQKLS0tIHRyeDUxTEFPMEMzWUVkZURzODdm\nSHdqbUpvNmFTS1QveFRpRHdnWHpHb28KnvdUkMkKGiBVHQD7Yv7n6WZjihCGJAR2\nMKl2WAn4g4jzgcXPwwIAIjUrMGSIdGpwCTUDcDnlKWAbRYO2B6P17A==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-19T17:11:17Z",
19 "mac": "ENC[AES256_GCM,data:yBIEqHhr4igoMlRcgg2SigKfejqeuNmuleYolsLJo+QOaW4BHITJTvLxRV1JHPpcMVQkF//zx4ZfUUrb8tTN0znGu3Jnpd0JVagbfCVyEuT6d1SB/GzyUVvoQ2GlcA9us+5gjI4oEJTQCfVqnLDBWsw+jXdr3nEIWo6Mvbqo3lI=,iv:I6Swk4wyd+96+tJKRY/FHlS7ZShMDROcbl+l+ZLRxhM=,tag:P1uQvB4NLdkPEKRMI6lLxw==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/vidhar/immich.nix b/hosts/vidhar/immich.nix
new file mode 100644
index 00000000..a1f145a8
--- /dev/null
+++ b/hosts/vidhar/immich.nix
@@ -0,0 +1,10 @@
1{ ... }:
2
3{
4 config = {
5 services.immich = {
6 enable = true;
7 host = "2a03:4000:52:ada:4:1::";
8 };
9 };
10}
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..6fcef9d8 100644
--- a/hosts/vidhar/network/default.nix
+++ b/hosts/vidhar/network/default.nix
@@ -1,9 +1,9 @@
1{ pkgs, lib, ... }: 1{ pkgs, lib, config, ... }:
2 2
3with lib; 3with lib;
4 4
5{ 5{
6 imports = [ ./gpon.nix ./bifrost ./dhcp ]; 6 imports = [ ./pppoe.nix ./bifrost ./dhcp ];
7 7
8 config = { 8 config = {
9 networking = { 9 networking = {
@@ -61,7 +61,9 @@ with lib;
61 firewall.enable = false; 61 firewall.enable = false;
62 nftables = { 62 nftables = {
63 enable = true; 63 enable = true;
64 rulesetFile = ./ruleset.nft; 64 rulesetFile = pkgs.replaceVars ./ruleset.nft {
65 inherit (config.networking) pppInterface;
66 };
65 }; 67 };
66 68
67 resolvconf = { 69 resolvconf = {
@@ -103,7 +105,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) 105 /srv/nfs/nix-store 10.141.0.0/24(ro,async,root_squash) 2a03:4000:52:ada:1::/80(ro,async,root_squash)
104 ''; 106 '';
105 }; 107 };
106 settings.nfsd.vers3 = false; 108 settings.nfsd = {
109 rdma = true;
110 vers3 = false;
111 vers4 = true;
112 "vers4.0" = false;
113 "vers4.1" = false;
114 "vers4.2" = true;
115 };
107 }; 116 };
108 117
109 fileSystems = { 118 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/pap-secrets b/hosts/vidhar/network/pap-secrets
deleted file mode 100644
index 3516de6c..00000000
--- a/hosts/vidhar/network/pap-secrets
+++ /dev/null
@@ -1,26 +0,0 @@
1{
2 "data": "ENC[AES256_GCM,data:BOyWdys7Oja54Ijv5j+kqufdokQe0onnqw/gVxpNOMf+YI/LlzJscaEtGqPh3ehVtYoSWGumCBPrjdq/zhYq4VV/PCtdeu3MnX1CH2B5bH0mFs0eXcqeGK6NErz/b5nEglv4Z19ig2CUDlbvi8h1zZAEjxTNKhT16ItCtJnBCsIoiMl2QcTTWMyh4a02v3wA1UrOQZvFuCgCHmRoBE6vpREyB23gQdrdKLk7D1LLw5C1aZnzQOhFsgs7bVjOcBnmwTao8ntXxw==,iv:X5FgYkl3DGA/lkRsoc+5XrK3Nlp/ldnFigpXpYNfSJE=,tag:qUH7m9NzuVNnOfr3rRgaOg==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1qffdqvy9arld9zd5a5cylt0n98xhcns5shxhrhwjq5g4qa844ejselaa4l",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwYTFaTDZRbDd6cFBRSTNN\nVk1kcFJXRG9TT21IMDZsVmtoZjBTNDNjeDFzCkhxNEI2Ujd6SW1STG43eE5EdzZa\nS3phenJZN0RxajBXQ1BnbUhTa3htdFEKLS0tIGVlT3lReHJSQ2UvQ1FST0M0RzVP\nNmxWNzJmNlFPclJTeDUycDJiUzA4Yk0K4JHtkEPY49TGnKPZzEoEZ131RxeQEWkR\nK1ftH2ilr2tUhiErhpqxoTqfAm33xvruqTsePxh1uC7svzKtKBlS2g==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2021-11-15T08:30:09Z",
15 "mac": "ENC[AES256_GCM,data:TAgZ4ktdN9sZPMo1UtwjKdTM2QBjLorcm84HYXTGYNNEorPoqrXAWOvyWRLjx+zxzpRuDLBPQHCkjwkVO2CctxnTaWPMwITbYtQqj/5ZxACuAeX8MaSximB8s5MJK2faCuVXEnFehbnnPr5Fs8ZsgHwu2iH6DU8ScLEkgckzGV0=,iv:keUbKwWfoIIBsp5Rsm2lEba1ZHAozQY2YpA6p5qDBiU=,tag:1llGytMGvOjSVYKJXGUmXg==,type:str]",
16 "pgp": [
17 {
18 "created_at": "2023-01-30T10:58:50Z",
19 "enc": "-----BEGIN PGP MESSAGE-----\n\nhF4DXxoViZlp6dISAQdA+cwEt6Gv5oKvym4ceJek+J/5guNpmsLLXWIY5CCCSXUw\npXyQpqxm7LQnasIqYNNsNCVbB1mAu6WU6MKn0BG03YWjr8buLB+7PpwZcxeZzRfD\n0l4BAsl+vKwa2YSMCR+EWYSfeEzEVHqoGBJ60dYXuiFiNZInCik+g69PdhsGygNH\nRtIcRiCB8t94GkvdWySTq5ohi1wKOe224l9evbt4zXntVngCHxixuufLrr3Cj+EE\n=3lw4\n-----END PGP MESSAGE-----\n",
20 "fp": "30D3453B8CD02FE2A3E7C78C0FB536FB87AE8F51"
21 }
22 ],
23 "unencrypted_suffix": "_unencrypted",
24 "version": "3.7.1"
25 }
26} \ No newline at end of file
diff --git a/hosts/vidhar/network/gpon.nix b/hosts/vidhar/network/pppoe.nix
index 1628159c..5cc84862 100644
--- a/hosts/vidhar/network/gpon.nix
+++ b/hosts/vidhar/network/pppoe.nix
@@ -8,7 +8,7 @@ in {
8 options = { 8 options = {
9 networking.pppInterface = mkOption { 9 networking.pppInterface = mkOption {
10 type = types.str; 10 type = types.str;
11 default = "gpon"; 11 default = "ppp";
12 }; 12 };
13 }; 13 };
14 14
@@ -26,14 +26,14 @@ in {
26 nodefaultroute 26 nodefaultroute
27 ifname ${pppInterface} 27 ifname ${pppInterface}
28 lcp-echo-adaptive 28 lcp-echo-adaptive
29 lcp-echo-failure 5 29 lcp-echo-failure 10
30 lcp-echo-interval 1 30 lcp-echo-interval 1
31 maxfail 0 31 maxfail 0
32 mtu 1492 32 mtu 1492
33 mru 1492 33 mru 1492
34 plugin pppoe.so 34 plugin pppoe.so
35 name telekom 35 user congstar
36 user 002576900250551137425220#0001@t-online.de 36 password congstar
37 nic-telekom 37 nic-telekom
38 debug 38 debug
39 +ipv6 39 +ipv6
@@ -43,62 +43,55 @@ in {
43 stopIfChanged = true; 43 stopIfChanged = true;
44 44
45 serviceConfig = { 45 serviceConfig = {
46 Type = lib.mkForce "notify";
47 ExecStart = lib.mkForce "${getBin config.services.pppd.package}/sbin/pppd call telekom up_sdnotify nolog";
46 PIDFile = "/run/pppd/${pppInterface}.pid"; 48 PIDFile = "/run/pppd/${pppInterface}.pid";
47 }; 49 };
48 restartTriggers = with config; [ 50 restartTriggers = with config; [
49 environment.etc."ppp/ip-pre-up".source 51 environment.etc."ppp/ip-pre-up".source
50 environment.etc."ppp/ip-up".source 52 environment.etc."ppp/ip-up".source
51 environment.etc."ppp/ip-down".source 53 environment.etc."ppp/ip-down".source
52 # sops.secrets."pap-secrets".sopsFile
53 ]; 54 ];
54 }; 55 };
55 sops.secrets."pap-secrets" = {
56 format = "binary";
57 sopsFile = ./pap-secrets;
58 path = "/etc/ppp/pap-secrets";
59 };
60 56
61 environment.etc = { 57 environment.etc = {
62 "ppp/ip-pre-up".source = let 58 "ppp/ip-pre-up".source = pkgs.resholve.writeScript "ip-pre-up" {
63 app = pkgs.writeShellApplication { 59 interpreter = pkgs.runtimeShell;
64 name = "ip-pre-up"; 60 inputs = [ pkgs.iproute2 pkgs.ethtool ];
65 runtimeInputs = with pkgs; [ iproute2 ethtool ]; 61 execer = [
66 text = '' 62 "cannot:${lib.getExe' pkgs.iproute2 "ip"}"
67 ethtool -K telekom tso off gso off gro off 63 "cannot:${lib.getExe' pkgs.iproute2 "tc"}"
64 ];
65 } ''
66 ethtool -K telekom tso off gso off gro off
68 67
69 ip link del "ifb4${pppInterface}" || true 68 ip link del "ifb4${pppInterface}" || true
70 ip link add name "ifb4${pppInterface}" type ifb 69 ip link add name "ifb4${pppInterface}" type ifb
71 ip link set "ifb4${pppInterface}" up 70 ip link set "ifb4${pppInterface}" up
72 71
73 tc qdisc del dev "ifb4${pppInterface}" root || true 72 tc qdisc del dev "ifb4${pppInterface}" root || true
74 tc qdisc del dev "${pppInterface}" ingress || true 73 tc qdisc del dev "${pppInterface}" ingress || true
75 tc qdisc del dev "${pppInterface}" root || true 74 tc qdisc del dev "${pppInterface}" root || true
76 75
77 tc qdisc add dev "${pppInterface}" handle ffff: ingress 76 tc qdisc add dev "${pppInterface}" handle ffff: ingress
78 tc filter add dev "${pppInterface}" parent ffff: basic action ctinfo dscp 0x0000003f 0x00000040 action mirred egress redirect dev "ifb4${pppInterface}" 77 tc filter add dev "${pppInterface}" parent ffff: basic action ctinfo dscp 0x0000003f 0x00000040 action mirred egress redirect dev "ifb4${pppInterface}"
79 tc qdisc replace dev "ifb4${pppInterface}" root cake memlimit 128Mb overhead 35 mpu 74 regional diffserv4 bandwidth 285mbit 78 tc qdisc replace dev "ifb4${pppInterface}" root cake memlimit 128Mb overhead 35 mpu 74 regional diffserv4 bandwidth ${toString (builtins.floor (177968 * 0.95))}kbit
80 tc qdisc replace dev "${pppInterface}" root cake memlimit 128Mb overhead 35 mpu 74 regional nat diffserv4 wash bandwidth 143mbit 79 tc qdisc replace dev "${pppInterface}" root cake memlimit 128Mb overhead 35 mpu 74 regional nat diffserv4 wash bandwidth ${toString (builtins.floor (41216 * 0.95))}kbit
81 ''; 80 '';
82 }; 81 "ppp/ip-up".source = pkgs.resholve.writeScript "ip-up" {
83 in "${app}/bin/${app.meta.mainProgram}"; 82 interpreter = pkgs.runtimeShell;
84 "ppp/ip-up".source = let 83 inputs = [ pkgs.iproute2 ];
85 app = pkgs.writeShellApplication { 84 execer = [ "cannot:${lib.getExe' pkgs.iproute2 "ip"}" ];
86 name = "ip-up"; 85 } ''
87 runtimeInputs = with pkgs; [ iproute2 ]; 86 ip route add default via "$5" dev "${pppInterface}" metric 512
88 text = '' 87 '';
89 ip route add default via "$5" dev "${pppInterface}" metric 512 88 "ppp/ip-down".source = pkgs.resholve.writeScript "ip-down" {
90 ''; 89 interpreter = pkgs.runtimeShell;
91 }; 90 inputs = [ pkgs.iproute2 ];
92 in "${app}/bin/${app.meta.mainProgram}"; 91 execer = [ "cannot:${lib.getExe' pkgs.iproute2 "ip"}" ];
93 "ppp/ip-down".source = let 92 } ''
94 app = pkgs.writeShellApplication { 93 ip link del "ifb4${pppInterface}"
95 name = "ip-down"; 94 '';
96 runtimeInputs = with pkgs; [ iproute2 ];
97 text = ''
98 ip link del "ifb4${pppInterface}"
99 '';
100 };
101 in "${app}/bin/${app.meta.mainProgram}";
102 }; 95 };
103 96
104 systemd.network.networks.${pppInterface} = { 97 systemd.network.networks.${pppInterface} = {
diff --git a/hosts/vidhar/network/ruleset.nft b/hosts/vidhar/network/ruleset.nft
index 9f519302..dd750394 100644
--- a/hosts/vidhar/network/ruleset.nft
+++ b/hosts/vidhar/network/ruleset.nft
@@ -1,18 +1,19 @@
1define icmp_protos = { ipv6-icmp, icmp, igmp } 1define icmp_protos = { ipv6-icmp, icmp, igmp }
2define bifrost_surtr = 2a03:4000:52:ada:4::/128
2 3
3table arp filter { 4table arp filter {
4 limit lim_arp_local { 5 limit lim_arp_local {
5 rate over 50 mbytes/second burst 50 mbytes 6 rate over 50 mbytes/second burst 50 mbytes
6 } 7 }
7 limit lim_arp_gpon { 8 limit lim_arp_ppp {
8 rate over 7500 kbytes/second burst 7500 kbytes 9 rate over 7500 kbytes/second burst 7500 kbytes
9 } 10 }
10 11
11 counter arp-rx {} 12 counter arp-rx {}
12 counter arp-tx {} 13 counter arp-tx {}
13 14
14 counter arp-ratelimit-gpon-rx {} 15 counter arp-ratelimit-ppp-rx {}
15 counter arp-ratelimit-gpon-tx {} 16 counter arp-ratelimit-ppp-tx {}
16 17
17 counter arp-ratelimit-local-rx {} 18 counter arp-ratelimit-local-rx {}
18 counter arp-ratelimit-local-tx {} 19 counter arp-ratelimit-local-tx {}
@@ -21,8 +22,8 @@ table arp filter {
21 type filter hook input priority filter 22 type filter hook input priority filter
22 policy accept 23 policy accept
23 24
24 iifname != gpon limit name lim_arp_local counter name arp-ratelimit-local-rx drop 25 iifname != @pppInterface@ limit name lim_arp_local counter name arp-ratelimit-local-rx drop
25 iifname gpon limit name lim_arp_gpon counter name arp-ratelimit-gpon-rx drop 26 iifname @pppInterface@ limit name lim_arp_ppp counter name arp-ratelimit-ppp-rx drop
26 27
27 counter name arp-rx 28 counter name arp-rx
28 } 29 }
@@ -31,8 +32,8 @@ table arp filter {
31 type filter hook output priority filter 32 type filter hook output priority filter
32 policy accept 33 policy accept
33 34
34 oifname != gpon limit name lim_arp_local counter name arp-ratelimit-local-tx drop 35 oifname != @pppInterface@ limit name lim_arp_local counter name arp-ratelimit-local-tx drop
35 oifname gpon limit name lim_arp_gpon counter name arp-ratelimit-gpon-tx drop 36 oifname @pppInterface@ limit name lim_arp_ppp counter name arp-ratelimit-ppp-tx drop
36 37
37 counter name arp-tx 38 counter name arp-tx
38 } 39 }
@@ -46,11 +47,11 @@ table inet filter {
46 limit lim_icmp_local { 47 limit lim_icmp_local {
47 rate over 50 mbytes/second burst 50 mbytes 48 rate over 50 mbytes/second burst 50 mbytes
48 } 49 }
49 limit lim_icmp_gpon { 50 limit lim_icmp_ppp {
50 rate over 7500 kbytes/second burst 7500 kbytes 51 rate over 7500 kbytes/second burst 7500 kbytes
51 } 52 }
52 53
53 counter icmp-ratelimit-gpon-fw {} 54 counter icmp-ratelimit-ppp-fw {}
54 counter icmp-ratelimit-local-fw {} 55 counter icmp-ratelimit-local-fw {}
55 56
56 counter icmp-fw {} 57 counter icmp-fw {}
@@ -58,7 +59,8 @@ table inet filter {
58 counter invalid-fw {} 59 counter invalid-fw {}
59 counter fw-lo {} 60 counter fw-lo {}
60 counter fw-lan {} 61 counter fw-lan {}
61 counter fw-gpon {} 62 counter fw-ppp {}
63 counter fw-kimai {}
62 64
63 counter fw-cups {} 65 counter fw-cups {}
64 66
@@ -73,7 +75,7 @@ table inet filter {
73 counter invalid-local4-rx {} 75 counter invalid-local4-rx {}
74 counter invalid-local6-rx {} 76 counter invalid-local6-rx {}
75 77
76 counter icmp-ratelimit-gpon-rx {} 78 counter icmp-ratelimit-ppp-rx {}
77 counter icmp-ratelimit-local-rx {} 79 counter icmp-ratelimit-local-rx {}
78 counter icmp-rx {} 80 counter icmp-rx {}
79 81
@@ -90,6 +92,11 @@ table inet filter {
90 counter http-rx {} 92 counter http-rx {}
91 counter tftp-rx {} 93 counter tftp-rx {}
92 counter pgbackrest-rx {} 94 counter pgbackrest-rx {}
95 counter immich-rx {}
96 counter paperless-rx {}
97 counter hledger-rx {}
98 counter audiobookshelf-rx {}
99 counter kimai-rx {}
93 100
94 counter established-rx {} 101 counter established-rx {}
95 102
@@ -101,7 +108,7 @@ table inet filter {
101 108
102 counter tx-lo {} 109 counter tx-lo {}
103 110
104 counter icmp-ratelimit-gpon-tx {} 111 counter icmp-ratelimit-ppp-tx {}
105 counter icmp-ratelimit-local-tx {} 112 counter icmp-ratelimit-local-tx {}
106 counter icmp-tx {} 113 counter icmp-tx {}
107 114
@@ -118,15 +125,20 @@ table inet filter {
118 counter http-tx {} 125 counter http-tx {}
119 counter tftp-tx {} 126 counter tftp-tx {}
120 counter pgbackrest-tx {} 127 counter pgbackrest-tx {}
128 counter immich-tx {}
129 counter paperless-tx {}
130 counter hledger-tx {}
131 counter audiobookshelf-tx {}
132 counter kimai-tx {}
121 133
122 counter tx {} 134 counter tx {}
123 135
124 136
125 chain forward_icmp_accept { 137 chain forward_icmp_accept {
126 oifname { gpon, bifrost } limit name lim_icmp_gpon counter name icmp-ratelimit-gpon-fw drop 138 oifname { @pppInterface@, bifrost } limit name lim_icmp_ppp counter name icmp-ratelimit-ppp-fw drop
127 iifname { gpon, bifrost } limit name lim_icmp_gpon counter name icmp-ratelimit-gpon-fw drop 139 iifname { @pppInterface@, bifrost } limit name lim_icmp_ppp counter name icmp-ratelimit-ppp-fw drop
128 oifname != { gpon, bifrost } limit name lim_icmp_local counter name icmp-ratelimit-local-fw drop 140 oifname != { @pppInterface@, bifrost } limit name lim_icmp_local counter name icmp-ratelimit-local-fw drop
129 iifname != { gpon, bifrost } limit name lim_icmp_local counter name icmp-ratelimit-local-fw drop 141 iifname != { @pppInterface@, bifrost } limit name lim_icmp_local counter name icmp-ratelimit-local-fw drop
130 counter name icmp-fw accept 142 counter name icmp-fw accept
131 } 143 }
132 chain forward { 144 chain forward {
@@ -139,10 +151,15 @@ table inet filter {
139 151
140 iifname lo counter name fw-lo accept 152 iifname lo counter name fw-lo accept
141 153
142 oifname { lan, gpon, bifrost } meta l4proto $icmp_protos jump forward_icmp_accept 154 oifname { lan, @pppInterface@, bifrost } meta l4proto $icmp_protos jump forward_icmp_accept
143 iifname lan oifname { gpon, bifrost } counter name fw-lan accept 155 iifname lan oifname { @pppInterface@, bifrost } counter name fw-lan accept
156 iifname ve-kimai oifname @pppInterface@ counter name fw-kimai accept
144 157
145 iifname gpon oifname lan ct state { established, related } counter name fw-gpon accept 158 iifname @pppInterface@ oifname lan ct state { established, related } counter name fw-ppp accept
159 iifname @pppInterface@ 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
146 163
147 164
148 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
@@ -163,22 +180,22 @@ table inet filter {
163 iif != lo ip daddr 127.0.0.1/8 counter name invalid-local4-rx reject 180 iif != lo ip daddr 127.0.0.1/8 counter name invalid-local4-rx reject
164 iif != lo ip6 daddr ::1/128 counter name invalid-local6-rx reject 181 iif != lo ip6 daddr ::1/128 counter name invalid-local6-rx reject
165 182
166 iifname { bifrost, gpon } meta l4proto $icmp_protos limit name lim_icmp_gpon counter name icmp-ratelimit-gpon-rx drop 183 iifname { bifrost, @pppInterface@ } meta l4proto $icmp_protos limit name lim_icmp_ppp counter name icmp-ratelimit-ppp-rx drop
167 iifname != { bifrost, gpon } meta l4proto $icmp_protos limit name lim_icmp_local counter name icmp-ratelimit-local-rx drop 184 iifname != { bifrost, @pppInterface@ } meta l4proto $icmp_protos limit name lim_icmp_local counter name icmp-ratelimit-local-rx drop
168 meta l4proto $icmp_protos counter name icmp-rx accept 185 meta l4proto $icmp_protos counter name icmp-rx accept
169 186
170 iifname { lan, mgmt, gpon, yggdrasil, bifrost } tcp dport 22 counter name ssh-rx accept 187 iifname { lan, mgmt, @pppInterface@, yggdrasil, bifrost } tcp dport 22 counter name ssh-rx accept
171 iifname { lan, mgmt, gpon, yggdrasil, bifrost } udp dport 60000-61000 counter name mosh-rx accept 188 iifname { lan, mgmt, @pppInterface@, yggdrasil, bifrost } udp dport 60000-61000 counter name mosh-rx accept
172 189
173 iifname { lan, mgmt, wifibh, yggdrasil } meta l4proto { tcp, udp } th dport 53 counter name dns-rx accept 190 iifname { lan, mgmt, wifibh, yggdrasil } meta l4proto { tcp, udp } th dport 53 counter name dns-rx accept
174 191
175 iifname { lan, yggdrasil } tcp dport 2049 counter name nfs-rx accept 192 iifname { lan, yggdrasil } tcp dport 2049 counter name nfs-rx accept
176 193
177 iifname { lan, mgmt, gpon } meta protocol ip udp dport 51820 counter name wg-rx accept 194 iifname { lan, mgmt, @pppInterface@ } meta protocol ip udp dport 51820 counter name wg-rx accept
178 iifname { lan, mgmt, gpon } meta protocol ip6 udp dport 51821 counter name wg-rx accept 195 iifname { lan, mgmt, @pppInterface@ } meta protocol ip6 udp dport 51821 counter name wg-rx accept
179 iifname "yggdrasil-wg-*" meta l4proto gre counter name yggdrasil-gre-rx accept 196 iifname "yggdrasil-wg-*" meta l4proto gre counter name yggdrasil-gre-rx accept
180 197
181 iifname gpon meta protocol ip6 udp dport 546 udp sport 547 counter name ipv6-pd-rx accept 198 iifname @pppInterface@ meta protocol ip6 udp dport 546 udp sport 547 counter name ipv6-pd-rx accept
182 199
183 iifname mgmt udp dport 123 counter name ntp-rx accept 200 iifname mgmt udp dport 123 counter name ntp-rx accept
184 201
@@ -193,6 +210,11 @@ table inet filter {
193 210
194 tcp dport 8432 counter name pgbackrest-rx accept 211 tcp dport 8432 counter name pgbackrest-rx accept
195 212
213 iifname bifrost tcp dport 2283 ip6 saddr $bifrost_surtr counter name immich-rx accept
214 iifname bifrost tcp dport 28981 ip6 saddr $bifrost_surtr counter name paperless-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
217
196 ct state { established, related } counter name established-rx accept 218 ct state { established, related } counter name established-rx accept
197 219
198 220
@@ -209,8 +231,8 @@ table inet filter {
209 231
210 oifname lo counter name tx-lo accept 232 oifname lo counter name tx-lo accept
211 233
212 oifname { bifrost, gpon } meta l4proto $icmp_protos limit name lim_icmp_gpon counter name icmp-ratelimit-gpon-tx drop 234 oifname { bifrost, @pppInterface@ } meta l4proto $icmp_protos limit name lim_icmp_ppp counter name icmp-ratelimit-ppp-tx drop
213 oifname != { bifrost, gpon } meta l4proto $icmp_protos limit name lim_icmp_local counter name icmp-ratelimit-local-tx drop 235 oifname != { bifrost, @pppInterface@ } meta l4proto $icmp_protos limit name lim_icmp_local counter name icmp-ratelimit-local-tx drop
214 meta l4proto $icmp_protos counter name icmp-tx accept 236 meta l4proto $icmp_protos counter name icmp-tx accept
215 237
216 238
@@ -240,34 +262,39 @@ table inet filter {
240 262
241 tcp sport 8432 counter name pgbackrest-tx accept 263 tcp sport 8432 counter name pgbackrest-tx accept
242 264
265 iifname bifrost tcp sport 2283 ip6 daddr $bifrost_surtr counter name immich-tx accept
266 iifname bifrost tcp sport 28981 ip6 daddr $bifrost_surtr counter name paperless-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
269
243 270
244 counter name tx 271 counter name tx
245 } 272 }
246} 273}
247 274
248table inet nat { 275table inet nat {
249 counter gpon-nat {} 276 counter ppp-nat {}
250 # counter container-nat {} 277 counter kimai-nat {}
251 278
252 chain postrouting { 279 chain postrouting {
253 type nat hook postrouting priority srcnat 280 type nat hook postrouting priority srcnat
254 policy accept 281 policy accept
255 282
256 283
257 meta nfproto ipv4 oifname gpon counter name gpon-nat masquerade 284 meta nfproto ipv4 oifname @pppInterface@ counter name ppp-nat masquerade
258 # iifname ve-* oifname gpon counter name container-nat masquerade 285 iifname ve-kimai oifname @pppInterface@ counter name kimai-nat masquerade
259 } 286 }
260} 287}
261 288
262table inet mss_clamp { 289table inet mss_clamp {
263 counter gpon-mss-clamp {} 290 counter ppp-mss-clamp {}
264 291
265 chain postrouting { 292 chain postrouting {
266 type filter hook postrouting priority mangle 293 type filter hook postrouting priority mangle
267 policy accept 294 policy accept
268 295
269 296
270 oifname gpon tcp flags & (syn|rst) == syn counter name gpon-mss-clamp tcp option maxseg size set rt mtu 297 oifname @pppInterface@ tcp flags & (syn|rst) == syn counter name ppp-mss-clamp tcp option maxseg size set rt mtu
271 } 298 }
272} 299}
273 300
@@ -402,7 +429,7 @@ table inet dscpclassify {
402 chain postrouting { 429 chain postrouting {
403 type filter hook postrouting priority filter + 1; policy accept 430 type filter hook postrouting priority filter + 1; policy accept
404 431
405 oifname != gpon return 432 oifname != @pppInterface@ return
406 433
407 ip dscp cs0 goto ct_set_cs0 434 ip dscp cs0 goto ct_set_cs0
408 ip dscp lephb goto ct_set_lephb 435 ip dscp lephb goto ct_set_lephb
diff --git a/hosts/vidhar/paperless/default.nix b/hosts/vidhar/paperless/default.nix
new file mode 100644
index 00000000..dd02da38
--- /dev/null
+++ b/hosts/vidhar/paperless/default.nix
@@ -0,0 +1,25 @@
1{ config, ... }:
2
3{
4 config = {
5 services.paperless = {
6 enable = true;
7 address = "2a03:4000:52:ada:4:1::";
8 passwordFile = config.sops.secrets."paperless-rootpw".path;
9 settings = {
10 PAPERLESS_OCR_LANGUAGE = "deu+eng";
11 PAPERLESS_URL = "https://paperless.yggdrasil.li";
12 PAPERLESS_FILENAME_FORMAT = "{{ created_year }}/{{ document_type }}/{{ correspondent }}/{{ created }}_{{ doc_pk }}_{{ title }}";
13 PAPERLESS_FILENAME_FORMAT_REMOVE_NONE = "true";
14 PAPERLESS_TASK_WORKERS = "3";
15 PAPERLESS_THREADS_PER_WORKER = "4";
16 };
17 database.createLocally = true;
18 };
19
20 sops.secrets."paperless-rootpw" = {
21 format = "binary";
22 sopsFile = ./rootpw;
23 };
24 };
25}
diff --git a/hosts/vidhar/paperless/rootpw b/hosts/vidhar/paperless/rootpw
new file mode 100644
index 00000000..11f48fcb
--- /dev/null
+++ b/hosts/vidhar/paperless/rootpw
@@ -0,0 +1,24 @@
1{
2 "data": "ENC[AES256_GCM,data:Bsns3bLs7aA++eTf2Vh4g2iAXhmrMRTF,iv:zQ6hgXEvgHAloN6UMW54f2nYCvEhHPXQSBVSihHFiC0=,tag:uiGTEs07dpx12PcAjmbr9Q==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBlVUJjdEdIZGd6UDJBRXlL\nODFyWDhHOU9oTEVCVlFiUXVXNm9XZmVuampVCkJ0YkFXTlZXVnRldmtlVkJaR3R2\nMFhpaHB5M3pLeDFkUkkzMUFydGNnOFEKLS0tIEJtNWc0V2JaaWYvQlp6TGxVdVZO\neVpzQzB5Um82TUZOeHBHeE50MGlqNWsKj1P54Fc+c5n35+Og9DwBWkvW947hgFsp\ni/G2QcaLHHJMTexTCZYsr1naSVa/cMBAbrZmtjz0HV4Q1kCJtvlrIg==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age1qffdqvy9arld9zd5a5cylt0n98xhcns5shxhrhwjq5g4qa844ejselaa4l",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA1UG1QSWtXcFZoQVRBOC9D\nT2VnTW9pcTRCMForcHdZVld0c1NmNFZpWUNBCkRkMERKUVliYXRqb25saWxyb2JN\nbC9YL2ZQbytRM0ZjNmlQOTlTZTQrV2sKLS0tIFZyUWtRcXNqZUZxMGN5d0tHUng2\nVXNSdFEwMmtIVEdVRVlWeVU1YmJVSkUKRJa42k551QtiC6S0tmMv7eVN7GRqpXWz\nvzNh+BM9TOJNaTMmVesr4vXNDLOSFS3PxYv95xuOBzVg3zOHuai72g==\n-----END AGE ENCRYPTED FILE-----\n"
16 }
17 ],
18 "lastmodified": "2025-02-13T19:20:33Z",
19 "mac": "ENC[AES256_GCM,data:mG6AC3L8MMeZ0Ajr7zV1mzPcHviQw2adtGjSbrbPRw1xqN7siu6svoybv8xkahP2Grq/xKAiyfXFOFo7Uyc3ub5fSovAEolNazqybZYsyam5vHpeC23dXcEkZUJSPJ9/CSB5uI9nX3NPC64QUjCxHZ7qfH5gcXT9D12H8LSqKlQ=,iv:4Skdj8l9jlTX9Unc2xE2hCKVawHBnHR8L4kZA6H8xNw=,tag:zJsJ3S//faAn7AGwLefNoA==,type:str]",
20 "pgp": null,
21 "unencrypted_suffix": "_unencrypted",
22 "version": "3.9.4"
23 }
24} \ No newline at end of file
diff --git a/hosts/vidhar/pgbackrest/default.nix b/hosts/vidhar/pgbackrest/default.nix
index ffb149f5..1e0828ce 100644
--- a/hosts/vidhar/pgbackrest/default.nix
+++ b/hosts/vidhar/pgbackrest/default.nix
@@ -130,8 +130,9 @@ in {
130 }; 130 };
131 131
132 systemd.tmpfiles.rules = [ 132 systemd.tmpfiles.rules = [
133 "d /var/lib/pgbackrest 0750 pgbackrest pgbackrest - -" 133 "d /var/lib/pgbackrest 0770 pgbackrest pgbackrest - -"
134 "d /var/spool/pgbackrest 0750 pgbackrest pgbackrest - -" 134 "d /var/spool/pgbackrest 0770 pgbackrest pgbackrest - -"
135 "d /tmp/pgbackrest 0770 pgbackrest pgbackrest - -"
135 ]; 136 ];
136 137
137 users = { 138 users = {
@@ -141,7 +142,9 @@ in {
141 isSystemUser = true; 142 isSystemUser = true;
142 home = "/var/lib/pgbackrest"; 143 home = "/var/lib/pgbackrest";
143 }; 144 };
144 groups.pgbackrest = {}; 145 groups.pgbackrest = {
146 members = [ "postgres" ];
147 };
145 }; 148 };
146 149
147 systemd.services."pgbackrest-tls-server".serviceConfig = { 150 systemd.services."pgbackrest-tls-server".serviceConfig = {
diff --git a/hosts/vidhar/postgresql.nix b/hosts/vidhar/postgresql.nix
new file mode 100644
index 00000000..7e44e69f
--- /dev/null
+++ b/hosts/vidhar/postgresql.nix
@@ -0,0 +1,36 @@
1{ pkgs, config, flake, flakeInputs, ... }:
2
3let
4 nixpkgs-pgbackrest = import (flakeInputs.nixpkgs-pgbackrest.outPath + "/pkgs/top-level") {
5 overlays = [ flake.overlays.libdscp ];
6 localSystem = config.nixpkgs.system;
7 };
8in {
9 config = {
10 services.postgresql = {
11 enable = true;
12 package = pkgs.postgresql_15;
13 };
14
15 services.pgbackrest = {
16 settings."vidhar" = {
17 pg1-path = config.services.postgresql.dataDir;
18
19 repo1-path = "/var/lib/pgbackrest";
20 repo1-retention-full-type = "time";
21 repo1-retention-full = 14;
22 repo1-retention-archive = 7;
23 };
24
25 backups."vidhar-daily" = {
26 stanza = "vidhar";
27 repo = "1";
28 timerConfig.OnCalendar = "daily";
29 };
30 };
31
32 systemd.services.postgresql.serviceConfig = {
33 ReadWritePaths = [ "/var/spool/pgbackrest" "/var/lib/pgbackrest/archive/vidhar" ];
34 };
35 };
36}
diff --git a/hosts/vidhar/prometheus/default.nix b/hosts/vidhar/prometheus/default.nix
index d368ad52..df135b58 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 = {
@@ -144,6 +145,17 @@ in {
144 ]; 145 ];
145 scrape_interval = "15s"; 146 scrape_interval = "15s";
146 } 147 }
148 { job_name = "zte";
149 static_configs = [
150 { targets = ["localhost:9900"]; }
151 ];
152 relabel_configs = [
153 { replacement = "dsl01";
154 target_label = "instance";
155 }
156 ];
157 scrape_interval = "15s";
158 }
147 { job_name = "unbound"; 159 { job_name = "unbound";
148 static_configs = [ 160 static_configs = [
149 { targets = ["localhost:${toString config.services.prometheus.exporters.unbound.port}"]; } 161 { targets = ["localhost:${toString config.services.prometheus.exporters.unbound.port}"]; }
@@ -287,6 +299,22 @@ in {
287 } 299 }
288 ]; 300 ];
289 } 301 }
302 { name = "dsl-disconnects";
303 rules = [
304 { record = "dsl_uptime_seconds:resets_per_hour";
305 expr = "resets(dsl_uptime_seconds[1h])";
306 }
307 { record = "dsl_uptime_seconds:resets_per_day";
308 expr = "resets(dsl_uptime_seconds[1d])";
309 }
310 { record = "dsl_uptime_seconds:resets_per_week";
311 expr = "resets(dsl_uptime_seconds[1w])";
312 }
313 { record = "dsl_uptime_seconds:avg_resets_per_day";
314 expr = "avg_over_time(dsl_uptime_seconds:resets_per_day[1w])";
315 }
316 ];
317 }
290 ]; 318 ];
291 }) 319 })
292 ]; 320 ];
@@ -424,6 +452,47 @@ in {
424 }; 452 };
425 }; 453 };
426 454
455 systemd.services."prometheus-zte-exporter@dsl01.mgmt.yggdrasil" = {
456 wantedBy = [ "multi-user.target" ];
457 after = [ "network.target" ];
458 serviceConfig = {
459 Restart = "always";
460 PrivateTmp = true;
461 WorkingDirectory = "/tmp";
462 DynamicUser = true;
463 CapabilityBoundingSet = [""];
464 DeviceAllow = [""];
465 LockPersonality = true;
466 MemoryDenyWriteExecute = true;
467 NoNewPrivileges = true;
468 PrivateDevices = true;
469 ProtectClock = true;
470 ProtectControlGroups = true;
471 ProtectHome = true;
472 ProtectHostname = true;
473 ProtectKernelLogs = true;
474 ProtectKernelModules = true;
475 ProtectKernelTunables = true;
476 ProtectSystem = "strict";
477 RemoveIPC = true;
478 RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
479 RestrictNamespaces = true;
480 RestrictRealtime = true;
481 RestrictSUIDSGID = true;
482 SystemCallArchitectures = "native";
483 UMask = "0077";
484
485 Type = "simple";
486 ExecStart = "${pkgs.zte-prometheus-exporter}/bin/zte-prometheus-exporter";
487 Environment = "ZTE_BASEURL=http://10.141.1.3 ZTE_HOSTNAME=localhost ZTE_PORT=9900";
488 EnvironmentFile = config.sops.secrets."zte_dsl01.mgmt.yggdrasil".path;
489 };
490 };
491 sops.secrets."zte_dsl01.mgmt.yggdrasil" = {
492 format = "binary";
493 sopsFile = ./zte_dsl01.mgmt.yggdrasil;
494 };
495
427 services.nginx = { 496 services.nginx = {
428 upstreams.prometheus = { 497 upstreams.prometheus = {
429 servers = { "localhost:${toString config.services.prometheus.port}" = {}; }; 498 servers = { "localhost:${toString config.services.prometheus.port}" = {}; };
diff --git a/hosts/vidhar/prometheus/zte_dsl01.mgmt.yggdrasil b/hosts/vidhar/prometheus/zte_dsl01.mgmt.yggdrasil
new file mode 100644
index 00000000..1c9c1fe0
--- /dev/null
+++ b/hosts/vidhar/prometheus/zte_dsl01.mgmt.yggdrasil
@@ -0,0 +1,26 @@
1{
2 "data": "ENC[AES256_GCM,data:nAsn7dhfDr0+V1cJjpqWn/kJQt2zGjlfQKi3n5speroJkL3IvMG/9fsTaXJQZSi2gPlrN8GbxKQ=,iv:9g0V3xRBC+sa/JPP2bUZMfg//VuKT5qI7ua9iU4QRCg=,tag:fzwih9OHUBLmx8dxL4BjGg==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1qffdqvy9arld9zd5a5cylt0n98xhcns5shxhrhwjq5g4qa844ejselaa4l",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIaEE3bUFBY0xKSDUrVnc2\nbFpjSkNOSm56amJTNjdXcTljdDNRREhITm1NCjZrOUEwNFpxN2FmTVV5T2xCbENk\nMEFmVzlPZ29CTlJ4dVNCRUsyRFFseXcKLS0tIEhscVZ4VUVsaG9OUnBIRFE4WXA2\ncGFnbWpNMlNIQzFLc1Ryc1Z3NUl1bVUKi9zYBlF2vslGKu4GP368ApbvuxjZnQpF\nuOujXSNoEps21wY6xUENm+CbYbgaJjSgmb5c1IjAmnubVI4JVY9OyQ==\n-----END AGE ENCRYPTED FILE-----\n"
12 }
13 ],
14 "lastmodified": "2021-12-31T15:00:33Z",
15 "mac": "ENC[AES256_GCM,data:sw2NVXHLibbuOChgScLhSTjGZBjSoHpzIuRqfCW0eL3DwhL5CekG6T/oYu06KjNmxVjxwb3OmqECSU0TUvPn9ySOWwMSoBfyJpDoTHnZ+YOjOH351IOAMBNcBDJse7aLGRWW5YXKLDfmp8Dhg2hlMhCmkVwAquQjPhfmAdJfj64=,iv:wgM/BlRU2XJSGj7KvAo1WRamecffUDnFvv2+4twtsQY=,tag:0mXblJtTGMTvxndedws94A==,type:str]",
16 "pgp": [
17 {
18 "created_at": "2023-01-30T10:58:49Z",
19 "enc": "-----BEGIN PGP MESSAGE-----\n\nhF4DXxoViZlp6dISAQdAcwl1Blp3J5wgpRJKbYI1G1yEZrRYeYuoDtYUh3ToMAQw\nd92/bIJJR5Ml91eDym9uBN0fFRRy72r6FOx4qZT7S4DhmuA84qCbASjF8bKSclc0\n0l4BBXvDS5Dz1Q7iYc+LxZjHASV1v73A+MaeCFvG/pjmHzF0z0EzBiAJD4ZWGcP0\nX2dDbjl+n9VFrvmeLRxQNh4XZW43iTXdRjwHDgm16zhd9X6VOVhr5UkC4Nyjq2Ar\n=4ZEa\n-----END PGP MESSAGE-----\n",
20 "fp": "30D3453B8CD02FE2A3E7C78C0FB536FB87AE8F51"
21 }
22 ],
23 "unencrypted_suffix": "_unencrypted",
24 "version": "3.7.1"
25 }
26} \ No newline at end of file
diff --git a/hosts/vidhar/zfs.nix b/hosts/vidhar/zfs.nix
index 518c3287..9d667fd6 100644
--- a/hosts/vidhar/zfs.nix
+++ b/hosts/vidhar/zfs.nix
@@ -34,7 +34,7 @@ with lib;
34 }; 34 };
35 35
36 "/etc/zfs/zfs-list.cache" = 36 "/etc/zfs/zfs-list.cache" =
37 { device = "ssd-raid1/local/zfs-zfs--list.cache"; 37 { device = "ssd-raid1/local/etc-zfs-zfs--list.cache";
38 fsType = "zfs"; 38 fsType = "zfs";
39 neededForBoot = true; 39 neededForBoot = true;
40 }; 40 };
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 cd1ee064..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
@@ -57,6 +57,10 @@ with lib;
57 57
58 system.disableInstallerTools = false; 58 system.disableInstallerTools = false;
59 59
60 xdg.autostart.enable = lib.mkForce false;
61 xdg.icons.enable = lib.mkForce false;
62 xdg.mime.enable = lib.mkForce false;
63
60 systemd.sysusers.enable = false; 64 systemd.sysusers.enable = false;
61 system.machine-id.generate.enable = false; 65 system.machine-id.generate.enable = false;
62 system.stateVersion = config.system.nixos.release; # No state in installer 66 system.stateVersion = config.system.nixos.release; # No state in installer
diff --git a/lib/pythonSet.nix b/lib/pythonSet.nix
new file mode 100644
index 00000000..c69216cc
--- /dev/null
+++ b/lib/pythonSet.nix
@@ -0,0 +1,42 @@
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 halo = (prev.halo.override {
27 sourcePreference = "sdist";
28 }).overrideAttrs (oldAttrs: {
29 nativeBuildInputs = (oldAttrs.nativeBuildInputs or []) ++ [
30 (final.resolveBuildSystem { setuptools = []; })
31 ];
32 });
33 unshare = (prev.unshare.override {
34 sourcePreference = "sdist";
35 }).overrideAttrs (oldAttrs: {
36 nativeBuildInputs = (oldAttrs.nativeBuildInputs or []) ++ [
37 (final.resolveBuildSystem { setuptools = []; })
38 ];
39 });
40 })
41 ]
42 )
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/backup-utils.nix b/modules/backup-utils.nix
index 82a42ecd..698140da 100644
--- a/modules/backup-utils.nix
+++ b/modules/backup-utils.nix
@@ -9,5 +9,8 @@ with lib;
9 9
10 config = { 10 config = {
11 services.borgsnap.archive-prefix = mkDefault "yggdrasil.${hostName}."; 11 services.borgsnap.archive-prefix = mkDefault "yggdrasil.${hostName}.";
12
13 systemd.services."zfssnap-prune".restartIfChanged = false;
14 systemd.services."zfssnap".restartIfChanged = false;
12 }; 15 };
13} 16}
diff --git a/modules/borgcopy/.envrc b/modules/borgcopy/.envrc
new file mode 100644
index 00000000..01e755c1
--- /dev/null
+++ b/modules/borgcopy/.envrc
@@ -0,0 +1,4 @@
1use flake
2
3uv venv && uv sync
4. .venv/bin/activate
diff --git a/modules/borgcopy/.gitignore b/modules/borgcopy/.gitignore
new file mode 100644
index 00000000..4ccfae70
--- /dev/null
+++ b/modules/borgcopy/.gitignore
@@ -0,0 +1,2 @@
1.venv
2**/__pycache__
diff --git a/modules/borgcopy/default.nix b/modules/borgcopy/default.nix
index 475edbd9..af021777 100644
--- a/modules/borgcopy/default.nix
+++ b/modules/borgcopy/default.nix
@@ -1,27 +1,35 @@
1{ config, pkgs, lib, utils, flakeInputs, ... }: 1{ config, pkgs, lib, utils, flake, flakeInputs, ... }:
2 2
3with lib; 3with lib;
4 4
5let 5let
6 copyBorg = 6 copyBorg = let
7 with pkgs.poetry2nix; 7 workspace = flakeInputs.uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
8 mkPoetryApplication { 8 pythonSet = flake.lib.pythonSet {
9 projectDir = cleanPythonSources { src = ./.; }; 9 inherit pkgs;
10 python = pkgs.python312;
11 overlay = workspace.mkPyprojectOverlay {
12 sourcePreference = "wheel";
13 };
14 };
15 virtualEnv = pythonSet.mkVirtualEnv "copy_borg" workspace.deps.default;
16 in virtualEnv.overrideAttrs (oldAttrs: {
17 meta = (oldAttrs.meta or {}) // {
18 mainProgram = "copy_borg";
19 };
10 20
11 overrides = overrides.withDefaults (self: super: { 21 nativeBuildInputs = (oldAttrs.nativeBuildInputs or []) ++ [ pkgs.makeWrapper ];
12 pyprctl = super.pyprctl.overridePythonAttrs (oldAttrs: {
13 buildInputs = (oldAttrs.buildInputs or []) ++ [super.setuptools];
14 });
15 inherit (pkgs.python3Packages) python-unshare;
16 });
17 22
18 postInstall = '' 23 postInstall = ''
19 wrapProgram $out/bin/copy_borg \ 24 ${oldAttrs.postInstall or ""}
20 --prefix PATH : ${makeBinPath (with pkgs; [util-linux borgbackup])}:${config.security.wrapperDir} 25
21 ''; 26 wrapProgram $out/bin/copy_borg \
22 }; 27 --prefix PATH : ${makeBinPath (with pkgs; [util-linux borgbackup])}:${config.security.wrapperDir}
28 '';
29 });
23 30
24 copyService = name: opts: nameValuePair "copy-borg@${utils.escapeSystemdPath name}" { 31 copyService = name: opts: nameValuePair "copy-borg@${utils.escapeSystemdPath name}" {
32 restartIfChanged = false;
25 serviceConfig = { 33 serviceConfig = {
26 Type = "oneshot"; 34 Type = "oneshot";
27 ExecStart = "${copyBorg}/bin/copy_borg --verbosity ${toString opts.verbosity} ${utils.escapeSystemdExecArgs [opts.from opts.to]}"; 35 ExecStart = "${copyBorg}/bin/copy_borg --verbosity ${toString opts.verbosity} ${utils.escapeSystemdExecArgs [opts.from opts.to]}";
diff --git a/modules/borgcopy/poetry.lock b/modules/borgcopy/poetry.lock
deleted file mode 100644
index 759ecfe9..00000000
--- a/modules/borgcopy/poetry.lock
+++ /dev/null
@@ -1,180 +0,0 @@
1# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
2
3[[package]]
4name = "colorama"
5version = "0.4.6"
6description = "Cross-platform colored terminal text."
7category = "main"
8optional = false
9python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
10files = [
11 {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
12 {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
13]
14
15[[package]]
16name = "halo"
17version = "0.0.31"
18description = "Beautiful terminal spinners in Python"
19category = "main"
20optional = false
21python-versions = ">=3.4"
22files = [
23 {file = "halo-0.0.31-py2-none-any.whl", hash = "sha256:5350488fb7d2aa7c31a1344120cee67a872901ce8858f60da7946cef96c208ab"},
24 {file = "halo-0.0.31.tar.gz", hash = "sha256:7b67a3521ee91d53b7152d4ee3452811e1d2a6321975137762eb3d70063cc9d6"},
25]
26
27[package.dependencies]
28colorama = ">=0.3.9"
29log-symbols = ">=0.0.14"
30six = ">=1.12.0"
31spinners = ">=0.0.24"
32termcolor = ">=1.1.0"
33
34[package.extras]
35ipython = ["IPython (==5.7.0)", "ipywidgets (==7.1.0)"]
36
37[[package]]
38name = "humanize"
39version = "4.6.0"
40description = "Python humanize utilities"
41category = "main"
42optional = false
43python-versions = ">=3.7"
44files = [
45 {file = "humanize-4.6.0-py3-none-any.whl", hash = "sha256:401201aca462749773f02920139f302450cb548b70489b9b4b92be39fe3c3c50"},
46 {file = "humanize-4.6.0.tar.gz", hash = "sha256:5f1f22bc65911eb1a6ffe7659bd6598e33dcfeeb904eb16ee1e705a09bf75916"},
47]
48
49[package.extras]
50tests = ["freezegun", "pytest", "pytest-cov"]
51
52[[package]]
53name = "log-symbols"
54version = "0.0.14"
55description = "Colored symbols for various log levels for Python"
56category = "main"
57optional = false
58python-versions = "*"
59files = [
60 {file = "log_symbols-0.0.14-py3-none-any.whl", hash = "sha256:4952106ff8b605ab7d5081dd2c7e6ca7374584eff7086f499c06edd1ce56dcca"},
61 {file = "log_symbols-0.0.14.tar.gz", hash = "sha256:cf0bbc6fe1a8e53f0d174a716bc625c4f87043cc21eb55dd8a740cfe22680556"},
62]
63
64[package.dependencies]
65colorama = ">=0.3.9"
66
67[[package]]
68name = "pyprctl"
69version = "0.1.3"
70description = "An interface to Linux's prctl() syscall written in pure Python using ctypes."
71category = "main"
72optional = false
73python-versions = ">=3.6"
74files = [
75 {file = "pyprctl-0.1.3-py3-none-any.whl", hash = "sha256:6302e5114f078fb33e5799835d0a69e2fc180bb6b28ad073515fa40c5272f1dd"},
76 {file = "pyprctl-0.1.3.tar.gz", hash = "sha256:1fb54d3ab030ec02e4afc38fb9662d6634c12834e91ae7959de56a9c09f69c26"},
77]
78
79[[package]]
80name = "python-dateutil"
81version = "2.8.2"
82description = "Extensions to the standard Python datetime module"
83category = "main"
84optional = false
85python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
86files = [
87 {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
88 {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
89]
90
91[package.dependencies]
92six = ">=1.5"
93
94[[package]]
95name = "python-unshare"
96version = "0.2"
97description = "Python bindings for the Linux unshare() syscall"
98category = "main"
99optional = false
100python-versions = "*"
101files = [
102 {file = "python-unshare-0.2.tar.gz", hash = "sha256:f79b7de441b6c27930b775085a6a4fd2f378b628737aaaebc2a6c519023fd47a"},
103]
104
105[[package]]
106name = "six"
107version = "1.16.0"
108description = "Python 2 and 3 compatibility utilities"
109category = "main"
110optional = false
111python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
112files = [
113 {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
114 {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
115]
116
117[[package]]
118name = "spinners"
119version = "0.0.24"
120description = "Spinners for terminals"
121category = "main"
122optional = false
123python-versions = "*"
124files = [
125 {file = "spinners-0.0.24-py3-none-any.whl", hash = "sha256:2fa30d0b72c9650ad12bbe031c9943b8d441e41b4f5602b0ec977a19f3290e98"},
126 {file = "spinners-0.0.24.tar.gz", hash = "sha256:1eb6aeb4781d72ab42ed8a01dcf20f3002bf50740d7154d12fb8c9769bf9e27f"},
127]
128
129[[package]]
130name = "termcolor"
131version = "2.2.0"
132description = "ANSI color formatting for output in terminal"
133category = "main"
134optional = false
135python-versions = ">=3.7"
136files = [
137 {file = "termcolor-2.2.0-py3-none-any.whl", hash = "sha256:91ddd848e7251200eac969846cbae2dacd7d71c2871e92733289e7e3666f48e7"},
138 {file = "termcolor-2.2.0.tar.gz", hash = "sha256:dfc8ac3f350788f23b2947b3e6cfa5a53b630b612e6cd8965a015a776020b99a"},
139]
140
141[package.extras]
142tests = ["pytest", "pytest-cov"]
143
144[[package]]
145name = "tqdm"
146version = "4.65.0"
147description = "Fast, Extensible Progress Meter"
148category = "main"
149optional = false
150python-versions = ">=3.7"
151files = [
152 {file = "tqdm-4.65.0-py3-none-any.whl", hash = "sha256:c4f53a17fe37e132815abceec022631be8ffe1b9381c2e6e30aa70edc99e9671"},
153 {file = "tqdm-4.65.0.tar.gz", hash = "sha256:1871fb68a86b8fb3b59ca4cdd3dcccbc7e6d613eeed31f4c332531977b89beb5"},
154]
155
156[package.dependencies]
157colorama = {version = "*", markers = "platform_system == \"Windows\""}
158
159[package.extras]
160dev = ["py-make (>=0.1.0)", "twine", "wheel"]
161notebook = ["ipywidgets (>=6)"]
162slack = ["slack-sdk"]
163telegram = ["requests"]
164
165[[package]]
166name = "xdg"
167version = "6.0.0"
168description = "Variables defined by the XDG Base Directory Specification"
169category = "main"
170optional = false
171python-versions = ">=3.7,<4.0"
172files = [
173 {file = "xdg-6.0.0-py3-none-any.whl", hash = "sha256:df3510755b4395157fc04fc3b02467c777f3b3ca383257397f09ab0d4c16f936"},
174 {file = "xdg-6.0.0.tar.gz", hash = "sha256:24278094f2d45e846d1eb28a2ebb92d7b67fc0cab5249ee3ce88c95f649a1c92"},
175]
176
177[metadata]
178lock-version = "2.0"
179python-versions = ">=3.10.0,<3.12"
180content-hash = "3c6b538852447a8f3ae34e1be122716d47e669a2b44f7c5d3d850e5d877353c7"
diff --git a/modules/borgcopy/pyproject.toml b/modules/borgcopy/pyproject.toml
index f3401ed2..d76d73c6 100644
--- a/modules/borgcopy/pyproject.toml
+++ b/modules/borgcopy/pyproject.toml
@@ -1,22 +1,25 @@
1[tool.poetry] 1[project]
2name = "copy_borg" 2name = "copy_borg"
3version = "0.0.0" 3version = "0.0.0"
4authors = ["Gregor Kleen <gkleen@yggdrasil.li>"]
5description = "" 4description = ""
5authors = [{ name = "Gregor Kleen", email = "gkleen@yggdrasil.li" }]
6requires-python = "~=3.12"
7dependencies = [
8 "humanize>=4.6.0,<5",
9 "tqdm>=4.65.0,<5",
10 "python-dateutil>=2.8.2,<3",
11 "xdg>=6.0.0,<7",
12 "pyprctl>=0.1.3,<0.2",
13 "halo>=0.0.31,<0.0.32",
14 "unshare>=0.22",
15]
6 16
7[tool.poetry.scripts] 17[project.scripts]
8copy_borg = "copy_borg.__main__:main" 18copy_borg = "copy_borg.__main__:main"
9 19
10[tool.poetry.dependencies]
11python = ">=3.10.0,<3.12"
12humanize = "^4.6.0"
13tqdm = "^4.65.0"
14python-dateutil = "^2.8.2"
15xdg = "^6.0.0"
16python-unshare = "^0.2"
17pyprctl = "^0.1.3"
18halo = "^0.0.31"
19
20[build-system] 20[build-system]
21requires = ["poetry-core>=1.0.0"] 21requires = ["hatchling"]
22build-backend = "poetry.core.masonry.api" \ No newline at end of file 22build-backend = "hatchling.build"
23
24[tool.hatch.build.targets.wheel]
25packages = ["copy_borg"]
diff --git a/modules/borgcopy/uv.lock b/modules/borgcopy/uv.lock
new file mode 100644
index 00000000..1a282598
--- /dev/null
+++ b/modules/borgcopy/uv.lock
@@ -0,0 +1,146 @@
1version = 1
2revision = 2
3requires-python = ">=3.12, <4"
4
5[[package]]
6name = "colorama"
7version = "0.4.6"
8source = { registry = "https://pypi.org/simple" }
9sdist = { 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" }
10wheels = [
11 { 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" },
12]
13
14[[package]]
15name = "copy-borg"
16version = "0.0.0"
17source = { editable = "." }
18dependencies = [
19 { name = "halo" },
20 { name = "humanize" },
21 { name = "pyprctl" },
22 { name = "python-dateutil" },
23 { name = "tqdm" },
24 { name = "unshare" },
25 { name = "xdg" },
26]
27
28[package.metadata]
29requires-dist = [
30 { name = "halo", specifier = ">=0.0.31,<0.0.32" },
31 { name = "humanize", specifier = ">=4.6.0,<5" },
32 { name = "pyprctl", specifier = ">=0.1.3,<0.2" },
33 { name = "python-dateutil", specifier = ">=2.8.2,<3" },
34 { name = "tqdm", specifier = ">=4.65.0,<5" },
35 { name = "unshare", specifier = ">=0.22" },
36 { name = "xdg", specifier = ">=6.0.0,<7" },
37]
38
39[[package]]
40name = "halo"
41version = "0.0.31"
42source = { registry = "https://pypi.org/simple" }
43dependencies = [
44 { name = "colorama" },
45 { name = "log-symbols" },
46 { name = "six" },
47 { name = "spinners" },
48 { name = "termcolor" },
49]
50sdist = { url = "https://files.pythonhosted.org/packages/ee/48/d53580d30b1fabf25d0d1fcc3f5b26d08d2ac75a1890ff6d262f9f027436/halo-0.0.31.tar.gz", hash = "sha256:7b67a3521ee91d53b7152d4ee3452811e1d2a6321975137762eb3d70063cc9d6", size = 11666, upload-time = "2020-11-10T02:36:48.335Z" }
51
52[[package]]
53name = "humanize"
54version = "4.12.3"
55source = { registry = "https://pypi.org/simple" }
56sdist = { url = "https://files.pythonhosted.org/packages/22/d1/bbc4d251187a43f69844f7fd8941426549bbe4723e8ff0a7441796b0789f/humanize-4.12.3.tar.gz", hash = "sha256:8430be3a615106fdfceb0b2c1b41c4c98c6b0fc5cc59663a5539b111dd325fb0", size = 80514, upload-time = "2025-04-30T11:51:07.98Z" }
57wheels = [
58 { url = "https://files.pythonhosted.org/packages/a0/1e/62a2ec3104394a2975a2629eec89276ede9dbe717092f6966fcf963e1bf0/humanize-4.12.3-py3-none-any.whl", hash = "sha256:2cbf6370af06568fa6d2da77c86edb7886f3160ecd19ee1ffef07979efc597f6", size = 128487, upload-time = "2025-04-30T11:51:06.468Z" },
59]
60
61[[package]]
62name = "log-symbols"
63version = "0.0.14"
64source = { registry = "https://pypi.org/simple" }
65dependencies = [
66 { name = "colorama" },
67]
68sdist = { url = "https://files.pythonhosted.org/packages/45/87/e86645d758a4401c8c81914b6a88470634d1785c9ad09823fa4a1bd89250/log_symbols-0.0.14.tar.gz", hash = "sha256:cf0bbc6fe1a8e53f0d174a716bc625c4f87043cc21eb55dd8a740cfe22680556", size = 3211, upload-time = "2019-08-08T06:32:22.538Z" }
69wheels = [
70 { url = "https://files.pythonhosted.org/packages/28/5d/d710c38be68b0fb54e645048fe359c3904cc3cb64b2de9d40e1712bf110c/log_symbols-0.0.14-py3-none-any.whl", hash = "sha256:4952106ff8b605ab7d5081dd2c7e6ca7374584eff7086f499c06edd1ce56dcca", size = 3081, upload-time = "2019-08-08T06:32:20.604Z" },
71]
72
73[[package]]
74name = "pyprctl"
75version = "0.1.3"
76source = { registry = "https://pypi.org/simple" }
77sdist = { url = "https://files.pythonhosted.org/packages/c9/16/6ed71ebcad76c1cd5f22185bcc6b31c0ee62fc5e693b626febea8fedeba3/pyprctl-0.1.3.tar.gz", hash = "sha256:1fb54d3ab030ec02e4afc38fb9662d6634c12834e91ae7959de56a9c09f69c26", size = 18739, upload-time = "2021-10-26T23:52:03.87Z" }
78wheels = [
79 { url = "https://files.pythonhosted.org/packages/bf/5e/62765de39bbce8111fb1f4453a4a804913bf49179fa265fb713ed66c9d15/pyprctl-0.1.3-py3-none-any.whl", hash = "sha256:6302e5114f078fb33e5799835d0a69e2fc180bb6b28ad073515fa40c5272f1dd", size = 20016, upload-time = "2021-10-26T23:52:02.986Z" },
80]
81
82[[package]]
83name = "python-dateutil"
84version = "2.9.0.post0"
85source = { registry = "https://pypi.org/simple" }
86dependencies = [
87 { name = "six" },
88]
89sdist = { 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" }
90wheels = [
91 { 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" },
92]
93
94[[package]]
95name = "six"
96version = "1.17.0"
97source = { registry = "https://pypi.org/simple" }
98sdist = { 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" }
99wheels = [
100 { 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" },
101]
102
103[[package]]
104name = "spinners"
105version = "0.0.24"
106source = { registry = "https://pypi.org/simple" }
107sdist = { url = "https://files.pythonhosted.org/packages/d3/91/bb331f0a43e04d950a710f402a0986a54147a35818df0e1658551c8d12e1/spinners-0.0.24.tar.gz", hash = "sha256:1eb6aeb4781d72ab42ed8a01dcf20f3002bf50740d7154d12fb8c9769bf9e27f", size = 5308, upload-time = "2020-02-19T21:42:32.326Z" }
108wheels = [
109 { url = "https://files.pythonhosted.org/packages/9f/8e/3310207a68118000ca27ac878b8386123628b335ecb3d4bec4743357f0d1/spinners-0.0.24-py3-none-any.whl", hash = "sha256:2fa30d0b72c9650ad12bbe031c9943b8d441e41b4f5602b0ec977a19f3290e98", size = 5499, upload-time = "2020-02-19T21:42:30.876Z" },
110]
111
112[[package]]
113name = "termcolor"
114version = "3.1.0"
115source = { registry = "https://pypi.org/simple" }
116sdist = { url = "https://files.pythonhosted.org/packages/ca/6c/3d75c196ac07ac8749600b60b03f4f6094d54e132c4d94ebac6ee0e0add0/termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970", size = 14324, upload-time = "2025-04-30T11:37:53.791Z" }
117wheels = [
118 { url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" },
119]
120
121[[package]]
122name = "tqdm"
123version = "4.67.1"
124source = { registry = "https://pypi.org/simple" }
125dependencies = [
126 { name = "colorama", marker = "sys_platform == 'win32'" },
127]
128sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
129wheels = [
130 { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
131]
132
133[[package]]
134name = "unshare"
135version = "0.22"
136source = { registry = "https://pypi.org/simple" }
137sdist = { url = "https://files.pythonhosted.org/packages/15/85/2ba218129c95b894efe87506489b525f859c40f6e21cb0521ff3cec754f4/unshare-0.22.tar.gz", hash = "sha256:d521d72cca6e876f22cbd5ff5eb51f1beef75e8f9c53b599b55fa05fba1dd3a6", size = 2041, upload-time = "2019-10-17T12:58:31.498Z" }
138
139[[package]]
140name = "xdg"
141version = "6.0.0"
142source = { registry = "https://pypi.org/simple" }
143sdist = { url = "https://files.pythonhosted.org/packages/2a/b9/0e6e6f19fb75cf5e1758f4f33c1256738f718966700cffc0fde2f966218b/xdg-6.0.0.tar.gz", hash = "sha256:24278094f2d45e846d1eb28a2ebb92d7b67fc0cab5249ee3ce88c95f649a1c92", size = 3453, upload-time = "2023-02-27T19:27:44.309Z" }
144wheels = [
145 { url = "https://files.pythonhosted.org/packages/dd/54/3516c1cf349060fc3578686d271eba242f10ec00b4530c2985af9faac49b/xdg-6.0.0-py3-none-any.whl", hash = "sha256:df3510755b4395157fc04fc3b02467c777f3b3ca383257397f09ab0d4c16f936", size = 3855, upload-time = "2023-02-27T19:27:42.151Z" },
146]
diff --git a/modules/envfs.nix b/modules/envfs.nix
deleted file mode 100644
index b5b453a5..00000000
--- a/modules/envfs.nix
+++ /dev/null
@@ -1,77 +0,0 @@
1{ pkgs, config, lib, ... }:
2
3let
4 cfg = config.services.envfs;
5 mounts = {
6 "/usr/bin" = {
7 device = "none";
8 fsType = "envfs";
9 options = [
10 "bind-mount=/bin"
11 "fallback-path=${pkgs.symlinkJoin {
12 name = "fallback-path";
13 inherit (cfg) paths;
14 }}"
15 "nofail"
16 ];
17 };
18 "/bin" = {
19 device = "/usr/bin";
20 fsType = "none";
21 options = [ "bind" "nofail" ];
22 };
23 };
24in {
25 disabledModules = [ "tasks/filesystems/envfs.nix" ];
26
27 options = {
28 services.envfs = {
29 enable = lib.mkEnableOption "Envfs filesystem" // {
30 default = true;
31 description = ''
32 Fuse filesystem that returns symlinks to executables based on the PATH
33 of the requesting process. This is useful to execute shebangs on NixOS
34 that assume hard coded locations in locations like /bin or /usr/bin
35 etc.
36 '';
37 };
38
39 package = lib.mkOption {
40 type = lib.types.package;
41 default = pkgs.envfs;
42 defaultText = lib.literalExpression "pkgs.envfs";
43 description = "Which package to use for the envfs.";
44 };
45
46 paths = lib.mkOption {
47 type = lib.types.listOf lib.types.package;
48 default = [
49 (pkgs.runCommand "fallback-path-environment" {} ''
50 mkdir -p $out
51 ln -s ${config.environment.usrbinenv} $out/env
52 ln -s ${config.environment.binsh} $out/sh
53 '')
54 ];
55 defaultText = lib.literalExpression ''
56 [ (pkgs.runCommand "fallback-path-environment" {} '''
57 mkdir -p $out
58 ln -s ''${config.environment.usrbinenv} $out/env
59 ln -s ''${config.environment.binsh} $out/sh
60 ''')
61 ]
62 '';
63 description = "Extra packages to join into collection of fallback executables in case not other executable is found";
64 };
65 };
66 };
67
68 config = lib.mkIf (cfg.enable) {
69 environment.systemPackages = [ cfg.package ];
70 # we also want these mounts in virtual machines.
71 fileSystems = if config.virtualisation ? qemu then lib.mkVMOverride mounts else mounts;
72
73 # We no longer need those when using envfs
74 system.activationScripts.usrbinenv = lib.mkForce "";
75 system.activationScripts.binsh = lib.mkForce "";
76 };
77}
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/impermanence-timezone.nix b/modules/impermanence-timezone.nix
new file mode 100644
index 00000000..a1edbfa5
--- /dev/null
+++ b/modules/impermanence-timezone.nix
@@ -0,0 +1,41 @@
1{ config, lib, utils, pkgs, ... }:
2
3{
4 options = {
5 environment.persistence = lib.mkOption {
6 type = lib.types.attrsOf (lib.types.submodule {
7 options = {
8 timezone = lib.mkEnableOption "storing system timezone";
9 };
10 });
11 };
12 };
13
14 config = {
15 systemd = lib.mkMerge (lib.mapAttrsToList (name: cfg: lib.mkIf cfg.timezone {
16 services = {
17 "timezone@${utils.escapeSystemdPath name}" = {
18 wantedBy = [ "multi-user.target" ];
19 serviceConfig = {
20 Type = "oneshot";
21 RemainAfterExit = true;
22 ExecStart = "${pkgs.coreutils}/bin/cp -vP ${utils.escapeSystemdExecArg "${name}/etc/localtime"} /etc/localtime";
23 ExecStop = "${pkgs.coreutils}/bin/cp -vP /etc/localtime ${utils.escapeSystemdExecArg "${name}/etc/localtime"}";
24 };
25 };
26 "etc-localtime@${utils.escapeSystemdPath name}" = {
27 serviceConfig = {
28 Type = "oneshot";
29 ExecStart = "${pkgs.coreutils}/bin/cp -vP /etc/localtime ${utils.escapeSystemdExecArg "${name}/etc/localtime"}";
30 };
31 };
32 };
33 paths."etc-localtime@${utils.escapeSystemdPath name}" = {
34 wantedBy = [ "timezone@${utils.escapeSystemdPath name}.service" ];
35 after = [ "timezone@${utils.escapeSystemdPath name}.service" ];
36
37 pathConfig.PathChanged = "/etc/localtime";
38 };
39 }) config.environment.persistence);
40 };
41}
diff --git a/modules/impermanence.nix b/modules/impermanence.nix
new file mode 100644
index 00000000..621576a3
--- /dev/null
+++ b/modules/impermanence.nix
@@ -0,0 +1,6 @@
1{ flakeInputs, ... }:
2{
3 imports = [
4 flakeInputs.impermanence.nixosModules.impermanence
5 ];
6}
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/niri.nix b/modules/niri.nix
new file mode 100644
index 00000000..4e2ddf8b
--- /dev/null
+++ b/modules/niri.nix
@@ -0,0 +1,6 @@
1{ flakeInputs, ... }:
2{
3 imports = [
4 flakeInputs.niri-flake.nixosModules.niri
5 ];
6}
diff --git a/modules/nix-access-tokens/default.nix b/modules/nix-access-tokens/default.nix
new file mode 100644
index 00000000..a3b7abfa
--- /dev/null
+++ b/modules/nix-access-tokens/default.nix
@@ -0,0 +1,24 @@
1{ lib, config, hostName ,... }:
2
3let
4 cfg = config.nix.includeAccessTokens;
5in {
6 options = {
7 nix.includeAccessTokens.enable = lib.mkEnableOption "including access tokens in nix.conf" // { default = lib.elem hostName ["sif" "surtr" "vidhar"]; };
8 };
9
10 config = lib.mkIf cfg.enable {
11 nix = {
12 extraOptions = ''
13 !include ${config.sops.secrets.nixAccessTokens.path}
14 '';
15 };
16
17 sops.secrets.nixAccessTokens = {
18 format = "binary";
19 sopsFile = ./nix.conf;
20 mode = "0440";
21 group = "wheel";
22 };
23 };
24}
diff --git a/modules/nix-access-tokens/nix.conf b/modules/nix-access-tokens/nix.conf
new file mode 100644
index 00000000..f0b394ef
--- /dev/null
+++ b/modules/nix-access-tokens/nix.conf
@@ -0,0 +1,32 @@
1{
2 "data": "ENC[AES256_GCM,data:/cdBpvCAFpgm0YWhy1WYlA09KlU6PzVfBYVLBD0boqGqvP+8wuyDzj5KWbcKsdGhoiklODiKR0ODXNU+fA35y862PFXvSb4xVyfbdKRndYdIA4W6vyobtoC9h7B1yR9pkq9L+1tqlU30Dgy2Gndg9rWHlIo+1lO/1A==,iv:B1Px2+cxCaopHZThkEG5saOib+PNvurPIS6aeAv2uPo=,tag:K3JqRaX3/iIqD3c//YdqSQ==,type:str]",
3 "sops": {
4 "kms": null,
5 "gcp_kms": null,
6 "azure_kv": null,
7 "hc_vault": null,
8 "age": [
9 {
10 "recipient": "age1rmmhetcmllq0ahl5qznlr0eya2zdxwl9h6y5wnl97d2wtyx5t99sm2u866",
11 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5NkZUUGI3M2pQYWVXeFV6\na2h2czRTeUJFekJCS012YlBkL1FDdTd3ekZ3ClJsTVh0R2JQM0Jua1JjL285RVA1\nRHhlbjlLdmNBUXVLelFGY2NGYWpLejQKLS0tIDBUWUhJNm8zWGoyQ0pBYnV1ZjBh\ndktNRkNPS1lpWXFITC81aEZJbXlONk0Km2c1xVKwSankaVs7O/utGJwRRX395upz\ndPbsOElTnbGmkb0esGtvGSPboTvK+gjn9w/GhaPyTnNDoos7GaIfyg==\n-----END AGE ENCRYPTED FILE-----\n"
12 },
13 {
14 "recipient": "age1fj65apkhfkrwyv5tx6zcs9nkjg8267fy733qph30sc7zfn7vapjqkd5kne",
15 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6bS9iY2lua3U4U3lJa1pK\nSlZNMmFZMEU5M1V2bWRjaXIwajZJVDJPMlM4Cmd3TTNFWjVuSGdtbC9iODltTS91\nOE5XOEVEQkh0SFpVVW5jc3IzbzNpTmMKLS0tIEtrSU54QUVPa2tBZDhLYlRFWitR\nc2x6MFlxL0tobDJTek42dEcyZXpoWDgKXzQfU+o6FkbJBwmm6oaHu4sDPi822uUR\n5VY6gY/h3g2kM4cuS03Q4NJmeRxuh7cx0UqGU3j5Mf8muE1LHpYEPw==\n-----END AGE ENCRYPTED FILE-----\n"
16 },
17 {
18 "recipient": "age19a7j77w267z04zls7m28a8hj4a0g5af6ltye2d5wypg33c3l89csd4r9zq",
19 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBaOVpNZ2lVT0VwbHVZNzFl\nenJsMGpnbkRvU0xOSU5obk5yT2p5ZVNzdXhNCnVlQzZtRjZNVmJLSUpKc3UwVXZs\nWi9EZ3kxZkJNeFJDSjl1L1IweTFNMXcKLS0tIDJUOTBwTldCUmlnU0tWVkZkNzJL\nejM4ajJVbVhvSm1YM2Vxa2JldllYN0UKAzxy2wkzRvCSiTy417AulpCu41z668HG\nto92eGF2ZRFfEG5LGlCKWeDcP3gM8QwKiVlm6wndbOkhMMfc4Sp3wA==\n-----END AGE ENCRYPTED FILE-----\n"
20 },
21 {
22 "recipient": "age1qffdqvy9arld9zd5a5cylt0n98xhcns5shxhrhwjq5g4qa844ejselaa4l",
23 "enc": "-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2ejRHcGttNUxYZnFzTU5J\nMTFvY3daQ1VMM2xxYTgvLzZwT1owazVNenhzCktaWFF6K2s5UjI2b20rSHFNSS9E\nMVlJSmZhQm15eUs3U0hGTGpSRndmSDgKLS0tIDVrcjl4eDhwak1pRithbnRWWEZy\nVE9EOEpKdEJoRTFrTXpQVDc1cmsrU1kK/goTdUmpZPeMRbY1QzLXAa6Qpg4YYYYo\n3v3GK1bzdey8szfgIr1dHTtQEzqE2WX1swzZizDXj/RiUWx01Ky3GA==\n-----END AGE ENCRYPTED FILE-----\n"
24 }
25 ],
26 "lastmodified": "2025-01-25T19:58:58Z",
27 "mac": "ENC[AES256_GCM,data:Oza4XgnTX3vly89nGluLbEytk1dUYAiOhIYewQyDLLLSSlUIpXmWhV+X0HUQ9AX5kUrEhNbVzRdvUG/9YwoWjTJfvd7tw41IYeTqgykMNXJUfGssoutXfeij9YR+t5aJaRhlTkIWcBhUjXSUNyJCl6Z3XmzWstTPZXEU9VmAvuE=,iv:LqVwIiit+WqI5NWSboexWsmPzg7e63nWJYsNFEK1Uog=,tag:ClR6oI62WXEfIYYAY6vL0A==,type:str]",
28 "pgp": null,
29 "unencrypted_suffix": "_unencrypted",
30 "version": "3.9.3"
31 }
32} \ No newline at end of file
diff --git a/modules/pgbackrest.nix b/modules/pgbackrest.nix
index 886840b9..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";
@@ -216,6 +218,7 @@ in {
216 }; 218 };
217 }; 219 };
218 } // mapAttrs' (name: backupCfg: nameValuePair "pgbackrest-backup@${escapeSystemdPath name}" { 220 } // mapAttrs' (name: backupCfg: nameValuePair "pgbackrest-backup@${escapeSystemdPath name}" {
221 restartIfChanged = false;
219 description = "Perform pgBackRest Backup (${name}${optionalString (!(isNull backupCfg.repo)) " repo${backupCfg.repo}"})"; 222 description = "Perform pgBackRest Backup (${name}${optionalString (!(isNull backupCfg.repo)) " repo${backupCfg.repo}"})";
220 serviceConfig = { 223 serviceConfig = {
221 Type = "oneshot"; 224 Type = "oneshot";
diff --git a/modules/postsrsd.nix b/modules/postsrsd.nix
new file mode 100644
index 00000000..bc941e3e
--- /dev/null
+++ b/modules/postsrsd.nix
@@ -0,0 +1,163 @@
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 configurePostfix = lib.mkOption {
106 type = lib.types.bool;
107 default = false;
108 description = "noop";
109 };
110 };
111 };
112
113 config = lib.mkIf cfg.enable {
114 users.users = lib.optionalAttrs (cfg.user == "postsrsd") {
115 postsrsd = {
116 group = cfg.group;
117 uid = config.ids.uids.postsrsd;
118 };
119 };
120
121 users.groups = lib.optionalAttrs (cfg.group == "postsrsd") {
122 postsrsd.gid = config.ids.gids.postsrsd;
123 };
124
125 systemd.services.postsrsd-generate-secrets = {
126 path = [ pkgs.coreutils ];
127 script = ''
128 if [ -e "${cfg.secretsFile}" ]; then
129 echo "Secrets file exists. Nothing to do!"
130 else
131 echo "WARNING: secrets file not found, autogenerating!"
132 DIR="$(dirname "${cfg.secretsFile}")"
133 install -m 750 -o ${cfg.user} -g ${cfg.group} -d "$DIR"
134 install -m 600 -o ${cfg.user} -g ${cfg.group} <(dd if=/dev/random bs=18 count=1 | base64) "${cfg.secretsFile}"
135 fi
136 '';
137 serviceConfig = {
138 Type = "oneshot";
139 };
140 };
141
142 systemd.services.postsrsd = {
143 description = "PostSRSd SRS rewriting server";
144 after = [
145 "network.target"
146 "postsrsd-generate-secrets.service"
147 ];
148 before = [ "postfix.service" ];
149 wantedBy = [ "multi-user.target" ];
150 requires = [ "postsrsd-generate-secrets.service" ];
151 confinement.enable = true;
152
153 serviceConfig = {
154 ExecStart = "${lib.getExe pkgs.postsrsd} -C ${configFile}";
155 User = cfg.user;
156 Group = cfg.group;
157 PermissionsStartOnly = true;
158 RuntimeDirectory = runtimeDirectoryName;
159 LoadCredential = "secrets-file:${cfg.secretsFile}";
160 };
161 };
162 };
163}
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 abca2acb..00000000
--- a/modules/uucp.nix
+++ /dev/null
@@ -1,398 +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 uucp = super.lib.overrideDerivation super.uucp (oldAttrs: {
318 configureFlags = "--with-newconfigdir=/etc/uucp";
319 patches = [
320 (super.writeText "mailprogram" ''
321 policy.h | 2 +-
322 1 file changed, 1 insertion(+), 1 deletion(-)
323
324 diff --git a/policy.h b/policy.h
325 index 5afe34b..8e92c8b 100644
326 --- a/policy.h
327 +++ b/policy.h
328 @@ -240,7 +240,7 @@
329 the sendmail choice below. Otherwise, select one of the other
330 choices as appropriate. */
331 #if 1
332 -#define MAIL_PROGRAM "/usr/lib/sendmail -t"
333 +#define MAIL_PROGRAM "${config.security.wrapperDir}/sendmail -t"
334 /* #define MAIL_PROGRAM "/usr/sbin/sendmail -t" */
335 #define MAIL_PROGRAM_TO_BODY 1
336 #define MAIL_PROGRAM_SUBJECT_BODY 1
337 '')
338 ];
339 });
340 rmail = super.writeScriptBin "rmail" ''
341 #!${super.stdenv.shell}
342
343 # Dummy UUCP rmail command for postfix/qmail systems
344
345 IFS=" " read junk from junk junk junk junk junk junk junk relay
346
347 case "$from" in
348 *[@!]*) ;;
349 *) from="$from@$relay";;
350 esac
351
352 exec ${config.security.wrapperDir}/sendmail -G -i -f "$from" -- "$@"
353 '';
354 })];
355
356 environment.systemPackages = with pkgs; [
357 uucp
358 ];
359
360 systemd.services."uucico@" = {
361 serviceConfig = {
362 User = "uucp";
363 Type = "oneshot";
364 ExecStart = "${config.security.wrapperDir}/uucico -D -S %i";
365 };
366 };
367
368 systemd.timers."uucico@" = {
369 timerConfig.OnActiveSec = cfg.interval;
370 timerConfig.OnUnitActiveSec = cfg.interval;
371 };
372
373 systemd.targets."multi-user" = {
374 wants = mapAttrsToList (name: node: "uucico@${name}.timer") cfg.remoteNodes;
375 };
376
377 systemd.kill-user.enable = true;
378 systemd.targets."sleep" = {
379 after = [ "kill-user@uucp.service" ];
380 wants = [ "kill-user@uucp.service" ];
381 };
382
383 networking.networkmanager.dispatcherScripts = optional cfg.nmDispatch {
384 type = "basic";
385 source = pkgs.writeScript "callRemotes.sh" ''
386 #!${pkgs.stdenv.shell}
387
388 shopt -s extglob
389
390 case "''${2}" in
391 (?(vpn-)up)
392 ${concatStringsSep "\n " (mapAttrsToList (name: node: "${pkgs.systemd}/bin/systemctl start uucico@${name}.service") cfg.remoteNodes)}
393 ;;
394 esac
395 '';
396 };
397 };
398}
diff --git a/nvfetcher.toml b/nvfetcher.toml
index c0566373..8e3ba905 100644
--- a/nvfetcher.toml
+++ b/nvfetcher.toml
@@ -78,12 +78,12 @@ git.fetchSubmodules = true
78src.git = "https://github.com/jgreco/mpv-youtube-quality" 78src.git = "https://github.com/jgreco/mpv-youtube-quality"
79fetch.git = "https://github.com/jgreco/mpv-youtube-quality" 79fetch.git = "https://github.com/jgreco/mpv-youtube-quality"
80 80
81[batman-adv] 81# [batman-adv]
82src.webpage = "https://www.open-mesh.org/projects/open-mesh/wiki/Download" 82# src.webpage = "https://www.open-mesh.org/projects/open-mesh/wiki/Download"
83src.regex = "The latest version of <a[^\\>]*>batman-adv</a> is <a[^\\>]*>batman-adv-([0-9\\.]+).tar.gz</a>" 83# src.regex = "The latest version of <a[^\\>]*>batman-adv</a> is <a[^\\>]*>batman-adv-([0-9\\.]+).tar.gz</a>"
84src.from_pattern = "^.*batman-adv-([0-9\\.]+).tar.gz.*$" 84# src.from_pattern = "^.*batman-adv-([0-9\\.]+).tar.gz.*$"
85src.to_pattern = "\\1" 85# src.to_pattern = "\\1"
86fetch.tarball = "https://downloads.open-mesh.org/batman/stable/sources/batman-adv/batman-adv-$ver.tar.gz" 86# fetch.tarball = "https://downloads.open-mesh.org/batman/stable/sources/batman-adv/batman-adv-$ver.tar.gz"
87 87
88[scutiger] 88[scutiger]
89src.github_tag = "bk2204/scutiger" 89src.github_tag = "bk2204/scutiger"
@@ -107,3 +107,23 @@ fetch.tarball = "https://github.com/JonathonReinhart/spice-record/archive/refs/t
107[yt-dlp] 107[yt-dlp]
108src.pypi = "yt_dlp" 108src.pypi = "yt_dlp"
109fetch.pypi = "yt_dlp" 109fetch.pypi = "yt_dlp"
110
111[mako]
112src.git = "https://github.com/emersion/mako"
113fetch.git = "https://github.com/emersion/mako"
114
115[swayosd]
116src.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"
126
127[quickshell]
128src.git = "https://git.outfoxxed.me/quickshell/quickshell.git"
129fetch.git = "https://git.outfoxxed.me/quickshell/quickshell.git"
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/batman-adv.nix b/overlays/batman-adv.nix
deleted file mode 100644
index cce7dc4f..00000000
--- a/overlays/batman-adv.nix
+++ /dev/null
@@ -1,15 +0,0 @@
1{ final, prev, sources, ... }: {
2 linuxPackages_latest = prev.linuxPackages_latest.extend (self: super: {
3 batman_adv = super.batman_adv.overrideAttrs (oldAttrs: {
4 version = "${sources.batman-adv.version}-${self.kernel.version}";
5 inherit (sources.batman-adv) src;
6 });
7 });
8
9 linuxPackages_6_2 = prev.linuxPackages_6_2.extend (self: super: {
10 batman_adv = super.batman_adv.overrideAttrs (oldAttrs: {
11 version = "${sources.batman-adv.version}-${self.kernel.version}";
12 inherit (sources.batman-adv) src;
13 });
14 });
15}
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/etesync-dav.nix b/overlays/etesync-dav.nix
index cec216e2..e0ced1e3 100644
--- a/overlays/etesync-dav.nix
+++ b/overlays/etesync-dav.nix
@@ -1,55 +1,58 @@
1{ final, prev, ... }: { 1{ final, prev, ... }: {
2 etesync-dav = prev.python3Packages.buildPythonApplication rec { 2 etesync-dav = final.python3Packages.buildPythonApplication rec {
3 pname = "etesync-dav"; 3 pname = "etesync-dav";
4 version = "0.33.4"; 4 version = "0.35.1";
5 pyproject = true;
5 6
6 src = prev.fetchFromGitHub { 7 src = prev.fetchFromGitHub {
7 owner = "etesync"; 8 owner = "etesync";
8 repo = "etesync-dav"; 9 repo = "etesync-dav";
9 rev = "v${version}"; 10 tag = "v${version}";
10 hash = "sha256-g+rK762tSWPDaBsaTwpTzfK/lqVs+Z/Qrpq2HCpipQE="; 11 hash = "sha256-y4BhU2kSn+RWqc5+pJQFhbwfat9cMWD0ED0EXJp25cY=";
11 }; 12 };
12 13
13 dependencies = with prev.python3Packages; [ 14 build-system = with final.python3Packages; [ setuptools ];
15
16 dependencies = with final.python3Packages; [
14 appdirs 17 appdirs
15 etebase 18 etebase
16 etesync 19 etesync
17 flask 20 flask
18 flask-wtf 21 flask-wtf
19 msgpack 22 msgpack
20 setuptools 23 requests
21 (toPythonModule (buildPythonApplication rec { 24 requests.optional-dependencies.socks
25 (buildPythonApplication rec {
22 pname = "radicale"; 26 pname = "radicale";
23 version = "3.2.3"; 27 version = "3.2.0";
24 pyproject = true; 28 pyproject = true;
25 29
26 src = prev.fetchFromGitHub { 30 src = prev.fetchFromGitHub {
27 owner = "Kozea"; 31 owner = "Kozea";
28 repo = "Radicale"; 32 repo = "Radicale";
29 rev = "refs/tags/v${version}"; 33 rev = "refs/tags/v${version}";
30 hash = "sha256-1IlnXVetQQuKBt6+QVKNeMM6qBQAiUhqc+4x3xOnSdE="; 34 hash = "sha256-RxC8VOfdTXJZiAroDHTKjJqGWu65Z5uyb4WK1LOqubQ=";
31 }; 35 };
32 36
37 postPatch = ''
38 sed -i '/addopts/d' setup.cfg
39 '';
40
33 build-system = [ 41 build-system = [
34 setuptools 42 setuptools
35 ]; 43 ];
36 44
37 dependencies = 45 dependencies = [
38 [ 46 defusedxml
39 defusedxml 47 passlib
40 passlib 48 vobject
41 vobject 49 pika
42 pika 50 python-dateutil
43 python-dateutil 51 pytz # https://github.com/Kozea/Radicale/issues/816
44 pytz # https://github.com/Kozea/Radicale/issues/816 52 ] ++ passlib.optional-dependencies.bcrypt;
45 ]
46 ++ passlib.optional-dependencies.bcrypt;
47 53
48 doCheck = false; 54 doCheck = false;
49 })) 55 })
50 requests
51 types-setuptools
52 requests.optional-dependencies.socks
53 ]; 56 ];
54 57
55 doCheck = false; 58 doCheck = false;
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/keepassxc/database-open-dialog.patch b/overlays/keepassxc/database-open-dialog.patch
new file mode 100644
index 00000000..dff84846
--- /dev/null
+++ b/overlays/keepassxc/database-open-dialog.patch
@@ -0,0 +1,129 @@
1diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp
2index 60412b5a..c0497d91 100644
3--- a/src/browser/BrowserService.cpp
4+++ b/src/browser/BrowserService.cpp
5@@ -249,7 +249,7 @@ QJsonObject BrowserService::createNewGroup(const QString& groupName)
6 return result;
7 }
8
9- auto dialogResult = MessageBox::warning(m_currentDatabaseWidget,
10+ auto dialogResult = MessageBox::warning(nullptr,
11 tr("KeePassXC - Create a new group"),
12 tr("A request for creating a new group \"%1\" has been received.\n"
13 "Do you want to create this group?\n")
14@@ -422,7 +422,7 @@ QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& entriesToConfirm,
15
16 m_dialogActive = true;
17 updateWindowState();
18- BrowserAccessControlDialog accessControlDialog(m_currentDatabaseWidget);
19+ BrowserAccessControlDialog accessControlDialog{};
20
21 connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &accessControlDialog, SLOT(reject()));
22
23@@ -512,7 +512,7 @@ QString BrowserService::storeKey(const QString& key)
24 QString id;
25
26 do {
27- QInputDialog keyDialog(m_currentDatabaseWidget);
28+ QInputDialog keyDialog{};
29 connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &keyDialog, SLOT(reject()));
30 keyDialog.setWindowTitle(tr("KeePassXC - New key association request"));
31 keyDialog.setLabelText(tr("You have received an association request for the following database:\n%1\n\n"
32@@ -535,7 +535,7 @@ QString BrowserService::storeKey(const QString& key)
33
34 contains = db->metadata()->customData()->contains(CustomData::BrowserKeyPrefix + id);
35 if (contains) {
36- dialogResult = MessageBox::warning(m_currentDatabaseWidget,
37+ dialogResult = MessageBox::warning(nullptr,
38 tr("KeePassXC - Overwrite existing key?"),
39 tr("A shared encryption key with the name \"%1\" "
40 "already exists.\nDo you want to overwrite it?")
41@@ -595,7 +595,7 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
42 const auto existingEntries = getPasskeyEntriesWithUserHandle(rpId, userId, keyList);
43
44 raiseWindow();
45- BrowserPasskeysConfirmationDialog confirmDialog(m_currentDatabaseWidget);
46+ BrowserPasskeysConfirmationDialog confirmDialog{};
47 confirmDialog.registerCredential(username, rpId, existingEntries, timeout);
48
49 auto dialogResult = confirmDialog.exec();
50@@ -612,7 +612,7 @@ QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& public
51 // If no entry is selected, show the import dialog for manual entry selection
52 auto selectedEntry = confirmDialog.getSelectedEntry();
53 if (!selectedEntry) {
54- PasskeyImporter passkeyImporter(m_currentDatabaseWidget);
55+ PasskeyImporter passkeyImporter{};
56 const auto result = passkeyImporter.showImportDialog(db,
57 nullptr,
58 origin,
59@@ -683,7 +683,7 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject&
60 const auto timeout = publicKeyOptions["timeout"].toInt();
61
62 raiseWindow();
63- BrowserPasskeysConfirmationDialog confirmDialog(m_currentDatabaseWidget);
64+ BrowserPasskeysConfirmationDialog confirmDialog{};
65 confirmDialog.authenticateCredential(entries, rpId, timeout);
66 auto dialogResult = confirmDialog.exec();
67 if (dialogResult == QDialog::Accepted) {
68@@ -760,7 +760,7 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
69
70 // Ask confirmation if entry already contains a Passkey
71 if (entry->hasPasskey()) {
72- if (MessageBox::question(m_currentDatabaseWidget,
73+ if (MessageBox::question(nullptr,
74 tr("KeePassXC - Update passkey"),
75 tr("Entry already has a passkey.\nDo you want to overwrite the passkey in %1 - %2?")
76 .arg(entry->title(), passkeyUtils()->getUsernameFromEntry(entry)),
77@@ -873,7 +873,7 @@ bool BrowserService::updateEntry(const EntryParameters& entryParameters, const Q
78 MessageBox::Button dialogResult = MessageBox::No;
79 if (!browserSettings()->alwaysAllowUpdate()) {
80 raiseWindow();
81- dialogResult = MessageBox::question(m_currentDatabaseWidget,
82+ dialogResult = MessageBox::question(nullptr,
83 tr("KeePassXC - Update Entry"),
84 tr("Do you want to update the information in %1 - %2?")
85 .arg(QUrl(entryParameters.siteUrl).host(), username),
86@@ -909,7 +909,7 @@ bool BrowserService::deleteEntry(const QString& uuid)
87 return false;
88 }
89
90- auto dialogResult = MessageBox::warning(m_currentDatabaseWidget,
91+ auto dialogResult = MessageBox::warning(nullptr,
92 tr("KeePassXC - Delete entry"),
93 tr("A request for deleting entry \"%1\" has been received.\n"
94 "Do you want to delete the entry?\n")
95@@ -1536,7 +1536,7 @@ QSharedPointer<Database> BrowserService::selectedDatabase()
96 }
97 }
98
99- BrowserEntrySaveDialog browserEntrySaveDialog(m_currentDatabaseWidget);
100+ BrowserEntrySaveDialog browserEntrySaveDialog{};
101 int openDatabaseCount = browserEntrySaveDialog.setItems(databaseWidgets, m_currentDatabaseWidget);
102 if (openDatabaseCount > 1) {
103 int res = browserEntrySaveDialog.exec();
104diff --git a/src/fdosecrets/objects/Prompt.cpp b/src/fdosecrets/objects/Prompt.cpp
105index e89cd499..347c98b8 100644
106--- a/src/fdosecrets/objects/Prompt.cpp
107+++ b/src/fdosecrets/objects/Prompt.cpp
108@@ -313,7 +313,7 @@ namespace FdoSecrets
109 if (!entries.isEmpty()) {
110 QString app = tr("%1 (PID: %2)").arg(client->name()).arg(client->pid());
111 auto ac = new AccessControlDialog(
112- findWindow(m_windowId), entries, app, client->processInfo(), AuthOption::Remember);
113+ nullptr, entries, app, client->processInfo(), AuthOption::Remember);
114 connect(ac, &AccessControlDialog::finished, this, &UnlockPrompt::itemUnlockFinished);
115 connect(ac, &AccessControlDialog::finished, ac, &AccessControlDialog::deleteLater);
116 ac->open();
117diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp
118index 805d4eab..4836199e 100644
119--- a/src/gui/DatabaseTabWidget.cpp
120+++ b/src/gui/DatabaseTabWidget.cpp
121@@ -41,7 +41,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
122 : QTabWidget(parent)
123 , m_dbWidgetStateSync(new DatabaseWidgetStateSync(this))
124 , m_dbWidgetPendingLock(nullptr)
125- , m_databaseOpenDialog(new DatabaseOpenDialog(this))
126+ , m_databaseOpenDialog(new DatabaseOpenDialog())
127 , m_databaseOpenInProgress(false)
128 {
129 auto* tabBar = new QTabBar(this);
diff --git a/overlays/keepassxc/default.nix b/overlays/keepassxc/default.nix
new file mode 100644
index 00000000..46b3a459
--- /dev/null
+++ b/overlays/keepassxc/default.nix
@@ -0,0 +1,8 @@
1{ final, prev, ... }:
2{
3 keepassxc = prev.keepassxc.overrideAttrs (oldAttrs: {
4 patches = (oldAttrs.patches or []) ++ prev.lib.optional (prev.lib.versionAtLeast oldAttrs.version "2.7.9") [
5 ./database-open-dialog.patch
6 ];
7 });
8}
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/mako.nix b/overlays/mako.nix
new file mode 100644
index 00000000..1c1464fb
--- /dev/null
+++ b/overlays/mako.nix
@@ -0,0 +1,5 @@
1{ final, prev, sources, ... }: {
2 mako = prev.mako.overrideAttrs (oldAttrs: {
3 inherit (sources.mako) version src;
4 });
5}
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/nftables-prometheus-exporter/nftables-prometheus-exporter.py b/overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py
index 484228c8..60ef4670 100644
--- a/overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py
+++ b/overlays/nftables-prometheus-exporter/nftables-prometheus-exporter.py
@@ -42,7 +42,7 @@ class NFTMetrics:
42 cls._instance = cls.__new__(cls) 42 cls._instance = cls.__new__(cls)
43 cls._instance.attrs = None 43 cls._instance.attrs = None
44 return cls._instance 44 return cls._instance
45 45
46 46
47 def __init__(self): 47 def __init__(self):
48 raise RuntimeError('Call instance() instead') 48 raise RuntimeError('Call instance() instead')
@@ -62,7 +62,7 @@ class NFTMetrics:
62 raise RuntimeError(f'nftables json schema v{version} is not supported') 62 raise RuntimeError(f'nftables json schema v{version} is not supported')
63 queries[query_name] = data['nftables'][1:] 63 queries[query_name] = data['nftables'][1:]
64 64
65 65
66 def extract_query(query_name, type_name): 66 def extract_query(query_name, type_name):
67 return [ 67 return [
68 item[type_name] 68 item[type_name]
@@ -98,21 +98,21 @@ class NFTMetrics:
98 metrics += _format_prom_metrics('nftables_counter_packets_count', 'counter', counter_packets) 98 metrics += _format_prom_metrics('nftables_counter_packets_count', 'counter', counter_packets)
99 99
100 map_counts = [] 100 map_counts = []
101 for meter in self.attrs['maps']: 101 for item in self.attrs['maps']:
102 labels = { k: v for k, v in counter.items() if k not in set(['elem']) } 102 labels = { k: v for k, v in counter.items() if k not in set(['elem']) }
103 map_counts += [(labels, len(meter['elem']))] 103 map_counts += [(labels, len(item['elem']) if 'elem' in item else 0)]
104 metrics += _format_prom_metrics('nftables_map_elem_count', 'gauge', map_counts) 104 metrics += _format_prom_metrics('nftables_map_elem_count', 'gauge', map_counts)
105 105
106 meter_counts = [] 106 meter_counts = []
107 for meter in self.attrs['meters']: 107 for item in self.attrs['meters']:
108 labels = { k: v for k, v in counter.items() if k not in set(['elem']) } 108 labels = { k: v for k, v in counter.items() if k not in set(['elem']) }
109 meter_counts += [(labels, len(meter['elem']))] 109 item_counts += [(labels, len(item['elem']) if 'elem' in item else 0)]
110 metrics += _format_prom_metrics('nftables_meter_elem_count', 'gauge', meter_counts) 110 metrics += _format_prom_metrics('nftables_meter_elem_count', 'gauge', meter_counts)
111 111
112 set_counts = [] 112 set_counts = []
113 for meter in self.attrs['sets']: 113 for item in self.attrs['sets']:
114 labels = { k: v for k, v in counter.items() if k not in set(['elem']) } 114 labels = { k: v for k, v in counter.items() if k not in set(['elem']) }
115 set_counts += [(labels, len(meter['elem']))] 115 set_counts += [(labels, len(item['elem']) if 'elem' in item else 0)]
116 metrics += _format_prom_metrics('nftables_set_elem_count', 'gauge', set_counts) 116 metrics += _format_prom_metrics('nftables_set_elem_count', 'gauge', set_counts)
117 117
118 return metrics.encode('utf-8') 118 return metrics.encode('utf-8')
@@ -120,7 +120,7 @@ class NFTMetrics:
120class NFTMetricsServer(BaseHTTPRequestHandler): 120class NFTMetricsServer(BaseHTTPRequestHandler):
121 def log_message(self, format, *args): 121 def log_message(self, format, *args):
122 pass 122 pass
123 123
124 def do_GET(self): 124 def do_GET(self):
125 nft_metrics = NFTMetrics.instance() 125 nft_metrics = NFTMetrics.instance()
126 nft_metrics.update() 126 nft_metrics.update()
@@ -138,7 +138,7 @@ class NFTMetricsServer(BaseHTTPRequestHandler):
138 self.send_response(200) 138 self.send_response(200)
139 self.send_header("Content-type", "text/plain") 139 self.send_header("Content-type", "text/plain")
140 self.end_headers() 140 self.end_headers()
141 141
142 self.wfile.write(nft_metrics.prometheus()) 142 self.wfile.write(nft_metrics.prometheus())
143 case _: 143 case _:
144 self.send_response(404) 144 self.send_response(404)
diff --git a/overlays/niri.nix b/overlays/niri.nix
new file mode 100644
index 00000000..95a918b0
--- /dev/null
+++ b/overlays/niri.nix
@@ -0,0 +1,9 @@
1{ final, prev, flakeInputs, ... }: prev.lib.composeExtensions
2 flakeInputs.niri-flake.overlays.niri
3 (final: prev: {
4 niri-unstable = prev.niri-unstable.overrideAttrs (oldAttrs: {
5 buildInputs = (oldAttrs.buildInputs or []) ++ [ final.libgbm ];
6 doCheck = false;
7 });
8 })
9 final prev
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/preserve-dscp/default.nix b/overlays/preserve-dscp/default.nix
index 105eccb9..208d69db 100644
--- a/overlays/preserve-dscp/default.nix
+++ b/overlays/preserve-dscp/default.nix
@@ -15,7 +15,7 @@
15 15
16 outputs = [ "out" "lib" ]; 16 outputs = [ "out" "lib" ];
17 17
18 buildInputs = with final; [ elfutils libpcap zlib ]; 18 buildInputs = with final; [ elfutils libpcap libcap zlib ];
19 nativeBuildInputs = with final; [ llvmPackages.clang llvmPackages.llvm pkg-config bpftools libmnl gnum4 glibc_multi makeWrapper ]; 19 nativeBuildInputs = with final; [ llvmPackages.clang llvmPackages.llvm pkg-config bpftools libmnl gnum4 glibc_multi makeWrapper ];
20 20
21 installPhase = '' 21 installPhase = ''
diff --git a/overlays/prometheus-lvm-exporter.nix b/overlays/prometheus-lvm-exporter.nix
index 6b671cab..72d9c7ca 100644
--- a/overlays/prometheus-lvm-exporter.nix
+++ b/overlays/prometheus-lvm-exporter.nix
@@ -3,7 +3,7 @@
3 pname = "prometheus-lvm-exporter"; 3 pname = "prometheus-lvm-exporter";
4 inherit (sources.prometheus-lvm-exporter) version src; 4 inherit (sources.prometheus-lvm-exporter) version src;
5 5
6 vendorHash = "sha256-vqxsg70ShMo4OVdzhqYDj/HT3RTpCUBGHze/EkbBJig="; 6 vendorHash = "sha256-CoTNTCBBugbHWDsOuZY1t8HrpdmEbbSMyVb3+1u0q+g=";
7 7
8 doCheck = false; 8 doCheck = false;
9 9
diff --git a/overlays/quickshell/default.nix b/overlays/quickshell/default.nix
new file mode 100644
index 00000000..c01fac20
--- /dev/null
+++ b/overlays/quickshell/default.nix
@@ -0,0 +1,13 @@
1{ final, prev, sources, ... }:
2{
3 quickshell = prev.quickshell.overrideAttrs (oldAttrs: {
4 inherit (sources.quickshell) version src;
5
6 patches = (oldAttrs.patches or []) ++ [
7 ./greetd-response.patch
8 ./lock-state-changed.patch
9 ./pipewire.patch
10 ./io.patch
11 ];
12 });
13}
diff --git a/overlays/quickshell/greetd-response.patch b/overlays/quickshell/greetd-response.patch
new file mode 100644
index 00000000..a0efb562
--- /dev/null
+++ b/overlays/quickshell/greetd-response.patch
@@ -0,0 +1,16 @@
1diff --git c/src/services/greetd/connection.cpp w/src/services/greetd/connection.cpp
2index bf0d1fd..a790ab7 100644
3--- c/src/services/greetd/connection.cpp
4+++ w/src/services/greetd/connection.cpp
5@@ -225,6 +225,11 @@ void GreetdConnection::onSocketReady() {
6
7 this->mResponseRequired = responseRequired;
8 emit this->authMessage(message, error, responseRequired, echoResponse);
9+
10+ if (!responseRequired)
11+ this->sendRequest({
12+ {"type", "post_auth_message_response"}
13+ });
14 } else goto unexpected;
15
16 return;
diff --git a/overlays/quickshell/io.patch b/overlays/quickshell/io.patch
new file mode 100644
index 00000000..961bdcaf
--- /dev/null
+++ b/overlays/quickshell/io.patch
@@ -0,0 +1,13 @@
1diff --git i/src/io/socket.cpp w/src/io/socket.cpp
2index 371f687..d12eaeb 100644
3--- i/src/io/socket.cpp
4+++ w/src/io/socket.cpp
5@@ -66,7 +66,7 @@ void Socket::onSocketDisconnected() {
6 }
7
8 void Socket::onSocketError(QLocalSocket::LocalSocketError error) {
9- qCWarning(logSocket) << "Socket error for" << this << error;
10+ // qCWarning(logSocket) << "Socket error for" << this << error;
11 emit this->error(error);
12 }
13
diff --git a/overlays/quickshell/lock-state-changed.patch b/overlays/quickshell/lock-state-changed.patch
new file mode 100644
index 00000000..4be273fa
--- /dev/null
+++ b/overlays/quickshell/lock-state-changed.patch
@@ -0,0 +1,12 @@
1diff --git i/src/wayland/session_lock.cpp w/src/wayland/session_lock.cpp
2index 0ecf9ec..3dbd19b 100644
3--- i/src/wayland/session_lock.cpp
4+++ w/src/wayland/session_lock.cpp
5@@ -127,6 +127,7 @@ void WlSessionLock::realizeLockTarget(WlSessionLock* old) {
6 this->updateSurfaces(false);
7
8 if (!this->manager->lock()) this->lockTarget = false;
9+ emit this->lockStateChanged();
10
11 this->updateSurfaces(true, old);
12 } else {
diff --git a/overlays/quickshell/pipewire.patch b/overlays/quickshell/pipewire.patch
new file mode 100644
index 00000000..2d98eefc
--- /dev/null
+++ b/overlays/quickshell/pipewire.patch
@@ -0,0 +1,488 @@
1diff --git i/src/services/pipewire/device.cpp w/src/services/pipewire/device.cpp
2index 616e7d0..0c55008 100644
3--- i/src/services/pipewire/device.cpp
4+++ w/src/services/pipewire/device.cpp
5@@ -3,6 +3,7 @@
6 #include <cstdint>
7 #include <functional>
8 #include <utility>
9+#include <algorithm>
10
11 #include <pipewire/device.h>
12 #include <qcontainerfwd.h>
13@@ -19,6 +20,8 @@
14 #include <spa/pod/pod.h>
15 #include <spa/pod/vararg.h>
16 #include <spa/utils/type.h>
17+#include <spa/monitor/device.h>
18+#include <spa/utils/keys.h>
19
20 #include "../../core/logcat.hpp"
21 #include "core.hpp"
22@@ -46,6 +49,25 @@ void PwDevice::unbindHooks() {
23 this->mWaitingForDevice = false;
24 }
25
26+void PwDevice::initProps(const spa_dict* props) {
27+ if (const auto* deviceName = spa_dict_lookup(props, SPA_KEY_DEVICE_NAME)) {
28+ this->name = deviceName;
29+ }
30+
31+ if (const auto* deviceDesc = spa_dict_lookup(props, SPA_KEY_DEVICE_DESCRIPTION)) {
32+ this->description = deviceDesc;
33+ }
34+
35+ if (const auto* deviceNick = spa_dict_lookup(props, SPA_KEY_DEVICE_NICK)) {
36+ this->nick = deviceNick;
37+ }
38+
39+ if (const auto* mediaClass = spa_dict_lookup(props, SPA_KEY_MEDIA_CLASS)) {
40+ this->type = mediaClass;
41+ }
42+}
43+
44+
45 const pw_device_events PwDevice::EVENTS = {
46 .version = PW_VERSION_DEVICE_EVENTS,
47 .info = &PwDevice::onInfo,
48@@ -71,6 +93,11 @@ void PwDevice::onInfo(void* data, const pw_device_info* info) {
49 }
50
51 break;
52+ } else if (param.id == SPA_PARAM_EnumProfile && param.flags & SPA_PARAM_INFO_READ) {
53+ self->validProfiles.clear();
54+ pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
55+ } else if (param.id == SPA_PARAM_Profile && param.flags & SPA_PARAM_INFO_READ) {
56+ pw_device_enum_params(self->proxy(), 0, param.id, 0, UINT32_MAX, nullptr);
57 }
58 }
59 }
60@@ -97,6 +124,15 @@ void PwDevice::onParam(
61 }
62
63 self->addDeviceIndexPairs(param);
64+ } else if (id == SPA_PARAM_EnumProfile) {
65+ PwProfile profile = PwProfile::parseSpaPod(param);
66+ self->profilesUpdated = true;
67+ self->profiles.insertOrAssign(profile.index, profile);
68+ self->validProfiles.insert(profile.index);
69+ } else if (id == SPA_PARAM_Profile) {
70+ PwProfile profile = PwProfile::parseSpaPod(param);
71+ self->currentProfileUpdated = true;
72+ self->currentProfile = profile;
73 }
74 }
75
76@@ -145,6 +181,21 @@ void PwDevice::polled() {
77 return false;
78 });
79 }
80+ if (this->profilesUpdated) {
81+ this->profiles.removeIf([&](const std::pair<qint32, PwProfile>& entry) {
82+ return !this->validProfiles.contains(entry.first);
83+ });
84+ this->profilesUpdated = false;
85+ QList<PwProfile> profiles = this->profiles.values();
86+ std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; });
87+ emit this->profilesChanged(profiles);
88+ }
89+ if (this->currentProfileUpdated) {
90+ this->currentProfileUpdated = false;
91+ if (this->currentProfile) {
92+ emit this->currentProfileChanged(*this->currentProfile);
93+ }
94+ }
95 }
96
97 bool PwDevice::setVolumes(qint32 routeDevice, const QVector<float>& volumes) {
98@@ -182,6 +233,15 @@ bool PwDevice::setMuted(qint32 routeDevice, bool muted) {
99 });
100 }
101
102+void PwDevice::setProfile(qint32 profileIndex) {
103+ auto buffer = std::array<uint8_t, 1024>();
104+ auto builder = SPA_POD_BUILDER_INIT(buffer.data(), buffer.size());
105+ auto* pod = spa_pod_builder_add_object(&builder,
106+ SPA_TYPE_OBJECT_ParamProfile, SPA_PARAM_Profile,
107+ SPA_PARAM_PROFILE_index, SPA_POD_Int(profileIndex));
108+ pw_device_set_param(this->proxy(), SPA_PARAM_Profile, 0, static_cast<spa_pod*>(pod));
109+}
110+
111 void PwDevice::waitForDevice() { this->mWaitingForDevice = true; }
112 bool PwDevice::waitingForDevice() const { return this->mWaitingForDevice; }
113
114@@ -222,4 +282,24 @@ bool PwDevice::setRouteProps(
115 return true;
116 }
117
118+PwProfile PwProfile::parseSpaPod(const spa_pod* param) {
119+ PwProfile profile;
120+
121+ const auto* indexProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_index);
122+ const auto* descProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_description);
123+ const auto* nameProp = spa_pod_find_prop(param, nullptr, SPA_PARAM_PROFILE_name);
124+
125+ spa_pod_get_int(&indexProp->value, &profile.index);
126+
127+ const char* desc_cstr = nullptr;
128+ spa_pod_get_string(&descProp->value, &desc_cstr);
129+ profile.description = QString(desc_cstr);
130+
131+ const char* name_cstr = nullptr;
132+ spa_pod_get_string(&nameProp->value, &name_cstr);
133+ profile.name = QString(name_cstr);
134+
135+ return profile;
136+}
137+
138 } // namespace qs::service::pipewire
139diff --git i/src/services/pipewire/device.hpp w/src/services/pipewire/device.hpp
140index 1a1f705..ee64858 100644
141--- i/src/services/pipewire/device.hpp
142+++ w/src/services/pipewire/device.hpp
143@@ -1,6 +1,7 @@
144 #pragma once
145
146 #include <functional>
147+#include <optional>
148
149 #include <pipewire/core.h>
150 #include <pipewire/device.h>
151@@ -17,6 +18,20 @@
152
153 namespace qs::service::pipewire {
154
155+struct PwProfile {
156+ Q_GADGET;
157+ Q_PROPERTY(qint32 index MEMBER index)
158+ Q_PROPERTY(QString description MEMBER description)
159+ Q_PROPERTY(QString name MEMBER name)
160+
161+public:
162+ qint32 index;
163+ QString description;
164+ QString name;
165+
166+ static PwProfile parseSpaPod(const spa_pod* param);
167+};
168+
169 class PwDevice;
170
171 class PwDevice: public PwBindable<pw_device, PW_TYPE_INTERFACE_Device, PW_VERSION_DEVICE> {
172@@ -25,6 +40,12 @@ class PwDevice: public PwBindable<pw_device, PW_TYPE_INTERFACE_Device, PW_VERSIO
173 public:
174 void bindHooks() override;
175 void unbindHooks() override;
176+ void initProps(const spa_dict* props) override;
177+
178+ QString name;
179+ QString description;
180+ QString nick;
181+ QString type;
182
183 bool setVolumes(qint32 routeDevice, const QVector<float>& volumes);
184 bool setMuted(qint32 routeDevice, bool muted);
185@@ -32,9 +53,16 @@ public:
186 void waitForDevice();
187 [[nodiscard]] bool waitingForDevice() const;
188
189+ void setProfile(qint32 profileIndex);
190+
191+ QHash<qint32, PwProfile> profiles;
192+ std::optional<PwProfile> currentProfile;
193+
194 signals:
195 void deviceReady();
196 void routeVolumesChanged(qint32 routeDevice, const PwVolumeProps& volumeProps);
197+ void profilesChanged(QList<PwProfile> profiles);
198+ void currentProfileChanged(PwProfile profile);
199
200 private slots:
201 void polled();
202@@ -49,6 +77,11 @@ private:
203 QList<qint32> stagingIndexes;
204 void addDeviceIndexPairs(const spa_pod* param);
205
206+ bool profilesUpdated = false;
207+ QSet<qint32> validProfiles;
208+
209+ bool currentProfileUpdated = false;
210+
211 bool
212 setRouteProps(qint32 routeDevice, const std::function<void*(spa_pod_builder*)>& propsCallback);
213
214diff --git i/src/services/pipewire/node.cpp w/src/services/pipewire/node.cpp
215index 3e68149..4721a58 100644
216--- i/src/services/pipewire/node.cpp
217+++ w/src/services/pipewire/node.cpp
218@@ -145,6 +145,10 @@ void PwNode::initProps(const spa_dict* props) {
219 this->type = PwNodeType::VideoSink;
220 } else if (strcmp(mediaClass, "Video/Source") == 0) {
221 this->type = PwNodeType::VideoSource;
222+ } else if (strcmp(mediaClass, "Stream/Output/Video") == 0) {
223+ this->type = PwNodeType::VideoOutStream;
224+ } else if (strcmp(mediaClass, "Stream/Input/Video") == 0) {
225+ this->type = PwNodeType::VideoInStream;
226 }
227 }
228
229diff --git i/src/services/pipewire/node.hpp w/src/services/pipewire/node.hpp
230index 0d4c92e..ee6f223 100644
231--- i/src/services/pipewire/node.hpp
232+++ w/src/services/pipewire/node.hpp
233@@ -144,6 +144,8 @@ public:
234 // This is equivalent to the media class `Video/Sink` and is composed of the
235 // @@PwNodeType.Video and @@PwNodeType.Sink flags.
236 VideoSink = Video | Sink,
237+ VideoOutStream = Video | Sink | Stream,
238+ VideoInStream = Video | Source | Stream,
239 };
240 Q_ENUM(Flag);
241 Q_DECLARE_FLAGS(Flags, Flag);
242diff --git i/src/services/pipewire/qml.cpp w/src/services/pipewire/qml.cpp
243index 9efb17e..921d12a 100644
244--- i/src/services/pipewire/qml.cpp
245+++ w/src/services/pipewire/qml.cpp
246@@ -9,6 +9,9 @@
247 #include <qtypes.h>
248 #include <qvariant.h>
249
250+#include <cstdint>
251+#include <algorithm>
252+
253 #include "../../core/model.hpp"
254 #include "connection.hpp"
255 #include "defaults.hpp"
256@@ -54,6 +57,12 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) {
257
258 QObject::connect(&connection->registry, &PwRegistry::nodeAdded, this, &Pipewire::onNodeAdded);
259
260+ for (auto* device: connection->registry.devices.values()) {
261+ this->onDeviceAdded(device);
262+ }
263+
264+ QObject::connect(&connection->registry, &PwRegistry::deviceAdded, this, &Pipewire::onDeviceAdded);
265+
266 for (auto* link: connection->registry.links.values()) {
267 this->onLinkAdded(link);
268 }
269@@ -123,6 +132,19 @@ void Pipewire::onNodeRemoved(QObject* object) {
270 this->mNodes.removeObject(iface);
271 }
272
273+ObjectModel<PwDeviceIface>* Pipewire::devices() { return &this->mDevices; }
274+
275+void Pipewire::onDeviceAdded(PwDevice* device) {
276+ auto* iface = PwDeviceIface::instance(device);
277+ QObject::connect(iface, &QObject::destroyed, this, &Pipewire::onDeviceRemoved);
278+ this->mDevices.insertObject(iface);
279+}
280+
281+void Pipewire::onDeviceRemoved(QObject* object) {
282+ auto* iface = static_cast<PwDeviceIface*>(object); // NOLINT
283+ this->mDevices.removeObject(iface);
284+}
285+
286 ObjectModel<PwLinkIface>* Pipewire::links() { return &this->mLinks; }
287
288 void Pipewire::onLinkAdded(PwLink* link) {
289@@ -357,6 +379,8 @@ QVariantMap PwNodeIface::properties() const {
290
291 PwNodeAudioIface* PwNodeIface::audio() const { return this->audioIface; }
292
293+PwDeviceIface* PwNodeIface::device() const { return PwDeviceIface::instance(this->mNode->device); }
294+
295 PwNodeIface* PwNodeIface::instance(PwNode* node) {
296 if (node == nullptr) return nullptr;
297
298@@ -481,4 +505,42 @@ void PwObjectTracker::objectDestroyed(QObject* object) {
299 emit this->objectsChanged();
300 }
301
302+PwDeviceIface::PwDeviceIface(PwDevice* device): PwObjectIface(device), mDevice(device) {
303+ QObject::connect(device, &PwDevice::profilesChanged, this, &PwDeviceIface::deviceProfilesChanged);
304+ QObject::connect(device, &PwDevice::currentProfileChanged, this, &PwDeviceIface::deviceCurrentProfileChanged);
305+}
306+
307+void PwDeviceIface::deviceProfilesChanged(QList<PwProfile>) { emit this->profilesChanged(); }
308+void PwDeviceIface::deviceCurrentProfileChanged(PwProfile) { emit this->currentProfileChanged(); }
309+
310+quint32 PwDeviceIface::id() const { return this->mDevice->id; }
311+QString PwDeviceIface::name() const { return this->mDevice->name; }
312+QString PwDeviceIface::description() const { return this->mDevice->description; }
313+QString PwDeviceIface::nickname() const { return this->mDevice->nick; }
314+QString PwDeviceIface::type() const { return this->mDevice->type; }
315+QList<PwProfile> PwDeviceIface::profiles() const {
316+ QList<PwProfile> profiles = this->mDevice->profiles.values();
317+ std::sort(profiles.begin(), profiles.end(), [](const PwProfile& a, const PwProfile& b) { return a.index < b.index; });
318+ return profiles;
319+}
320+qint32 PwDeviceIface::currentProfile() const { return this->mDevice->currentProfile->index; }
321+
322+PwDeviceIface* PwDeviceIface::instance(PwDevice* device) {
323+ if (device == nullptr) return nullptr;
324+
325+ auto v = device->property("iface");
326+ if (v.canConvert<PwDeviceIface*>()) {
327+ return v.value<PwDeviceIface*>();
328+ }
329+
330+ auto* instance = new PwDeviceIface(device);
331+ device->setProperty("iface", QVariant::fromValue(instance));
332+
333+ return instance;
334+}
335+
336+void PwDeviceIface::setProfile(qint32 profileIndex) {
337+ this->mDevice->setProfile(profileIndex);
338+}
339+
340 } // namespace qs::service::pipewire
341diff --git i/src/services/pipewire/qml.hpp w/src/services/pipewire/qml.hpp
342index e3489a1..e5e1891 100644
343--- i/src/services/pipewire/qml.hpp
344+++ w/src/services/pipewire/qml.hpp
345@@ -12,11 +12,13 @@
346 #include "../../core/model.hpp"
347 #include "link.hpp"
348 #include "node.hpp"
349+#include "device.hpp"
350 #include "registry.hpp"
351
352 namespace qs::service::pipewire {
353
354 class PwNodeIface;
355+class PwDeviceIface;
356 class PwLinkIface;
357 class PwLinkGroupIface;
358
359@@ -65,6 +67,8 @@ class Pipewire: public QObject {
360 /// - @@PwNode.audio - if non null the node is an audio node.
361 QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwNodeIface>*);
362 Q_PROPERTY(UntypedObjectModel* nodes READ nodes CONSTANT);
363+ QSDOC_TYPE_OVERRIDE(ObjectModel<qs::service::pipewire::PwDeviceIface>*);
364+ Q_PROPERTY(UntypedObjectModel* devices READ devices CONSTANT);
365 /// All links present in pipewire.
366 ///
367 /// Links connect pipewire nodes to each other, and can be used to determine
368@@ -134,6 +138,7 @@ public:
369 explicit Pipewire(QObject* parent = nullptr);
370
371 [[nodiscard]] ObjectModel<PwNodeIface>* nodes();
372+ [[nodiscard]] ObjectModel<PwDeviceIface>* devices();
373 [[nodiscard]] ObjectModel<PwLinkIface>* links();
374 [[nodiscard]] ObjectModel<PwLinkGroupIface>* linkGroups();
375
376@@ -159,7 +164,9 @@ signals:
377
378 private slots:
379 void onNodeAdded(PwNode* node);
380+ void onDeviceAdded(PwDevice* node);
381 void onNodeRemoved(QObject* object);
382+ void onDeviceRemoved(QObject* object);
383 void onLinkAdded(PwLink* link);
384 void onLinkRemoved(QObject* object);
385 void onLinkGroupAdded(PwLinkGroup* group);
386@@ -167,6 +174,7 @@ private slots:
387
388 private:
389 ObjectModel<PwNodeIface> mNodes {this};
390+ ObjectModel<PwDeviceIface> mDevices {this};
391 ObjectModel<PwLinkIface> mLinks {this};
392 ObjectModel<PwLinkGroupIface> mLinkGroups {this};
393 };
394@@ -315,6 +323,7 @@ class PwNodeIface: public PwObjectIface {
395 /// > [!NOTE] The node may be used before it is fully bound, but some data
396 /// > may be missing or incorrect.
397 Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged);
398+ Q_PROPERTY(qs::service::pipewire::PwDeviceIface* device READ device CONSTANT);
399 QML_NAMED_ELEMENT(PwNode);
400 QML_UNCREATABLE("PwNodes cannot be created directly");
401
402@@ -332,6 +341,7 @@ public:
403 [[nodiscard]] PwNodeType::Flags type() const;
404 [[nodiscard]] QVariantMap properties() const;
405 [[nodiscard]] PwNodeAudioIface* audio() const;
406+ [[nodiscard]] PwDeviceIface* device() const;
407
408 static PwNodeIface* instance(PwNode* node);
409
410@@ -344,6 +354,44 @@ private:
411 PwNodeAudioIface* audioIface = nullptr;
412 };
413
414+class PwDeviceIface: public PwObjectIface {
415+ Q_OBJECT;
416+ Q_PROPERTY(quint32 id READ id CONSTANT);
417+ Q_PROPERTY(QString name READ name CONSTANT);
418+ Q_PROPERTY(QString description READ description CONSTANT);
419+ Q_PROPERTY(QString nickname READ nickname CONSTANT);
420+ Q_PROPERTY(QString type READ type CONSTANT);
421+ Q_PROPERTY(QList<PwProfile> profiles READ profiles NOTIFY profilesChanged);
422+ Q_PROPERTY(qint32 currentProfile READ currentProfile NOTIFY currentProfileChanged);
423+
424+ QML_NAMED_ELEMENT(PwDevice);
425+ QML_UNCREATABLE("PwDevices cannot be created directly");
426+
427+signals:
428+ void profilesChanged();
429+ void currentProfileChanged();
430+
431+public:
432+ explicit PwDeviceIface(PwDevice* node);
433+
434+ [[nodiscard]] quint32 id() const;
435+ [[nodiscard]] QString name() const;
436+ [[nodiscard]] QString description() const;
437+ [[nodiscard]] QString nickname() const;
438+ [[nodiscard]] QString type() const;
439+ QList<PwProfile> profiles() const;
440+ qint32 currentProfile() const;
441+
442+ Q_INVOKABLE void setProfile(qint32 profileIndex);
443+
444+ static PwDeviceIface* instance(PwDevice* node);
445+private:
446+ PwDevice* mDevice;
447+
448+ void deviceProfilesChanged(QList<PwProfile> profiles);
449+ void deviceCurrentProfileChanged(PwProfile profile);
450+};
451+
452 ///! A connection between pipewire nodes.
453 /// Note that there is one link per *channel* of a connection between nodes.
454 /// You usually want @@PwLinkGroup.
455diff --git i/src/services/pipewire/registry.cpp w/src/services/pipewire/registry.cpp
456index c08fc1d..50c6d7a 100644
457--- i/src/services/pipewire/registry.cpp
458+++ w/src/services/pipewire/registry.cpp
459@@ -196,6 +196,7 @@ void PwRegistry::onGlobal(
460 device->initProps(props);
461
462 self->devices.emplace(id, device);
463+ emit self->deviceAdded(device);
464 }
465 }
466
467@@ -211,6 +212,9 @@ void PwRegistry::onGlobalRemoved(void* data, quint32 id) {
468 } else if (auto* node = self->nodes.value(id)) {
469 self->nodes.remove(id);
470 node->safeDestroy();
471+ } else if (auto* device = self->devices.value(id)) {
472+ self->devices.remove(id);
473+ device->safeDestroy();
474 }
475 }
476
477diff --git i/src/services/pipewire/registry.hpp w/src/services/pipewire/registry.hpp
478index 8473f04..87e0766 100644
479--- i/src/services/pipewire/registry.hpp
480+++ w/src/services/pipewire/registry.hpp
481@@ -132,6 +132,7 @@ public:
482
483 signals:
484 void nodeAdded(PwNode* node);
485+ void deviceAdded(PwDevice* node);
486 void linkAdded(PwLink* link);
487 void linkGroupAdded(PwLinkGroup* group);
488 void metadataAdded(PwMetadata* metadata);
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/spice-record.nix b/overlays/spice-record.nix
index 06a114da..2d37079b 100644
--- a/overlays/spice-record.nix
+++ b/overlays/spice-record.nix
@@ -8,5 +8,7 @@
8 wrapProgram $out/bin/spice-record \ 8 wrapProgram $out/bin/spice-record \
9 --prefix PATH : ${prev.lib.makeBinPath (with prev; [ ffmpeg-full ])} 9 --prefix PATH : ${prev.lib.makeBinPath (with prev; [ ffmpeg-full ])}
10 ''; 10 '';
11 pyproject = true;
12 build-system = [ prev.python3Packages.setuptools ];
11 }; 13 };
12} 14}
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/waybar.nix b/overlays/waybar.nix
index 20f37255..e7e3b807 100644
--- a/overlays/waybar.nix
+++ b/overlays/waybar.nix
@@ -1,3 +1,8 @@
1{ final, prev, flakeInputs, ... }: 1{ final, prev, flakeInputs, ... }: prev.lib.composeExtensions
2 2 flakeInputs.waybar.overlays.default
3flakeInputs.waybar.overlays.default final prev 3 (final: prev: {
4 waybar = prev.waybar.overrideAttrs (oldAttrs: {
5 dontVersionCheck = true;
6 });
7 })
8 final prev
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 5ecdf149..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.python310; 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 54182b09..00000000
--- a/overlays/worktime/poetry.lock
+++ /dev/null
@@ -1,248 +0,0 @@
1# This file is automatically @generated by Poetry 1.6.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"
9files = [
10 {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"},
11 {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"},
12]
13
14[[package]]
15name = "certifi"
16version = "2022.12.7"
17description = "Python package for providing Mozilla's CA Bundle."
18optional = false
19python-versions = ">=3.6"
20files = [
21 {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"},
22 {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"},
23]
24
25[[package]]
26name = "charset-normalizer"
27version = "3.1.0"
28description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
29optional = false
30python-versions = ">=3.7.0"
31files = [
32 {file = "charset-normalizer-3.1.0.tar.gz", hash = "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5"},
33 {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b"},
34 {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60"},
35 {file = "charset_normalizer-3.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1"},
36 {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0"},
37 {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f"},
38 {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0"},
39 {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795"},
40 {file = "charset_normalizer-3.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c"},
41 {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203"},
42 {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1"},
43 {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137"},
44 {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce"},
45 {file = "charset_normalizer-3.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a"},
46 {file = "charset_normalizer-3.1.0-cp310-cp310-win32.whl", hash = "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448"},
47 {file = "charset_normalizer-3.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8"},
48 {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19"},
49 {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017"},
50 {file = "charset_normalizer-3.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df"},
51 {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a"},
52 {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41"},
53 {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1"},
54 {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62"},
55 {file = "charset_normalizer-3.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6"},
56 {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5"},
57 {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be"},
58 {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb"},
59 {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac"},
60 {file = "charset_normalizer-3.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324"},
61 {file = "charset_normalizer-3.1.0-cp311-cp311-win32.whl", hash = "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909"},
62 {file = "charset_normalizer-3.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755"},
63 {file = "charset_normalizer-3.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373"},
64 {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab"},
65 {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9"},
66 {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f"},
67 {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28"},
68 {file = "charset_normalizer-3.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d"},
69 {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d"},
70 {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d"},
71 {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6"},
72 {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84"},
73 {file = "charset_normalizer-3.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c"},
74 {file = "charset_normalizer-3.1.0-cp37-cp37m-win32.whl", hash = "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974"},
75 {file = "charset_normalizer-3.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23"},
76 {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531"},
77 {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c"},
78 {file = "charset_normalizer-3.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14"},
79 {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb"},
80 {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1"},
81 {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b"},
82 {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0"},
83 {file = "charset_normalizer-3.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649"},
84 {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326"},
85 {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11"},
86 {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b"},
87 {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd"},
88 {file = "charset_normalizer-3.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8"},
89 {file = "charset_normalizer-3.1.0-cp38-cp38-win32.whl", hash = "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0"},
90 {file = "charset_normalizer-3.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59"},
91 {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e"},
92 {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31"},
93 {file = "charset_normalizer-3.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f"},
94 {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e"},
95 {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f"},
96 {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854"},
97 {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706"},
98 {file = "charset_normalizer-3.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e"},
99 {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0"},
100 {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230"},
101 {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7"},
102 {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e"},
103 {file = "charset_normalizer-3.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f"},
104 {file = "charset_normalizer-3.1.0-cp39-cp39-win32.whl", hash = "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1"},
105 {file = "charset_normalizer-3.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b"},
106 {file = "charset_normalizer-3.1.0-py3-none-any.whl", hash = "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d"},
107]
108
109[[package]]
110name = "idna"
111version = "3.4"
112description = "Internationalized Domain Names in Applications (IDNA)"
113optional = false
114python-versions = ">=3.5"
115files = [
116 {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
117 {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
118]
119
120[[package]]
121name = "jsonpickle"
122version = "3.0.2"
123description = "Python library for serializing any arbitrary object graph into JSON"
124optional = false
125python-versions = ">=3.7"
126files = [
127 {file = "jsonpickle-3.0.2-py3-none-any.whl", hash = "sha256:4a8442d97ca3f77978afa58068768dba7bff2dbabe79a9647bc3cdafd4ef019f"},
128 {file = "jsonpickle-3.0.2.tar.gz", hash = "sha256:e37abba4bfb3ca4a4647d28bb9f4706436f7b46c8a8333b4a718abafa8e46b37"},
129]
130
131[package.extras]
132docs = ["jaraco.packaging (>=3.2)", "rst.linker (>=1.9)", "sphinx"]
133testing = ["ecdsa", "feedparser", "gmpy2", "numpy", "pandas", "pymongo", "pytest (>=3.5,!=3.7.3)", "pytest-black-multipy", "pytest-checkdocs (>=1.2.3)", "pytest-cov", "pytest-flake8 (>=1.1.1)", "scikit-learn", "sqlalchemy"]
134testing-libs = ["simplejson", "ujson"]
135
136[[package]]
137name = "python-dateutil"
138version = "2.8.2"
139description = "Extensions to the standard Python datetime module"
140optional = false
141python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
142files = [
143 {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"},
144 {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"},
145]
146
147[package.dependencies]
148six = ">=1.5"
149
150[[package]]
151name = "pyxdg"
152version = "0.28"
153description = "PyXDG contains implementations of freedesktop.org standards in python."
154optional = false
155python-versions = "*"
156files = [
157 {file = "pyxdg-0.28-py2.py3-none-any.whl", hash = "sha256:bdaf595999a0178ecea4052b7f4195569c1ff4d344567bccdc12dfdf02d545ab"},
158 {file = "pyxdg-0.28.tar.gz", hash = "sha256:3267bb3074e934df202af2ee0868575484108581e6f3cb006af1da35395e88b4"},
159]
160
161[[package]]
162name = "requests"
163version = "2.28.2"
164description = "Python HTTP for Humans."
165optional = false
166python-versions = ">=3.7, <4"
167files = [
168 {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"},
169 {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"},
170]
171
172[package.dependencies]
173certifi = ">=2017.4.17"
174charset-normalizer = ">=2,<4"
175idna = ">=2.5,<4"
176urllib3 = ">=1.21.1,<1.27"
177
178[package.extras]
179socks = ["PySocks (>=1.5.6,!=1.5.7)"]
180use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
181
182[[package]]
183name = "six"
184version = "1.16.0"
185description = "Python 2 and 3 compatibility utilities"
186optional = false
187python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
188files = [
189 {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
190 {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
191]
192
193[[package]]
194name = "tabulate"
195version = "0.9.0"
196description = "Pretty-print tabular data"
197optional = false
198python-versions = ">=3.7"
199files = [
200 {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"},
201 {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"},
202]
203
204[package.extras]
205widechars = ["wcwidth"]
206
207[[package]]
208name = "toml"
209version = "0.10.2"
210description = "Python Library for Tom's Obvious, Minimal Language"
211optional = false
212python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
213files = [
214 {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
215 {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
216]
217
218[[package]]
219name = "uritools"
220version = "4.0.1"
221description = "URI parsing, classification and composition"
222optional = false
223python-versions = "~=3.7"
224files = [
225 {file = "uritools-4.0.1-py3-none-any.whl", hash = "sha256:d122d394ed6e6e15ac0fddba6a5b19e9fa204e7797507815cbfb0e1455ac0475"},
226 {file = "uritools-4.0.1.tar.gz", hash = "sha256:efc5c3a6de05404850685a8d3f34da8476b56aa3516fbf8eff5c8704c7a2826f"},
227]
228
229[[package]]
230name = "urllib3"
231version = "1.26.15"
232description = "HTTP library with thread-safe connection pooling, file post, and more."
233optional = false
234python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
235files = [
236 {file = "urllib3-1.26.15-py2.py3-none-any.whl", hash = "sha256:aa751d169e23c7479ce47a0cb0da579e3ede798f994f5816a74e4f4500dcea42"},
237 {file = "urllib3-1.26.15.tar.gz", hash = "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305"},
238]
239
240[package.extras]
241brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
242secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
243socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
244
245[metadata]
246lock-version = "2.0"
247python-versions = "^3.10"
248content-hash = "d9137b4f8e37bba934abf732e4a2aeeb9924c4b6576830d8ae08bdb43b4e147f"
diff --git a/overlays/worktime/pyproject.toml b/overlays/worktime/pyproject.toml
index 08002d4d..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.10"
9pyxdg = "^0.28"
10python-dateutil = "^2.8.2"
11uritools = "^4.0.1"
12requests = "^2.28.2"
13tabulate = "^0.9.0"
14backoff = "^2.2.1"
15toml = "^0.10.2"
16jsonpickle = "^3.0.2"
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 362c8da4..ffeb1b84 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
@@ -23,80 +25,80 @@ import argparse
23from copy import deepcopy 25from copy import deepcopy
24 26
25import sys 27import sys
26from sys import stderr 28from 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
43import json
44
45import asyncio
46
47from frozendict import frozendict
48from contextlib import closing
49import os
50from time import clock_gettime_ns, CLOCK_MONOTONIC
51from atomicwriter import AtomicWriter
52import desktop_notify.aio as notify
53
54class BearerAuth(requests.auth.AuthBase):
55 def __init__(self, token):
56 self.token = token
57 def __call__(self, r):
58 r.headers["authorization"] = "Bearer " + self.token
59 return r
60
61class KimaiSession(requests.Session):
62 def __init__(self, base_url: str, api_token: str):
63 super().__init__()
64 self.base_url = base_url
65 self.auth = BearerAuth(api_token)
66 retries = Retry(total=5, backoff_factor=0.1, status_forcelist=[500, 502, 503, 504])
67 super().mount(base_url, HTTPAdapter(max_retries=retries))
68
69 def request(self, method, url, *args, **kwargs):
70 joined_url = urljoin(self.base_url, url)
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']})
72
73class KimaiAPI(object):
74 def __init__(self, base_url: str, api_token: str, clients: Iterable[str]):
75 self._session = KimaiSession(base_url, api_token)
76 self._kimai_clients = self._session.get('/api/customers').json()
77 self._client_ids = self.resolve_clients(clients)
78 kimai_user = self._session.get('/api/users/me').json()
79 self._tz = gettz(kimai_user['timezone'])
80
81 def resolve_clients(self, clients: Iterable[str]) -> frozenset[int]:
82 return frozenset({ client['id'] for client in self._kimai_clients if client['name'] in clients })
83
84 def render_datetime(self, datetime: datetime) -> str:
85 return datetime.astimezone(self._tz).strftime('%Y-%m-%dT%H:%M:%S')
86
87 def get_timesheets(self, params: dict[str, Any] = {}) -> Generator[Any]:
88 for page in count(start=1):
89 resp = self._session.get('/api/timesheets', params=params | {'size': 100, 'page': page})
90 if resp.status_code == 404:
91 break
92 yield from resp.json()
41 93
42class TogglAPISection(Enum): 94 def entry_durations(self, start_date: datetime, *, end_date: datetime, clients: Iterable[str] | None = None) -> Generator[timedelta]:
43 TOGGL = '/api/v9' 95 client_ids = None
44 REPORTS = '/reports/api/v2' 96 if clients is not None and not clients:
45
46class TogglAPIError(Exception):
47 def __init__(self, response, *, http_error=None):
48 self.http_error = http_error
49 self.response = response
50
51 def __str__(self):
52 if not self.http_error is None:
53 return str(self.http_error)
54 else:
55 return self.response.text
56
57class TogglAPI(object):
58 def __init__(self, api_token, workspace_id, client_ids):
59 self._api_token = api_token
60 self._workspace_id = workspace_id
61 self._client_ids = set(map(int, client_ids.split(','))) if client_ids else None
62
63 def _make_url(self, api=TogglAPISection.TOGGL, section=['me', 'time_entries', 'current'], params={}):
64 if api is TogglAPISection.REPORTS:
65 params.update({'user_agent': 'worktime', 'workspace_id': self._workspace_id})
66
67 api_path = api.value
68 section_path = '/'.join(section)
69 uri = uricompose(scheme='https', host='api.track.toggl.com', path=f"{api_path}/{section_path}", query=params)
70
71 return uri
72
73 def _query(self, url, method):
74 response = self._raw_query(url, method)
75 response.raise_for_status()
76 return response
77
78 @backoff.on_predicate(
79 backoff.expo,
80 factor=0.1, max_value=2,
81 predicate=lambda r: r.status_code == 429,
82 max_time=10,
83 )
84 def _raw_query(self, url, method):
85 headers = {'content-type': 'application/json', 'accept': 'application/json'}
86 response = None
87
88 if method == 'GET':
89 response = requests.get(url, headers=headers, auth=HTTPBasicAuth(self._api_token, 'api_token'))
90 elif method == 'POST':
91 response = requests.post(url, headers=headers, auth=HTTPBasicAuth(self._api_token, 'api_token'))
92 else:
93 raise ValueError(f"Undefined HTTP method “{method}”")
94
95 return response
96
97 def entry_durations(self, start_date, *, end_date, rounding=False, client_ids):
98 if client_ids is not None and not client_ids:
99 return 97 return
98 elif clients is None:
99 client_ids = self._client_ids
100 else:
101 client_ids = self.resolve_clients(clients)
100 102
101 cache_dir = Path(BaseDirectory.save_cache_path('worktime')) / 'entry_durations' 103 cache_dir = Path(BaseDirectory.save_cache_path('worktime')) / 'entry_durations'
102 step = timedelta(days = 120) 104 step = timedelta(days = 120)
@@ -115,11 +117,8 @@ class TogglAPI(object):
115 cache_key = blake2s(jsonpickle.encode({ 117 cache_key = blake2s(jsonpickle.encode({
116 'start': req_start, 118 'start': req_start,
117 'end': req_end, 119 'end': req_end,
118 'rounding': rounding, 120 'client_ids': client_ids,
119 'clients': client_ids, 121 }).encode('utf-8'), key = self._session.auth.token.encode('utf-8')).hexdigest()
120 'workspace': self._workspace_id,
121 'workspace_clients': self._client_ids
122 }).encode('utf-8'), key = self._api_token.encode('utf-8')).hexdigest()
123 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'
124 try: 123 try:
125 with cache_path.open('r', encoding='utf-8') as ch: 124 with cache_path.open('r', encoding='utf-8') as ch:
@@ -129,85 +128,83 @@ class TogglAPI(object):
129 pass 128 pass
130 129
131 entries = list() 130 entries = list()
132 params = { 'since': (req_start - timedelta(days=1)).date().isoformat(), 131 params = {
133 'until': (req_end + timedelta(days=1)).date().isoformat(), 132 'begin': self.render_datetime(req_start),
134 'rounding': 'yes' if rounding else 'no', 133 'end': self.render_datetime(req_end),
135 'billable': 'yes' 134 'customers[]': list(client_ids),
136 } 135 'billable': 1,
137 if client_ids is not None: 136 }
138 params |= { 'client_ids': ','.join(map(str, client_ids)) } 137
139 for page in count(start = 1): 138 for entry in self.get_timesheets(params):
140 url = self._make_url(api = TogglAPISection.REPORTS, section = ['details'], params = params | { 'page': page }) 139 if entry['end'] is None:
141 r = self._query(url = url, method='GET') 140 continue
142 if not r or not r.json(): 141
143 raise TogglAPIError(r) 142 start = isoparse(entry['begin'])
144 report = r.json() 143 end = isoparse(entry['end'])
145 for entry in report['data']:
146 start = isoparse(entry['start'])
147 end = isoparse(entry['end'])
148
149 if start > req_end or end < req_start:
150 continue
151 144
152 x = min(end, req_end) - max(start, req_start) 145 if start > req_end or end < req_start:
153 if cache_key: 146 continue
154 entries.append(x) 147
155 yield x 148 x = min(end, req_end) - max(start, req_start)
156 if not report['data']: 149 if cache_key:
157 break 150 entries.append(x)
151 yield x
158 152
159 if cache_path: 153 if cache_path:
160 cache_path.parent.mkdir(parents=True, exist_ok=True) 154 cache_path.parent.mkdir(parents=True, exist_ok=True)
161 with cache_path.open('w', encoding='utf-8') as ch: 155 with cache_path.open('w', encoding='utf-8') as ch:
162 ch.write(jsonpickle.encode(entries)) 156 ch.write(jsonpickle.encode(entries))
163 # res = timedelta(milliseconds=report['total_billable']) if report['total_billable'] else timedelta(milliseconds=0)
164 # return res
165
166 def get_billable_hours(self, start_date, end_date=datetime.now(timezone.utc), rounding=False):
167 billable_acc = timedelta(milliseconds = 0)
168 if 0 in self._client_ids:
169 url = self._make_url(api = TogglAPISection.TOGGL, section = ['workspaces', self._workspace_id, 'clients'])
170 r = self._query(url = url, method = 'GET')
171 if not r or not r.json():
172 raise TogglAPIError(r)
173
174 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))
175 157
176 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)) 158 def get_billable_hours(self, start_date: datetime, end_date: datetime = datetime.now(timezone.utc)) -> timedelta:
159 return sum(self.entry_durations(start_date, end_date=end_date), start=timedelta(milliseconds=0))
177 160
178 return billable_acc 161 def get_running_entry(self) -> Any | None:
162 kimai_entries = self._session.get('/api/timesheets/active').json()
163 if not kimai_entries:
164 return None
165 entry = kimai_entries[0]
179 166
180 def get_running_clock(self, now=datetime.now(timezone.utc)): 167 if entry['project']['customer']['id'] not in self._client_ids:
181 url = self._make_url(api = TogglAPISection.TOGGL, section = ['me', 'time_entries', 'current']) 168 return None
182 r = self._query(url = url, method='GET')
183 169
184 if not r or (not r.json() and r.json() is not None): 170 return entry
185 raise TogglAPIError(r)
186 171
187 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:
188 return None 175 return None
176 start = isoparse(entry['begin'])
177 return now - start if start <= now else None
189 178
190 if self._client_ids is not None: 179 def get_recent_entries(self) -> Generator[Any]:
191 if 'pid' in r.json() and r.json()['pid']: 180 step = timedelta(days = 7)
192 url = self._make_url(api = TogglAPISection.TOGGL, section = ['projects', str(r.json()['pid'])]) 181 now = datetime.now().astimezone(timezone.utc)
193 pr = self._query(url = url, method = 'GET') 182 ids = set()
194 if not pr or not pr.json(): 183 for req_end in (now - step * i for i in count()):
195 raise TogglAPIError(pr) 184 params = {
196 185 'begin': self.render_datetime(req_end - step),
197 if not pr.json(): 186 'end': self.render_datetime(req_end),
198 return None 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
199 194
200 if 'cid' in pr.json() and pr.json()['cid']: 195 def start_clock(self, project_id: int, activity_id: int, description: str | None = None, tags: Iterable[str] | None = None, billable: bool = True):
201 if pr.json()['cid'] not in self._client_ids: 196 self._session.post('/api/timesheets', json={
202 return None 197 'begin': self.render_datetime(datetime.now()),
203 elif 0 not in self._client_ids: 198 'project': project_id,
204 return None 199 'activity': activity_id,
205 elif 0 not in self._client_ids: 200 'description': description if description else '',
206 return None 201 'tags': (','.join(tags)) if tags else '',
202 'billable': billable,
203 }).raise_for_status()
207 204
208 start = isoparse(r.json()['start']) 205 def stop_clock(self, running_id: int):
206 self._session.patch(f'/api/timesheets/{running_id}/stop').raise_for_status()
209 207
210 return now - start if start <= now else None
211 208
212class Worktime(object): 209class Worktime(object):
213 time_worked = timedelta() 210 time_worked = timedelta()
@@ -223,6 +220,7 @@ class Worktime(object):
223 leave_budget = dict() 220 leave_budget = dict()
224 time_per_day = None 221 time_per_day = None
225 workdays = None 222 workdays = None
223 pull_forward = dict()
226 224
227 @staticmethod 225 @staticmethod
228 @cache 226 @cache
@@ -279,10 +277,10 @@ class Worktime(object):
279 277
280 config = Worktime.config() 278 config = Worktime.config()
281 config_dir = BaseDirectory.load_first_config('worktime') 279 config_dir = BaseDirectory.load_first_config('worktime')
282 api = TogglAPI( 280 api = KimaiAPI(
283 api_token=config.get("TOGGL", {}).get("ApiToken", None), 281 base_url=config.get("KIMAI", {}).get("BaseUrl", None),
284 workspace_id=config.get("TOGGL", {}).get("Workspace", None), 282 api_token=config.get("KIMAI", {}).get("ApiToken", None),
285 client_ids=config.get("TOGGL", {}).get("ClientIds", None) 283 clients=config.get("KIMAI", {}).get("Clients", None)
286 ) 284 )
287 date_format = config.get("WORKTIME", {}).get("DateFormat", '%Y-%m-%d') 285 date_format = config.get("WORKTIME", {}).get("DateFormat", '%Y-%m-%d')
288 286
@@ -377,10 +375,7 @@ class Worktime(object):
377 parse_datestr(stripped_line) 375 parse_datestr(stripped_line)
378 376
379 for day in [fromDay + timedelta(days = x) for x in range(0, (toDay - fromDay).days + 1)]: 377 for day in [fromDay + timedelta(days = x) for x in range(0, (toDay - fromDay).days + 1)]:
380 if self.end_date.date() < day or day < self.start_date.date(): 378 if self.would_be_workday(day) and self.start_date.date() <= day and day <= self.end_date.date():
381 continue
382
383 if self.would_be_workday(day):
384 if excused_kind == 'leave': 379 if excused_kind == 'leave':
385 self.leave_days.add(day) 380 self.leave_days.add(day)
386 elif time is not None and time >= self.time_per_day(day): 381 elif time is not None and time >= self.time_per_day(day):
@@ -390,14 +385,34 @@ class Worktime(object):
390 if e.errno != 2: 385 if e.errno != 2:
391 raise e 386 raise e
392 387
393 pull_forward = dict() 388 self.time_per_day = lambda day: timedelta(hours = hours_per_week(day)) / len(self.workdays) - (holidays[day] if day in holidays else timedelta())
394 389
395 start_day = self.start_date.date() 390 start_day = self.start_date.date()
396 end_day = self.end_date.date() 391 end_day = self.end_date.date()
397 392
393 self.extra_days_to_work = dict()
394
398 try: 395 try:
399 with open(Path(config_dir) / "pull-forward", 'r') as excused: 396 with open(Path(config_dir) / "days-to-work", 'r') as extra_days_to_work_file:
400 for line in excused: 397 for line in extra_days_to_work_file:
398 stripped_line = line.strip()
399 if stripped_line:
400 splitLine = stripped_line.split(' ')
401 if len(splitLine) == 2:
402 [hours, datestr] = splitLine
403 day = datetime.strptime(datestr, date_format).replace(tzinfo=tzlocal()).date()
404 self.extra_days_to_work[day] = timedelta(hours = float(hours))
405 else:
406 day = datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()
407 self.extra_days_to_work[day] = self.time_per_day(day)
408 except IOError as e:
409 if e.errno != 2:
410 raise e
411
412
413 try:
414 with open(Path(config_dir) / "pull-forward", 'r') as pull_forward:
415 for line in pull_forward:
401 stripped_line = line.strip() 416 stripped_line = line.strip()
402 if stripped_line: 417 if stripped_line:
403 [hours, datestr] = stripped_line.split(' ') 418 [hours, datestr] = stripped_line.split(' ')
@@ -418,44 +433,29 @@ class Worktime(object):
418 if not d == datetime.strptime(c, date_format).replace(tzinfo=tzlocal()).date(): break 433 if not d == datetime.strptime(c, date_format).replace(tzinfo=tzlocal()).date(): break
419 else: 434 else:
420 if d >= self.end_date.date(): 435 if d >= self.end_date.date():
421 pull_forward[d] = min(timedelta(hours = float(hours)), self.time_per_day(d) - (holidays[d] if d in holidays else timedelta())) 436 time_for_day = self.time_per_day(d) if d.isoweekday() in self.workdays else timedelta()
437 if d in self.extra_days_to_work:
438 time_for_day += self.extra_days_to_work[d]
439 self.pull_forward[d] = min(timedelta(hours = float(hours)), time_for_day)
422 except IOError as e: 440 except IOError as e:
423 if e.errno != 2: 441 if e.errno != 2:
424 raise e 442 raise e
425 443
444 if self.pull_forward:
445 for year in range(self.end_date.year + 1, max(self.pull_forward.keys()).year + 1):
446 holidays |= {k: v * timedelta(hours = hours_per_week(k)) / len(self.workdays) for k, v in Worktime.holidays(year).items()}
447
426 self.days_to_work = dict() 448 self.days_to_work = dict()
427 449
428 if pull_forward: 450 # if self.pull_forward:
429 end_day = max(end_day, max(list(pull_forward))) 451 # end_day = max(end_day, max(self.pull_forward.keys()))
430 452
431 for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]: 453 for day in [start_day + timedelta(days = x) for x in range(0, (end_day - start_day).days + 1)]:
432 if day.isoweekday() in self.workdays: 454 if day.isoweekday() in self.workdays:
433 time_to_work = self.time_per_day(day) 455 time_to_work = self.time_per_day(day)
434 if day in holidays.keys():
435 time_to_work -= holidays[day]
436 if time_to_work > timedelta(): 456 if time_to_work > timedelta():
437 self.days_to_work[day] = time_to_work 457 self.days_to_work[day] = time_to_work
438 458
439 self.extra_days_to_work = dict()
440
441 try:
442 with open(Path(config_dir) / "days-to-work", 'r') as extra_days_to_work_file:
443 for line in extra_days_to_work_file:
444 stripped_line = line.strip()
445 if stripped_line:
446 splitLine = stripped_line.split(' ')
447 if len(splitLine) == 2:
448 [hours, datestr] = splitLine
449 day = datetime.strptime(datestr, date_format).replace(tzinfo=tzlocal()).date()
450 self.extra_days_to_work[day] = timedelta(hours = float(hours))
451 else:
452 day = datetime.strptime(stripped_line, date_format).replace(tzinfo=tzlocal()).date()
453 self.extra_days_to_work[day] = self.time_per_day(day)
454 except IOError as e:
455 if e.errno != 2:
456 raise e
457
458
459 self.now_is_workday = self.is_workday(self.now.date()) 459 self.now_is_workday = self.is_workday(self.now.date())
460 460
461 self.time_worked = timedelta() 461 self.time_worked = timedelta()
@@ -470,33 +470,46 @@ class Worktime(object):
470 self.extra_days_to_work[self.now.date()] = timedelta() 470 self.extra_days_to_work[self.now.date()] = timedelta()
471 471
472 self.time_to_work = sum([self.days_to_work[day] for day in self.days_to_work.keys() if day <= self.end_date.date()], timedelta()) 472 self.time_to_work = sum([self.days_to_work[day] for day in self.days_to_work.keys() if day <= self.end_date.date()], timedelta())
473 for day in [d for d in list(pull_forward) if d > self.end_date.date()]: 473 for day in [d for d in list(self.pull_forward) if d > self.end_date.date()]:
474 days_forward = set([d for d in self.days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in pull_forward or d == self.end_date.date())]) 474 days_forward = set([d for d in [start_day + timedelta(days = x) for x in range(0, (day - start_day).days + 1)] if d >= self.end_date.date() and d < day and (d not in self.pull_forward or d == self.end_date.date())])
475 extra_days_forward = set([d for d in self.extra_days_to_work.keys() if d >= self.end_date.date() and d < day and (not d in pull_forward or d == self.end_date.date())]) 475 extra_days_forward = set([d for d in self.extra_days_to_work.keys() if d >= self.end_date.date() and d < day and (d not in self.pull_forward or d == self.end_date.date())])
476 days_forward = days_forward.union(extra_days_forward) 476 days_forward |= extra_days_forward
477 477
478 extra_day_time_left = timedelta() 478 extra_day_time_left = timedelta()
479 for extra_day in extra_days_forward: 479 for extra_day in extra_days_forward:
480 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day]) 480 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day])
481 extra_day_time_left += day_time 481 extra_day_time_left += day_time
482 extra_day_time = min(extra_day_time_left, pull_forward[day]) 482 extra_day_time = min(extra_day_time_left, self.pull_forward[day])
483 time_forward = pull_forward[day] - extra_day_time 483 time_forward = self.pull_forward[day] - extra_day_time
484 if extra_day_time_left > timedelta(): 484 if extra_day_time_left > timedelta():
485 for extra_day in extra_days_forward: 485 for extra_day in extra_days_forward:
486 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day]) 486 day_time = max(timedelta(), self.time_per_day(extra_day) - self.extra_days_to_work[extra_day])
487 self.extra_days_to_work[extra_day] += extra_day_time * (day_time / extra_day_time_left) 487 self.extra_days_to_work[extra_day] += extra_day_time * (day_time / extra_day_time_left)
488 488
489 hours_per_day_forward = time_forward / len(days_forward) if len(days_forward) > 0 else timedelta() 489 def days_count(days_forward):
490 r = 0
491 for day in sorted(days_forward):
492 day_time = timedelta()
493 if day in self.extra_days_to_work:
494 day_time += self.extra_days_to_work[day]
495 if day in holidays and not day in self.extra_days_to_work:
496 day_time -= holidays[day]
497 if day.isoweekday() in self.workdays:
498 day_time += timedelta(hours = hours_per_week(day)) / len(self.workdays)
499 r += max(timedelta(), day_time) / (timedelta(hours = hours_per_week(day)) / len(self.workdays))
500 return r
501
502 hours_per_day_forward = time_forward / days_count(days_forward) if days_count(days_forward) > 0 else timedelta()
490 days_forward.discard(self.end_date.date()) 503 days_forward.discard(self.end_date.date())
491 504
492 self.time_pulled_forward += time_forward - hours_per_day_forward * len(days_forward) 505 self.time_pulled_forward += time_forward - hours_per_day_forward * days_count(days_forward)
493 506
494 if self.end_date.date() in self.extra_days_to_work: 507 if self.end_date.date() in self.extra_days_to_work:
495 self.time_pulled_forward += self.extra_days_to_work[self.end_date.date()] 508 self.time_pulled_forward += self.extra_days_to_work[self.end_date.date()]
496 509
497 self.time_to_work += self.time_pulled_forward 510 # self.time_to_work += self.time_pulled_forward
498 511
499 self.time_worked += api.get_billable_hours(self.start_date, self.now, rounding = config.get("WORKTIME", {}).get("rounding", True)) 512 self.time_worked += api.get_billable_hours(self.start_date, self.now)
500 513
501def format_days(worktime, days, date_format=None): 514def format_days(worktime, days, date_format=None):
502 if not date_format: 515 if not date_format:
@@ -518,7 +531,14 @@ def format_days(worktime, days, date_format=None):
518 return ', '.join(map(lambda group: ','.join(map(format_group, group)), groups)) 531 return ', '.join(map(lambda group: ','.join(map(format_group, group)), groups))
519 532
520 533
521def worktime(**args): 534def tooltip_timedelta(td):
535 if td < timedelta(seconds = 0):
536 return "-" + tooltip_timedelta(-td)
537 mm, ss = divmod(td.total_seconds(), 60)
538 hh, mm = divmod(mm, 60)
539 return "%d:%02d:%02d" % (hh, mm, ss)
540
541def worktime(pull_forward_cutoff, waybar, **args):
522 worktime = Worktime(**args) 542 worktime = Worktime(**args)
523 543
524 def format_worktime(worktime): 544 def format_worktime(worktime):
@@ -557,24 +577,41 @@ def worktime(**args):
557 return f"{indicator}{difference_string}" 577 return f"{indicator}{difference_string}"
558 else: 578 else:
559 difference_string = difference_string(total_minutes_difference * timedelta(minutes = 1)) 579 difference_string = difference_string(total_minutes_difference * timedelta(minutes = 1))
560 if worktime.now_is_workday: 580 return difference_string
561 return difference_string 581
562 else: 582 out_class = "running" if worktime.running_entry else "stopped"
563 return f"({difference_string})" 583 difference = worktime.time_to_work - worktime.time_worked
564 584 if worktime.running_entry and -min(timedelta(milliseconds=0), difference) > sum(worktime.pull_forward.values(), start=timedelta(milliseconds=0)) or not worktime.running_entry and max(timedelta(milliseconds=0), difference) > worktime.time_per_day(worktime.now.date()) and worktime.now_is_workday:
565 if worktime.time_pulled_forward >= timedelta(minutes = 15): 585 out_class = "over"
586 pull_forward_sum = sum(worktime.pull_forward.values(), start=timedelta(milliseconds=0))
587 if pull_forward_sum >= min(pull_forward_cutoff, timedelta(seconds = 1)):
566 worktime_no_pulled_forward = deepcopy(worktime) 588 worktime_no_pulled_forward = deepcopy(worktime)
567 worktime_no_pulled_forward.time_to_work -= worktime_no_pulled_forward.time_pulled_forward 589 # worktime_no_pulled_forward.time_to_work -= worktime_no_pulled_forward.time_pulled_forward
568 worktime_no_pulled_forward.time_pulled_forward = timedelta() 590 worktime_no_pulled_forward.time_pulled_forward = timedelta()
591 worktime_no_pulled_forward.pull_forward = dict()
592 worktime.time_to_work += pull_forward_sum
569 593
570 difference_string = format_worktime(worktime)
571 difference_string_no_pulled_forward = format_worktime(worktime_no_pulled_forward) 594 difference_string_no_pulled_forward = format_worktime(worktime_no_pulled_forward)
572 595
573 print(f"{difference_string_no_pulled_forward}…{difference_string}") 596 tooltip = tooltip_timedelta(worktime_no_pulled_forward.time_to_work - worktime_no_pulled_forward.time_worked) + "…" + tooltip_timedelta(difference + pull_forward_sum)
597 if pull_forward_sum >= pull_forward_cutoff:
598 out_text = f"{difference_string_no_pulled_forward}…{format_worktime(worktime)}"
599 else:
600 out_text = format_worktime(worktime)
601 else:
602 tooltip = tooltip_timedelta(difference)
603 out_text = format_worktime(worktime)
604
605 if waybar:
606 json.dump({"text": out_text, "class": out_class, "tooltip": tooltip}, stdout)
574 else: 607 else:
575 print(format_worktime(worktime)) 608 print(out_text)
576 609
577def time_worked(now, **args): 610def pull_forward(**args):
611 worktime = Worktime(**args)
612 print(tooltip_timedelta(sum(worktime.pull_forward.values(), start=timedelta(milliseconds=0))))
613
614def time_worked(now, waybar, **args):
578 then = now.replace(hour = 0, minute = 0, second = 0, microsecond = 0) 615 then = now.replace(hour = 0, minute = 0, second = 0, microsecond = 0)
579 if now.time() == time(): 616 if now.time() == time():
580 now = now + timedelta(days = 1) 617 now = now + timedelta(days = 1)
@@ -584,33 +621,62 @@ def time_worked(now, **args):
584 621
585 worked = now.time_worked - then.time_worked 622 worked = now.time_worked - then.time_worked
586 623
624 out_text = None
625 out_class = "running" if now.running_entry else "stopped"
626 tooltip = tooltip_timedelta(worked)
627 target_time = max(then.time_per_day(then.now.date()), now.time_per_day(now.now.date())) if then.time_per_day(then.now.date()) and now.time_per_day(now.now.date()) else (then.time_per_day(then.now.date()) if then.time_per_day(then.now.date()) else now.time_per_day(now.now.date()))
628 difference = target_time - worked
629 difference_pull_forward = difference + now.time_pulled_forward
630 if now.running_entry and difference_pull_forward < timedelta(seconds=0):
631 out_class = "over"
587 if args['do_round']: 632 if args['do_round']:
588 total_minutes_difference = 5 * ceil(worked / timedelta(minutes = 5)) 633 total_minutes_difference = 5 * ceil(worked / timedelta(minutes = 5))
589 (hours_difference, minutes_difference) = divmod(abs(total_minutes_difference), 60) 634 (hours_difference, minutes_difference) = divmod(abs(total_minutes_difference), 60)
590 sign = '' if total_minutes_difference >= 0 else '-' 635 sign = '' if total_minutes_difference >= 0 else '-'
591
592 difference_string = f"{sign}"
593 if hours_difference != 0:
594 difference_string += f"{hours_difference}h"
595 if hours_difference == 0 or minutes_difference != 0:
596 difference_string += f"{minutes_difference}m"
597
598 clockout_time = None
599 clockout_difference = None
600 if then.now_is_workday or now.now_is_workday:
601 target_time = max(then.time_per_day(then.now.date()), now.time_per_day(now.now.date())) if then.time_per_day(then.now.date()) and now.time_per_day(now.now.date()) else (then.time_per_day(then.now.date()) if then.time_per_day(then.now.date()) else now.time_per_day(now.now.date()));
602 difference = target_time - worked
603 clockout_difference = 5 * ceil(difference / timedelta(minutes = 5))
604 clockout_time = now.now + difference
605 clockout_time += (5 - clockout_time.minute % 5) * timedelta(minutes = 1)
606 clockout_time = clockout_time.replace(second = 0, microsecond = 0)
607 636
608 if now.running_entry and clockout_time and clockout_difference >= 0: 637 difference_string = f"{sign}"
609 print(f"{difference_string}/{clockout_time:%H:%M}") 638 if hours_difference != 0:
610 else: 639 difference_string += f"{hours_difference}h"
611 print(difference_string) 640 if hours_difference == 0 or minutes_difference != 0:
641 difference_string += f"{minutes_difference}m"
642
643 def round_clockout_time(difference):
644 clockout_time = None
645 clockout_difference = None
646 exact_clockout_time = None
647 if then.now_is_workday or now.now_is_workday:
648 clockout_difference = 5 * ceil(difference / timedelta(minutes = 5))
649 clockout_time = now.now + difference
650 exact_clockout_time = clockout_time
651 clockout_time += (5 - clockout_time.minute % 5) * timedelta(minutes = 1)
652 clockout_time = clockout_time.replace(second = 0, microsecond = 0)
653
654 return clockout_time, exact_clockout_time, clockout_difference
655
656 clockout_time, exact_clockout_time, clockout_difference = round_clockout_time(difference)
657 clockout_time_pull_forward, exact_clockout_time_pull_forward, clockout_difference_pull_forward = round_clockout_time(difference_pull_forward)
658 clockout_pull_forward_sum, exact_clockout_pull_forward_sum, _ = round_clockout_time(now.time_to_work - now.time_worked + sum(now.pull_forward.values(), start=timedelta(milliseconds=0)))
659
660 if now.running_entry and clockout_time and (clockout_difference >= 0 or clockout_difference_pull_forward >= 0):
661 out_text = f"{difference_string}/{clockout_time:%H:%M}"
662 tooltip = f"{tooltip_timedelta(worked)}/{exact_clockout_time:%H:%M:%S}"
663
664 if clockout_pull_forward_sum >= clockout_time_pull_forward and clockout_time_pull_forward != clockout_time:
665 out_text += f"…{clockout_time_pull_forward:%H:%M}"
666 if exact_clockout_pull_forward_sum >= exact_clockout_time_pull_forward and exact_clockout_time_pull_forward != exact_clockout_time:
667 tooltip += f"…{exact_clockout_time_pull_forward:%H:%M:%S}"
668 else:
669 out_text = difference_string
612 else: 670 else:
613 print(worked) 671 out_text = str(worked)
672
673 if not now.now_is_workday:
674 out_text = f'({out_text})'
675
676 if waybar:
677 json.dump({"text": out_text, "class": out_class, "tooltip": tooltip}, stdout)
678 else:
679 print(out_text)
614 680
615def diff(now, **args): 681def diff(now, **args):
616 now = now.replace(hour = 0, minute = 0, second = 0, microsecond = 0) 682 now = now.replace(hour = 0, minute = 0, second = 0, microsecond = 0)
@@ -798,18 +864,54 @@ def classification(classification_name, table, table_format, **args):
798def main(): 864def main():
799 def isotime(s): 865 def isotime(s):
800 return datetime.fromisoformat(s).replace(tzinfo=tzlocal()) 866 return datetime.fromisoformat(s).replace(tzinfo=tzlocal())
867 def duration_minutes(s):
868 return timedelta(minutes = float(s))
869
870 def set_default_subparser(self, name, args=None, positional_args=0):
871 """default subparser selection. Call after setup, just before parse_args()
872 name: is the name of the subparser to call by default
873 args: if set is the argument list handed to parse_args()
874
875 , tested with 2.7, 3.2, 3.3, 3.4
876 it works with 2.6 assuming argparse is installed
877 """
878 subparser_found = False
879 for arg in sys.argv[1:]:
880 if arg in ['-h', '--help']: # global help if no subparser
881 break
882 else:
883 for x in self._subparsers._actions:
884 if not isinstance(x, argparse._SubParsersAction):
885 continue
886 for sp_name in x._name_parser_map.keys():
887 if sp_name in sys.argv[1:]:
888 subparser_found = True
889 if not subparser_found:
890 # insert default in last position before global positional
891 # arguments, this implies no global options are specified after
892 # first positional argument
893 if args is None:
894 sys.argv.insert(len(sys.argv) - positional_args, name)
895 else:
896 args.insert(len(args) - positional_args, name)
897
898 argparse.ArgumentParser.set_default_subparser = set_default_subparser
801 899
802 config = Worktime.config() 900 config = Worktime.config()
803 901
804 parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using toggl API') 902 parser = argparse.ArgumentParser(prog = "worktime", description = 'Track worktime using Kimai API')
805 parser.add_argument('--time', dest = 'now', metavar = 'TIME', type = isotime, help = 'Time to calculate status for (default: current time)', default = datetime.now(tzlocal())) 903 parser.add_argument('--time', dest = 'now', metavar = 'TIME', type = isotime, help = 'Time to calculate status for (default: current time)', default = datetime.now(tzlocal()))
806 parser.add_argument('--start', dest = 'start_datetime', metavar = 'TIME', type = isotime, help = 'Time to calculate status from (default: None)', default = None) 904 parser.add_argument('--start', dest = 'start_datetime', metavar = 'TIME', type = isotime, help = 'Time to calculate status from (default: None)', default = None)
807 parser.add_argument('--no-running', dest = 'include_running', action = 'store_false') 905 parser.add_argument('--no-running', dest = 'include_running', action = 'store_false')
808 parser.add_argument('--no-force-day-to-work', dest = 'force_day_to_work', action = 'store_false') 906 parser.add_argument('--no-force-day-to-work', dest = 'force_day_to_work', action = 'store_false')
809 subparsers = parser.add_subparsers(help = 'Subcommands') 907 subparsers = parser.add_subparsers(help = 'Subcommands')
810 parser.set_defaults(cmd = worktime) 908 worktime_parser = subparsers.add_parser('time_worked', aliases = ['time', 'worked'])
811 time_worked_parser = subparsers.add_parser('time_worked', aliases = ['time', 'worked', 'today']) 909 worktime_parser.add_argument('--pull-forward-cutoff', dest = 'pull_forward_cutoff', metavar = 'MINUTES', type = duration_minutes, default = timedelta(minutes = 15))
910 worktime_parser.add_argument('--waybar', action='store_true')
911 worktime_parser.set_defaults(cmd = worktime)
912 time_worked_parser = subparsers.add_parser('today')
812 time_worked_parser.add_argument('--no-round', dest = 'do_round', action = 'store_false') 913 time_worked_parser.add_argument('--no-round', dest = 'do_round', action = 'store_false')
914 time_worked_parser.add_argument('--waybar', action='store_true')
813 time_worked_parser.set_defaults(cmd = time_worked) 915 time_worked_parser.set_defaults(cmd = time_worked)
814 diff_parser = subparsers.add_parser('diff') 916 diff_parser = subparsers.add_parser('diff')
815 diff_parser.set_defaults(cmd = diff) 917 diff_parser.set_defaults(cmd = diff)
@@ -827,9 +929,146 @@ def main():
827 classification_parser.add_argument('--table', action = 'store_true') 929 classification_parser.add_argument('--table', action = 'store_true')
828 classification_parser.add_argument('--table-format', dest='table_format', type=str, default='fancy_grid') 930 classification_parser.add_argument('--table-format', dest='table_format', type=str, default='fancy_grid')
829 classification_parser.set_defaults(cmd = partial(classification, classification_name=classification_name)) 931 classification_parser.set_defaults(cmd = partial(classification, classification_name=classification_name))
932 pull_forward_parser = subparsers.add_parser('pull-forward')
933 pull_forward_parser.set_defaults(cmd = pull_forward)
934 parser.set_default_subparser('time_worked')
830 args = parser.parse_args() 935 args = parser.parse_args()
831 936
832 args.cmd(**vars(args)) 937 args.cmd(**vars(args))
833 938
939async def ui_update_options(api, cache_path):
940 options = set()
941 sort_order = dict()
942 entry_iter = enumerate(api.get_recent_entries())
943 loop = asyncio.get_event_loop()
944 start = clock_gettime_ns(CLOCK_MONOTONIC)
945 while item := await loop.run_in_executor(None, next, entry_iter):
946 ix, entry = item
947 if len(options) >= 20 or ix >= 1000:
948 break
949 elif len(options) >= 3:
950 now = clock_gettime_ns(CLOCK_MONOTONIC)
951 if now - start >= 4000000000:
952 break
953
954 option = frozendict({
955 'tags': frozenset(entry['tags']),
956 'activity': frozendict({'id': entry['activity']['id'], 'name': entry['activity']['name']}),
957 'project': frozendict({'id': entry['project']['id'], 'customer': entry['project']['customer']['name'], 'name': entry['project']['name']}),
958 'description': entry['description'] if entry['description'] else None,
959 'billable': entry['billable'],
960 })
961 sort_value = isoparse(entry['begin'])
962 if option in sort_order:
963 sort_value = max(sort_value, sort_order[option])
964 sort_order[option] = sort_value
965 options.add(option)
966
967 options = list(sorted(options, key = lambda o: sort_order[o], reverse = True))
968
969 with AtomicWriter(cache_path, overwrite=True) as ch:
970 ch.write_text(jsonpickle.encode(options))
971
972 return options
973
974def ui_render_option(option):
975 res = ''
976 if option['description']:
977 res += '„{}“, '.format(option['description'])
978 res += option['activity']['name'] + ', '
979 res += option['project']['name']
980 if option['project']['customer'] not in option['project']['name']:
981 res += ' ({})'.format(option['project']['customer'])
982 if option['tags']:
983 res += ', {}'.format(' '.join(map(lambda t: '#{}'.format(t), option['tags'])))
984 if not option['billable']:
985 res += ', not billable'
986 return res
987
988async def ui_main():
989 cache_path = Path(BaseDirectory.save_cache_path('worktime-ui')) / 'options.json'
990 options = None
991 try:
992 with cache_path.open('r', encoding='utf-8') as ch:
993 options = jsonpickle.decode(ch.read())
994 except FileNotFoundError:
995 pass
996
997 config = Worktime.config()
998 api = KimaiAPI(
999 base_url=config.get("KIMAI", {}).get("BaseUrl", None),
1000 api_token=config.get("KIMAI", {}).get("ApiToken", None),
1001 clients=config.get("KIMAI", {}).get("Clients", None)
1002 )
1003 running_entry = api.get_running_entry()
1004
1005 async with asyncio.TaskGroup() as tg:
1006 update_options = tg.create_task(ui_update_options(api, cache_path))
1007 if not options:
1008 options = await update_options
1009
1010 read_fd, write_fd = os.pipe()
1011 w_pipe = open(write_fd, 'wb', 0)
1012 loop = asyncio.get_event_loop()
1013 w_transport, _ = await loop.connect_write_pipe(
1014 asyncio.Protocol,
1015 w_pipe,
1016 )
1017 r_pipe = open(read_fd, 'rb', 0)
1018
1019 proc = await asyncio.create_subprocess_exec(
1020 "fuzzel", "--dmenu", "--index", "--width=60",
1021 stdout = asyncio.subprocess.PIPE,
1022 stdin = r_pipe,
1023 )
1024
1025 with closing(w_transport) as t:
1026 if running_entry:
1027 t.write(b'Stop running timesheet\n')
1028 for option in options:
1029 t.write(ui_render_option(option).encode('utf-8') + b'\n')
1030
1031 stdout, _ = await proc.communicate()
1032 if proc.returncode != 0:
1033 return
1034 fuzzel_out = int(stdout.decode('utf-8'))
1035 if fuzzel_out < 0 or fuzzel_out >= len(options):
1036 return
1037 elif running_entry and fuzzel_out == 0:
1038 api.stop_clock(running_entry['id'])
1039 await notify.Server('worktime').Notify("Stopped running timesheet").set_timeout(65000).show()
1040 else:
1041 if running_entry:
1042 fuzzel_out -= 1
1043 option = options[fuzzel_out]
1044 api.start_clock(
1045 project_id = option['project']['id'],
1046 activity_id = option['activity']['id'],
1047 description = option['description'],
1048 tags = option['tags'],
1049 billable = option['billable'],
1050 )
1051 await notify.Server('worktime').Notify("Timesheet started…").set_timeout(65000).show()
1052
1053
1054def ui():
1055 asyncio.run(ui_main())
1056
1057async def stop_main():
1058 config = Worktime.config()
1059 api = KimaiAPI(
1060 base_url=config.get("KIMAI", {}).get("BaseUrl", None),
1061 api_token=config.get("KIMAI", {}).get("ApiToken", None),
1062 clients=config.get("KIMAI", {}).get("Clients", None)
1063 )
1064 if running_entry := api.get_running_entry():
1065 api.stop_clock(running_entry['id'])
1066 await notify.Server('worktime').Notify("Stopped running timesheet").set_timeout(65000).show()
1067 else:
1068 await notify.Server('worktime').Notify("No timesheet currently running").set_timeout(65000).show()
1069
1070def stop():
1071 asyncio.run(stop_main())
1072
834if __name__ == "__main__": 1073if __name__ == "__main__":
835 sys.exit(main()) 1074 sys.exit(main())
diff --git a/overlays/wttrbar/default.nix b/overlays/wttrbar/default.nix
deleted file mode 100644
index 876fa699..00000000
--- a/overlays/wttrbar/default.nix
+++ /dev/null
@@ -1,7 +0,0 @@
1{ prev, ... }: {
2 wttrbar = prev.wttrbar.overrideAttrs (oldAttrs: {
3 patches = (oldAttrs.patches or []) ++ [
4 ./icons.patch
5 ];
6 });
7}
diff --git a/overlays/wttrbar/icons.patch b/overlays/wttrbar/icons.patch
deleted file mode 100644
index e7e721c8..00000000
--- a/overlays/wttrbar/icons.patch
+++ /dev/null
@@ -1,154 +0,0 @@
1diff --git a/src/constants.rs b/src/constants.rs
2index 81b1926..3619d8f 100644
3--- a/src/constants.rs
4+++ b/src/constants.rs
5@@ -1,64 +1,52 @@
6 pub const WEATHER_CODES: &[(i32, &str)] = &[
7- (113, "☀️"),
8- (116, "🌤️"),
9- (119, "☁️"),
10- (122, "🌥️"),
11- (143, "🌫️"),
12- (176, "🌦️"),
13- (179, "🌧️"),
14- (182, "🌨️"),
15- (185, "🌨️"),
16- (200, "🌩️"),
17- (227, "❄️"),
18- (230, "❄️"),
19- (248, "🌫️"),
20- (260, "🌫️"),
21- (263, "🌧️"),
22- (266, "🌧️"),
23- (281, "🌦️"),
24- (284, "🌦️"),
25- (293, "🌧️"),
26- (296, "🌧️"),
27- (299, "🌧️"),
28- (302, "🌧️"),
29- (305, "🌧️"),
30- (308, "🌧️"),
31- (311, "🌧️"),
32- (314, "🌧️"),
33- (317, "🌧️"),
34- (320, "🌨️"),
35- (323, "🌨️"),
36- (326, "🌨️"),
37- (329, "🌨️"),
38- (332, "🌨️"),
39- (335, "🌨️"),
40- (338, "🌨️"),
41- (350, "🌨️"),
42- (353, "🌧️"),
43- (356, "🌧️"),
44- (359, "🌧️"),
45- (362, "🌨️"),
46- (365, "🌨️"),
47- (368, "🌨️"),
48- (371, "🌨️"),
49- (374, "🌨️"),
50- (377, "🌨️"),
51- (386, "🌩️"),
52- (389, "🌨️"),
53- (392, "🌨️"),
54- (395, "🌨️"),
55- (398, "🌨️"),
56- (401, "🌨️"),
57- (404, "🌨️"),
58- (407, "🌨️"),
59- (410, "🌨️"),
60- (413, "🌨️"),
61- (416, "🌨️"),
62- (419, "🌨️"),
63- (422, "🌨️"),
64- (425, "🌨️"),
65- (428, "🌨️"),
66- (431, "🌨️"),
67+ (113, "<span font=\"Symbols Nerd Font Mono\"></span>"),
68+ (116, "<span font=\"Symbols Nerd Font Mono\"></span>"),
69+ (119, "<span font=\"Symbols Nerd Font Mono\"></span>"),
70+ (122, "<span font=\"Symbols Nerd Font Mono\"></span>"),
71+ (143, "<span font=\"Symbols Nerd Font Mono\"></span>"),
72+ (176, "<span font=\"Symbols Nerd Font Mono\"></span>"),
73+ (179, "<span font=\"Symbols Nerd Font Mono\"></span>"),
74+ (182, "<span font=\"Symbols Nerd Font Mono\"></span>"),
75+ (185, "<span font=\"Symbols Nerd Font Mono\"></span>"),
76+ (200, "<span font=\"Symbols Nerd Font Mono\"></span>"),
77+ (227, "<span font=\"Symbols Nerd Font Mono\"></span>"),
78+ (230, "<span font=\"Symbols Nerd Font Mono\"></span>"),
79+ (248, "<span font=\"Symbols Nerd Font Mono\"></span>"),
80+ (260, "<span font=\"Symbols Nerd Font Mono\"></span>"),
81+ (263, "<span font=\"Symbols Nerd Font Mono\"></span>"),
82+ (266, "<span font=\"Symbols Nerd Font Mono\"></span>"),
83+ (281, "<span font=\"Symbols Nerd Font Mono\"></span>"),
84+ (284, "<span font=\"Symbols Nerd Font Mono\"></span>"),
85+ (293, "<span font=\"Symbols Nerd Font Mono\"></span>"),
86+ (296, "<span font=\"Symbols Nerd Font Mono\"></span>"),
87+ (299, "<span font=\"Symbols Nerd Font Mono\"></span>"),
88+ (302, "<span font=\"Symbols Nerd Font Mono\"></span>"),
89+ (305, "<span font=\"Symbols Nerd Font Mono\"></span>"),
90+ (308, "<span font=\"Symbols Nerd Font Mono\"></span>"),
91+ (311, "<span font=\"Symbols Nerd Font Mono\"></span>"),
92+ (314, "<span font=\"Symbols Nerd Font Mono\"></span>"),
93+ (317, "<span font=\"Symbols Nerd Font Mono\"></span>"),
94+ (320, "<span font=\"Symbols Nerd Font Mono\"></span>"),
95+ (323, "<span font=\"Symbols Nerd Font Mono\"></span>"),
96+ (326, "<span font=\"Symbols Nerd Font Mono\"></span>"),
97+ (329, "<span font=\"Symbols Nerd Font Mono\"></span>"),
98+ (332, "<span font=\"Symbols Nerd Font Mono\"></span>"),
99+ (335, "<span font=\"Symbols Nerd Font Mono\"></span>"),
100+ (338, "<span font=\"Symbols Nerd Font Mono\"></span>"),
101+ (350, "<span font=\"Symbols Nerd Font Mono\"></span>"),
102+ (353, "<span font=\"Symbols Nerd Font Mono\"></span>"),
103+ (356, "<span font=\"Symbols Nerd Font Mono\"></span>"),
104+ (359, "<span font=\"Symbols Nerd Font Mono\"></span>"),
105+ (362, "<span font=\"Symbols Nerd Font Mono\"></span>"),
106+ (365, "<span font=\"Symbols Nerd Font Mono\"></span>"),
107+ (368, "<span font=\"Symbols Nerd Font Mono\"></span>"),
108+ (371, "<span font=\"Symbols Nerd Font Mono\"></span>"),
109+ (374, "<span font=\"Symbols Nerd Font Mono\"></span>"),
110+ (377, "<span font=\"Symbols Nerd Font Mono\"></span>"),
111+ (386, "<span font=\"Symbols Nerd Font Mono\"></span>"),
112+ (389, "<span font=\"Symbols Nerd Font Mono\"></span>"),
113+ (392, "<span font=\"Symbols Nerd Font Mono\"></span>"),
114+ (395, "<span font=\"Symbols Nerd Font Mono\"></span>"),
115 ];
116
117 pub const ICON_PLACEHOLDER: &str = "{ICON}";
118diff --git a/src/main.rs b/src/main.rs
119index 6ac4654..1b84207 100644
120--- a/src/main.rs
121+++ b/src/main.rs
122@@ -175,20 +175,20 @@ fn main() {
123
124 if args.fahrenheit {
125 tooltip += &format!(
126- "⬆️ {}° ⬇️ {}° ",
127+ "<span font=\"Symbols Nerd Font Mono\">󰸃</span> {}° <span font=\"Symbols Nerd Font Mono\">󰸂</span> {}° ",
128 day["maxtempF"].as_str().unwrap(),
129 day["mintempF"].as_str().unwrap(),
130 );
131 } else {
132 tooltip += &format!(
133- "⬆️ {}° ⬇️ {}° ",
134+ "<span font=\"Symbols Nerd Font Mono\">󰸃</span> {}° <span font=\"Symbols Nerd Font Mono\">󰸂</span> {}° ",
135 day["maxtempC"].as_str().unwrap(),
136 day["mintempC"].as_str().unwrap(),
137 );
138 };
139
140 tooltip += &format!(
141- "🌅 {} 🌇 {}\n",
142+ "<span font=\"Symbols Nerd Font Mono\"></span> {} <span font=\"Symbols Nerd Font Mono\"></span> {}\n",
143 format_ampm_time(day, "sunrise", args.ampm),
144 format_ampm_time(day, "sunset", args.ampm),
145 );
146@@ -207,7 +207,7 @@ fn main() {
147 }
148
149 let mut tooltip_line = format!(
150- "{} {} {} {}",
151+ "{} {}{} {}",
152 format_time(hour["time"].as_str().unwrap(), args.ampm),
153 WEATHER_CODES
154 .iter()
diff --git a/overlays/yt-dlp.nix b/overlays/yt-dlp.nix
index 94ab1fdd..9a54a32b 100644
--- a/overlays/yt-dlp.nix
+++ b/overlays/yt-dlp.nix
@@ -1,5 +1,7 @@
1{ prev, sources, ... }: { 1{ prev, sources, ... }: {
2 yt-dlp = prev.yt-dlp.overrideAttrs (oldAttrs: { 2 yt-dlp = prev.yt-dlp.overrideAttrs (oldAttrs: {
3 inherit (sources.yt-dlp) pname version src; 3 inherit (sources.yt-dlp) pname version src;
4
5 postPatch = "";
4 }); 6 });
5} 7}
diff --git a/overlays/zte-prometheus-exporter/default.nix b/overlays/zte-prometheus-exporter/default.nix
index 2188e7b3..6295567d 100644
--- a/overlays/zte-prometheus-exporter/default.nix
+++ b/overlays/zte-prometheus-exporter/default.nix
@@ -1,18 +1,17 @@
1{ final, prev, ... }: 1{ final, prev, ... }:
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.python3.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/overlays/zte-prometheus-exporter/zte-prometheus-exporter.py b/overlays/zte-prometheus-exporter/zte-prometheus-exporter.py
index fc719a96..bc8326ff 100644
--- a/overlays/zte-prometheus-exporter/zte-prometheus-exporter.py
+++ b/overlays/zte-prometheus-exporter/zte-prometheus-exporter.py
@@ -54,13 +54,13 @@ class ZTEMetrics:
54 cls._instance.password = environ.get('ZTE_PASSWORD') 54 cls._instance.password = environ.get('ZTE_PASSWORD')
55 cls._instance.attrs = None 55 cls._instance.attrs = None
56 return cls._instance 56 return cls._instance
57 57
58 58
59 def __init__(self): 59 def __init__(self):
60 raise RuntimeError('Call instance() instead') 60 raise RuntimeError('Call instance() instead')
61 61
62 _error_pattern = re.compile('^IF_ERROR(PARAM|TYPE|STR|ID)$') 62 _error_pattern = re.compile(r'^IF_ERROR(PARAM|TYPE|STR|ID)$')
63 _obj_pattern = re.compile('^(?:OBJ_(.+)_ID)|(?:ID_(WAN_COMFIG))$') 63 _obj_pattern = re.compile(r'^(?:OBJ_(.+)_ID)|(?:ID_(WAN_COMFIG))$')
64 def update(self): 64 def update(self):
65 attrs = dict() 65 attrs = dict()
66 66
@@ -106,6 +106,8 @@ class ZTEMetrics:
106 value = child.text 106 value = child.text
107 case _: 107 case _:
108 pass 108 pass
109 if value == '0,0':
110 value = '0'
109 if not name is None and not value is None: 111 if not name is None and not value is None:
110 instance_dict[name] = value 112 instance_dict[name] = value
111 name = None 113 name = None
@@ -120,8 +122,8 @@ class ZTEMetrics:
120 def json_text(self): 122 def json_text(self):
121 return json.dumps(self.attrs) 123 return json.dumps(self.attrs)
122 124
123 _link_pattern = re.compile('^IGD\.WD1\.LINE([0-9]+)$') 125 _link_pattern = re.compile(r'^IGD\.WD1\.LINE([0-9]+)$')
124 _eth_pattern = re.compile('^IGD\.LD1\.ETH([0-9]+)$') 126 _eth_pattern = re.compile(r'^IGD\.LD1\.ETH([0-9]+)$')
125 def prometheus(self): 127 def prometheus(self):
126 metrics = '' 128 metrics = ''
127 129
@@ -133,34 +135,41 @@ class ZTEMetrics:
133 link_match = self._link_pattern.match(link) 135 link_match = self._link_pattern.match(link)
134 link_number = link_match.group(1) 136 link_number = link_match.group(1)
135 137
136 if 'crc_errors_count' not in link_metrics: 138 link_is_up = self.attrs['DSLINTERFACE'][link]['Status'] == 'Up'
137 link_metrics['crc_errors_count'] = {'type': 'counter', 'metrics': []} 139
138 link_metrics['crc_errors_count']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['UpCrc_errors']))] 140 if link_is_up:
139 link_metrics['crc_errors_count']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['DownCrc_errors']))] 141 if 'crc_errors_count' not in link_metrics:
142 link_metrics['crc_errors_count'] = {'type': 'counter', 'metrics': []}
143 link_metrics['crc_errors_count']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['UpCrc_errors']))]
144 link_metrics['crc_errors_count']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['DownCrc_errors']))]
140 145
141 if 'noise_margin_db' not in link_metrics: 146 if 'noise_margin_db' not in link_metrics:
142 link_metrics['noise_margin_db'] = {'type': 'gauge', 'metrics': []} 147 link_metrics['noise_margin_db'] = {'type': 'gauge', 'metrics': []}
143 link_metrics['noise_margin_db']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_noise_margin']))] 148 link_metrics['noise_margin_db']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_noise_margin']) / 10)]
144 link_metrics['noise_margin_db']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_noise_margin']))] 149 link_metrics['noise_margin_db']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_noise_margin']) / 10)]
145 150
146 if 'attenuation_db' not in link_metrics: 151 if 'attenuation_db' not in link_metrics:
147 link_metrics['attenuation_db'] = {'type': 'gauge', 'metrics': []} 152 link_metrics['attenuation_db'] = {'type': 'gauge', 'metrics': []}
148 link_metrics['attenuation_db']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_attenuation']))] 153 link_metrics['attenuation_db']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_attenuation']) / 10)]
149 link_metrics['attenuation_db']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_attenuation']))] 154 link_metrics['attenuation_db']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_attenuation']) / 10)]
150 155
151 if 'max_rate_kbps' not in link_metrics: 156 if 'max_rate_kbps' not in link_metrics:
152 link_metrics['max_rate_kbps'] = {'type': 'gauge', 'metrics': []} 157 link_metrics['max_rate_kbps'] = {'type': 'gauge', 'metrics': []}
153 link_metrics['max_rate_kbps']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_max_rate']))] 158 link_metrics['max_rate_kbps']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_max_rate']))]
154 link_metrics['max_rate_kbps']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_max_rate']))] 159 link_metrics['max_rate_kbps']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_max_rate']))]
155 160
156 if 'current_rate_kbps' not in link_metrics: 161 if 'current_rate_kbps' not in link_metrics:
157 link_metrics['current_rate_kbps'] = {'type': 'gauge', 'metrics': []} 162 link_metrics['current_rate_kbps'] = {'type': 'gauge', 'metrics': []}
158 link_metrics['current_rate_kbps']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_current_rate']))] 163 link_metrics['current_rate_kbps']['metrics'] += [({"direction": "up", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Upstream_current_rate']))]
159 link_metrics['current_rate_kbps']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_current_rate']))] 164 link_metrics['current_rate_kbps']['metrics'] += [({"direction": "down", "link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Downstream_current_rate']))]
160 165
161 if 'dsl_uptime_seconds' not in link_metrics: 166 if 'dsl_uptime_seconds' not in link_metrics:
162 link_metrics['dsl_uptime_seconds'] = {'type': 'gauge', 'metrics': []} 167 link_metrics['uptime_seconds'] = {'type': 'gauge', 'metrics': []}
163 link_metrics['dsl_uptime_seconds']['metrics'] += [({"link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Showtime_start']))] 168 link_metrics['uptime_seconds']['metrics'] += [({"link": link_number}, int(self.attrs['DSLINTERFACE'][link]['Showtime_start']) if link_is_up else 0)]
169
170 if 'status' not in link_metrics:
171 link_metrics['status'] = {'type': 'gauge', 'metrics': []}
172 link_metrics['status']['metrics'] += [({"link": link_number, "status": self.attrs['DSLINTERFACE'][link]['Status']}, 1)]
164 if link_metrics: 173 if link_metrics:
165 for metric_name in link_metrics: 174 for metric_name in link_metrics:
166 metrics += _format_prom_metrics(f'dsl_{metric_name}', link_metrics[metric_name]['type'], link_metrics[metric_name]['metrics']) 175 metrics += _format_prom_metrics(f'dsl_{metric_name}', link_metrics[metric_name]['type'], link_metrics[metric_name]['metrics'])
@@ -203,7 +212,7 @@ class ZTEMetricsServer(BaseHTTPRequestHandler):
203 self.send_response(200) 212 self.send_response(200)
204 self.send_header("Content-type", "text/plain") 213 self.send_header("Content-type", "text/plain")
205 self.end_headers() 214 self.end_headers()
206 215
207 self.wfile.write(zte_metrics.prometheus()) 216 self.wfile.write(zte_metrics.prometheus())
208 case _: 217 case _:
209 self.send_response(404) 218 self.send_response(404)
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/bcachefs.nix b/system-profiles/bcachefs.nix
index f9f048b9..be12bf20 100644
--- a/system-profiles/bcachefs.nix
+++ b/system-profiles/bcachefs.nix
@@ -1,6 +1,16 @@
1{ pkgs, ... } : { 1{ pkgs, lib, ... } : {
2 config = { 2 config = {
3 boot.supportedFilesystems.bcachefs = true; 3 boot.supportedFilesystems.bcachefs = true;
4 environment.systemPackages = with pkgs; [ bcachefs-tools ]; 4 environment.systemPackages = with pkgs; [ bcachefs-tools ];
5
6 boot.kernelPatches = [
7 {
8 name = "bcachefs-casefold-fix";
9 patch = null;
10 structuredExtraConfig = with lib.kernel; {
11 UNICODE = lib.mkOverride 90 no;
12 };
13 }
14 ];
5 }; 15 };
6} 16}
diff --git a/system-profiles/core/default.nix b/system-profiles/core/default.nix
index 71d0619a..e5f9dc16 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,11 +157,9 @@ 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; }; 162 extraSpecialArgs = { inherit flake flakeInputs path; hostConfig = config; };
185 }; 163 };
186 164
187 sops = mkIf hasSops { 165 sops = mkIf hasSops {
@@ -202,17 +180,22 @@ in {
202 }; 180 };
203 environment.systemPackages = with pkgs; [ git-annex scutiger ]; 181 environment.systemPackages = with pkgs; [ git-annex scutiger ];
204 } 182 }
205 ] ++ (optional (options ? system.switch.enableNg) { 183 ] ++ (optional (options ? system.rebuild.enableNg) {
206 system.switch = lib.mkDefault { 184 system.rebuild.enableNg = lib.mkDefault true;
207 enable = false; 185 })
208 enableNg = true; 186 ++ (optional (options ? services.userborn) {
187 services.userborn = {
188 enable = lib.mkDefault true;
189 passwordFilesLocation = lib.mkDefault "/var/lib/nixos";
209 }; 190 };
210 }) 191 })
192 ++ (optional (!(options ? services.userborn) && (options ? system.etc)) {
193 systemd.sysusers.enable = lib.mkDefault true;
194 })
211 ++ (optional (options ? system.etc) { 195 ++ (optional (options ? system.etc) {
212 boot.initrd.systemd.enable = lib.mkDefault true; 196 boot.initrd.systemd.enable = lib.mkDefault true;
213 system.etc.overlay.enable = lib.mkDefault true; 197 system.etc.overlay.enable = lib.mkDefault true;
214 system.etc.overlay.mutable = lib.mkDefault (!config.systemd.sysusers.enable); 198 system.etc.overlay.mutable = lib.mkDefault (!config.systemd.sysusers.enable);
215 systemd.sysusers.enable = lib.mkDefault true;
216 199
217 # Random perl remnants 200 # Random perl remnants
218 system.disableInstallerTools = lib.mkDefault true; 201 system.disableInstallerTools = lib.mkDefault true;
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/initrd-all-crypto-modules.nix b/system-profiles/initrd-all-crypto-modules.nix
index 45cd4b74..da6c781e 100644
--- a/system-profiles/initrd-all-crypto-modules.nix
+++ b/system-profiles/initrd-all-crypto-modules.nix
@@ -18,7 +18,7 @@ in {
18 { 18 {
19 name = "encrypted_key"; 19 name = "encrypted_key";
20 patch = null; 20 patch = null;
21 extraStructuredConfig.ENCRYPTED_KEYS = lib.kernel.yes; 21 structuredExtraConfig.ENCRYPTED_KEYS = lib.kernel.yes;
22 } 22 }
23 ]; 23 ];
24} 24}
diff --git a/system-profiles/lanzaboote.nix b/system-profiles/lanzaboote.nix
new file mode 100644
index 00000000..f1e179cf
--- /dev/null
+++ b/system-profiles/lanzaboote.nix
@@ -0,0 +1,14 @@
1{ flakeInputs, pkgs, ... }:
2{
3 imports = [
4 flakeInputs.lanzaboote.nixosModules.lanzaboote
5 ];
6
7 config = {
8 environment.systemPackages = [ pkgs.sbctl ];
9 boot.lanzaboote = {
10 enable = true;
11 pkiBundle = "/var/lib/sbctl";
12 };
13 };
14}
diff --git a/system-profiles/nfsroot.nix b/system-profiles/nfsroot.nix
index 1cd930d9..e3dc2d2e 100644
--- a/system-profiles/nfsroot.nix
+++ b/system-profiles/nfsroot.nix
@@ -1,4 +1,4 @@
1{ config, options, pkgs, lib, flake, flakeInputs, ... }: 1{ config, options, pkgs, lib, flake, ... }:
2 2
3with lib; 3with lib;
4 4
@@ -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
@@ -86,7 +86,7 @@ in {
86 mkdir -p /mnt-root/etc/ 86 mkdir -p /mnt-root/etc/
87 cp /etc/resolv.conf /mnt-root/etc/resolv.conf 87 cp /etc/resolv.conf /mnt-root/etc/resolv.conf
88 ''; 88 '';
89 networking.useDHCP = true; 89 networking.useDHCP = mkImageMediaOverride true;
90 networking.resolvconf.enable = false; 90 networking.resolvconf.enable = false;
91 networking.dhcpcd.persistent = true; 91 networking.dhcpcd.persistent = true;
92 92
diff --git a/system-profiles/niri-flake.nix b/system-profiles/niri-flake.nix
new file mode 100644
index 00000000..b28d51ff
--- /dev/null
+++ b/system-profiles/niri-flake.nix
@@ -0,0 +1,4 @@
1{ ... }:
2{
3 config.niri-flake.cache.enable = false;
4}
diff --git a/system-profiles/niri-unstable.nix b/system-profiles/niri-unstable.nix
new file mode 100644
index 00000000..3a8b393d
--- /dev/null
+++ b/system-profiles/niri-unstable.nix
@@ -0,0 +1,11 @@
1{ config, pkgs, lib, ... }:
2{
3 config = {
4 programs.niri.package = lib.mkDefault pkgs.niri-unstable;
5 home-manager.sharedModules = [
6 {
7 programs.niri.package = lib.mkDefault config.programs.niri.package;
8 }
9 ];
10 };
11}
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/system-profiles/zfs.nix b/system-profiles/zfs.nix
index 149decee..af9f1c17 100644
--- a/system-profiles/zfs.nix
+++ b/system-profiles/zfs.nix
@@ -1,8 +1,8 @@
1{ pkgs, lib, ... } : { 1{ config, pkgs, lib, ... } : {
2 config = { 2 config = {
3 boot = { 3 boot = {
4 kernelPackages = pkgs.linuxPackages_6_11; 4 kernelPackages = pkgs.linuxPackages_6_12;
5 zfs.package = pkgs.zfs_unstable; 5 zfs.package = pkgs.zfs_2_3;
6 6
7 supportedFilesystems.zfs = true; 7 supportedFilesystems.zfs = true;
8 }; 8 };
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/mpv/default.nix b/user-profiles/mpv/default.nix
index 2df87994..8cf330e8 100644
--- a/user-profiles/mpv/default.nix
+++ b/user-profiles/mpv/default.nix
@@ -1,6 +1,6 @@
1{ config, lib, userName, pkgs, sources, ... }: 1{ lib, userName, pkgs, sources, ... }:
2{ 2{
3 home-manager.users.${userName} = { 3 home-manager.users.${userName} = { config, ... }: {
4 programs.mpv = { 4 programs.mpv = {
5 enable = true; 5 enable = true;
6 package = pkgs.symlinkJoin { 6 package = pkgs.symlinkJoin {
@@ -10,7 +10,7 @@
10 (pkgs.stdenv.mkDerivation (sources.mpv-reload // rec { 10 (pkgs.stdenv.mkDerivation (sources.mpv-reload // rec {
11 installPhase = '' 11 installPhase = ''
12 install -d $out/share/mpv/scripts 12 install -d $out/share/mpv/scripts
13 install -m 0644 reload.lua $out/share/mpv/scripts/${passthru.scriptName} 13 install -m 0644 main.lua $out/share/mpv/scripts/${passthru.scriptName}
14 ''; 14 '';
15 15
16 passthru.scriptName = "reload.lua"; 16 passthru.scriptName = "reload.lua";
@@ -105,6 +105,8 @@
105 }; 105 };
106 config = { 106 config = {
107 ytdl = true; 107 ytdl = true;
108 ytdl-format = "ytdl";
109 # ytdl-raw-options = "sub-langs=\"${config.programs.yt-dlp.settings.sub-langs}\"";
108 subs-with-matching-audio = false; 110 subs-with-matching-audio = false;
109 audio-display = false; 111 audio-display = false;
110 osd-font = "Fira Sans"; 112 osd-font = "Fira Sans";
diff --git a/user-profiles/tmux/default.nix b/user-profiles/tmux/default.nix
index 11c53788..7ea0c0d5 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 ${lib.getExe config.programs.zsh.package} \
20 --subst-var-by man ${lib.getExe 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..edf6da11 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,25 @@ in {
55 }; 42 };
56 43
57 jq.enable = true; 44 jq.enable = true;
45
46 lesspipe.enable = true;
47
48 man.enable = true;
49
50 vim.enable = true;
58 }; 51 };
59 52
60 home.sessionVariables = { 53 home.sessionVariables = {
61 LESSCOLORIZER = "pygmentize -O style=rrt"; 54 LESSCOLORIZER = "${lib.getExe' pkgs.python3Packages.pygments "pygmentize"} -O style=rrt";
62 }; 55 };
63 56
64 home.packages = with pkgs; [ 57 home.packages = with pkgs; [
65 autossh usbutils pciutils eza silver-searcher pwgen xkcdpass 58 autossh usbutils pciutils eza silver-searcher pwgen xkcdpass
66 unzip magic-wormhole qrencode tty-clock dnsutils openssl sshfs 59 unzip magic-wormhole dnsutils openssl sshfs
67 psmisc mosh tree vnstat file pv bc zip nmap aspell 60 psmisc mosh tree vnstat file pv bc zip nmap aspell
68 aspellDicts.de aspellDicts.en borgbackup man-pages rsync socat 61 aspellDicts.de aspellDicts.en borgbackup man-pages rsync socat
69 inetutils yq cached-nix-shell persistent-nix-shell rage 62 inetutils yq cached-nix-shell persistent-nix-shell rage
70 smartmontools hdparm nix-output-monitor wrappedLess dscp 63 smartmontools hdparm nix-output-monitor less dscp
71 iputils 64 iputils
72 ]; 65 ];
73 }; 66 };
diff --git a/user-profiles/yt-dlp.nix b/user-profiles/yt-dlp.nix
index 0f0b2204..eefa673f 100644
--- a/user-profiles/yt-dlp.nix
+++ b/user-profiles/yt-dlp.nix
@@ -7,17 +7,26 @@
7 cookies-from-browser = "firefox::none"; 7 cookies-from-browser = "firefox::none";
8 mark-watched = true; 8 mark-watched = true;
9 format = lib.concatStringsSep "/" [ 9 format = lib.concatStringsSep "/" [
10 "bestvideo*[width<=2560][height<=1440][fps<=60][vcodec!*=av01][width>=1920]+bestaudio"
11 "best[width<=2560][height<=1440][fps<=60][vcodec!*=av01][width>=1920]"
12 "bestvideo*[vcodec!*=av01][width>=1920]+bestaudio"
13 "best[vcodec!*=av01][width>=1920]"
14 "bestvideo*[width<=2560][height<=1440][fps<=60][vcodec!*=av01][height>=1080]+bestaudio"
15 "best[width<=2560][height<=1440][fps<=60][vcodec!*=av01][height>=1080]"
16 "bestvideo*[vcodec!*=av01][height>=1080]+bestaudio"
17 "best[vcodec!*=av01][height>=1080]"
10 "bestvideo*[width<=2560][height<=1440][fps<=60]+bestaudio" 18 "bestvideo*[width<=2560][height<=1440][fps<=60]+bestaudio"
11 "best[width<=2560][height<=1440][fps<=60]" 19 "best[width<=2560][height<=1440][fps<=60]"
12 "bestvideo*+bestaudio" 20 "bestvideo*+bestaudio"
13 "best" 21 "best"
14 ]; 22 ];
15 embed-subs = true; 23 # embed-subs = true;
24 embed-thumbnail = true;
25 embed-metadata = true;
16 # write-subs = true; 26 # write-subs = true;
17 # write-auto-subs = true; 27 # write-auto-subs = true;
18 sub-langs = "en(-(gb|us|orig))?,de(-(de|orig))?,-live_chat,-rechat"; 28 # sub-langs = "en(-(gb|us|orig))?,de(-(de|orig))?,-live_chat,-rechat";
19 prefer-free-formats = true; 29 prefer-free-formats = true;
20 embed-metadata = true;
21 # downloader = "${pkgs.axel}/bin/axel"; 30 # downloader = "${pkgs.axel}/bin/axel";
22 concurrent-fragments = 12; 31 concurrent-fragments = 12;
23 buffer-size = "16K"; 32 buffer-size = "16K";
@@ -28,7 +37,8 @@
28 # "youtube:formats=dashy" 37 # "youtube:formats=dashy"
29 # ]; 38 # ];
30 remux-video = "mp4>mkv"; 39 remux-video = "mp4>mkv";
31 output = "\"%(title)s [%(uploader)s %(webpage_url)s].%(ext)s\""; 40 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\"";
41 audio-multistreams = true;
32 }; 42 };
33 }; 43 };
34 }; 44 };
diff --git a/user-profiles/zsh/default.nix b/user-profiles/zsh/default.nix
index daeb7e82..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 13 append = true;
14 plugins = [ 14 expireDuplicatesFirst = true;
15 { name = "powerlevel10k"; 15 extended = true;
16 file = "share/zsh-powerlevel10k/powerlevel10k.zsh-theme"; 16 findNoDups = true;
17 src = pkgs.zsh-powerlevel10k; 17 };
18 } 18 syntaxHighlighting.enable = true;
19 ]; 19 zsh-abbr = {
20 initExtraFirst = '' 20 enable = true;
21 if [[ $TERM == "dumb" ]]; then 21 abbreviations = {
22 unsetopt zle 22 re = "systemctl restart";
23 PS1='$ ' 23 ure = "systemctl --user restart";
24 return 24 st = "systemctl status";
25 fi 25 ust = "systemctl --user status";
26 ''; 26 };
27 initExtraBeforeCompInit = '' 27 globalAbbreviations = {
28 source "${cfg.home.homeDirectory}/${p10kZsh}" 28 "L" = "| less";
29 ''; 29 "S" = "&> /dev/null";
30 initExtra = lib.mkAfter '' 30 "G" = "| grep";
31 source ${./zshrc} 31 "B" = "&> /dev/null &";
32 source "${pkgs.zsh-syntax-highlighting}/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" 32 "BB" = "&> /dev/null &!";
33 ''; 33 "J" = lib.mkIf config.programs.jq.enable "| jq '.'";
34 };
35 };
36
37 plugins = [
38 { name = "powerlevel10k";
39 file = "share/zsh-powerlevel10k/powerlevel10k.zsh-theme";
40 src = pkgs.zsh-powerlevel10k;
41 }
42 ];
43 initContent = lib.mkMerge [
44 (lib.mkBefore ''
45 if [[ $TERM == "dumb" ]]; then
46 unsetopt zle
47 PS1='$ '
48 return
49 fi
50 '')
51 (lib.mkOrder 550 ''
52 source "$HOME/${config.xdg.configFile."zsh/.p10k.zsh".target}"
53 '')
54 (lib.mkAfter ''
55 source ${./zshrc}
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 a83a8069..af3aca64 100644
--- a/user-profiles/zsh/zshrc
+++ b/user-profiles/zsh/zshrc
@@ -24,7 +24,12 @@ setopt ignore_eof
24bindkey -e 24bindkey -e
25bindkey ';5C' emacs-forward-word 25bindkey ';5C' emacs-forward-word
26bindkey ';5D' emacs-backward-word 26bindkey ';5D' emacs-backward-word
27bindkey '^[[1;5C' emacs-forward-word
28bindkey '^[[1;5D' emacs-backward-word
29bindkey '^H' backward-kill-word
27 30
28autoload -Uz url-quote-magic bracketed-paste-magic 31autoload -Uz url-quote-magic bracketed-paste-magic
29zle -N self-insert url-quote-magic 32zle -N self-insert url-quote-magic
30zle -N bracketed-paste bracketed-paste-magic \ No newline at end of file 33zle -N bracketed-paste bracketed-paste-magic
34
35setopt extended_glob
diff --git a/users/gkleen/default.nix b/users/gkleen/default.nix
index 4ddf4be3..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} = {
@@ -29,9 +29,39 @@
29 userName = "Gregor Kleen"; 29 userName = "Gregor Kleen";
30 delta.enable = true; 30 delta.enable = true;
31 extraConfig = { 31 extraConfig = {
32 pull.rebase = false; 32 core.excludesfile = toString ./gitignore;
33 pull.rebase = true;
33 submodule.recurse = true; 34 submodule.recurse = true;
34 init.defaultBranch = "main"; 35 init.defaultBranch = "main";
36 column.ui = "auto";
37 branch.sort = "-committerdate";
38 tag.sort = "version:refname";
39 diff = {
40 algorithm = "histogram";
41 colorMoved = "plain";
42 mnemonicPrefix = true;
43 renames = true;
44 };
45 push = {
46 default = "simple";
47 autoSetupRemote = true;
48 followTags = true;
49 };
50 fetch = {
51 prune = true;
52 pruneTags = true;
53 all = true;
54 };
55 rerere = {
56 enabled = true;
57 autoupdate = true;
58 };
59 rebase = {
60 autoSquash = true;
61 autoStash = true;
62 updateRefs = true;
63 };
64 merge.conflictstyle = "zdiff3";
35 }; 65 };
36 }; 66 };
37 67
diff --git a/users/gkleen/gitignore b/users/gkleen/gitignore
new file mode 100644
index 00000000..f7082b20
--- /dev/null
+++ b/users/gkleen/gitignore
@@ -0,0 +1,2 @@
1**/#*#
2**/.#*
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 {