From 95a533c8762f2a7c7e05daa787c3441939dfa8dd Mon Sep 17 00:00:00 2001 From: sid Date: Mon, 23 Feb 2026 20:34:35 +0100 Subject: [PATCH] initial commit --- .envrc | 1 + .forgejo/workflows/build-tests.yml | 22 + .forgejo/workflows/deploy-docs.yml | 22 + .forgejo/workflows/flake-check.yml | 13 + .gitignore | 6 + README.md | 55 ++ apps/create/create.sh | 135 ++++ apps/create/default.nix | 24 + apps/deploy/default.nix | 18 + apps/deploy/deploy.sh | 84 ++ apps/install/default.nix | 18 + apps/install/install.sh | 173 ++++ apps/rebuild/default.nix | 30 + apps/rebuild/rebuild.sh | 246 ++++++ apps/update-packages/default.nix | 20 + apps/update-packages/update-packages.sh | 65 ++ apps/wake-host/default.nix | 20 + apps/wake-host/wake-host.sh | 28 + docs/getting-started/add-configs.md | 48 ++ docs/getting-started/create-nix-config.md | 30 + docs/getting-started/install-instructions.md | 127 +++ docs/index.md | 12 + docs/introduction-to-nix/derivations.md | 92 +++ docs/introduction-to-nix/flakes.md | 209 +++++ docs/introduction-to-nix/install-nix.md | 22 + docs/introduction-to-nix/nix-speedrun.md | 227 ++++++ docs/introduction-to-nix/nix-store.md | 40 + docs/introduction-to-nix/nixos.md | 200 +++++ docs/introduction-to-nix/nixpkgs-firefox.png | Bin 0 -> 116893 bytes docs/introduction-to-nix/nixpkgs.md | 84 ++ docs/introduction-to-nix/overview.md | 9 + docs/modules/home/bemenu.md | 10 + docs/modules/home/common.md | 17 + docs/modules/home/gemini-cli.md | 65 ++ docs/modules/home/gpg.md | 51 ++ docs/modules/home/hyprland.md | 153 ++++ docs/modules/home/kitty.md | 10 + docs/modules/home/lf.md | 11 + docs/modules/home/networkmanager-dmenu.md | 10 + docs/modules/home/nextcloud-sync.md | 75 ++ docs/modules/home/nixvim.md | 97 +++ docs/modules/home/password-manager.md | 84 ++ docs/modules/home/sops.md | 99 +++ docs/modules/home/stylix.md | 90 +++ docs/modules/home/virtualisation.md | 11 + docs/modules/home/waybar.md | 10 + docs/modules/home/yazi.md | 13 + docs/modules/nixos/audio.md | 9 + docs/modules/nixos/baibot.md | 90 +++ docs/modules/nixos/cifsmount.md | 25 + docs/modules/nixos/common.md | 17 + docs/modules/nixos/device.md | 20 + docs/modules/nixos/ftp-webserver.md | 7 + docs/modules/nixos/headplane.md | 70 ++ docs/modules/nixos/headscale.md | 58 ++ docs/modules/nixos/i2pd.md | 26 + docs/modules/nixos/jellyfin.md | 30 + docs/modules/nixos/jirafeau.md | 9 + docs/modules/nixos/mailserver.md | 62 ++ docs/modules/nixos/matrix-synapse.md | 144 ++++ docs/modules/nixos/maubot.md | 99 +++ docs/modules/nixos/mcpo.md | 48 ++ docs/modules/nixos/miniflux.md | 42 + docs/modules/nixos/normalUsers.md | 20 + docs/modules/nixos/nvidia.md | 25 + docs/modules/nixos/open-webui-oci.md | 44 + docs/modules/nixos/print-server.md | 7 + docs/modules/nixos/radicale.md | 66 ++ docs/modules/nixos/rss-bridge.md | 11 + docs/modules/nixos/sops.md | 58 ++ docs/modules/nixos/tailscale.md | 36 + docs/modules/nixos/virtualisation.md | 172 ++++ docs/modules/nixos/webpage.md | 5 + docs/modules/nixos/windows-oci.md | 33 + docs/tips/dependency-tracing.md | 49 ++ docs/tips/useful-links.md | 23 + flake.lock | 751 ++++++++++++++++++ flake.nix | 285 +++++++ lib/utils.nix | 85 ++ mkdocs.yml | 81 ++ modules/home/bemenu/default.nix | 21 + modules/home/bitwarden/default.nix | 32 + modules/home/common/cdf.sh | 77 ++ modules/home/common/default.nix | 30 + modules/home/common/packages.nix | 11 + modules/home/common/shellAliases.nix | 33 + modules/home/common/zsh.nix | 17 + modules/home/default.nix | 17 + modules/home/gpg/default.nix | 31 + .../hyprland/applications/bemenu/default.nix | 21 + .../home/hyprland/applications/default.nix | 202 +++++ .../applications/dmenu-bluetooth/default.nix | 22 + .../applications/element-desktop/default.nix | 25 + .../hyprland/applications/feh/default.nix | 45 ++ .../applications/genMimeAssociations.nix | 8 + .../hyprland/applications/kitty/default.nix | 21 + .../applications/libreoffice/default.nix | 20 + .../applications/librewolf/default.nix | 41 + .../hyprland/applications/mpv/default.nix | 15 + .../hyprland/applications/ncmpcpp/default.nix | 22 + .../networkmanager_dmenu/default.nix | 22 + .../applications/newsboat/default.nix | 47 ++ .../applications/newsboat/extra-config | 35 + .../applications/newsboat/newsboat-reload.nix | 10 + .../applications/passwordmanager/default.nix | 22 + .../applications/powermenu-bemenu/default.nix | 18 + .../powermenu-bemenu/powermenu-bemenu.nix | 10 + .../presentation-mode-bemenu/default.nix | 22 + .../presentation-mode-bemenu.sh | 32 + .../applications/qbittorrent/default.nix | 34 + .../applications/screenshot/default.nix | 18 + .../applications/screenshot/screenshot.nix | 11 + .../applications/thunderbird/default.nix | 43 + .../hyprland/applications/yazi/default.nix | 20 + .../hyprland/applications/zathura/default.nix | 33 + modules/home/hyprland/binds/default.nix | 34 + modules/home/hyprland/binds/hyprland.nix | 3 + modules/home/hyprland/binds/mediakeys.nix | 22 + modules/home/hyprland/binds/mouse.nix | 4 + modules/home/hyprland/binds/windows.nix | 27 + modules/home/hyprland/binds/workspaces.nix | 38 + modules/home/hyprland/chromium.nix | 46 ++ modules/home/hyprland/cursor.nix | 26 + modules/home/hyprland/default.nix | 104 +++ modules/home/hyprland/hyprlock.nix | 38 + modules/home/hyprland/packages.nix | 16 + modules/home/hyprland/settings.nix | 56 ++ modules/home/hyprland/xdg/default.nix | 27 + modules/home/kitty/default.nix | 22 + modules/home/librewolf/default.nix | 51 ++ modules/home/librewolf/extensions.nix | 9 + modules/home/librewolf/search/default.nix | 88 ++ modules/home/librewolf/search/engines.nix | 87 ++ modules/home/librewolf/settings.nix | 20 + modules/home/networkmanager-dmenu/default.nix | 55 ++ modules/home/nixvim/default.nix | 104 +++ modules/home/nixvim/keymaps.nix | 347 ++++++++ modules/home/nixvim/plugins/cmp.nix | 54 ++ modules/home/nixvim/plugins/default.nix | 18 + modules/home/nixvim/plugins/lsp.nix | 69 ++ modules/home/nixvim/plugins/lualine.nix | 18 + modules/home/nixvim/plugins/telescope.nix | 52 ++ modules/home/nixvim/plugins/treesitter.nix | 42 + modules/home/nixvim/plugins/trouble.nix | 43 + modules/home/nixvim/spellfiles.nix | 26 + modules/home/password-manager/default.nix | 115 +++ modules/home/password-manager/passmenu | 38 + modules/home/rofi-rbw/default.nix | 55 ++ modules/home/sops/default.nix | 22 + modules/home/stylix/default.nix | 91 +++ modules/home/stylix/print-colors/default.nix | 22 + modules/home/stylix/print-colors/print-colors | 81 ++ modules/home/stylix/print-colors/setup.py | 7 + modules/home/stylix/schemes/moonfly.yaml | 22 + modules/home/stylix/schemes/oxocarbon.yaml | 22 + modules/home/stylix/targets/bemenu.nix | 55 ++ modules/home/stylix/targets/default.nix | 8 + modules/home/stylix/targets/hyprland.nix | 40 + modules/home/stylix/targets/nixvim.nix | 14 + modules/home/stylix/targets/waybar.nix | 130 +++ modules/home/virtualisation/default.nix | 15 + modules/home/waybar/default.nix | 133 ++++ modules/home/waybar/modules/battery.nix | 28 + modules/home/waybar/modules/bluetooth.nix | 20 + modules/home/waybar/modules/cpu.nix | 25 + modules/home/waybar/modules/disk.nix | 11 + modules/home/waybar/modules/memory.nix | 18 + modules/home/waybar/modules/network.nix | 48 ++ modules/home/waybar/modules/newsboat.nix | 29 + .../home/waybar/modules/pulseaudio/input.nix | 20 + .../home/waybar/modules/pulseaudio/output.nix | 27 + modules/home/waybar/modules/timer/default.nix | 28 + modules/home/waybar/modules/timer/timer.sh | 78 ++ modules/home/waybar/modules/wireplumber.nix | 18 + modules/nixos/amd/default.nix | 24 + modules/nixos/audio/default.nix | 28 + modules/nixos/baibot/default.nix | 98 +++ modules/nixos/bluetooth/default.nix | 22 + modules/nixos/cifsMount/default.nix | 91 +++ modules/nixos/common/default.nix | 14 + modules/nixos/common/environment.nix | 63 ++ modules/nixos/common/htop.nix | 8 + modules/nixos/common/nationalization.nix | 31 + modules/nixos/common/networking.nix | 40 + modules/nixos/common/nix.nix | 19 + modules/nixos/common/sudo.nix | 26 + modules/nixos/common/well-known.nix | 17 + modules/nixos/common/zsh.nix | 26 + modules/nixos/coturn/default.nix | 125 +++ modules/nixos/default.nix | 36 + modules/nixos/device/default.nix | 6 + modules/nixos/device/desktop.nix | 8 + modules/nixos/device/laptop.nix | 33 + modules/nixos/device/server.nix | 69 ++ modules/nixos/device/vm.nix | 10 + modules/nixos/ftp-webserver/default.nix | 54 ++ modules/nixos/headplane/default.nix | 78 ++ modules/nixos/headscale/acl.hujson | 17 + modules/nixos/headscale/default.nix | 105 +++ modules/nixos/hyprland/default.nix | 27 + modules/nixos/i2pd/default.nix | 48 ++ modules/nixos/jellyfin/default.nix | 66 ++ modules/nixos/jirafeau/default.nix | 44 + modules/nixos/mailserver/default.nix | 104 +++ modules/nixos/matrix-synapse/bridges.nix | 151 ++++ modules/nixos/matrix-synapse/default.nix | 189 +++++ modules/nixos/matrix-synapse/livekit.nix | 61 ++ modules/nixos/maubot/default.nix | 110 +++ modules/nixos/mcpo/default.nix | 111 +++ modules/nixos/miniflux/default.nix | 57 ++ modules/nixos/nginx/default.nix | 80 ++ modules/nixos/normalUsers/default.nix | 69 ++ modules/nixos/nvidia/default.nix | 30 + modules/nixos/ollama/default.nix | 47 ++ modules/nixos/open-webui-oci/default.nix | 168 ++++ modules/nixos/openssh/default.nix | 16 + modules/nixos/print-server/default.nix | 83 ++ modules/nixos/radicale/default.nix | 116 +++ modules/nixos/rss-bridge/default.nix | 40 + modules/nixos/sops/default.nix | 21 + modules/nixos/tailscale/default.nix | 50 ++ modules/nixos/virtualisation/default.nix | 92 +++ modules/nixos/virtualisation/hugepages.nix | 45 ++ modules/nixos/virtualisation/iommu-groups.sh | 6 + modules/nixos/virtualisation/kvmfr.nix | 157 ++++ modules/nixos/virtualisation/quickemu.nix | 28 + modules/nixos/virtualisation/vfio.nix | 103 +++ modules/nixos/webPage/default.nix | 66 ++ modules/nixos/webPage/sample.html | 12 + modules/nixos/windows-oci/default.nix | 170 ++++ modules/shared/common/default.nix | 5 + modules/shared/common/nix.nix | 85 ++ overlays/default.nix | 29 + pkgs/arxiv-mcp-server/default.nix | 66 ++ pkgs/baibot/default.nix | 46 ++ pkgs/blender-mcp/default.nix | 40 + pkgs/bulk-rename/bulk-rename.sh | 137 ++++ pkgs/bulk-rename/default.nix | 16 + pkgs/cppman/default.nix | 55 ++ pkgs/default.nix | 24 + pkgs/fetcher-mcp/default.nix | 69 ++ pkgs/freecad-mcp/default.nix | 36 + pkgs/kicad-mcp/default.nix | 69 ++ pkgs/kicad-mcp/kicad-skip.nix | 35 + pkgs/marker-pdf/default.nix | 106 +++ pkgs/marker-pdf/fix-output-dir.patch | 11 + pkgs/marker-pdf/pdftext.nix | 43 + pkgs/marker-pdf/skip-font-download.patch | 24 + pkgs/marker-pdf/surya-ocr.nix | 58 ++ pkgs/mcpo/default.nix | 53 ++ pkgs/pass2bw/convert_csvs.py | 77 ++ pkgs/pass2bw/default.nix | 32 + pkgs/pass2bw/pass2bw.sh | 8 + pkgs/pyman/default.nix | 22 + pkgs/pyman/pyman | 26 + pkgs/pyman/setup.py | 7 + pkgs/quicknote/default.nix | 16 + pkgs/quicknote/quicknote.sh | 30 + pkgs/synapse_change_display_name/default.nix | 26 + .../synapse_change_display_name.c | 105 +++ pkgs/synix-docs/default.nix | 25 + pkgs/trelis-gitingest-mcp/default.nix | 48 ++ templates/container/README.md | 70 ++ templates/container/config/base.nix | 32 + templates/container/config/configuration.nix | 5 + templates/container/config/default.nix | 6 + templates/container/flake.nix | 58 ++ templates/container/modules/default.nix | 3 + templates/container/overlays/default.nix | 7 + templates/container/pkgs/default.nix | 5 + templates/dev/c-hello/.envrc | 1 + .../dev/c-hello/.github/workflows/c-nix.yml | 23 + templates/dev/c-hello/.gitignore | 7 + templates/dev/c-hello/Makefile | 51 ++ templates/dev/c-hello/build.sh | 2 + templates/dev/c-hello/flake.nix | 144 ++++ templates/dev/c-hello/src/main.c | 8 + templates/dev/esp-blink/.envrc | 1 + templates/dev/esp-blink/.gitignore | 6 + templates/dev/esp-blink/CMakeLists.txt | 5 + templates/dev/esp-blink/README.md | 33 + templates/dev/esp-blink/flake.nix | 103 +++ templates/dev/esp-blink/main/CMakeLists.txt | 4 + .../dev/esp-blink/main/idf_component.yml | 2 + templates/dev/esp-blink/main/main.c | 33 + templates/dev/flask-hello/.envrc | 1 + .../.github/workflows/python-nix.yml | 23 + templates/dev/flask-hello/.gitignore | 30 + templates/dev/flask-hello/app.py | 5 + templates/dev/flask-hello/flake.nix | 77 ++ .../dev/flask-hello/flask_hello/__init__.py | 17 + .../flask_hello/blueprints/__init__.py | 0 .../flask_hello/blueprints/home.py | 8 + .../flask_hello/static/css/style.css | 9 + .../flask_hello/templates/base.html | 12 + .../flask_hello/templates/errors.html | 7 + .../flask_hello/templates/index.html | 6 + templates/dev/flask-hello/nix/module.nix | 132 +++ templates/dev/flask-hello/nix/package.nix | 31 + templates/dev/flask-hello/nix/shell.nix | 17 + templates/dev/flask-hello/pyproject.toml | 13 + .../py-hello/.github/workflows/python-nix.yml | 23 + templates/dev/py-hello/.gitignore | 29 + templates/dev/py-hello/flake.nix | 131 +++ templates/dev/py-hello/pyproject.toml | 14 + .../dev/py-hello/src/hello_world/__init__.py | 1 + .../dev/py-hello/src/hello_world/__main__.py | 5 + .../rs-hello/.github/workflows/rust-nix.yml | 26 + templates/dev/rs-hello/.gitignore | 3 + templates/dev/rs-hello/Cargo.toml | 8 + templates/dev/rs-hello/flake.nix | 127 +++ templates/dev/rs-hello/src/main.rs | 11 + templates/microvm/.envrc | 1 + templates/microvm/.gitignore | 2 + templates/microvm/README.md | 67 ++ templates/microvm/config/base.nix | 84 ++ templates/microvm/config/configuration.nix | 10 + templates/microvm/config/default.nix | 6 + templates/microvm/flake.nix | 96 +++ templates/microvm/modules/default.nix | 3 + templates/microvm/overlays/default.nix | 7 + templates/microvm/pkgs/default.nix | 5 + templates/nix-configs/hetzner-amd/flake.nix | 89 +++ .../hetzner-amd/hosts/HOSTNAME/boot.nix | 7 + .../hetzner-amd/hosts/HOSTNAME/default.nix | 22 + .../hetzner-amd/hosts/HOSTNAME/disks.sh | 63 ++ .../hetzner-amd/hosts/HOSTNAME/hardware.nix | 48 ++ .../hetzner-amd/hosts/HOSTNAME/networking.nix | 4 + .../hetzner-amd/hosts/HOSTNAME/packages.nix | 5 + .../hosts/HOSTNAME/services/default.nix | 6 + .../hosts/HOSTNAME/services/nginx.nix | 14 + .../hosts/HOSTNAME/services/openssh.nix | 12 + .../hetzner-amd/hosts/HOSTNAME/users.nix | 9 + .../modules/nixos/common/default.nix | 5 + .../modules/nixos/common/overlays.nix | 11 + .../hetzner-amd/modules/nixos/default.nix | 3 + .../hetzner-amd/overlays/default.nix | 35 + .../nix-configs/hetzner-amd/pi4/.sops.yaml | 18 + .../nix-configs/hetzner-amd/pi4/flake.nix | 93 +++ .../hetzner-amd/pi4/hosts/HOSTNAME/boot.nix | 7 + .../pi4/hosts/HOSTNAME/default.nix | 22 + .../hetzner-amd/pi4/hosts/HOSTNAME/disks.sh | 66 ++ .../pi4/hosts/HOSTNAME/hardware.nix | 41 + .../pi4/hosts/HOSTNAME/networking.nix | 4 + .../pi4/hosts/HOSTNAME/packages.nix | 5 + .../pi4/hosts/HOSTNAME/services/default.nix | 6 + .../pi4/hosts/HOSTNAME/services/nginx.nix | 14 + .../pi4/hosts/HOSTNAME/services/openssh.nix | 12 + .../hetzner-amd/pi4/hosts/HOSTNAME/users.nix | 9 + .../pi4/modules/nixos/common/default.nix | 5 + .../pi4/modules/nixos/common/overlays.nix | 11 + .../hetzner-amd/pi4/modules/nixos/default.nix | 3 + .../hetzner-amd/pi4/overlays/default.nix | 35 + .../hetzner-amd/pi4/pkgs/default.nix | 8 + .../pi4/users/USERNAME/default.nix | 8 + .../users/USERNAME/pubkeys/YOUR_PUBKEY.pub | 0 .../nix-configs/hetzner-amd/pkgs/default.nix | 8 + .../hetzner-amd/users/USERNAME/default.nix | 8 + .../users/USERNAME/pubkeys/YOUR_PUBKEY.pub | 0 templates/nix-configs/hyprland/flake.nix | 125 +++ .../hyprland/hosts/HOSTNAME/boot.nix | 7 + .../hyprland/hosts/HOSTNAME/default.nix | 22 + .../hyprland/hosts/HOSTNAME/disks.sh | 63 ++ .../hyprland/hosts/HOSTNAME/hardware.nix | 48 ++ .../hyprland/hosts/HOSTNAME/networking.nix | 4 + .../hyprland/hosts/HOSTNAME/packages.nix | 5 + .../hosts/HOSTNAME/services/default.nix | 6 + .../hosts/HOSTNAME/services/nginx.nix | 14 + .../hosts/HOSTNAME/services/openssh.nix | 12 + .../hyprland/hosts/HOSTNAME/users.nix | 9 + .../hyprland/modules/nixos/common/default.nix | 5 + .../modules/nixos/common/overlays.nix | 11 + .../hyprland/modules/nixos/default.nix | 3 + .../nix-configs/hyprland/overlays/default.nix | 35 + .../nix-configs/hyprland/pkgs/default.nix | 8 + templates/nix-configs/hyprland/shell.nix | 9 + .../hyprland/users/USERNAME/default.nix | 8 + .../hyprland/users/USERNAME/home/default.nix | 24 + .../USERNAME/home/hosts/HOSTNAME/default.nix | 1 + .../users/USERNAME/home/hyprland/default.nix | 17 + .../users/USERNAME/home/hyprland/packages.nix | 5 + .../users/USERNAME/pubkeys/YOUR_PUBKEY.pub | 0 templates/nix-configs/pi4/flake.nix | 89 +++ .../nix-configs/pi4/hosts/HOSTNAME/boot.nix | 8 + .../pi4/hosts/HOSTNAME/default.nix | 22 + .../nix-configs/pi4/hosts/HOSTNAME/disks.sh | 63 ++ .../pi4/hosts/HOSTNAME/hardware.nix | 23 + .../pi4/hosts/HOSTNAME/networking.nix | 4 + .../pi4/hosts/HOSTNAME/packages.nix | 5 + .../pi4/hosts/HOSTNAME/services/default.nix | 6 + .../pi4/hosts/HOSTNAME/services/nginx.nix | 14 + .../pi4/hosts/HOSTNAME/services/openssh.nix | 12 + .../nix-configs/pi4/hosts/HOSTNAME/users.nix | 9 + .../pi4/modules/nixos/common/default.nix | 5 + .../pi4/modules/nixos/common/overlays.nix | 11 + .../nix-configs/pi4/modules/nixos/default.nix | 3 + .../nix-configs/pi4/overlays/default.nix | 35 + templates/nix-configs/pi4/pkgs/default.nix | 8 + .../pi4/users/USERNAME/default.nix | 8 + .../users/USERNAME/pubkeys/YOUR_PUBKEY.pub | 0 templates/nix-configs/server/flake.nix | 89 +++ .../server/hosts/HOSTNAME/boot.nix | 7 + .../server/hosts/HOSTNAME/default.nix | 22 + .../server/hosts/HOSTNAME/disks.sh | 63 ++ .../server/hosts/HOSTNAME/hardware.nix | 48 ++ .../server/hosts/HOSTNAME/networking.nix | 4 + .../server/hosts/HOSTNAME/packages.nix | 5 + .../hosts/HOSTNAME/services/default.nix | 6 + .../server/hosts/HOSTNAME/services/nginx.nix | 14 + .../hosts/HOSTNAME/services/openssh.nix | 12 + .../server/hosts/HOSTNAME/users.nix | 9 + .../server/modules/nixos/common/default.nix | 5 + .../server/modules/nixos/common/overlays.nix | 11 + .../server/modules/nixos/default.nix | 3 + .../nix-configs/server/overlays/default.nix | 35 + templates/nix-configs/server/pkgs/default.nix | 8 + .../server/users/USERNAME/default.nix | 8 + .../users/USERNAME/pubkeys/YOUR_PUBKEY.pub | 0 templates/nix-configs/vm-uefi/flake.nix | 89 +++ .../vm-uefi/hosts/HOSTNAME/boot.nix | 7 + .../vm-uefi/hosts/HOSTNAME/default.nix | 22 + .../vm-uefi/hosts/HOSTNAME/disks.sh | 59 ++ .../vm-uefi/hosts/HOSTNAME/hardware.nix | 41 + .../vm-uefi/hosts/HOSTNAME/networking.nix | 4 + .../vm-uefi/hosts/HOSTNAME/packages.nix | 5 + .../hosts/HOSTNAME/services/default.nix | 6 + .../vm-uefi/hosts/HOSTNAME/services/nginx.nix | 14 + .../hosts/HOSTNAME/services/openssh.nix | 12 + .../vm-uefi/hosts/HOSTNAME/users.nix | 9 + .../vm-uefi/modules/nixos/common/default.nix | 5 + .../vm-uefi/modules/nixos/common/overlays.nix | 11 + .../vm-uefi/modules/nixos/default.nix | 3 + .../nix-configs/vm-uefi/overlays/default.nix | 35 + .../nix-configs/vm-uefi/pkgs/default.nix | 8 + .../vm-uefi/users/USERNAME/default.nix | 8 + .../users/USERNAME/pubkeys/YOUR_PUBKEY.pub | 0 tests/build/hm-hyprland/default.nix | 28 + tests/build/nixos-hyprland/boot.nix | 7 + tests/build/nixos-hyprland/default.nix | 20 + tests/build/nixos-hyprland/hardware.nix | 48 ++ tests/build/nixos-hyprland/networking.nix | 4 + tests/build/nixos-hyprland/users.nix | 13 + tests/build/nixos-server/boot.nix | 7 + tests/build/nixos-server/default.nix | 20 + tests/build/nixos-server/hardware.nix | 48 ++ tests/build/nixos-server/networking.nix | 4 + tests/build/nixos-server/services/default.nix | 6 + tests/build/nixos-server/services/nginx.nix | 14 + tests/build/nixos-server/services/openssh.nix | 12 + tests/build/nixos-server/users.nix | 13 + tests/run/synapse.nix | 95 +++ 451 files changed, 18255 insertions(+) create mode 100644 .envrc create mode 100644 .forgejo/workflows/build-tests.yml create mode 100644 .forgejo/workflows/deploy-docs.yml create mode 100644 .forgejo/workflows/flake-check.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 apps/create/create.sh create mode 100644 apps/create/default.nix create mode 100644 apps/deploy/default.nix create mode 100644 apps/deploy/deploy.sh create mode 100644 apps/install/default.nix create mode 100755 apps/install/install.sh create mode 100644 apps/rebuild/default.nix create mode 100755 apps/rebuild/rebuild.sh create mode 100644 apps/update-packages/default.nix create mode 100644 apps/update-packages/update-packages.sh create mode 100644 apps/wake-host/default.nix create mode 100644 apps/wake-host/wake-host.sh create mode 100644 docs/getting-started/add-configs.md create mode 100644 docs/getting-started/create-nix-config.md create mode 100644 docs/getting-started/install-instructions.md create mode 100644 docs/index.md create mode 100644 docs/introduction-to-nix/derivations.md create mode 100644 docs/introduction-to-nix/flakes.md create mode 100644 docs/introduction-to-nix/install-nix.md create mode 100644 docs/introduction-to-nix/nix-speedrun.md create mode 100644 docs/introduction-to-nix/nix-store.md create mode 100644 docs/introduction-to-nix/nixos.md create mode 100644 docs/introduction-to-nix/nixpkgs-firefox.png create mode 100644 docs/introduction-to-nix/nixpkgs.md create mode 100644 docs/introduction-to-nix/overview.md create mode 100644 docs/modules/home/bemenu.md create mode 100644 docs/modules/home/common.md create mode 100644 docs/modules/home/gemini-cli.md create mode 100644 docs/modules/home/gpg.md create mode 100644 docs/modules/home/hyprland.md create mode 100644 docs/modules/home/kitty.md create mode 100644 docs/modules/home/lf.md create mode 100644 docs/modules/home/networkmanager-dmenu.md create mode 100644 docs/modules/home/nextcloud-sync.md create mode 100644 docs/modules/home/nixvim.md create mode 100644 docs/modules/home/password-manager.md create mode 100644 docs/modules/home/sops.md create mode 100644 docs/modules/home/stylix.md create mode 100644 docs/modules/home/virtualisation.md create mode 100644 docs/modules/home/waybar.md create mode 100644 docs/modules/home/yazi.md create mode 100644 docs/modules/nixos/audio.md create mode 100644 docs/modules/nixos/baibot.md create mode 100644 docs/modules/nixos/cifsmount.md create mode 100644 docs/modules/nixos/common.md create mode 100644 docs/modules/nixos/device.md create mode 100644 docs/modules/nixos/ftp-webserver.md create mode 100644 docs/modules/nixos/headplane.md create mode 100644 docs/modules/nixos/headscale.md create mode 100644 docs/modules/nixos/i2pd.md create mode 100644 docs/modules/nixos/jellyfin.md create mode 100644 docs/modules/nixos/jirafeau.md create mode 100644 docs/modules/nixos/mailserver.md create mode 100644 docs/modules/nixos/matrix-synapse.md create mode 100644 docs/modules/nixos/maubot.md create mode 100644 docs/modules/nixos/mcpo.md create mode 100644 docs/modules/nixos/miniflux.md create mode 100644 docs/modules/nixos/normalUsers.md create mode 100644 docs/modules/nixos/nvidia.md create mode 100644 docs/modules/nixos/open-webui-oci.md create mode 100644 docs/modules/nixos/print-server.md create mode 100644 docs/modules/nixos/radicale.md create mode 100644 docs/modules/nixos/rss-bridge.md create mode 100644 docs/modules/nixos/sops.md create mode 100644 docs/modules/nixos/tailscale.md create mode 100644 docs/modules/nixos/virtualisation.md create mode 100644 docs/modules/nixos/webpage.md create mode 100644 docs/modules/nixos/windows-oci.md create mode 100644 docs/tips/dependency-tracing.md create mode 100644 docs/tips/useful-links.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 lib/utils.nix create mode 100644 mkdocs.yml create mode 100644 modules/home/bemenu/default.nix create mode 100644 modules/home/bitwarden/default.nix create mode 100644 modules/home/common/cdf.sh create mode 100644 modules/home/common/default.nix create mode 100644 modules/home/common/packages.nix create mode 100644 modules/home/common/shellAliases.nix create mode 100644 modules/home/common/zsh.nix create mode 100644 modules/home/default.nix create mode 100644 modules/home/gpg/default.nix create mode 100644 modules/home/hyprland/applications/bemenu/default.nix create mode 100644 modules/home/hyprland/applications/default.nix create mode 100644 modules/home/hyprland/applications/dmenu-bluetooth/default.nix create mode 100644 modules/home/hyprland/applications/element-desktop/default.nix create mode 100644 modules/home/hyprland/applications/feh/default.nix create mode 100644 modules/home/hyprland/applications/genMimeAssociations.nix create mode 100644 modules/home/hyprland/applications/kitty/default.nix create mode 100644 modules/home/hyprland/applications/libreoffice/default.nix create mode 100644 modules/home/hyprland/applications/librewolf/default.nix create mode 100644 modules/home/hyprland/applications/mpv/default.nix create mode 100644 modules/home/hyprland/applications/ncmpcpp/default.nix create mode 100644 modules/home/hyprland/applications/networkmanager_dmenu/default.nix create mode 100644 modules/home/hyprland/applications/newsboat/default.nix create mode 100644 modules/home/hyprland/applications/newsboat/extra-config create mode 100644 modules/home/hyprland/applications/newsboat/newsboat-reload.nix create mode 100644 modules/home/hyprland/applications/passwordmanager/default.nix create mode 100644 modules/home/hyprland/applications/powermenu-bemenu/default.nix create mode 100644 modules/home/hyprland/applications/powermenu-bemenu/powermenu-bemenu.nix create mode 100644 modules/home/hyprland/applications/presentation-mode-bemenu/default.nix create mode 100644 modules/home/hyprland/applications/presentation-mode-bemenu/presentation-mode-bemenu.sh create mode 100644 modules/home/hyprland/applications/qbittorrent/default.nix create mode 100644 modules/home/hyprland/applications/screenshot/default.nix create mode 100644 modules/home/hyprland/applications/screenshot/screenshot.nix create mode 100644 modules/home/hyprland/applications/thunderbird/default.nix create mode 100644 modules/home/hyprland/applications/yazi/default.nix create mode 100644 modules/home/hyprland/applications/zathura/default.nix create mode 100644 modules/home/hyprland/binds/default.nix create mode 100644 modules/home/hyprland/binds/hyprland.nix create mode 100644 modules/home/hyprland/binds/mediakeys.nix create mode 100644 modules/home/hyprland/binds/mouse.nix create mode 100644 modules/home/hyprland/binds/windows.nix create mode 100644 modules/home/hyprland/binds/workspaces.nix create mode 100644 modules/home/hyprland/chromium.nix create mode 100644 modules/home/hyprland/cursor.nix create mode 100644 modules/home/hyprland/default.nix create mode 100644 modules/home/hyprland/hyprlock.nix create mode 100644 modules/home/hyprland/packages.nix create mode 100644 modules/home/hyprland/settings.nix create mode 100644 modules/home/hyprland/xdg/default.nix create mode 100644 modules/home/kitty/default.nix create mode 100644 modules/home/librewolf/default.nix create mode 100644 modules/home/librewolf/extensions.nix create mode 100644 modules/home/librewolf/search/default.nix create mode 100644 modules/home/librewolf/search/engines.nix create mode 100644 modules/home/librewolf/settings.nix create mode 100644 modules/home/networkmanager-dmenu/default.nix create mode 100644 modules/home/nixvim/default.nix create mode 100644 modules/home/nixvim/keymaps.nix create mode 100644 modules/home/nixvim/plugins/cmp.nix create mode 100644 modules/home/nixvim/plugins/default.nix create mode 100644 modules/home/nixvim/plugins/lsp.nix create mode 100644 modules/home/nixvim/plugins/lualine.nix create mode 100644 modules/home/nixvim/plugins/telescope.nix create mode 100644 modules/home/nixvim/plugins/treesitter.nix create mode 100644 modules/home/nixvim/plugins/trouble.nix create mode 100644 modules/home/nixvim/spellfiles.nix create mode 100644 modules/home/password-manager/default.nix create mode 100644 modules/home/password-manager/passmenu create mode 100644 modules/home/rofi-rbw/default.nix create mode 100644 modules/home/sops/default.nix create mode 100644 modules/home/stylix/default.nix create mode 100644 modules/home/stylix/print-colors/default.nix create mode 100644 modules/home/stylix/print-colors/print-colors create mode 100644 modules/home/stylix/print-colors/setup.py create mode 100644 modules/home/stylix/schemes/moonfly.yaml create mode 100644 modules/home/stylix/schemes/oxocarbon.yaml create mode 100644 modules/home/stylix/targets/bemenu.nix create mode 100644 modules/home/stylix/targets/default.nix create mode 100644 modules/home/stylix/targets/hyprland.nix create mode 100644 modules/home/stylix/targets/nixvim.nix create mode 100644 modules/home/stylix/targets/waybar.nix create mode 100644 modules/home/virtualisation/default.nix create mode 100644 modules/home/waybar/default.nix create mode 100644 modules/home/waybar/modules/battery.nix create mode 100644 modules/home/waybar/modules/bluetooth.nix create mode 100644 modules/home/waybar/modules/cpu.nix create mode 100644 modules/home/waybar/modules/disk.nix create mode 100644 modules/home/waybar/modules/memory.nix create mode 100644 modules/home/waybar/modules/network.nix create mode 100644 modules/home/waybar/modules/newsboat.nix create mode 100644 modules/home/waybar/modules/pulseaudio/input.nix create mode 100644 modules/home/waybar/modules/pulseaudio/output.nix create mode 100644 modules/home/waybar/modules/timer/default.nix create mode 100644 modules/home/waybar/modules/timer/timer.sh create mode 100644 modules/home/waybar/modules/wireplumber.nix create mode 100644 modules/nixos/amd/default.nix create mode 100644 modules/nixos/audio/default.nix create mode 100644 modules/nixos/baibot/default.nix create mode 100644 modules/nixos/bluetooth/default.nix create mode 100644 modules/nixos/cifsMount/default.nix create mode 100644 modules/nixos/common/default.nix create mode 100644 modules/nixos/common/environment.nix create mode 100644 modules/nixos/common/htop.nix create mode 100644 modules/nixos/common/nationalization.nix create mode 100644 modules/nixos/common/networking.nix create mode 100644 modules/nixos/common/nix.nix create mode 100644 modules/nixos/common/sudo.nix create mode 100644 modules/nixos/common/well-known.nix create mode 100644 modules/nixos/common/zsh.nix create mode 100644 modules/nixos/coturn/default.nix create mode 100644 modules/nixos/default.nix create mode 100644 modules/nixos/device/default.nix create mode 100644 modules/nixos/device/desktop.nix create mode 100644 modules/nixos/device/laptop.nix create mode 100644 modules/nixos/device/server.nix create mode 100644 modules/nixos/device/vm.nix create mode 100644 modules/nixos/ftp-webserver/default.nix create mode 100644 modules/nixos/headplane/default.nix create mode 100644 modules/nixos/headscale/acl.hujson create mode 100644 modules/nixos/headscale/default.nix create mode 100644 modules/nixos/hyprland/default.nix create mode 100644 modules/nixos/i2pd/default.nix create mode 100644 modules/nixos/jellyfin/default.nix create mode 100644 modules/nixos/jirafeau/default.nix create mode 100644 modules/nixos/mailserver/default.nix create mode 100644 modules/nixos/matrix-synapse/bridges.nix create mode 100644 modules/nixos/matrix-synapse/default.nix create mode 100644 modules/nixos/matrix-synapse/livekit.nix create mode 100644 modules/nixos/maubot/default.nix create mode 100644 modules/nixos/mcpo/default.nix create mode 100644 modules/nixos/miniflux/default.nix create mode 100644 modules/nixos/nginx/default.nix create mode 100644 modules/nixos/normalUsers/default.nix create mode 100644 modules/nixos/nvidia/default.nix create mode 100644 modules/nixos/ollama/default.nix create mode 100644 modules/nixos/open-webui-oci/default.nix create mode 100644 modules/nixos/openssh/default.nix create mode 100644 modules/nixos/print-server/default.nix create mode 100644 modules/nixos/radicale/default.nix create mode 100644 modules/nixos/rss-bridge/default.nix create mode 100644 modules/nixos/sops/default.nix create mode 100644 modules/nixos/tailscale/default.nix create mode 100644 modules/nixos/virtualisation/default.nix create mode 100644 modules/nixos/virtualisation/hugepages.nix create mode 100644 modules/nixos/virtualisation/iommu-groups.sh create mode 100644 modules/nixos/virtualisation/kvmfr.nix create mode 100644 modules/nixos/virtualisation/quickemu.nix create mode 100644 modules/nixos/virtualisation/vfio.nix create mode 100644 modules/nixos/webPage/default.nix create mode 100644 modules/nixos/webPage/sample.html create mode 100644 modules/nixos/windows-oci/default.nix create mode 100644 modules/shared/common/default.nix create mode 100644 modules/shared/common/nix.nix create mode 100644 overlays/default.nix create mode 100644 pkgs/arxiv-mcp-server/default.nix create mode 100644 pkgs/baibot/default.nix create mode 100644 pkgs/blender-mcp/default.nix create mode 100644 pkgs/bulk-rename/bulk-rename.sh create mode 100644 pkgs/bulk-rename/default.nix create mode 100644 pkgs/cppman/default.nix create mode 100644 pkgs/default.nix create mode 100644 pkgs/fetcher-mcp/default.nix create mode 100644 pkgs/freecad-mcp/default.nix create mode 100644 pkgs/kicad-mcp/default.nix create mode 100644 pkgs/kicad-mcp/kicad-skip.nix create mode 100644 pkgs/marker-pdf/default.nix create mode 100644 pkgs/marker-pdf/fix-output-dir.patch create mode 100644 pkgs/marker-pdf/pdftext.nix create mode 100644 pkgs/marker-pdf/skip-font-download.patch create mode 100644 pkgs/marker-pdf/surya-ocr.nix create mode 100644 pkgs/mcpo/default.nix create mode 100644 pkgs/pass2bw/convert_csvs.py create mode 100644 pkgs/pass2bw/default.nix create mode 100644 pkgs/pass2bw/pass2bw.sh create mode 100644 pkgs/pyman/default.nix create mode 100644 pkgs/pyman/pyman create mode 100644 pkgs/pyman/setup.py create mode 100644 pkgs/quicknote/default.nix create mode 100644 pkgs/quicknote/quicknote.sh create mode 100644 pkgs/synapse_change_display_name/default.nix create mode 100644 pkgs/synapse_change_display_name/synapse_change_display_name.c create mode 100644 pkgs/synix-docs/default.nix create mode 100644 pkgs/trelis-gitingest-mcp/default.nix create mode 100644 templates/container/README.md create mode 100644 templates/container/config/base.nix create mode 100644 templates/container/config/configuration.nix create mode 100644 templates/container/config/default.nix create mode 100644 templates/container/flake.nix create mode 100644 templates/container/modules/default.nix create mode 100644 templates/container/overlays/default.nix create mode 100644 templates/container/pkgs/default.nix create mode 100644 templates/dev/c-hello/.envrc create mode 100644 templates/dev/c-hello/.github/workflows/c-nix.yml create mode 100644 templates/dev/c-hello/.gitignore create mode 100644 templates/dev/c-hello/Makefile create mode 100644 templates/dev/c-hello/build.sh create mode 100644 templates/dev/c-hello/flake.nix create mode 100644 templates/dev/c-hello/src/main.c create mode 100644 templates/dev/esp-blink/.envrc create mode 100644 templates/dev/esp-blink/.gitignore create mode 100644 templates/dev/esp-blink/CMakeLists.txt create mode 100644 templates/dev/esp-blink/README.md create mode 100644 templates/dev/esp-blink/flake.nix create mode 100644 templates/dev/esp-blink/main/CMakeLists.txt create mode 100644 templates/dev/esp-blink/main/idf_component.yml create mode 100644 templates/dev/esp-blink/main/main.c create mode 100644 templates/dev/flask-hello/.envrc create mode 100644 templates/dev/flask-hello/.github/workflows/python-nix.yml create mode 100644 templates/dev/flask-hello/.gitignore create mode 100644 templates/dev/flask-hello/app.py create mode 100644 templates/dev/flask-hello/flake.nix create mode 100644 templates/dev/flask-hello/flask_hello/__init__.py create mode 100644 templates/dev/flask-hello/flask_hello/blueprints/__init__.py create mode 100644 templates/dev/flask-hello/flask_hello/blueprints/home.py create mode 100644 templates/dev/flask-hello/flask_hello/static/css/style.css create mode 100644 templates/dev/flask-hello/flask_hello/templates/base.html create mode 100644 templates/dev/flask-hello/flask_hello/templates/errors.html create mode 100644 templates/dev/flask-hello/flask_hello/templates/index.html create mode 100644 templates/dev/flask-hello/nix/module.nix create mode 100644 templates/dev/flask-hello/nix/package.nix create mode 100644 templates/dev/flask-hello/nix/shell.nix create mode 100644 templates/dev/flask-hello/pyproject.toml create mode 100644 templates/dev/py-hello/.github/workflows/python-nix.yml create mode 100644 templates/dev/py-hello/.gitignore create mode 100644 templates/dev/py-hello/flake.nix create mode 100644 templates/dev/py-hello/pyproject.toml create mode 100644 templates/dev/py-hello/src/hello_world/__init__.py create mode 100644 templates/dev/py-hello/src/hello_world/__main__.py create mode 100644 templates/dev/rs-hello/.github/workflows/rust-nix.yml create mode 100644 templates/dev/rs-hello/.gitignore create mode 100644 templates/dev/rs-hello/Cargo.toml create mode 100644 templates/dev/rs-hello/flake.nix create mode 100644 templates/dev/rs-hello/src/main.rs create mode 100644 templates/microvm/.envrc create mode 100644 templates/microvm/.gitignore create mode 100644 templates/microvm/README.md create mode 100644 templates/microvm/config/base.nix create mode 100644 templates/microvm/config/configuration.nix create mode 100644 templates/microvm/config/default.nix create mode 100644 templates/microvm/flake.nix create mode 100644 templates/microvm/modules/default.nix create mode 100644 templates/microvm/overlays/default.nix create mode 100644 templates/microvm/pkgs/default.nix create mode 100644 templates/nix-configs/hetzner-amd/flake.nix create mode 100644 templates/nix-configs/hetzner-amd/hosts/HOSTNAME/boot.nix create mode 100644 templates/nix-configs/hetzner-amd/hosts/HOSTNAME/default.nix create mode 100644 templates/nix-configs/hetzner-amd/hosts/HOSTNAME/disks.sh create mode 100644 templates/nix-configs/hetzner-amd/hosts/HOSTNAME/hardware.nix create mode 100644 templates/nix-configs/hetzner-amd/hosts/HOSTNAME/networking.nix create mode 100644 templates/nix-configs/hetzner-amd/hosts/HOSTNAME/packages.nix create mode 100644 templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/default.nix create mode 100644 templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/nginx.nix create mode 100644 templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/openssh.nix create mode 100644 templates/nix-configs/hetzner-amd/hosts/HOSTNAME/users.nix create mode 100644 templates/nix-configs/hetzner-amd/modules/nixos/common/default.nix create mode 100644 templates/nix-configs/hetzner-amd/modules/nixos/common/overlays.nix create mode 100644 templates/nix-configs/hetzner-amd/modules/nixos/default.nix create mode 100644 templates/nix-configs/hetzner-amd/overlays/default.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/.sops.yaml create mode 100644 templates/nix-configs/hetzner-amd/pi4/flake.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/boot.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/default.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/disks.sh create mode 100644 templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/hardware.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/networking.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/packages.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/default.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/nginx.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/openssh.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/users.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/modules/nixos/common/default.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/modules/nixos/common/overlays.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/modules/nixos/default.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/overlays/default.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/pkgs/default.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/users/USERNAME/default.nix create mode 100644 templates/nix-configs/hetzner-amd/pi4/users/USERNAME/pubkeys/YOUR_PUBKEY.pub create mode 100644 templates/nix-configs/hetzner-amd/pkgs/default.nix create mode 100644 templates/nix-configs/hetzner-amd/users/USERNAME/default.nix create mode 100644 templates/nix-configs/hetzner-amd/users/USERNAME/pubkeys/YOUR_PUBKEY.pub create mode 100644 templates/nix-configs/hyprland/flake.nix create mode 100644 templates/nix-configs/hyprland/hosts/HOSTNAME/boot.nix create mode 100644 templates/nix-configs/hyprland/hosts/HOSTNAME/default.nix create mode 100644 templates/nix-configs/hyprland/hosts/HOSTNAME/disks.sh create mode 100644 templates/nix-configs/hyprland/hosts/HOSTNAME/hardware.nix create mode 100644 templates/nix-configs/hyprland/hosts/HOSTNAME/networking.nix create mode 100644 templates/nix-configs/hyprland/hosts/HOSTNAME/packages.nix create mode 100644 templates/nix-configs/hyprland/hosts/HOSTNAME/services/default.nix create mode 100644 templates/nix-configs/hyprland/hosts/HOSTNAME/services/nginx.nix create mode 100644 templates/nix-configs/hyprland/hosts/HOSTNAME/services/openssh.nix create mode 100644 templates/nix-configs/hyprland/hosts/HOSTNAME/users.nix create mode 100644 templates/nix-configs/hyprland/modules/nixos/common/default.nix create mode 100644 templates/nix-configs/hyprland/modules/nixos/common/overlays.nix create mode 100644 templates/nix-configs/hyprland/modules/nixos/default.nix create mode 100644 templates/nix-configs/hyprland/overlays/default.nix create mode 100644 templates/nix-configs/hyprland/pkgs/default.nix create mode 100644 templates/nix-configs/hyprland/shell.nix create mode 100644 templates/nix-configs/hyprland/users/USERNAME/default.nix create mode 100644 templates/nix-configs/hyprland/users/USERNAME/home/default.nix create mode 100644 templates/nix-configs/hyprland/users/USERNAME/home/hosts/HOSTNAME/default.nix create mode 100644 templates/nix-configs/hyprland/users/USERNAME/home/hyprland/default.nix create mode 100644 templates/nix-configs/hyprland/users/USERNAME/home/hyprland/packages.nix create mode 100644 templates/nix-configs/hyprland/users/USERNAME/pubkeys/YOUR_PUBKEY.pub create mode 100644 templates/nix-configs/pi4/flake.nix create mode 100644 templates/nix-configs/pi4/hosts/HOSTNAME/boot.nix create mode 100644 templates/nix-configs/pi4/hosts/HOSTNAME/default.nix create mode 100644 templates/nix-configs/pi4/hosts/HOSTNAME/disks.sh create mode 100644 templates/nix-configs/pi4/hosts/HOSTNAME/hardware.nix create mode 100644 templates/nix-configs/pi4/hosts/HOSTNAME/networking.nix create mode 100644 templates/nix-configs/pi4/hosts/HOSTNAME/packages.nix create mode 100644 templates/nix-configs/pi4/hosts/HOSTNAME/services/default.nix create mode 100644 templates/nix-configs/pi4/hosts/HOSTNAME/services/nginx.nix create mode 100644 templates/nix-configs/pi4/hosts/HOSTNAME/services/openssh.nix create mode 100644 templates/nix-configs/pi4/hosts/HOSTNAME/users.nix create mode 100644 templates/nix-configs/pi4/modules/nixos/common/default.nix create mode 100644 templates/nix-configs/pi4/modules/nixos/common/overlays.nix create mode 100644 templates/nix-configs/pi4/modules/nixos/default.nix create mode 100644 templates/nix-configs/pi4/overlays/default.nix create mode 100644 templates/nix-configs/pi4/pkgs/default.nix create mode 100644 templates/nix-configs/pi4/users/USERNAME/default.nix create mode 100644 templates/nix-configs/pi4/users/USERNAME/pubkeys/YOUR_PUBKEY.pub create mode 100644 templates/nix-configs/server/flake.nix create mode 100644 templates/nix-configs/server/hosts/HOSTNAME/boot.nix create mode 100644 templates/nix-configs/server/hosts/HOSTNAME/default.nix create mode 100644 templates/nix-configs/server/hosts/HOSTNAME/disks.sh create mode 100644 templates/nix-configs/server/hosts/HOSTNAME/hardware.nix create mode 100644 templates/nix-configs/server/hosts/HOSTNAME/networking.nix create mode 100644 templates/nix-configs/server/hosts/HOSTNAME/packages.nix create mode 100644 templates/nix-configs/server/hosts/HOSTNAME/services/default.nix create mode 100644 templates/nix-configs/server/hosts/HOSTNAME/services/nginx.nix create mode 100644 templates/nix-configs/server/hosts/HOSTNAME/services/openssh.nix create mode 100644 templates/nix-configs/server/hosts/HOSTNAME/users.nix create mode 100644 templates/nix-configs/server/modules/nixos/common/default.nix create mode 100644 templates/nix-configs/server/modules/nixos/common/overlays.nix create mode 100644 templates/nix-configs/server/modules/nixos/default.nix create mode 100644 templates/nix-configs/server/overlays/default.nix create mode 100644 templates/nix-configs/server/pkgs/default.nix create mode 100644 templates/nix-configs/server/users/USERNAME/default.nix create mode 100644 templates/nix-configs/server/users/USERNAME/pubkeys/YOUR_PUBKEY.pub create mode 100644 templates/nix-configs/vm-uefi/flake.nix create mode 100644 templates/nix-configs/vm-uefi/hosts/HOSTNAME/boot.nix create mode 100644 templates/nix-configs/vm-uefi/hosts/HOSTNAME/default.nix create mode 100644 templates/nix-configs/vm-uefi/hosts/HOSTNAME/disks.sh create mode 100644 templates/nix-configs/vm-uefi/hosts/HOSTNAME/hardware.nix create mode 100644 templates/nix-configs/vm-uefi/hosts/HOSTNAME/networking.nix create mode 100644 templates/nix-configs/vm-uefi/hosts/HOSTNAME/packages.nix create mode 100644 templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/default.nix create mode 100644 templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/nginx.nix create mode 100644 templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/openssh.nix create mode 100644 templates/nix-configs/vm-uefi/hosts/HOSTNAME/users.nix create mode 100644 templates/nix-configs/vm-uefi/modules/nixos/common/default.nix create mode 100644 templates/nix-configs/vm-uefi/modules/nixos/common/overlays.nix create mode 100644 templates/nix-configs/vm-uefi/modules/nixos/default.nix create mode 100644 templates/nix-configs/vm-uefi/overlays/default.nix create mode 100644 templates/nix-configs/vm-uefi/pkgs/default.nix create mode 100644 templates/nix-configs/vm-uefi/users/USERNAME/default.nix create mode 100644 templates/nix-configs/vm-uefi/users/USERNAME/pubkeys/YOUR_PUBKEY.pub create mode 100644 tests/build/hm-hyprland/default.nix create mode 100644 tests/build/nixos-hyprland/boot.nix create mode 100644 tests/build/nixos-hyprland/default.nix create mode 100644 tests/build/nixos-hyprland/hardware.nix create mode 100644 tests/build/nixos-hyprland/networking.nix create mode 100644 tests/build/nixos-hyprland/users.nix create mode 100644 tests/build/nixos-server/boot.nix create mode 100644 tests/build/nixos-server/default.nix create mode 100644 tests/build/nixos-server/hardware.nix create mode 100644 tests/build/nixos-server/networking.nix create mode 100644 tests/build/nixos-server/services/default.nix create mode 100644 tests/build/nixos-server/services/nginx.nix create mode 100644 tests/build/nixos-server/services/openssh.nix create mode 100644 tests/build/nixos-server/users.nix create mode 100644 tests/run/synapse.nix diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.forgejo/workflows/build-tests.yml b/.forgejo/workflows/build-tests.yml new file mode 100644 index 0000000..fedb2a3 --- /dev/null +++ b/.forgejo/workflows/build-tests.yml @@ -0,0 +1,22 @@ +name: Build tests + +on: + pull_request: + branches: + - release-25.11 + +jobs: + build-hosts: + runs-on: runner + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build NixOS server config + run: nix build -L .#nixosConfigurations.nixos-server.config.system.build.toplevel + + - name: Build NixOS client config + run: nix build -L .#nixosConfigurations.nixos-hyprland.config.system.build.toplevel + + - name: Build Home Manager config w/ Hyprland + run: nix build -L .#homeConfigurations.hm-hyprland.activationPackage diff --git a/.forgejo/workflows/deploy-docs.yml b/.forgejo/workflows/deploy-docs.yml new file mode 100644 index 0000000..6ac9020 --- /dev/null +++ b/.forgejo/workflows/deploy-docs.yml @@ -0,0 +1,22 @@ +name: Deploy docs + +on: + push: + branches: + - release-25.11 + +jobs: + build-and-deploy: + runs-on: host + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build documentation + run: | + STORE_PATH=$(nix build .#synix-docs --print-out-paths --no-link) + echo "STORE_PATH=$STORE_PATH" >> $GITHUB_ENV + + - name: Update symlink + run: ln -sfn ${{ env.STORE_PATH }} /var/www/doc diff --git a/.forgejo/workflows/flake-check.yml b/.forgejo/workflows/flake-check.yml new file mode 100644 index 0000000..002f079 --- /dev/null +++ b/.forgejo/workflows/flake-check.yml @@ -0,0 +1,13 @@ +name: Flake check + +on: [pull_request] + +jobs: + flake-check: + runs-on: runner + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run flake check + run: nix flake check --impure diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..153700e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.direnv/ +.pre-commit-config.yaml +result +site/ +templates/**/*.lock +templates/**/.pre-commit-config.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fd56ce --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# synix + +NixOS and Home Manager modules, packages, and automation for client and server deployments. + +Explore the outputs: +```bash +nix flake show git+https://git.sid.ovh/sid/synix +``` + +**Support:** `x86_64-linux` (stable), `aarch64-linux` (experimental) + +## Directory Structure + +- `apps/`: Deployment, installation, and update tooling. +- `docs/`: Project documentation. +- `lib/`: Helper functions. +- `modules/`: NixOS and Home Manager modules. +- `overlays/`: Package overrides and fixes. +- `pkgs/`: Custom packages. +- `templates/`: Boilerplates for `nix-config` and dev environments. + +## Usage + +Add this repo to your flake inputs: +```nix +# flake.nix +inputs.synix.url = "git+https://git.sid.ovh/sid/synix.git"; +``` + +See the [documentation](https://doc.sid.ovh/synix) for a full setup guide. + +## Templates + +Initialize a template in a new directory: +```bash +nix flake init -t git+https://git.sid.ovh/sid/synix#TEMPLATE +``` + +> Available templates: `nix-config`, `c-hello`, `microvm`, etc. See [flake.nix](./flake.nix) for the full list. + +## Contributing + +1. Clone & checkout `develop`: + ```bash + git clone https://git.sid.ovh/sid/synix && cd synix + git checkout develop + ``` +2. Validate changes: + ```bash + nix fmt # formats the code + nix flake check # checks formatting, runs build and runtime tests + ``` +3. Submit patches via `git format-patch` to **sid@sid.ovh** or Matrix **@sid:sid.ovh**. + +Thank you for contributing! diff --git a/apps/create/create.sh b/apps/create/create.sh new file mode 100644 index 0000000..20fb7a8 --- /dev/null +++ b/apps/create/create.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash + +# Default values +GIT_NAME="" +GIT_EMAIL="" +FLAKE="$HOME/.config/nixos" +TEMPLATE="" +USERNAME="" +HOSTNAME="" + +# Templates with Home Manager configurations +HM_CONFIGS=("hyprland") + +# This will get overwritten by the derivation +TEMPLATES_DIR="" + +# Print usage information +usage() { + cat < $1\033[0m"; } +success() { echo -e "\033[0;32m$1\033[0m"; } +error() { echo -e "\033[0;31mError: $1\033[0m" >&2; exit 1; } + +while [[ $# -gt 0 ]]; do + case "$1" in + switch|boot|test) ACTION="$1"; shift ;; + -f|--flake) FLAKE_URI="$2"; shift 2 ;; + -c|--config) CONFIG_FILE="$2"; shift 2 ;; + --no-sudo) USE_SUDO=false; shift ;; + --skip-build) DO_BUILD=false; shift ;; + -h|--help) usage; exit 0 ;; + *) error "Invalid argument '$1'" ;; + esac +done + +command -v jq &> /dev/null || error "jq is not installed." +[ -f "$CONFIG_FILE" ] || error "Config '$CONFIG_FILE' not found." + +BUILD_HOST=$(jq -r '.buildHost // "localhost"' "$CONFIG_FILE") +[[ "$BUILD_HOST" =~ ^(127\.0\.0\.1|::1)$ ]] && BUILD_HOST="localhost" + +mapfile -t HOST_ENTRIES < <(jq -r '.hosts[] | "\(.name) \(.address)"' "$CONFIG_FILE") +[ ${#HOST_ENTRIES[@]} -eq 0 ] && error "No hosts defined in $CONFIG_FILE" + +echo "Action: $ACTION" +echo "Flake: $FLAKE_URI" +echo "Builder: $BUILD_HOST" + +if [ "$DO_BUILD" = true ]; then + _status "Building configurations..." + for entry in "${HOST_ENTRIES[@]}"; do + read -r name address <<< "$entry" + echo "------------------------------------------------" + echo "Building host '$name':" + + CMD=("nixos-rebuild" "build" "--flake" "${FLAKE_URI}#${name}") + [[ "$BUILD_HOST" != "localhost" ]] && CMD+=("--build-host" "$BUILD_HOST") + + "${CMD[@]}" || error "Build failed for $name" + success "Build for host '$name' successful." + done +fi + +_status "Deploying to targets..." +for entry in "${HOST_ENTRIES[@]}"; do + read -r name address <<< "$entry" + echo "------------------------------------------------" + echo "Deploying to host '$name' ($address):" + + CMD=("nixos-rebuild" "$ACTION" "--flake" "${FLAKE_URI}#${name}" "--target-host" "$address") + [[ "$BUILD_HOST" != "localhost" ]] && CMD+=("--build-host" "$BUILD_HOST") + [[ "$USE_SUDO" = true ]] && CMD+=("--sudo" "--ask-sudo-password") + + "${CMD[@]}" || error "Activation failed for $name" + success "Host '$name' updated." +done + +success "Deployment complete." diff --git a/apps/install/default.nix b/apps/install/default.nix new file mode 100644 index 0000000..64133bf --- /dev/null +++ b/apps/install/default.nix @@ -0,0 +1,18 @@ +{ + writeShellApplication, + git, + ... +}: + +let + name = "install"; + text = builtins.readFile ./${name}.sh; +in +writeShellApplication { + inherit name text; + meta.mainProgram = name; + + runtimeInputs = [ + git + ]; +} diff --git a/apps/install/install.sh b/apps/install/install.sh new file mode 100755 index 0000000..1f53b98 --- /dev/null +++ b/apps/install/install.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash + +# NixOS install script + + +### VARIABLES ### + +ASK_VERIFICATION=1 # Default to ask for verification +CONFIG_DIR="/tmp/nixos" # Directory to copy flake to / clone flake into +GIT_BRANCH="master" # Default Git branch +GIT_REPO="" # Git repository URL +HOSTNAME="" # Hostname +MNT="/mnt" # root mount point +SEPARATOR="________________________________________" # line separator + +### FUNCTIONS ### + +# Function to display help information +Show_help() { + echo "Usage: $0 [-r REPO] [-n HOSTNAME] [-b BRANCH] [-y] [-h]" + echo + echo "Options:" + echo " -r, --repo REPO Your NixOS configuration Git repository URL" + echo " -n, --hostname HOSTNAME Specify the hostname for the NixOS configuration" + echo " -b, --branch BRANCH Specify the Git branch to use (default: $GIT_BRANCH)" + echo " -y, --yes Do not ask for user verification before proceeding" + echo " -h, --help Show this help message and exit" +} + +# Function to format, partition, and mount disks for $HOSTNAME using disko +Run_disko() { + echo "$SEPARATOR" + echo "Running disko..." + nix --experimental-features "nix-command flakes" run github:nix-community/disko/latest -- --mode disko "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.nix +} + +# Function to format, partition, and mount disks for $HOSTNAME using a partitioning script +Run_script() { + echo "$SEPARATOR" + echo "Running partitioning script..." + bash "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.sh +} + +# Function to check mount points and partitioning +Check_partitioning() { + echo "$SEPARATOR" + echo "Printing mount points and partitioning..." + mount | grep "$MNT" + lsblk -f + [[ "$ASK_VERIFICATION" == 1 ]] && read -rp "Verify the mount points and partitioning. Press Ctrl+c to cancel or Enter to continue..." +} + +# Function to generate hardware configuration +Generate_hardware_config() { + [[ "$ASK_VERIFICATION" == 1 ]] && read -rp "No hardware configuration found. Press Ctrl+c to cancel or Enter to generate one..." + + echo "$SEPARATOR" + echo "Generating hardware configuration..." + nixos-generate-config --root "$MNT" --show-hardware-config > "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix + + # Check if hardware configuration has been generated + if [[ ! -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix ]]; then + echo "Error: Hardware configuration cannot be generated." + exit 1 + fi + + # Add configuration to git + # TODO: get rid of cd + cd "$CONFIG_DIR"/hosts/"$HOSTNAME" || exit 1 + git add "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix + cd || exit 1 + + echo "Hardware configuration generated successfully." +}; + +# Function to install configuration for $HOSTNAME +Install() { + # Check if hardware configuration exists + [[ ! -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/hardware.nix ]] && Generate_hardware_config + + echo "$SEPARATOR" + echo "Installing NixOS..." + nixos-install --root "$MNT" --no-root-password --flake "$CONFIG_DIR"#"$HOSTNAME" && echo "You can reboot the system now." +} + +### PARSE ARGUMENTS ### + +while [[ "$#" -gt 0 ]]; do + case $1 in + -r|--repo) GIT_REPO="$2"; shift ;; + -b|--branch) GIT_BRANCH="$2"; shift ;; + -y|--yes) ASK_VERIFICATION=0 ;; + -h|--help) Show_help; exit 0 ;; + -n|--hostname) HOSTNAME="$2"; shift ;; + *) echo "Unknown option: $1"; Show_help; exit 1 ;; + esac + shift +done + +### PREREQUISITES ### + +echo "$SEPARATOR" +mkdir -p "$CONFIG_DIR" + +# Clone NixOS configuration from $GIT_REPO if provided +if [[ ! -z "$GIT_REPO" ]]; then + # Install git if not already installed + if ! command -v git &> /dev/null; then + echo "Git is not installed. Installing..." + nix-env -iA nixos.git + fi + + # Clone Git repo if directory is empty + if [[ -z "$(ls -A "$CONFIG_DIR" 2>/dev/null)" ]]; then + echo "Cloning NixOS configuration repo..." + git clone --depth 1 -b "$GIT_BRANCH" "$GIT_REPO" "$CONFIG_DIR" + + # Check if git repository has been cloned + if [[ ! -d "$CONFIG_DIR"/.git ]]; then + echo "Error: Git repository could not be cloned." + exit 1 + fi + else + echo "$CONFIG_DIR is not empty. Skip cloning $GIT_REPO." + fi +fi + +if [[ ! -f "$CONFIG_DIR"/flake.nix ]]; then + echo "Error: $CONFIG_DIR does not contain 'flake.nix'." + exit 1 +fi + +### CHOOSE CONFIG ### + +# If hostname is not provided via options, prompt the user +if [[ -z "$HOSTNAME" ]]; then + # Get list of available hostnames + HOSTNAMES=$(ls "$CONFIG_DIR"/hosts) + + echo "$SEPARATOR" + echo "Please choose a hostname to install its NixOS configuration." + echo "$HOSTNAMES" + read -rp "Enter hostname: " HOSTNAME + + # Check if hostname is empty + if [[ -z "$HOSTNAME" ]]; then + echo "Error: Hostname cannot be empty." + exit 1 + fi +fi + +### INSTALLATION ### + +# Check if NixOS configuration exists +if [[ -d "$CONFIG_DIR"/hosts/"$HOSTNAME" ]]; then + + # Check for existing disko configuration + if [[ -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.nix ]]; then + Run_disko || ( echo "Error: disko failed." && exit 1 ) + # Check for partitioning script + elif [[ -f "$CONFIG_DIR"/hosts/"$HOSTNAME"/disks.sh ]]; then + Run_script || ( echo "Error: Partitioning script failed." && exit 1 ) + else + echo "Error: No disko configuration (disks.nix) or partitioning script (disks.sh) found for host '$HOSTNAME'." + exit 1 + fi + + Check_partitioning + Install || ( echo "Error: Installation failed." && exit 1 ) +else + echo "Error: Configuration for host '$HOSTNAME' does not exist." + exit 1 +fi diff --git a/apps/rebuild/default.nix b/apps/rebuild/default.nix new file mode 100644 index 0000000..1a0eb11 --- /dev/null +++ b/apps/rebuild/default.nix @@ -0,0 +1,30 @@ +{ + writeShellApplication, + coreutils, + gnugrep, + gnused, + home-manager, + hostname, + nix, + nixos-rebuild, + ... +}: + +let + name = "rebuild"; + text = builtins.readFile ./${name}.sh; +in +writeShellApplication { + inherit name text; + meta.mainProgram = name; + + runtimeInputs = [ + coreutils + gnugrep + gnused + home-manager + hostname + nix + nixos-rebuild + ]; +} diff --git a/apps/rebuild/rebuild.sh b/apps/rebuild/rebuild.sh new file mode 100755 index 0000000..752b9b7 --- /dev/null +++ b/apps/rebuild/rebuild.sh @@ -0,0 +1,246 @@ +# NixOS and standalone Home Manager rebuild script + +# Defaults +FLAKE_PATH="$HOME/.config/nixos" # Default flake path +HOME_USER="$(whoami)" # Default username. Used to identify the Home Manager configuration +NIXOS_HOST="$(hostname)" # Default hostname. Used to identify the NixOS and Home Manager configuration +BUILD_HOST="" # Default build host. Empty means localhost +TARGET_HOST="" # Default target host. Empty means localhost +UPDATE=0 # Default to not update flake repositories +UPDATE_INPUTS="" # Default list of inputs to update. Empty means all +ROLLBACK=0 # Default to not rollback +SHOW_TRACE=0 # Default to not show detailed error messages + +# Function to display the help message +Help() { + echo "Wrapper script for 'nixos-rebuild switch' and 'home-manager switch' commands." + echo "Usage: rebuild [OPTIONS]" + echo + echo "Commands:" + echo " nixos Rebuild NixOS configuration" + echo " home Rebuild Home Manager configuration" + echo " all Rebuild both NixOS and Home Manager configurations" + echo " help Show this help message" + echo + echo "Options (for NixOS and Home Manager):" + echo " -H, --host Specify the hostname (as in 'nixosConfiguraions.'). Default: $NIXOS_HOST" + echo " -p, --path Set the path to the flake directory. Default: $FLAKE_PATH" + echo " -U, --update [inputs] Update all flake inputs. Optionally provide comma-separated list of inputs to update instead." + echo " -r, --rollback Don't build the new configuration, but use the previous generation instead" + echo " -t, --show-trace Show detailed error messages" + echo + echo "NixOS only options:" + echo " -B, --build-host Use a remote host for building the configuration via SSH" + echo " -T, --target-host Deploy the configuration to a remote host via SSH. If '--host' is specified, it will be used as the target host." + echo + echo "Home Manager only options:" + echo " -u, --user Specify the username (as in 'homeConfigurations.@'). Default: $HOME_USER" +} + +# Function to handle errors +error() { + echo "Error: $1" + exit 1 +} + +# Function to rebuild NixOS configuration +Rebuild_nixos() { + local FLAKE="$FLAKE_PATH#$NIXOS_HOST" + + # Construct rebuild command + local CMD=("nixos-rebuild" "switch" "--sudo") + [[ -n "$TARGET_HOST" || -n "$BUILD_HOST" ]] && CMD+=("--ask-sudo-password") + CMD+=("--flake" "$FLAKE") + [ "$ROLLBACK" = 1 ] && CMD+=("--rollback") + [ "$SHOW_TRACE" = 1 ] && CMD+=("--show-trace") + [ -n "$BUILD_HOST" ] && CMD+=("--build-host" "$BUILD_HOST") + if [ "$NIXOS_HOST" != "$(hostname)" ] && [ -z "$TARGET_HOST" ]; then + TARGET_HOST="$NIXOS_HOST" + echo "Using '$TARGET_HOST' as target host." + fi + [ -n "$TARGET_HOST" ] && CMD+=("--target-host" "$TARGET_HOST") + + # Rebuild NixOS configuration + if [ "$ROLLBACK" = 0 ]; then + echo "Rebuilding NixOS configuration '$FLAKE'..." + else + echo "Rolling back to last NixOS generation..." + fi + + echo "Executing command: ${CMD[*]}" + "${CMD[@]}" || error "NixOS rebuild failed" + echo "NixOS rebuild completed successfully." +} + +# Function to rebuild Home Manager configuration +Rebuild_home() { + local FLAKE="$FLAKE_PATH#$HOME_USER@$NIXOS_HOST" + + if [ -n "$BUILD_HOST" ] || [ -n "$TARGET_HOST" ]; then + error "Remote building is not supported for Home Manager." + fi + + # Construct rebuild command + local CMD=() + if [ "$ROLLBACK" = 1 ]; then + local rollback_path + rollback_path=$(home-manager generations | sed -n '2p' | grep -o '/nix/store[^ ]*') + CMD+=("$rollback_path/activate") + else + CMD=("home-manager" "switch" "--flake" "$FLAKE") + [ "$SHOW_TRACE" = 1 ] && CMD+=("--show-trace") + fi + + # Rebuild Home Manager configuration + if [ "$ROLLBACK" = 0 ]; then + echo "Rebuilding Home Manager configuration '$FLAKE'..." + else + echo "Rolling back to last Home Manager generation..." + fi + + echo "Executing command: ${CMD[*]}" + "${CMD[@]}" || error "Home Manager rebuild failed" + echo "Home Manager rebuild completed successfully." +} + +# Function to Update flake repositories +Update() { + echo "Updating flake inputs..." + + # Construct update command as an array + local CMD=("nix" "flake" "update" "--flake" "$FLAKE_PATH") + if [ -n "$UPDATE_INPUTS" ]; then + # Split comma-separated inputs and pass them to nix flake update + IFS=',' read -ra INPUTS <<< "$UPDATE_INPUTS" + for input in "${INPUTS[@]}"; do + CMD+=("$input") + done + fi + + echo "Executing command: ${CMD[*]}" + "${CMD[@]}" || error "Failed to update flake repositories" + echo "Flake repositories updated successfully." +} + +# Parse command-line options +if [[ -z "${1:-}" ]]; then + echo "Error: No command specified. Printing help page." + Help + exit 1 +fi +COMMAND=$1 +shift + +# Handle help command early +if [ "$COMMAND" = "help" ] || [ "$COMMAND" = "--help" ] || [ "$COMMAND" = "-h" ]; then + Help + exit 0 +fi + +while [ $# -gt 0 ]; do + case "${1:-}" in + -H|--host) + if [ -n "${2:-}" ]; then + NIXOS_HOST="$2" + shift 2 + else + error "-H|--host option requires an argument" + fi + ;; + -u|--user) + if [ -n "${2:-}" ]; then + HOME_USER="$2" + shift 2 + else + error "-u|--user option requires an argument" + fi + ;; + -p|--path) + if [ -n "${2:-}" ]; then + FLAKE_PATH="$2" + shift 2 + else + error "-p|--path option requires an argument" + fi + ;; + -U|--update) + UPDATE=1 + # Check if next argument is a non-option + if [ $# -gt 1 ] && [ "${2#-}" = "${2:-}" ]; then + UPDATE_INPUTS="$2" + shift 2 + else + shift + fi + ;; + -r|--rollback) + ROLLBACK=1 + shift + ;; + -t|--show-trace) + SHOW_TRACE=1 + shift + ;; + -B|--build-host) + if [ -n "${2:-}" ]; then + BUILD_HOST="$2" + shift 2 + else + error "-B|--build-host option requires an argument" + fi + ;; + -T|--target-host) + if [ -n "${2:-}" ]; then + TARGET_HOST="$2" + shift 2 + else + error "-T|--target-host option requires an argument" + fi + ;; + *) + echo "Error: Unknown option '$1'" + Help + exit 1 + ;; + esac +done + +# Check if script is run with sudo +if [ "$EUID" -eq 0 ]; then + error "Do not run this script with sudo." +fi + +# Check if flake path exists +if [ ! -d "$FLAKE_PATH" ]; then + error "Flake path '$FLAKE_PATH' does not exist" +fi + +# Ignore trailing slash in flake path +FLAKE_PATH="${FLAKE_PATH%/}" + +# Check if flake.nix exists +if [ ! -f "$FLAKE_PATH/flake.nix" ]; then + error "flake.nix does not exist in '$FLAKE_PATH'" +fi + +# Execute updates and rebuilds based on the command +[ "$UPDATE" = 1 ] && Update + +case "$COMMAND" in + nixos) + Rebuild_nixos + ;; + home) + Rebuild_home + ;; + all) + Rebuild_nixos + Rebuild_home + ;; + *) + echo "Error: Unknown command '$COMMAND'" + echo "Printing help page:" + Help + exit 1 + ;; +esac diff --git a/apps/update-packages/default.nix b/apps/update-packages/default.nix new file mode 100644 index 0000000..a82bb3e --- /dev/null +++ b/apps/update-packages/default.nix @@ -0,0 +1,20 @@ +{ + writeShellApplication, + jq, + nix-update, + ... +}: + +let + name = "update-packages"; + text = builtins.readFile ./${name}.sh; +in +writeShellApplication { + inherit name text; + meta.mainProgram = name; + + runtimeInputs = [ + jq + nix-update + ]; +} diff --git a/apps/update-packages/update-packages.sh b/apps/update-packages/update-packages.sh new file mode 100644 index 0000000..725ed4f --- /dev/null +++ b/apps/update-packages/update-packages.sh @@ -0,0 +1,65 @@ +SYSTEM="x86_64-linux" +IGNORE_PACKAGES=( + "pyman" + "synapse_change_display_name" +) + +error() { + echo "Error: $1" >&2 + exit 1 +} + +if [[ "$#" -gt 0 ]]; then + error "This script does not accept arguments." +fi + +TEMP_PACKAGE_LIST="/tmp/nix_flake_packages.$$" + +nix eval .#packages."$SYSTEM" --apply 'pkgs: builtins.attrNames pkgs' --json > "$TEMP_PACKAGE_LIST" 2>/dev/null || \ +error "Could not determine flake package attributes." + +PACKAGES=$(jq -r '.[]' "$TEMP_PACKAGE_LIST") + +if [ -z "$PACKAGES" ]; then + echo "No packages found in the flake outputs. Exiting." + rm -f "$TEMP_PACKAGE_LIST" + exit 0 +fi + +IGNORE_PATTERNS=$(printf "%s\n" "${IGNORE_PACKAGES[@]}") +PACKAGES=$(echo "$PACKAGES" | grep -v -F -f <(echo "$IGNORE_PATTERNS")) + +echo "Found the following packages to consider for update:" +echo "$PACKAGES" + +UPDATED_COUNT=0 +FAILED_UPDATES=() +for PACKAGE_NAME in $PACKAGES; do + echo "Attempting to update package: $PACKAGE_NAME" + + if nix-update "$PACKAGE_NAME" --flake --format; then + echo "Successfully updated $PACKAGE_NAME." + UPDATED_COUNT=$((UPDATED_COUNT + 1)) + else + echo "Failed to update $PACKAGE_NAME." >&2 + FAILED_UPDATES+=("$PACKAGE_NAME") + fi +done + +if [ -f "$TEMP_PACKAGE_LIST" ]; then + rm "$TEMP_PACKAGE_LIST" +fi + +echo +echo "Summary:" +echo "Packages scanned: $(echo "$PACKAGES" | wc -l)" +echo "Packages updated: $UPDATED_COUNT" + +if [ ${#FAILED_UPDATES[@]} -gt 0 ]; then + echo "Packages that failed to update:" >&2 + echo "${FAILED_UPDATES[@]}" + exit 1 +else + echo "All packages processed successfully." + exit 0 +fi diff --git a/apps/wake-host/default.nix b/apps/wake-host/default.nix new file mode 100644 index 0000000..86cc782 --- /dev/null +++ b/apps/wake-host/default.nix @@ -0,0 +1,20 @@ +{ + writeShellApplication, + iputils, + wakeonlan, + ... +}: + +let + name = "wake-host"; + text = builtins.readFile ./${name}.sh; +in +writeShellApplication { + inherit name text; + meta.mainProgram = name; + + runtimeInputs = [ + iputils + wakeonlan + ]; +} diff --git a/apps/wake-host/wake-host.sh b/apps/wake-host/wake-host.sh new file mode 100644 index 0000000..7c35155 --- /dev/null +++ b/apps/wake-host/wake-host.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +if [ "$#" -ne 2 ]; then + echo "Usage: wake-host " + echo "Example: wake-host AA:BB:CC:DD:EE:FF 100.64.0.10" + exit 1 +fi + +TARGET_MAC=$1 +TARGET_IP=$2 + +echo "Sending Magic Packet to $TARGET_MAC..." +wakeonlan "$TARGET_MAC" + +echo "Waiting for $TARGET_IP to wake up..." +MAX_RETRIES=24 +COUNT=0 +until ping -c 1 -W 2 "$TARGET_IP" > /dev/null 2>&1; do + COUNT=$((COUNT + 1)) + if [ $COUNT -ge $MAX_RETRIES ]; then + echo "Error: Host failed to wake up after $MAX_RETRIES pings." + exit 1 + fi + echo "[$COUNT/$MAX_RETRIES] Host is still sleeping..." + sleep 5 +done + +echo "Success: $TARGET_IP is awake." diff --git a/docs/getting-started/add-configs.md b/docs/getting-started/add-configs.md new file mode 100644 index 0000000..006bf3c --- /dev/null +++ b/docs/getting-started/add-configs.md @@ -0,0 +1,48 @@ +# Add NixOS and Home Manager configurations + +Choose a configuration template from [this list](https://git.sid.ovh/sid/synix/tree/master/apps/create/templates). + +Run the `create` script to add your desired configuration template to your nix-config flake: + +```bash +nix --experimental-features "nix-command flakes" run git+https://git.sid.ovh/sid/synix#apps.x86_64-linux.create -- \ +-t TEMPLATE \ +-u USERNAME \ +-H HOST \ +--git-name GIT_NAME \ +--git-email GIT_EMAIL \ +-f ~/.config/nixos +``` + +> Change the architecture if needed. Supported architectures are listet under `supportedSystems` inside [`flake.nix`](https://git.sid.ovh/sid/synix/blob/master/flake.nix). + +See the script's help page for reference: + +``` +Usage: create -t|--template TEMPLATE -u|--user USERNAME -H|--host HOSTNAME [-f|--flake PATH/TO/YOUR/NIX-CONFIG] [--git-name GIT_NAME] [--git-email GIT_EMAIL] + +Options: + -t, --template TEMPLATE Configuration template to use (mandatory) + -u, --user USERNAME Specify the username (mandatory) + -H, --host HOSTNAME Specify the hostname (mandatory) + -f, --flake FLAKE Path to your flake directory (optional, default: ~/.config/nixos) + --git-name GIT_NAME Specify the git name (optional, default: USERNAME) + --git-email GIT_EMAIL Specify the git email (optional, default: USERNAME@HOSTNAME) + -h, --help Show this help message + +Available configuration templates: + hyprland + server + pi4 + vm-uefi +``` + +All templates should work right out of the box. You only need to edit the disk partitioning script (`disks.sh`) or provide a [disko](https://github.com/nix-community/disko) configuration (`disko.nix`) in your host directory. A basic single disk partitioning script is provided. Set your disk by its ID, which comes from `ls -lAh /dev/disk/by-id`. + +> Warning: The create script applies patch files. It will print what it patched to stdout. It is strongly recommended to verify them manually. + +If you like, you can lock your flake before committing by running: + +```bash +nix --experimental-features "nix-command flakes" flake lock +``` diff --git a/docs/getting-started/create-nix-config.md b/docs/getting-started/create-nix-config.md new file mode 100644 index 0000000..0532763 --- /dev/null +++ b/docs/getting-started/create-nix-config.md @@ -0,0 +1,30 @@ +# Create your own nix-config flake + +Create an empty directory and apply a [nix-config template](https://git.sid.ovh/sid/synix/tree/master/templates/nix-config) to it: + +```bash +mkdir -p ~/.config/nixos +cd ~/.config/nixos +nix flake init -t "git+https://git.sid.ovh/sid/synix#templates.TEMPLATE" +``` + +Available templates are: +- hetzner-amd +- hyprland +- pi4 +- server +- vm-uefi + +> Note: You do not have to use `~/.config/nixos`, but configuration related scripts in this repository will use this directory as the default nix-config flake directory. + +Alternatively, use this flake's create script: + +```bash +nix run "git+https://git.sid.ovh/sid/synix#create" -- -t TEMPLATE -u YOUR_USER -h YOUR_HOSTNAME +``` + +Check: + +```bash +nix run "git+https://git.sid.ovh/sid/synix#create" -- --help +``` diff --git a/docs/getting-started/install-instructions.md b/docs/getting-started/install-instructions.md new file mode 100644 index 0000000..97488f3 --- /dev/null +++ b/docs/getting-started/install-instructions.md @@ -0,0 +1,127 @@ +# Installation Guide + +This guide will walk you through installing NixOS using the provided installation script [`install.sh`](https://git.sid.ovh/sid/synix/blob/master/apps/install/install.sh). + +## Prerequisites + +1. **Bootable NixOS Installation Medium**: Make sure you have booted into NixOS live environment from the [Minimal ISO image](https://nixos.org/download/#nixos-iso). Read the [official NixOS installation guide](https://nixos.org/manual/nixos/unstable/#sec-obtaining) for more information on how to create a bootable NixOS USB drive. +1. **Network Connection**: Ensure the target machine is connected to the internet. +1. **Host configuration**: The target machine needs to have a working NixOS configuration inside your own flake. A hardware configuration is not required as it can be generated automatically during installation. +1. **Disks setup**: The target machine needs to have a working disk configuration or partitioning script inside `hosts/HOSTNAME`. Disko expects its configuration to be in `hosts/HOSTNAME/disks.nix`. Alternatively, a shell script can be provided at `hosts/HOSTNAME/disks.sh` that will format, partition, and mount disks. + +> Using UEFI is recommended. + +### Optional: Virt-Manager config for Wayland + +If you want to install NixOS with Wayland support inside a VM using Virt-Manager, enable 3D acceleration by checking `Customize configuration before install`: + +1. Go to `Display ` and select `Spice Server` under `Type`. Select `None` under `Listen type`. Check `OpenGL` and select a device that is *not* from Nvidia. +1. Go to `Video ` and select `Virtio` under `Model`. Check `3D acceleration`. +1. Click `Begin installation` in the top left corner. + +If you get the error: + +```plaintext +Unable to complete install: 'unsupported configuration: domain configuration does not support video model 'virtio'' +``` + +Install the package `qemu-full`: + +```shell +sudo pacman -Syy qemu-full +``` + +> assuming you are on Arch Linux + +Then, reboot. + +## Steps + +Boot into NixOS ISO image on your target machine. + +### 0. SSH into the Target Machine +If you are using a remote machine, set a password for the user _nixos_ using `passwd`. Then, SSH into it using the following command: + +```bash +ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no nixos@ +``` + +> Replace `` with the IP address of the target machine which can be found using `ip a`. + +### 1. Become root +The default user `nixos` has sudo privileges. Become root to run the install script: + +```bash +sudo -i +``` + +### 2. Run the Install Script +Download the install script to the target machine and run it: + +```bash +nix --experimental-features "nix-command flakes" run git+https://git.sid.ovh/sid/synix#apps.x86_64-linux.install -- \ +-n HOST \ +-r REPOSITORY +``` + +> Replace `HOST` with the name of your target machine. +> Replace `REPOSITORY` with your flake URL. +> You can specify a branch with `-b BRANCH` (default: `master`) +> Print the usage page with `-h`. +> Change the architecture if needed. + +> Tip: If your Flake is not a public Git repository, you may provide the source code manually. First, copy your Flake directory to `/tmp/nixos` on the host machine. Then, you can omit the `-r` flag. + +### 3. Reboot your System +Once the installation completes, unmount the installation medium: + +```bash +umount -Rl /mnt +``` + +> If you have your root file system on ZFS, export all pools: `zpool export -a` + +Then, you can safely remove the installation medium and reboot your machine: + +> If you generated a new hardware configuration, you should save it before rebooting: +> `cat /tmp/nixos/hosts/HOSTNAME/hardware.nix` + +```bash +reboot now +``` + +### 4. Login +Upon reboot, your system will boot into the newly installed NixOS. Login as a valid user defined in the configuration of the host (`hosts/HOSTNAME/default.nix`). The default initial password is `changeme`. Change your password with `passwd` after login. + +### 5. Optional: Import age keys +If you use sops-nix with age in you Home Manager configuration, you need to import your age keys: + +```bash +mkdir -p ~/.config/sops/age +cp /PATH/TO/YOUR/keys.txt ~/.config/sops/age/keys.txt +``` + +### 6. Clone your Repository +Git is installed on every system by default. Clone your flake repository to your home directory: + +```bash +git clone YOUR_GIT_REPO_URL ~/.config/nixos +``` + +> The rebuild script expects your flake to be in `~/.config/nixos` + +### 7. Apply your Home Manager Configuration +Home Manager is not installed by default. Enter the development shell to apply the configuration: + +```bash +nix-shell ~/.config/nixos/shell.nix --run 'rebuild home' +``` + +### 8. Reboot your System +Once the home-manager configuration is applied, reboot your system: + +```bash +sudo reboot now +``` + +You may now log in. Your system is now fully configured. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..7c253c9 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,12 @@ +# synix docs + +Welcome to the documentation for [synix](https://git.sid.ovh/sid/synix). + +The goal of this project is to provide modules and packages for your NixOS and Home Manager configurations, suitable for client and server applications. Scripts are also included to automate the creation and installation of your own flake. + +## Explore the documentation: + +- **Introduction to Nix**: Understand the fundamentals of the Nix language and package manager, as well as NixOS. Start here if you are new. +- **Getting Started:** A guide to go from scratch to a complete, working NixOS configuration using synix. Start here if you know about Nix and NixOS. +- **Modules:** Information about available NixOS and Home Manager modules. +- **Tips:** Find recommendations and resources to navigate the Nix ecosystem. diff --git a/docs/introduction-to-nix/derivations.md b/docs/introduction-to-nix/derivations.md new file mode 100644 index 0000000..ce854de --- /dev/null +++ b/docs/introduction-to-nix/derivations.md @@ -0,0 +1,92 @@ +# Derivations + +At its core, Nix is about building software. Nix doesn't install software directly from a global repository; instead, it builds *derivations*. A derivation is a description of how to build a package. It's a pure function `inputs -> output`, meaning given the same inputs, it will always produce the same output. + +## Your first Derivation + +Let's build a simple "hello world" program. + +First, create a C source file `hello.c`: + +```c +// hello.c +#include + +int main() { + printf("Hello from C!\n"); + return 0; +} +``` + +Then, create a `default.nix` file that imports Nixpkgs and then calls the package definition below. + +```nix +# default.nix +{ pkgs ? import {} }: # Fetch Nixpkgs +# Nixpkgs is a collection of Nix expressions. +# We need some functions (like `callPackage`) that are defined there. +# Nixpkgs will be covered later in this guide. + +pkgs.callPackage ./my-hello.nix { } +# `callPackage` is a helper function for Package derivations. +# It automatically resolves all needed input arguments the derivation needs from Nixpkgs. +``` + +> Hint: `default.nix` will get replaced by Nix Flakes later. You do not need to know what Flakes are at the moment, but keep this relationship in mind. + +Now, define how to build the C source file a Nix file, `my-hello`.nix: + +```nix +# my-hello.nix +{ stdenv }: # Inputs + +stdenv.mkDerivation { + pname = "my-hello"; # Package name + version = "0.1.0"; # Package version + + src = ./.; # The source code for the package is in the current directory + + # Phases of the build process + # mkDerivation defines standard phases like unpackPhase, patchPhase, configurePhase, buildPhase, installPhase + # For simple builds, we just need build and install. + + buildPhase = '' + # Compile command + ${stdenv.cc}/bin/gcc hello.c -o hello + ''; + + installPhase = '' + # Install the compiled program into the output directory ($out) + mkdir -p $out/bin + cp hello $out/bin/hello + ''; +} +``` + +Let's break this down: + +- `stdenv`: This derivation is a function that expects `stdenv` (standard environment, providing common build tools and phases) as an argument. It will be automatically resolved from Nixpkgs. +- `stdenv.mkDerivation`: This is the core function to create a derivation. It sets up a standard build environment and provides a set of common build phases. +- `pname`, `version`: Standard metadata for the package. +- `src = ./.;`: This tells Nix to copy all files from the current directory into the build sandbox. +- `buildPhase`: This is where you put commands to compile your software. Here, `gcc` is used from the standard C compiler provided by `stdenv.cc` to compile `hello.c` into an executable `hello`. +- `installPhase`: This is where you put commands to install the build artifacts into the `$out` directory, which is the final location in the Nix store. Here, a `bin` directory is created to move the `hello` executable into. + +## Building and Running a Derivation + +To build this derivation, use `nix build`: + +```bash +nix build --file default.nix +``` + +You'll see output from the build process. If successful, Nix creates a `result` symlink in your current directory. This `result` symlink points to the package in the Nix store. + +Now, run your compiled program: + +```bash +./result/bin/hello +``` +``` +Hello from C! +``` diff --git a/docs/introduction-to-nix/flakes.md b/docs/introduction-to-nix/flakes.md new file mode 100644 index 0000000..3291734 --- /dev/null +++ b/docs/introduction-to-nix/flakes.md @@ -0,0 +1,209 @@ +# Flakes + +> Flakes are still an experimental feature in Nix. However, they are so widely used by the community that they almost became standard. Furthermore, *synix* uses Flakes. + +Nix flakes are a reproducible way to define, build, and deploy Nix projects, making them reliable and portable. + +Flakes accomplish that by: + +## Standardized Input + +They define a fixed, declarative input (the `flake.nix` file) that specifies all project dependencies, sources, and outputs. This eliminates implicit dependencies or environment variables that could cause builds to differ. + +Example in `flake.nix`: + +```nix +inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; # Declare we need nixpkgs, specifically this branch +}; +``` + +## Reproducible "Lock File" + +When you build or develop with a flake, Nix generates a `flake.lock` file. This file records the *exact* content-addressable hashes of *all* transitive inputs used for that specific build. This lock file can be committed to version control, ensuring that anyone else cloning the repository (or a CI system) will use precisely the same set of inputs and thus achieve the identical result. + +Example `flake.lock` entry for `nixpkgs`: + +```json +"nixpkgs": { + "locked": { + "lastModified": 1709259160, + "narHash": "sha256-...", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "b2f67f0b5d1a8e1b3c9f2d1e0f0e0c0b0a090807", // The exact commit! + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github", + "url": "github:NixOS/nixpkgs/nixos-23.11" + } +} +``` + +## Flake Schema + +The `flake.nix` has a well-defined structure for `inputs` (sources like Git repos, other flakes) and `outputs` (packages, applications, modules, etc.). This consistent schema makes flakes composable and predictable. + +A `flake.nix` file typically looks like this: + +```nix +# flake.nix +{ + description = "A simple example flake"; + + inputs = { + # Inputs are other flakes or external resources + nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.11"; # Locked to a specific branch/version + # This is how you would add synix to your flake: + # synix.url = "git+https://git.sid.ovh/sid/synix" + }; + + outputs = { self, nixpkgs, ... }@inputs: # 'self' refers to this flake, inputs are available + let + # Define common arguments for packages from nixpkgs + # This ensures all packages use the same version of Nixpkgs on this system + pkgs = import nixpkgs { + system = "x86_64-linux"; # The target system architecture + }; + in + { + # Outputs include packages, devShells, modules, etc. + # Packages that can be built by `nix build .#` + packages.x86_64-linux.my-app = pkgs.callPackage ./pkgs/my-app { }; + packages.x86_64-linux.my-other-app = pkgs.hello; # From nixpkgs directly + + # Development shells that can be entered using `nix develop` + devShells.x86_64-linux.default = pkgs.mkShell { + name = "my-dev-env"; + buildInputs = [ pkgs.nodejs pkgs.python3 ]; + shellHook = "echo 'Welcome to my dev environment!'"; + }; + + # NixOS modules (for system config) + # nixosConfigurations..modules = [ ./nixos-modules/webserver.nix ]; + # (This is more advanced and will be covered in NixOS section) + }; +} +``` + +Key parts of a `flake.nix`: + +- `description`: A human-readable description of your flake. +- `inputs`: Defines all dependencies of your flake. Each input has a `url` pointing to another flake (e.g., a GitHub repository, a local path, or a Git URL) and an optional `follows` attribute to link inputs. +- `outputs`: A function that takes `self` (this flake) and all `inputs` as arguments. It returns an attribute set defining what this flake provides. Common outputs are `packages`, `devShells`, `nixosConfigurations`, etc., usually segregated by system architecture. You can read more about flake outputs in the [NixOS & Flakes Book](https://nixos-and-flakes.thiscute.world/other-usage-of-flakes/outputs). + +## `nix flake` Commands + +The `nix flake` subcommand is your primary interface for interacting with flakes. Let's create a new flake to demonstrate them: + +Initialize the flake: + +```bash +mkdir my-flake && cd my-flake +nix flake init +``` + +This creates a minimal `flake.nix`. + +Lock your flake: + +```bash +nix flake lock +``` + +This creates `flake.lock`, a file that locks the exact versions of your inputs. + +Update flake inputs: + +```bash +nix flake update +``` + +This updates all inputs to their latest versions allowed by their `url` (e.g., the latest commit on `nixos-unstable` for `nixpkgs`) and then updates the `flake.lock` file. Since we just locked the flake for the first time, there probably won't be any updates available. + +Print flake inputs: + +```bash +nix flake metadata +``` + +Print flake outputs: + +```bash +nix flake show +``` + +Build packages from a flake: + +```bash +nix build .#hello # The '.' refers to the current directory's flake +./result/bin/hello +``` + +Run a package from a flake: + +```bash +nix run .#hello +``` + +Since the `packages..default` output exists, you can just do `nix run`. + +## `nix develop` + +This command spins up a temporary shell environment with all the tools and dependencies specified in your flake's `devShells` output. + +Let's expand your `flake.nix`: + +```nix +# flake.nix +{ + description = "A very basic flake"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; + }; + + outputs = + { self, nixpkgs }: + let + # Define `pkgs` for the current system + pkgs = import nixpkgs { + system = "x86_64-linux"; + }; + in + { + packages.x86_64-linux.hello = nixpkgs.legacyPackages.x86_64-linux.hello; + # With `pkgs` defined, we could also do this: + # packages.x86_64-linux.hello = pkgs.hello; + + packages.x86_64-linux.default = self.packages.x86_64-linux.hello; + + devShells.x86_64-linux.default = pkgs.mkShell { + # Packages available in the shell + packages = [ + pkgs.git + pkgs.go + pkgs.neovim + ]; + # Environment variables for the shell + GIT_COMMITTER_EMAIL = "your-email@example.com"; + # Commands to run when entering the shell + shellHook = '' + echo "Entering development shell for my project." + echo "You have Git, Go, and Neovim available." + ''; + }; + }; +} +``` + +Now, from your project directory: + +```bash +nix develop +``` + +You'll instantly find yourself in a shell where `git`, `go`, and `nvim` are available, and your `GIT_COMMITTER_EMAIL` is set. When you exit, your regular shell environment is restored – no lingering installations or modified global state. This makes it incredibly easy to switch between projects, each with its specific toolchain and dependencies, without conflicts. diff --git a/docs/introduction-to-nix/install-nix.md b/docs/introduction-to-nix/install-nix.md new file mode 100644 index 0000000..4287e8b --- /dev/null +++ b/docs/introduction-to-nix/install-nix.md @@ -0,0 +1,22 @@ +# Install Nix + +Install the Nix package manager according to the official documentation on [nixos.org](https://nixos.org/download/). + +On Linux, simply run: + +```bash +sh <(curl --proto '=https' --tlsv1.2 -L https://nixos.org/nix/install) --daemon +``` + +## Configuration + +Add the following to `~/.config/nix/nix.conf` (recommended) or `/etc/nix/nix.conf`: + +```ini +experimental-features = nix-command flakes +``` + +- `nix-command` enables the [new `nix` CLI](https://nix.dev/manual/nix/2.29/command-ref/new-cli/nix.html) Nix is transitioning to. +- `flakes` will be covered later in this guide. Don't worry about them for now. + +Reload your session to get access to the `nix` command. diff --git a/docs/introduction-to-nix/nix-speedrun.md b/docs/introduction-to-nix/nix-speedrun.md new file mode 100644 index 0000000..b4d1194 --- /dev/null +++ b/docs/introduction-to-nix/nix-speedrun.md @@ -0,0 +1,227 @@ +# Nix Speedrun + +This section will cover some Nix language basics as fast as possible. + +## Comments + +```nix +# This is a comment + +/* + This is a block comment +*/ +``` + +## Data types + +Every value in Nix has a type. Some basic types are: + +```nix +16 # integer + +3.14 # float + +false # boolean + +"Hello, world!" # string + +'' + This is also a string, + but over multiple lines! +'' +``` + +Assign a value to a variable: + +```nix +myVar = "99"; +``` + +And then inject it into a string: + +```nix +'' + I got ${myVar} problems, + but Nix ain't one. +'' +``` + +Nix also has compound values. This is a list: + +```nix +[ 123 "hello" true null [ 1 2 ] ] +``` + +You can mix different types in a list. This is an attribute set: + +```nix +{ + foo = 4.56; + bar = { + baz = "this"; + qux = false; + }; +} +``` + +An attribute set is like an object. It is a collection of name-value-pairs called *attributes*. The expression above is equivalent to: + +```nix +{ + foo = 4.56; + bar.baz = "this"; + bar.qux = false; +} +``` + +## Evaluation + +In Nix, everything is an expression that evaluates to a value. Create a `hello.nix`-file with the following content: + +```nix +"Hello, world!" +``` + +Then, evaluate the file: + +```bash +nix eval --file hello.nix +``` + +``` +Hello, world! +``` + +A let-expression allows you to define local variables for an expression: + +```nix +let + alice = { + name = "Alice"; + age = "26"; + }; +in +'' + Her name is ${alice.name}. + She is ${alice.age} years old. +'' +``` + +## Functions + +Functions have the following form: + +```nix +pattern: body +``` + +The pattern specifies what the argument of the function must look like, and binds variables in the body to (parts of) the argument. + +```nix +let + increment = num: num + 1; +in +increment 49 +``` + +Functions can only have a single argument. For multiple arguments, nest functions: + +```nix +let + isAllowedToDrive = + name: age: + if age >= 18 then "${name} is eligible to drive." else "${name} is too young to drive yet."; +in +isAllowedToDrive "Charlie" 19 +``` + +It is common to pass multiple arguments in an attribute set instead. Since Nix is lazily evaluated, you can define multiple bindings in the same let-statement. + +```nix +let + add = { a, b }: a + b; + result = add { a = 34; b = 35; }; +in +result +``` + +You can also set optional arguments by providing default values: + +```nix +let + greet = { greeting ? "Hello", name }: "${greeting}, ${name}!"; +in +greet { name = "Bob"; } +``` + +Let's look at one last example: + +```nix +let + myFunc = { a, b, c }: a + b * c; + + numbers = { + a = 1; + b = 2; + c = 3; + }; + + result = myFunc { a = numbers.a; b = numbers.b; c = numbers.c; }; +in +result +``` + +Nix provides some syntactical sugar to simplify that function call. The `with` keyword brings all attributes from an attribute set into the scope: + +```nix +# ... + result = with numbers; myFunc { a = a; b = b; c = c; }; +# ... +``` + +However, this syntax is discouraged. Use `inherit` instead to explicitly list attributes to bring into the scope: + +```nix +# ... + inherit (numbers) a b c; + result = myFunc { inherit a b c; }; +# ... +``` + +## Builtin functions + +Nix provides [builtin functions](https://nix.dev/manual/nix/2.25/language/builtins) by default through the global `builtins` constant. For example, `builtins.attrNames` gives you a list of all attributes of the given attribute set: + +```nix +builtins.attrNames { a = 1; b = 2; } +# => [ "a" "b" ] +``` + +> Yes, this means that attribute keys, though defined as variables, are available as strings. + +Some builtins are so common that the `builtins` prefix can be omitted. `map` is a builtin function that applies a function to each element of a list. + +```nix +# squares.nix +let + numbers = [ 5 2 1 4 3 ]; + squares = map (n: n * n) numbers; +in +{ + inherit numbers squares; +} +``` + +The `import` function allows to separate the codebase into multiple files: + +```nix +# sort.nix +let + results = import ./squares.nix; # paths have their own type + inherit (results) squares; + inherit (builtins) sort lessThan; +in +sort lessThan squares +``` + +> The `sort` function can be found in the [Nix manual](https://nix.dev/manual/nix/2.25/language/builtins#builtins-sort). diff --git a/docs/introduction-to-nix/nix-store.md b/docs/introduction-to-nix/nix-store.md new file mode 100644 index 0000000..e2b08ab --- /dev/null +++ b/docs/introduction-to-nix/nix-store.md @@ -0,0 +1,40 @@ +# Nix Store + +You've built a package, and it landed in the `/nix/store`. The Nix store is the heart of Nix's reproducibility, atomicity, and rollback capabilities. + +## Unique Paths (Hashing) + +Every piece of software, configuration, or data managed by Nix lives in the Nix store under a unique, cryptographically hashed path. For example, `nix build` might produce something like: + +``` +/nix/store/zx9qxw749wmla1fad93al7yw2mg1jvzf-my-hello-0.1.0 +``` + +A Nix store path consists of its hash and a human readable name with a version, which are defined in the corresponding derivation. The hash ensures: + +1. **Immutability:** Entries in the Nix Store are read only. Once something is in the Nix store, it never changes. If you modify a source file or a build instruction, it creates a *new* derivation with a *new* hash, and thus a *new* path in the store. The old version remains untouched. +2. **Reproducibility:** If two different systems build the exact same derivation, they will produce the exact same hash and thus the exact same path. This guarantees that "it works on my machine" translates to "it works on *any* Nix machine." +3. **Collision Avoidance:** Because the path includes a hash of all its inputs (source code, build script, compiler, libraries, etc.), different versions or configurations of the same package can coexist peacefully in the store without conflicting. + +You can inspect the contents of a store path directly: + +```bash +ls -l /nix/store/zx9qxw749wmla1fad93al7yw2mg1jvzf-my-hello-0.1.0/bin +``` + +Replace the hash with the actual hash from your previous `nix build` command or `ls -l result`. + +## Dependency Resolution + +The Nix store is also a giant, explicit dependency graph. +When you define a derivation for `my-hello` that uses `stdenv` and `gcc`, Nix doesn't just build `my-hello`. It first ensures that `stdenv` and `gcc` (and their own dependencies, recursively) are also present in the Nix store. + +Let's look at the dependencies of your `my-hello` derivation: + +```bash +nix path-info --recursive ./result +``` + +This command will list all the Nix store paths that `my-hello` directly or indirectly depends on. You'll see things like `glibc`, `gcc`, and many other low-level system libraries. Each of these is itself a derivation built and stored in the Nix store under its own unique hash. + +This means that conflicts are impossible because different versions of the same library (e.g., `libssl-1.0` and `libssl-3.0`) can coexist peacefully in `/nix/store` under their distinct hashes. diff --git a/docs/introduction-to-nix/nixos.md b/docs/introduction-to-nix/nixos.md new file mode 100644 index 0000000..c82d1eb --- /dev/null +++ b/docs/introduction-to-nix/nixos.md @@ -0,0 +1,200 @@ +# NixOS + +NixOS is a Linux distribution built entirely on top of the Nix package manager and the Nix language. This means your entire operating system, from the kernel to user-space applications and system services, is declared in a set of Nix expressions. This brings all the benefits of Nix (reproducibility, atomic upgrades, easy rollbacks) to your whole system. + +## NixOS Configuration (with Flakes) + +With flakes, your NixOS configuration typically resides in a `flake.nix` file that exports a `nixosConfigurations` output. + +Let's have a look at a basic `flake.nix` for a NixOS machine. + +```nix +# flake.nix +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + }; + + outputs = + { + self, # The flake itself + nixpkgs, # The nixpkgs input + ... + }@inputs: # `self` and `nixpkgs` are available under `inputs` + let + inherit (self) outputs; + in + { + # Define NixOS configurations + nixosConfigurations = { + # Name for this specific system configuration + your-pc = nixpkgs.lib.nixosSystem { + # Arguments passed to all NixOS modules + specialArgs = { + inherit inputs outputs; + }; + # List of all configuration files (modules) + modules = [ ./configuration.nix ]; + }; + }; + }; +} +``` + +The `nixosSystem` function takes a list of `modules`. Each module is a Nix expression that defines desired system state and settings. So the actual system configuration lives in `configuration.nix`: + +```nix +# configuration.nix +{ config, pkgs, ... }: # The arguments provided to a NixOS module + +{ + # Enable a display manager and desktop environment + services.displayManager.lightdm.enable = true; + services.desktopManager.gnome.enable = true; # Or kde, xfce, etc. + + # List of packages to be installed globally + environment.systemPackages = with pkgs; [ + firefox + neovim + git + ]; + + # Configure networking + networking.hostName = "my-nixos-desktop"; + + # Users + users.users.Alice = { + isNormalUser = true; + extraGroups = [ "wheel" "networkmanager" ]; # Add user to groups for sudo and network management + initialPassword = "changeme"; # Set a temporary password + }; + + # Set system-wide locale + i18n.defaultLocale = "en_US.UTF-8"; + + # Set the system time zone + time.timeZone = "America/New_York"; + + # ... many more options ... +} +``` + +> Please note that the above configuration is not a complete working NixOS configuration. It just showcases how to you can define your system declaratively. + +The `config` argument is the *evaluated* final configuration of your system. You use it to refer to other parts of your configuration. For example, you might make one service depend on another's path: + +```nix +myService.dataPath = config.services.otherService.dataPath; +``` + +It's primarily used for referencing options *within* the configuration. + +## The Module System + +NixOS uses a powerful *module system*. A module is a Nix expression that declares: + +- **`options`**: What configurable parameters this module exposes. +- **`config`**: How this module sets those parameters (and potentially other system parameters). +- **`imports`**: Other modules to include. + +When you build your NixOS configuration using `nixos-rebuild switch --flake path/to/flake/directory#your-pc`, NixOS collects all the options and configurations from all activated modules, merges them, and then builds a new system closure in the Nix store. + +## Searching NixOS Options + +There are thousands of options in NixOS. You can search them in the [NixOS Options Search](https://search.nixos.org/options?channel=unstable). + +For example, search for `services.desktopManager` to list all options regarding desktop managers. + +## Home Manager + +While NixOS manages system-wide configurations, **Home Manager** applies the power of Nix to your *user-specific* configuration files and dotfiles. Instead of manually symlinking dotfiles or writing install scripts, you define your user environment declaratively in Nix. Home Manager applies Nix's declarative power to the user space, much like NixOS does for the system space. + +Let's extend our `flake.nix`: + +```nix +# flake.nix +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + home-manager.url = "github:nix-community/home-manager"; + home-manager.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + home-manager, + ... + }@inputs: + let + inherit (self) outputs; + in + { + nixosConfigurations = { + your-pc = nixpkgs.lib.nixosSystem { + specialArgs = { + inherit inputs outputs; + }; + modules = [ ./configuration.nix ]; + }; + }; + + homeConfigurations = { + your-user = home-manager.lib.homeManagerConfiguration { + pkgs = nixpkgs.legacyPackages.x86_64-linux; + extraSpecialArgs = { + inherit inputs outputs; + }; + modules = [ ./home.nix ]; + }; + }; + }; +} +``` + +`home.nix` might look like this: + +```nix +# home.nix +{ config, pkgs, ... }: + +{ + # Define your user's home directory + home.username = "youruser"; + home.homeDirectory = "/home/youruser"; + + # Install user-specific packages + home.packages = with pkgs; [ + htop + cowsay + ]; + + # Configure zsh + programs.zsh.enable = true; + programs.zsh.ohMyZsh.enable = true; + programs.zsh.ohMyZsh.plugins = [ "git" "history" ]; + + # Git configuration + programs.git = { + enable = true; + userName = "Your Name"; + userEmail = "your.email@example.com"; + }; + + # ... many more options for things like VS Code, Tmux, themes, fonts etc. +} +``` + +You could now build your Home Manager configuration with `home-manager switch --flake path/to/flake/directory#your-user`. + +Search for Home Manager options in the [Home Manager Options Search](https://home-manager-options.extranix.com/?release=master). + +## What synix does + +The [`synix` repository](https://git.sid.ovh/sid/synix) attempts to automate your NixOS and Home Manager experience. It exposes NixOS and Home Manager modules that sit on top of the already existing modules in NixOS and Home Manager respectively. Module options are added and opinionated defaults are set to get your configuration running with less configuration options needed to be set. + +Create your NixOS and Home Manager configuration flake (we call that `nix-config`) with synix as an input using a template provided in the repository. Adding NixOS and Home Manager configurations is automated through a shell script. You can choose between some configuration templates for server or client systems. The installation process is automated through a shell script as well. Also, an installation guide is provided. Rebuilding your NixOS and Home Manager configurations is wrapped in synix's rebuild script. + +The [Getting Started Guide](../getting-started/create-nix-config.md) will take you from nothing to a working NixOS configuration using synix. diff --git a/docs/introduction-to-nix/nixpkgs-firefox.png b/docs/introduction-to-nix/nixpkgs-firefox.png new file mode 100644 index 0000000000000000000000000000000000000000..22275849cd24b63d5756711b3c2ad51415bf0bc7 GIT binary patch literal 116893 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 g7s&9wh9GIuf0>OB2s$fhXn-5zopr0E=iXvH$=8 literal 0 HcmV?d00001 diff --git a/docs/introduction-to-nix/nixpkgs.md b/docs/introduction-to-nix/nixpkgs.md new file mode 100644 index 0000000..5110bea --- /dev/null +++ b/docs/introduction-to-nix/nixpkgs.md @@ -0,0 +1,84 @@ +# Nixpkgs + +`Nixpkgs` is the massive collection of Nix expressions that define essentially *all* software available for Nix. It's the standard library for Nix, containing tens of thousands of packages. When you interact with `nixpkgs` in your Nix expressions, you're using this vast resource. + +## How to Get Packages from Nixpkgs + +The simplest way to use a package from Nixpkgs is to import it: + +```bash +nix eval --impure --expr 'with import {}; pkgs.hello' +``` + +This evaluates an expression that imports Nixpkgs and makes its contents available as `pkgs`. The result will be a Nix store path for the `hello` package derivation. + +You can also use `nix build`: + +```bash +nix build nixpkgs#hello +``` + +This will build and symlink `hello` into your current directory as `result`. + +And `nix run`: + +```bash +nix run nixpkgs#hello/bin/hello +``` +``` +Hello, world! +``` + +> You could also run `./result/bin/hello`. + +This command tells Nix to run the `hello` executable from the `hello` package in Nixpkgs. + +## Searching Nixpkgs + +You can search for packages directly from your terminal: + +```bash +nix search nixpkgs firefox +``` + +This command will list all packages in your Nixpkgs channel that contain "firefox" in their name or description. You'll likely see results like `firefox` and `firefox-bin`. + +However, the CLI is slow and not convenient to use. You should use the [Nixpkgs search](https://search.nixos.org/packages?channel=unstable) instead. + +This is the entry of Firefox in Nixpkgs: + +![Firefox in Nixpkgs](./nixpkgs-firefox.png) + +### How to install *firefox*? + +You can use Nix as a traditional package manager: + +```bash +nix-env -iA nixpkgs.firefox +``` + +This is **not recommended** as packages installed this way must be updated and maintained by the user in the same way as with a traditional package manager. To temporarily install a package for testing purposes, use `nix-shell` instead: + +```bash +nix-shell -p firefox +``` + +This will spawn a shell with `firefox` available. To permanently install a package with Nix, add it to your NixOS or Home Manager configuration. NixOS and Home Manager will be covered later in this guide. + +## `pkgs.lib` utility functions + +The `pkgs` argument (or `nixpkgs` itself) isn't just a list of applications; it also provides a powerful utility library called `pkgs.lib`. This library contains helper functions for working with Nix expressions, strings, lists, and more. + +Many of these functions are used extensively within Nixpkgs itself to define packages and modules. You can browse the full [`pkgs.lib` documentation online](https://nixos.org/manual/nixpkgs/stable/#sec-functions-library) for more details. + +## The Nixpkgs GitHub Repository + +Nixpkgs is an open-source project hosted on GitHub: [github.com/NixOS/nixpkgs](https://github.com/NixOS/nixpkgs). You can explore its source code to see how packages are defined. Every package definition is a Nix expression! + +For example, you could find the definition for `hello` at `pkgs/by-name/he/hello/package.nix`. It uses `stdenv.mkDerivation` just like our example. + +## Binary Caches + +Building everything from source every time can be slow. Nix solves this with **binary caches**. When someone builds a derivation, if that exact derivation (with its exact hash) has already been built and uploaded to a binary cache (like [`cache.nixos.org`](https://cache.nixos.org/)), Nix will simply *download* the pre-built binaries from the cache instead of building it locally. + +This is possible because of the unique hashing of store paths. If the hash matches, the content *must* be identical, so a downloaded binary is guaranteed to be the same as one built locally. This significantly speeds up package installation and system updates. diff --git a/docs/introduction-to-nix/overview.md b/docs/introduction-to-nix/overview.md new file mode 100644 index 0000000..1fa7db7 --- /dev/null +++ b/docs/introduction-to-nix/overview.md @@ -0,0 +1,9 @@ +# Introduction to Nix + +Welcome to the world of Nix! This guide aims to take you from a complete beginner to confidently using Nix for reproducible development environments and even managing your entire operating system with NixOS. + +Nix is a powerful package manager that brings functional programming principles to system configuration. This means your builds are reproducible, changes are atomic, and rollbacks are easy. Forget "it works on my machine" – with Nix, it works everywhere. + +We'll start with the fundamentals of the Nix language, then explore how Nix builds software. From there, we'll dive into the massive Nixpkgs collection, cover how Nix ensures reproducibility with flakes, and finally, show you how to manage your entire system with NixOS. + +Let's begin! diff --git a/docs/modules/home/bemenu.md b/docs/modules/home/bemenu.md new file mode 100644 index 0000000..d661c58 --- /dev/null +++ b/docs/modules/home/bemenu.md @@ -0,0 +1,10 @@ +# bemenu + +`bemenu` is a dynamic menu library and client program inspired by dmenu. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/bemenu). +If you use this repository's [Hyprland module](./hyprland.md), it is enabled by default. + +## References + +- [GitHub](https://github.com/Cloudef/bemenu) diff --git a/docs/modules/home/common.md b/docs/modules/home/common.md new file mode 100644 index 0000000..0334bfc --- /dev/null +++ b/docs/modules/home/common.md @@ -0,0 +1,17 @@ +# Common + +The common module sets some opinionated defaults. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/common). + +It is recommended to import it in your Home Manager configuration as some synix modules may depend on it: + +```nix +{ inputs, ... }: + +{ + imports = [ + inputs.synix.homeModules.common + ]; +} +``` diff --git a/docs/modules/home/gemini-cli.md b/docs/modules/home/gemini-cli.md new file mode 100644 index 0000000..02a26a8 --- /dev/null +++ b/docs/modules/home/gemini-cli.md @@ -0,0 +1,65 @@ +# Gemini CLI + +An open-source AI agent that brings the power of Gemini directly into your terminal. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/gemini-cli). + +## References + +- [GitHub](https://github.com/google-gemini/gemini-cli) +- [CLI Docs](https://github.com/google-gemini/gemini-cli/tree/main/docs/cli) + +## Setup + +The package must be set by you. Easiest option is to use the synix overlay: + +```nix +{ inputs, pkgs, ... }: + +{ + imports = [ + inputs.synix.homeModules.gemini-cli + ]; + + programs.gemini-cli = { + enable = true; + package = pkgs.synix.gemini-cli; + }; +} +``` + +Gemini CLI reads environment variables, such as your API key, from `~/.gemini/.env`. You can manage it with sops-nix: + +```nix +{ config, ... }: + +{ + sops.secrets.gemini-api-key = { }; + sops.templates.gemini-cli-env = { + content = '' + GEMINI_API_KEY=${config.sops.placeholder.gemini-api-key} + ''; + path = config.home.homeDirectory + "/.gemini/.env"; + }; +} +``` + +Set `gemini-api-key` in your `secrets.yaml`: + +> Replace `abc123` with your Gemini API key. + +```yaml +gemini-api-key: abc123 +``` + +## Troubleshooting + +These are some common warnings and errors you might encounter when using Gemini CLI: + +### Error saving user settings file + +``` +Error saving user settings file: Error: EROFS: read-only file system, open '/home/you/.gemini/settings.json' +``` + +This is intended behavior. diff --git a/docs/modules/home/gpg.md b/docs/modules/home/gpg.md new file mode 100644 index 0000000..5f613fc --- /dev/null +++ b/docs/modules/home/gpg.md @@ -0,0 +1,51 @@ +# GPG + +This module sets some defaults for gpg, mainly to let your gpg-agent handle ssh keys. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/gpg). + +## SSH Setup + +### GPG + +You need a GPG authentication subkey. Follow the steps below to create one. If you already have a GPG key, skip to step 2. + +#### 1. Generate a new GPG key + +```sh +gpg --full-gen-key --allow-freeform-uid +``` + +1. Select `1` as the type of key. +1. Select `4096` for the keysize. +1. Select `0` to choose 'Never expire'. +1. Enter your name, email address, and a comment (if you want). Select `0` for 'Okay'. + +#### 2. Create an authentication subkey + +```sh +gpg --expert --edit-key KEY-ID +``` + +1. At the new `gpg>` prompt, enter: `addkey` +1. When prompted, enter your passphrase. +1. When asked for the type of key you want, select: (8) RSA (set your own capabilities). +1. Enter `S` to toggle the ‘Sign’ action off. +1. Enter `E` to toggle the ‘Encrypt’ action off. +1. Enter `A` to toggle the ‘Authenticate’ action on. The output should now include Current allowed actions: Authenticate, with nothing else on that line. +1. Enter `Q` to continue. +1. When asked for a keysize, choose `4096`. +1. Select `0` to choose 'Never expire'. +1. Once the key is created, enter `quit` to leave the gpg prompt, and `y` at the prompt to save changes. + +### HM config + +```nix +imports = [ + inputs.synix.homeModules.gpg +]; + +services.gpg-agent.sshKeys = [ "YOUR_AUTH_SUBKEY_KEYGRIP" ]; +``` + +> Get the keygrip of your authentication subkey with: `gpg -K --with-keygrip` diff --git a/docs/modules/home/hyprland.md b/docs/modules/home/hyprland.md new file mode 100644 index 0000000..38102a8 --- /dev/null +++ b/docs/modules/home/hyprland.md @@ -0,0 +1,153 @@ +# Hyprland + +This module extends the options of and sets some defaults for [Hyprland](https://hyprland.org/): + +- XDG Desktop Portal for screen sharing on Wayland +- XDG mime support and user directories +- enable Waybar as status bar +- enable dunst as notification service +- some [packages](https://git.sid.ovh/sid/synix/blob/master/modules/home/hyprland/packages.nix) +- [keybindings](https://git.sid.ovh/sid/synix/blob/master/modules/home/hyprland/binds/default.nix) +- manage default applications via the new `applications` option + +> Always import both NixOS and Home Manager modules from `synix` when using Hyprland. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/hyprland). + +## Keybindings + +The ["Master Layout"](https://wiki.hyprland.org/Configuring/Master-Layout/) is the only supported window layout. + +> `$mod`, `modifier` or `SUPER` refer to the same key which is the Windows key by default. + +Keybinding | Function +---|--- +`SUPER SHIFT c` | Kill active window +`SUPER 0..9` | Focus workspace 1-10 (`0` maps to workspace 10) +`SUPER SHIFT 0..9` | Move active window to workspace 1-10 +`SUPER CTRL 0..9` | Focus workspace 1-10 on active monitor (moves if necessary) +`SUPER Tab` | Focus previous workspace on active monitor +`SUPER SHIFT Tab` | Move active window to previous workspace on active monitor +`SUPER Comma` | Focus left monitor +`SUPER Period` | Focus right monitor +`SUPER SHIFT Comma` | Move active workspace to left monitor +`SUPER SHIFT Period` | Move active workspace to right monitor +`SUPER SHIFT Return` | Make active window master +`SUPER CTRL Return` | Focus master window +`SUPER j` | Focus next window +`SUPER k` | Focus previous window +`SUPER SHIFT j` | Swap active window with the next window +`SUPER SHIFT k` | Swap active window with the previous window +`SUPER h` | Decrease horizontal space of master stack +`SUPER l` | Increase horizontal space of master stack +`SUPER SHIFT h` | Shrink active window vertically +`SUPER SHIFT l` | Expand active window vertically +`SUPER i` | Add active window to master stack +`SUPER SHIFT i` | Remove active window from master stack +`SUPER o` | Toggle between left and top orientation +`SUPER Left` | Focus window to the left +`SUPER Right` | Focus window to the right +`SUPER Up` | Focus upper window +`SUPER Down` | Focus lower window +`SUPER SHIFT Left` | Swap active window with window to the left +`SUPER SHIFT Right` | Swap active window with window to the right +`SUPER SHIFT Up` | Swap active window with upper window +`SUPER SHIFT Down` | Swap active window with lower window +`SUPER f` | Toggle floating for active window +`SUPER CTRL f` | Toggle floating for all windows on workspace +`SUPER SHIFT f` | Toggle fullscreen for active window +`SUPER LMB` | Move window by dragging +`SUPER RMB` | Resize window by dragging + +Some [media keys](https://git.sid.ovh/sid/synix/blob/master/modules/home/hyprland/binds/mediakeys.nix) are also supported. + +## Default applications + +For clarification purposes, let's define the following terms: + +- ``: The literal name of the application/program. For example, `firefox`. +- ``: The category of the application. For example, `browser`. +- ``: Available options are listed [here](https://specifications.freedesktop.org/desktop-entry-spec/latest/exec-variables.html). For example, `%U`. + +To add default applications to Hyprland, you need to do the following steps: + +### 1. Look for an existing category + +Check if a fitting category for your application exists in [`applications/default.nix`](https://git.sid.ovh/sid/synix/blob/master/modules/home/hyprland/applications/default.nix). +Categories are listed under `options.wayland.windowManager.hyprland.applications`, for example: + +```nix +# ... +emailclient = mkAppAttrs { + default = "thunderbird"; + bind = [ "$mod, m, exec, ${emailclient}" ]; +}; + +filemanager = mkAppAttrs { + default = "lf"; + bind = [ "$mod, e, exec, ${terminal} -T ${filemanager} -e ${filemanager}" ]; + windowRule = [ + "float, title:^${filemanager}$" + "size 50% 50%, title:^${filemanager}$" + ]; +}; +# ... +``` + +If no fitting category exists, create a new one and assign a default application with optional binds and window rules. + +### 2. Create a directory to configure the application in + +```nix +# applications//default.nix + +{ inputs, outputs, config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.; +in +{ + imports = [ + # Import a module if available. + outputs.homeModules. # or `inputs.synix.homeModules.` + ]; + + config = mkIf (cfg.enable && app == "") { + programs. = { + enable = true; + # Add more config here if needed. + }; + + # Define a desktop entry if the app's module or package does not ship with one + xdg.desktopEntries. = { + name = ""; # Use capital letters. For example, "Firefox". + genericName = ""; # Be a bit more specific. For example, "Web Browser". + exec = " "; # Program to execute, possibly with arguments. + terminal = false; # Whether the program runs in a terminal window. + mimeType = [ "" "" ]; # The MIME type(s) supported by this application. For example, "text/html". + }; + }; +} +``` + +> The function [`genMimeAssociations`](https://git.sid.ovh/sid/synix/blob/master/modules/home/hyprland/applications/genMimeAssociations.nix) might be useful here. See [`feh`'s config](https://git.sid.ovh/sid/synix/blob/master/modules/home/hyprland/applications/feh/default.nix) as an example. + +> Available MIME types can be found [here](https://www.iana.org/assignments/media-types/media-types.xhtml). + +### 3. Import the directory + +You then need to import this directory in [`applications/default.nix`](https://git.sid.ovh/sid/synix/blob/master/modules/home/hyprland/applications/default.nix). +Look for the comment `# add your application directories here`: + +```nix +# applications/default.nix + +imports = [ + ./lf + ./thunderbird + # add your application directories here +]; +``` diff --git a/docs/modules/home/kitty.md b/docs/modules/home/kitty.md new file mode 100644 index 0000000..592fbdb --- /dev/null +++ b/docs/modules/home/kitty.md @@ -0,0 +1,10 @@ +# Kitty + +`kitty` is a cross-platform, fast, feature-rich, GPU based terminal emulator. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/kitty). +If you use this repository's [Hyprland module](./hyprland.md), it is enabled by default. + +## References + +- [GitHub](https://github.com/kovidgoyal/kitty) diff --git a/docs/modules/home/lf.md b/docs/modules/home/lf.md new file mode 100644 index 0000000..ef7a2b6 --- /dev/null +++ b/docs/modules/home/lf.md @@ -0,0 +1,11 @@ +# lf + +> Note: This module is not actively maintained. Expect things to break! + +`lf` is a terminal file manager. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/lf). + +## References + +- [GitHub](https://github.com/gokcehan/lf) diff --git a/docs/modules/home/networkmanager-dmenu.md b/docs/modules/home/networkmanager-dmenu.md new file mode 100644 index 0000000..0415c1d --- /dev/null +++ b/docs/modules/home/networkmanager-dmenu.md @@ -0,0 +1,10 @@ +# networkmanager-dmenu + +networkmanager-dmenu allows you to control NetworkManager via dmenu. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/networkmanager-dmenu). +If you use this repository's [Hyprland module](./hyprland.md), it is enabled by default. + +## References + +- [GitHub](https://github.com/firecat53/networkmanager-dmenu) diff --git a/docs/modules/home/nextcloud-sync.md b/docs/modules/home/nextcloud-sync.md new file mode 100644 index 0000000..bea2110 --- /dev/null +++ b/docs/modules/home/nextcloud-sync.md @@ -0,0 +1,75 @@ +# Nextcloud sync client + +Because every other client sucks. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/nextcloud-sync). + +## Setup + +This is an example home config: + +```nix +{ inputs, config, ... }: + +{ + imports = [ + inputs.synix.homeModules.nextcloud-sync + ]; + + services.nextcloud-sync = { + enable = true; + remote = "cloud.sid.ovh"; # just the URL without `https://` + passwordFile = config.sops.secrets.nextcloud.path; + connections = [ # absolute paths without trailing / + { + local = "/home/sid/aud"; + remote = "/aud"; + } + { + local = "/home/sid/doc"; + remote = "/doc"; + } + { + local = "/home/sid/img"; + remote = "/img"; + } + { + local = "/home/sid/vid"; + remote = "/vid"; + } + ]; + }; +} +``` + +## Usage + +You can manually sync by running: + +```bash +nextcloud-sync-all +``` + +This will synchronize all defined connections. + +## Troubleshooting + +Each listed connection spawns a systemd user service and timer. Using the example above, we get: + +```plaintext +nextcloud-sync-aud.service +nextcloud-sync-aud.timer +nextcloud-sync-doc.service +nextcloud-sync-doc.timer +nextcloud-sync-img.service +nextcloud-sync-img.timer +nextcloud-sync-vid.service +nextcloud-sync-vid.timer +``` + +Check their status to know what might go wrong: + +```bash +systemctl --user status nextcloud-sync-doc.service +journalctl --user -xeu nextcloud-sync-doc.service +``` diff --git a/docs/modules/home/nixvim.md b/docs/modules/home/nixvim.md new file mode 100644 index 0000000..01e613d --- /dev/null +++ b/docs/modules/home/nixvim.md @@ -0,0 +1,97 @@ +# Nixvim + +This module provides some defaults to quickly set up Nixvim with some plugins. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/nixvim). + +## Config + +Here is an example configuration: + +```nix +# flake.nix +inputs = { + nixvim.url = "github:nix-community/nixvim"; + nixvim.inputs.nixpkgs.follows = "nixpkgs"; +}; +``` + +```nix +# home/YOU/default.nix +{ inputs, lib, config, pkgs, ... }: + +{ + imports = [ + inputs.synix.homeModules.stylix # This module works great with stylix + inputs.synix.homeMmodules.nixvim # You need to import this module + ]; + + programs.nixvim = { + enable = true; + #colorschemes.SCHEME.enable = true; # If you do not use the stylix module, set a scheme manually + # This module provides defaults for the following plugins. + # They are all enabled by default. + plugins = { + cmp.enable = true; # Auto completion + dap.enable = true; # Debugging + lsp.enable = true; # Language server + lualine.enable = true; # Statusline + luasnip.enable = true; # Coding snippets + markdown-preview.enable = true; # Markdown preview in Browser + telescope.enable = true; # Fuzzy finder + treesitter.enable = true; # Syntax highlighting + trouble.enable = true; # Diagnostic messages + }; + }; + + stylix = { + enable = true; + scheme = "dracula"; # This automatically sets the nixvim scheme as well + }; +} +``` + +## Keymaps + +This module sets some keymaps. Here are some important ones: + +> `` defaults to the space key + +key | action +---|--- +`pv` | ex command (file explorer) +`s` | search and replace +`` | select whole buffer +`ss` | toggle spell checking +`se` | switch to english spell checking +`sg` | switch to german spell checking +`z=` | correction suggestions for a misspelled word +`zg` | add word to spell list +`` | confirm selection in completion menu +`` | select next item in completion menu +`` | select previous item in completion menu +`gd` | go to definition +`K` | display more information about word under cursor +`bl` | list buffers +`` | next buffer +`` | previous buffer +`fb` or `` | open file browser +`ff` | find files by name +`fg` or `` | find files containing string +`xd` | toggle diagnostics +`xq` | toggle quick fix list +`m` | run make command +`xl` | toggle loclist list +`xx` | toggle diagnostics list +`xq` | toggle quifick list +`` | previous quickfix item +`` | next quickfix item +`ca` | apply code action + +See [keymaps.nix](https://git.sid.ovh/sid/synix/blob/master/modules/home/nixvim/keymaps.nix) and [plugins](https://git.sid.ovh/sid/synix/blob/master/modules/home/nixvim/plugins/) for more details. + +These commands do not have keymaps yet but might be useful anyway: + +command | action +---|--- +`:MarkdownPreview` | live render the current markdown buffer diff --git a/docs/modules/home/password-manager.md b/docs/modules/home/password-manager.md new file mode 100644 index 0000000..a67db88 --- /dev/null +++ b/docs/modules/home/password-manager.md @@ -0,0 +1,84 @@ +# Password Manager + +This module will automatically install [`pass`](https://www.passwordstore.org/) as your password manager. It also provides a custom version of [`passmenu`](https://git.zx2c4.com/password-store/tree/contrib/dmenu/passmenu) using `bemenu` for Wayland sessions called `passmenu-bemenu` and configures [passff](https://codeberg.org/PassFF/passff) for your web browser. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/password-manager). + +## Setup + +It is assumed that you have a GPG key. + +### HM config + +```nix +imports = [ + inputs.synix.homeModules.passwordManager +]; + +programs.passwordManager = { + enable = true; + key = "YOUR_GPG_KEYGRIP"; + wayland = true; # if you are using Wayland +}; +``` + +> Get your keygrip with `gpg -K --with-keygrip` + +### Password Store + +`pass` uses a Password Store to manage your password files. If this is your first time using `pass`, follow option _a)_. If you already have a remote git repository to store your password-store, follow option _b)_. + +#### a) Initialize a new Password Store + +Read the introduction and setup guide on the [pass home page](https://passwordstore.org). + +#### b) Cloning your remote password-store repository + +The following guide assumes that you have your private GPG key on a luks encrypted USB partition which is needed to access your remote repo through ssh. + +1. **Identify the USB device**: + Identify the device name for your USB drive using the `lsblk` or `fdisk -l` command. + + ```bash + lsblk + ``` + + Look for the device corresponding to your USB drive (e.g., `/dev/sdb1`). + +2. **Unlock the LUKS partition**: + Unlock the LUKS partition with the `cryptsetup luksOpen` command. Replace `/dev/sdX1` with the actual device name of your USB partition. + + ```bash + sudo cryptsetup luksOpen /dev/sdX1 crypt + ``` + + You will be prompted to enter the passphrase for the LUKS partition. + +3. **Mount the unlocked partition**: + Mount the unlocked LUKS partition to access the files. + + ```bash + sudo mount /dev/mapper/crypt /mnt + ``` + +4. **Import the GPG key**: + Use the `gpg --import` command to import the GPG key from the mounted USB partition. + + ```bash + gpg --import /mnt/path/to/privatekey.gpg + ``` + +5. **Unmount and close the LUKS partition**: + After importing the key, unmount the partition and close the LUKS mapping. + + ```bash + sudo umount /mnt + sudo cryptsetup luksClose crypt + ``` + +6. **Clone your password store repository**: + Clone your password store repository using the `git clone` command, for example: + + ```bash + git clone ssh://example.tld:/home/you/git/password-store.git ~/.local/share/password-store + ``` diff --git a/docs/modules/home/sops.md b/docs/modules/home/sops.md new file mode 100644 index 0000000..5cb973e --- /dev/null +++ b/docs/modules/home/sops.md @@ -0,0 +1,99 @@ +# Sops + +For more information on how to use this module, see the [Sops NixOS module documentation](../nixos/sops.md). + +For extensive documentation, read the [Readme on GitHub](https://github.com/Mic92/sops-nix/blob/master/README.md). + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/sops). + +## 1. Generate an age key + +```bash +mkdir -p ~/.config/sops/age +age-keygen -o ~/.config/sops/age/keys.txt +``` + +> Take note of your public key. You can print it again with: +> `age-keygen -y ~/.config/sops/age/keys.txt` + + +## 2. Edit `.sops.yaml` + +This file manages access to all secrets in this repository (NixOS and Home Manager configurations). + +```bash +vim ~/.config/nixos/.sops.yaml +``` + +Add your public key under `keys` and set creation rules for your config: + +```yaml +keys: + - &you age12zlz6lvcdk6eqaewfylg35w0syh58sm7gh53q5vvn7hd7c6nngyseftjxl +creation_rules: + - path_regex: users/you/home/secrets/secrets.yaml$ + key_groups: + - age: + - *you +``` + +## 3. Create a `secrets` directory + +This directory in your Home Manager configuration will hold your secrets and sops configuration. + +```bash +mkdir -p ~/.config/nixos/users/$(whoami)/home/secrets +``` + +## 4. Create a sops file + +A sops file contains secrets in plain text. This file will then be encrypted with age. Make sure to follow the path regex in the creation rules. + +```bash +cd ~/.config/nixos +sops users/$(whoami)/home/secrets/secrets.yaml +``` + +```yaml +# Files must always have a string value +example-key: example-value +# Nesting the key results in the creation of directories. +myservice: + my_subdir: + my_secret: password1 +``` + +## 5. Deploy the secrets to the Nix store + +Define your secrets under `sops.secrets`. + +```bash +vim ~/.config/nixos/users/$(whoami)/home/secrets/default.nix +``` + +```nix +{ + sops.secrets.example-key = {}; + sops.secrets."myservice/my_subdir/my_secret" = {}; +} +``` + +## 6. Reference secrets in your Home Manager configuration + +Now you can use these secrets in your Home Manager configuration: + +```nix +{ outputs, ... }: + +{ + imports = [ + ./secrets + + outputs.homeModules.sops # includes all necessary configuration for sops-nix + ]; + + someOption.secretFile = config.sops.secrets.example-key.path; + + anotherOption.passwordFile = config.sops.secrets."myservice/my_subdir/my_secret".path; +} +``` diff --git a/docs/modules/home/stylix.md b/docs/modules/home/stylix.md new file mode 100644 index 0000000..2c09718 --- /dev/null +++ b/docs/modules/home/stylix.md @@ -0,0 +1,90 @@ +# Stylix + +This module wraps [stylix](https://github.com/nix-community/stylix), a theming framework for NixOS, Home Manager, nix-darwin, and Nix-on-Droid. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/stylix). + +## References + +- [docs](https://nix-community.github.io/stylix/) + +## Usage + +Add stylix to your flake inputs: + +```nix +inputs = { + stylix.url = "github:nix-community/stylix"; + stylix.inputs.nixpkgs.follows = "nixpkgs"; +}; +``` + +For example, in your home configuration, set: + +```nix +imports = [ inputs.synix.homeModules.stylix ]; + +stylix = { + enable = true; + scheme = "SCHEME"; +}; +``` + +Replace `SCHEME` with the name of your scheme. Available schemes are listed as `validSchemes` in [our stylix module](https://git.sid.ovh/sid/synix/tree/master/modules/home/stylix/default.nix). + +## Create a scheme + +You can create your own scheme in `schemes/.yaml`. To make it available via `stylix.scheme`, you need to add it to `validSchemes` and `customSchemes` in [the module's `default.nix`](https://git.sid.ovh/sid/synix/tree/master/modules/home/stylix/default.nix). Make sure that the resulting scheme name is a valid [colorscheme in nixvim](https://github.com/nix-community/nixvim/tree/main/plugins/colorschemes). + +It is recommended to set colors according to their purpose / name. This means that `base00` should always be a rather dark color for the background and `base08` a reddish color. + +```yaml +# .yaml +system: "base16" +name: "SCHEME" +author: "AUTHOR" +description: "A dark theme inspired by the SCHEME color scheme." +slug: "SCHEME-theme" +variant: "dark" +palette: + base00: "080808" # background + base01: "323437" # alternate background + base02: "9e9e9e" # selection background + base03: "bdbdbd" # comments + base04: "b2ceee" # alternate text + base05: "c6c6c6" # default text + base06: "e4e4e4" # light foreground + base07: "eeeeee" # light background + base08: "ff5454" # error / red + base09: "cf87e8" # urgent / orange + base0A: "8cc85f" # warning / yellow + base0B: "e3c78a" # green + base0C: "79dac8" # cyan + base0D: "80a0ff" # blue + base0E: "36c692" # magenta + base0F: "74b2ff" # brown +``` + +Refer to [Stylix's style guide](https://stylix.danth.me/styling.html) for more information on where and how these colors will be used. + +You can preview your color schemes with the [base16-viewer](https://sesh.github.io/base16-viewer/) (*Disable your dark reader*) or `print-colors` - a Python script to view color schemes in the terminal: + +```bash +print-colors PATH/TO/colors.yaml +``` + +## Wallpaper + +You can set a wallpaper with: + +```nix +stylix.image = ./path/to/wallpaper.png; +``` + +This can be any image as a PNG file. You might want to take a look at [some Nix themed wallpapers](https://github.com/NixOS/nixos-artwork/tree/master/wallpapers) or [nix-wallpaper](https://github.com/lunik1/nix-wallpaper/tree/master) to create your own wallpaper with the Nix logo and custom colors. + +Or create a solid color image with: + +```bash +convert -size 3840x2160 "xc:#080808" wallpaper.png +``` diff --git a/docs/modules/home/virtualisation.md b/docs/modules/home/virtualisation.md new file mode 100644 index 0000000..5f6ec54 --- /dev/null +++ b/docs/modules/home/virtualisation.md @@ -0,0 +1,11 @@ +# Virtualisation + +Home Manager module to go with the [Virtualisation NixOS module](../nixos/virtualisation.md). + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/virtualisation). + +## Setup + +1. Import this module in your Home Manager configuration and the corresponding [NixOS module](../nixos/virtualisation.md) in your NixOS configuration. +1. Rebuild and reboot: `rebuild all && sudo reboot now` +1. Start the default network: `virsh net-autostart default` diff --git a/docs/modules/home/waybar.md b/docs/modules/home/waybar.md new file mode 100644 index 0000000..45ae43f --- /dev/null +++ b/docs/modules/home/waybar.md @@ -0,0 +1,10 @@ +# Waybar + +Waybar is a highly customizable Wayland bar for Sway and Wlroots based compositors. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/waybar). +If you use this repository's [Hyprland module](./hyprland.md), it is enabled by default. + +## References + +- [GitHub](https://github.com/Alexays/Waybar) diff --git a/docs/modules/home/yazi.md b/docs/modules/home/yazi.md new file mode 100644 index 0000000..f8154a5 --- /dev/null +++ b/docs/modules/home/yazi.md @@ -0,0 +1,13 @@ +# yazi + +Terminal file manager written in Rust. + +View the [*synix* Home Manager module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/home/yazi). +If you use this repository's [Hyprland module](./hyprland.md), it is enabled by default. + +## References + +- [GitHub](https://github.com/sxyazi/yazi) +- [docs](https://yazi-rs.github.io/docs/quick-start) +- [default keybindings](https://github.com/sxyazi/yazi/blob/shipped/yazi-config/preset/keymap-default.toml) +- [Plugins in Nixpkgs](https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=yaziPlugins) diff --git a/docs/modules/nixos/audio.md b/docs/modules/nixos/audio.md new file mode 100644 index 0000000..ff61a31 --- /dev/null +++ b/docs/modules/nixos/audio.md @@ -0,0 +1,9 @@ +# Audio + +PipeWire is a server for handling audio, video streams, and hardware on Linux. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/audio). + +## References + +- [Homepage](https://pipewire.org/) diff --git a/docs/modules/nixos/baibot.md b/docs/modules/nixos/baibot.md new file mode 100644 index 0000000..31658ea --- /dev/null +++ b/docs/modules/nixos/baibot.md @@ -0,0 +1,90 @@ +# Baibot + +Baibot is a Matrix AI bot. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/baibot). + +## References + +- [GitHub](https://github.com/etkecc/baibot) + +## Setup + +### Configuration + +Since baibot's configuration file requires setting secrets as plain text strings, configuring the baibot service through Nix is not supported. You have to create a configuration file on your machine and point to it with `services.baibot.configFile`. + +Use the [template configuration file](https://github.com/etkecc/baibot/blob/main/etc/app/config.yml.dist) for reference. + +### User Creation + +Create the `baibot` user on your Matrix instance. If you are using the [synix Matrix module](./matrix-synapse.md), this can be done with the `register_new_matrix_user` alias: + +```bash +register_new_matrix_user +``` + +Set the `user localpart` and `password` according to your configuration. + +Restart both `matrix-synapse.service` and `baibot.service`. You can then invite Baibot to any room you like. + +### OpenAI API + +Send this message in a room where Baibot has joined: + +``` +!bai agent create-global openai openai +``` + +The bot will reply with a YAML configuration which you need to edit and send back: + +```yaml +base_url: https://api.openai.com/v1 +api_key: YOUR_API_KEY_HERE +text_generation: + model_id: gpt-4o + prompt: 'You are a brief, but helpful bot called {{ baibot_name }} powered by the {{ baibot_model_id }} model. The date/time of this conversation''s start is: {{ baibot_conversation_start_time_utc }}.' + temperature: 1.0 + max_response_tokens: 16384 + max_context_tokens: 128000 +speech_to_text: + model_id: whisper-1 +text_to_speech: + model_id: tts-1-hd + voice: onyx + speed: 1.0 + response_format: opus +image_generation: + model_id: dall-e-3 + style: vivid + size: 1024x1024 + quality: standard +``` + +Set `openai` as the default for any purpose you like: + +``` +!bai config global set-handler text-generation global/openai +!bai config global set-handler speech-to-text global/openai +!bai config global set-handler text-to-speech global/openai +!bai config global set-handler image-generation global/openai +``` + +## Tips + +### Set STT to Transcribe Only +``` +!bai config global speech-to-text set-flow-type only_transcribe +``` + +### Set user access +``` +!bai access set-users SPACE_SEPARATED_PATTERNS +``` + +> For example: `@*:example.com` + +## Todo + +1. Set up a local LLM for speech-to-text with Ollama. +1. Whitelist each user for the speech-to-text engine only. diff --git a/docs/modules/nixos/cifsmount.md b/docs/modules/nixos/cifsmount.md new file mode 100644 index 0000000..31f22bb --- /dev/null +++ b/docs/modules/nixos/cifsmount.md @@ -0,0 +1,25 @@ +# cifsMount + +> Warning: This module is not actively maintained. Expect things to break! + +This module allows you to automount cifs shares after the login of the specified user. The remote has to have a running samba server. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/cifsMount). + +## Config + +```nix +config.services.cifsMount = { + enable = true; + remotes = [ + { + host = "ip_address"; + shareName = "share_name"; + mountPoint = "/home/user/mount_point"; + credentialsFile = "/home/user/.smbcredentials"; + user = "user"; + } + # more remotes ... + ]; +}; +``` diff --git a/docs/modules/nixos/common.md b/docs/modules/nixos/common.md new file mode 100644 index 0000000..df82663 --- /dev/null +++ b/docs/modules/nixos/common.md @@ -0,0 +1,17 @@ +# Common + +The common module sets some opinionated defaults. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/common). + +It is recommended to import it in your NixOS configuration as some synix modules may depend on it: + +```nix +{ inputs, ... }: + +{ + imports = [ + inputs.synix.nixosModules.common + ]; +} +``` diff --git a/docs/modules/nixos/device.md b/docs/modules/nixos/device.md new file mode 100644 index 0000000..44f021f --- /dev/null +++ b/docs/modules/nixos/device.md @@ -0,0 +1,20 @@ +# Device + +This module lets you set some defaults for a device type. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/device). + +Available devices are: + +- desktop +- laptop +- server +- vm + +To enable these defaults, you need to import this module in your host configuration. For example: + +```nix +# hosts/HOSTNAME/default.nix + +imports = [ inputs.synix.nixosModules.device.vm ]; # this imports all defaults for VMs. See `vm.nix` +``` diff --git a/docs/modules/nixos/ftp-webserver.md b/docs/modules/nixos/ftp-webserver.md new file mode 100644 index 0000000..3b2c3f0 --- /dev/null +++ b/docs/modules/nixos/ftp-webserver.md @@ -0,0 +1,7 @@ +# FTP web server + +> Warning: This module is not actively maintained. Expect things to break! + +This module sets up a simple ftp web server behind a reverse proxy (`ftp.domain.tld` by default). + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/ftp-webserver). diff --git a/docs/modules/nixos/headplane.md b/docs/modules/nixos/headplane.md new file mode 100644 index 0000000..e7b29a8 --- /dev/null +++ b/docs/modules/nixos/headplane.md @@ -0,0 +1,70 @@ +# Headplane + +A feature-complete Web UI for Headscale. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/headplane). + +## References + +- [Website](https://headplane.net) +- [GitHub](https://github.com/tale/headplane) +- [NixOS options](https://headplane.net/NixOS-options) + +## Sops + +Provide the following entries to your `secrets.yaml`: + +> Replace `abc123` with your actual secrets + +```yaml +headplane: + cookie_secret: abc123 + agent_pre_authkey: abc123 +``` + +Generate your cookie secret with: + +```bash +nix-shell -p openssl --run "openssl rand -hex 16" +``` + +Generate your agent pre-authkey with: + +```bash +sudo headscale users create headplane-agent +sudo headscale users list # get headplane-agent user id +sudo headscale preauthkeys create --expiration 99y --reusable --user +``` + +## Setup + +Set a CNAME record for your Headplane subdomain (`headplane` by default) pointing to your domain. + +## Config + +```nix +# flake.nix +headplane.url = "github:tale/headplane"; +headplane.inputs.nixpkgs.follows = "nixpkgs"; +``` + +```nix +# configuration.nix +{ + imports = [ inputs.synix.nixosModules.headplane ]; + + services.headplane = { + enable = true; + }; +} +``` + +## Usage + +Create a Headscale API key: + +```bash +sudo headscale apikeys create +``` + +Visit the admin login page: `https://sub.domain.tld/admin/login` diff --git a/docs/modules/nixos/headscale.md b/docs/modules/nixos/headscale.md new file mode 100644 index 0000000..0842ac5 --- /dev/null +++ b/docs/modules/nixos/headscale.md @@ -0,0 +1,58 @@ +# Headscale + +Headscale is an open source, self-hosted implementation of the Tailscale control server. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/headscale). + +## References + +- [Website](https://headscale.net/stable/) +- [GitHub](https://github.com/juanfont/headscale) +- [Example configuration file](https://github.com/juanfont/headscale/blob/main/config-example.yaml) + +## Setup + +Set a CNAME record for your Headscale subdomain (`headscale` by default) pointing to your domain. + +## Config + +```nix +{ + imports = [ inputs.synix.nixosModules.headscale ]; + + services.headscale = { + enable = true; + openFirewall = true; + }; +} +``` + +## Usage + +Create a new user: + +```bash +sudo headscale users create +``` + +Get the user's id: + +```bash +sudo headscale users list +``` + +Create a pre auth key for that user: + +```bash +sudo headscale preauthkeys create --expiration 99y --reusable --user +``` + +Give the user the pre-auth key. + +## Troubleshooting + +Check if your ACL config is valid: + +```bash +sudo headscale policy check --file PATH/TO/acl.hujson +``` diff --git a/docs/modules/nixos/i2pd.md b/docs/modules/nixos/i2pd.md new file mode 100644 index 0000000..ce33e93 --- /dev/null +++ b/docs/modules/nixos/i2pd.md @@ -0,0 +1,26 @@ +# I2P Daemon + +I2P is an End-to-End encrypted and anonymous Internet. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/i2pd). + +## References + +- [Homepage](https://i2pd.website/) +- [Documentation](https://i2pd.readthedocs.io/en/latest/) +- [GitHub](https://github.com/PurpleI2P/i2pd) +- [I2P on NixOS guide](https://voidcruiser.nl/rambles/i2p-on-nixos/) + +## Configuration + +### NixOS + +```nix +{ inputs, ... }: + +{ + imports = [ inputs.synix.nixosModules.i2pd ]; + + services.i2pd.enable = true; +} +``` diff --git a/docs/modules/nixos/jellyfin.md b/docs/modules/nixos/jellyfin.md new file mode 100644 index 0000000..930c3d6 --- /dev/null +++ b/docs/modules/nixos/jellyfin.md @@ -0,0 +1,30 @@ +# Jellyfin + +Jellyfin is a free and open-source media server and suite of multimedia applications. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/jellyfin). + +## References + +[docs](https://jellyfin.org/docs/) + +## Setup + +Users, Plugins, and Libraries are managed in the web interface. You have to declare them manually. + +Visit the web interface and follow the on screen instructions. Create libraries corresponding to `config.services.jellyfin.libraries`. + +## Upload files + +```bash +rsync -arvzP -e 'ssh -p SSH_PORT' LOCAL_PATH YOU@REMOTE:JELLYFIN_DATA_DIR/libraries/LIBRARY +``` + +> the user `YOU` has to be in the *jellyfin* group on the remote machine `REMOTE` + +- `SSH_PORT`: Your SSH port +- `LOCAL_PATH`: Local path to your media file(s) +- `YOU`: Your user on your remote machine +- `REMOTE`: IP/domain of your remote machine +- `JELLYFIN_DATA_DIR`: `config.services.jellyfin.dataDir` +- `LIBRARY`: Target library. See `config.services.jellyfin.libraries` diff --git a/docs/modules/nixos/jirafeau.md b/docs/modules/nixos/jirafeau.md new file mode 100644 index 0000000..b1fa844 --- /dev/null +++ b/docs/modules/nixos/jirafeau.md @@ -0,0 +1,9 @@ +# Jirafeau + +Jirafeau is a project that allows "one-click filesharing", making it easy to upload a file and give it a unique link. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/jirafeau). + +## References + +- [docs](https://github.com/Newlode/jirafeauhttps://github.com/Newlode/jirafeau) diff --git a/docs/modules/nixos/mailserver.md b/docs/modules/nixos/mailserver.md new file mode 100644 index 0000000..b6bf41d --- /dev/null +++ b/docs/modules/nixos/mailserver.md @@ -0,0 +1,62 @@ +# Mail + +A simple NixOS mailserver. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/mailserver). + +## References + +- [docs](https://nixos-mailserver.readthedocs.io/en/latest/index.html) + +## Setup + +Follow the [setup guide](https://nixos-mailserver.readthedocs.io/en/master/setup-guide.html#setup-dns-a-record-for-server). + +## Sops + +Provide every user's hashed password to your host's `secrets.yaml`: + +> Replace `abc123` with your actual secrets + +```yaml +mailserver: + accounts: + user1: abc123 + user2: abc123 + # ... +``` + +Generate hashed passwords with: + +```sh +nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' +``` + +## Config + +### `flake.nix` + +```nix +inputs = { + nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver"; + nixos-mailserver.inputs.nixpkgs.follows = "nixpkgs"; +}; +``` + +### Host configuration: + +```nix +imports = [ inputs.synix.nixosModules.mailserver ] + +mailserver = { + enable = true; + accounts = { + admin = { + aliases = [ "postmaster" ]; + }; + alice = { }; + }; +}; +``` + +You may need to set [`mailserver.stateVersion`](https://nixos-mailserver.readthedocs.io/en/master/migrations.html). At the time of writing, you need to set it to `3`, but you should check the mailserver docs yourself. diff --git a/docs/modules/nixos/matrix-synapse.md b/docs/modules/nixos/matrix-synapse.md new file mode 100644 index 0000000..a106122 --- /dev/null +++ b/docs/modules/nixos/matrix-synapse.md @@ -0,0 +1,144 @@ +# Matrix-Synapse + +Synapse is a [Matrix](https://matrix.org/) homeserver. Matrix is an open network for secure, decentralised communication. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/matrix-synapse). + +## References + +- [Synapse repository](https://github.com/element-hq/synapse) +- [Synapse documentation](https://matrix-org.github.io/synapse/latest/welcome_and_overview.html) +- [Coturn repository](https://github.com/coturn/coturn) +- [Coturn example configuration](https://github.com/coturn/coturn/blob/master/examples/etc/turnserver.conf) + +## Setup + +### DNS + +Make sure you have a CNAME record for `turn` pointing to your machine running Coturn. +The fqdn is set by `services.coturn.realm`. + +### Sops + +Provide the following entries to your secrets.yaml: + +> Replace `abc123` with your actual secret(s) + +```yaml +coturn: + static-auth-secret: abc123 +matrix: + registration-shared-secret: abc123 +livekit: + key: abc123 +``` +Generate the livekit key with: + +```bash +nix-shell -p livekit --run "livekit-server generate-keys | tail -1 | awk '{print $3}'" +``` + +## Config + +[Coturn has its own module](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/matrix-synapse), making it easy to outsource to a small VPS with a static IPv4 address. +If you do so, both machines need the secret `coturn/static-auth-secret`. + +In the following example, both services run on the same machine: + +```nix +{ + imports = [ + inputs.synix.nixosModules.coturn + inputs.synix.nixosModules.matrix-synapse + ]; + + networking.domain = "example.tld"; + + services.coturn = { + enable = true; + sops = true; + openFirewall = true; + }; + + services.matrix-synapse = { + enable = true; + sops = true; + coturn.enable = true; + # see below + bridges = { + whatsapp = { + enable = true; + admin = "@you:example.tld"; + }; + signal = { + enable = true; + admin = "@you:example.tld"; + }; + }; + }; + + # You only need this if you want to use bridges + nixpkgs.config.permittedInsecurePackages = [ + "olm-3.2.16" + ]; +} +``` + +## Bridges + +> Warning: Bridges use [`mautrix-go`](https://github.com/mautrix/go) which relies on [deprecated `libolm`](https://github.com/mautrix/go/issues/262). + +### Sops + +Provide the following entries to your secrets.yaml: + +> Replace `abc123` with your actual secret(s) and `BRIDGE` with the name of your bridge (e.g., `whatsapp` or `signal`) + +```yaml +mautrix-BRIDGE: + encryption-pickle-key: abc123 + provisioning-shared-secret: abc123 + public-media-signing-key: abc123 + direct-media-server-key: abc123 +``` + +Generate the secrets with: + +```bash +nix-shell -p openssl --run "openssl rand -base64 32" +``` + +### NixOS configuration + +The `config.yaml` for each bridge is managed through `services.mautrix-BRIDGE.settings`: + +- [services.mautrix-signal.settings](https://search.nixos.org/options?channel=unstable&query=services.mautrix-signal.settings): Generate an example config with: `mautrix-signal -c signal.yaml --generate-example-config` +- [services.mautrix-whatsapp.settings](https://search.nixos.org/options?channel=unstable&query=services.mautrix-whatsapp.settings): Generate an example config with: `mautrix-whatsapp -c whatsapp.yaml --generate-example-config` + +### Authentication + +1. Open chat with bridge bot: `@BOT:DOMAIN.TLD` + - WhatsApp: `whatsappbot` + - Signal: `signalbot` +1. Send: `login qr` +1. Scan QR code +1. Switch puppets: `login-matrix ACCESS_TOKEN` + - Get your token with: Settings > Help & About > Advanced > Access Token + +## Administration + +### Register users + +```bash +register_new_matrix_user -u USERNAME -p PASSWORD +``` + +## Troubleshooting + +### Bridges: Specified admin user is not an admin in portal rooms + +There seems to be a bug that the user specified under `services.matrix-synapse.bridges.whatsapp.admin` does not have admin permissions in portal rooms. You can set the power level manually inside each portal room: + +```plaintext +!wa set-pl @YOU:DOMAIN.TLD 100 +``` diff --git a/docs/modules/nixos/maubot.md b/docs/modules/nixos/maubot.md new file mode 100644 index 0000000..8e8a9a6 --- /dev/null +++ b/docs/modules/nixos/maubot.md @@ -0,0 +1,99 @@ +# Maubot + +A plugin-based Matrix bot system. + +> Warning: Maubot uses [deprecated `libolm`](https://github.com/mautrix/go/issues/262). + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/maubot). + +## References + +- [GitHub repository](https://github.com/maubot/maubot) + +## Sops + +Provide the following entries to your host's `secrets.yaml`: + +> Replace `abc123` with your actual secrets as well as `alice` and `bob` with your actual admin user names. + +```yaml +maubot: + admins: + alice: abc123 + bob: abc123 + # ... +``` + +## Config + +This module only works if Matrix Synapse is running on the same machine. +See [the module on synix](./matrix-synapse.md). + +```nix +{ + imports = [ + inputs.synix.nixosModules.maubot + inputs.synix.nixosModules.matrix-synapse + ]; + + nixpkgs.config.permittedInsecurePackages = [ + "olm-3.2.16" + ]; + + services.maubot = { + enable = true; + sops = true; + admins = [ + "alice" + "bob" + ]; + plugins = with config.services.maubot.package.plugins; [ + gitlab + reminder + ]; + }; + + services.matrix-synapse = { + enable = true; + # ... + }; +} +``` + +## Setup + +1. Create a bot: `$ register_new_matrix_user` +1. Login as your admin user: `$ mbc login` +1. Authenticate as bot: `$ mbc auth` +1. Take note of the access token and device ID +1. Visit `https:/EXAMPLE.TLD/_matrix/maubot` +1. Create a client (if not already preset) +1. Create an instance + +## Bots + +### GitLab + +> See [Readme on GitHub](https://github.com/maubot/gitlab?tab=readme-ov-file) + +Create a personal access token with full API access. + +``` +!gitlab server login https://git.example.com PERSONAL_ACCESS_TOKEN +!gitlab webhook add https://git.example.com user/project +``` + +Check the webhook URL for potential errors. + +## Tips + +### Upload a profile picture to Matrix + +```sh +curl -X POST "https://YOUR_HOMESERVER_URL/_matrix/media/v3/upload" \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ + -H "Content-Type: image/png" \ + --data-binary "@/path/to/your/image.png" +``` + +The respond body contains a valid avatar URL (`mxc://...`). diff --git a/docs/modules/nixos/mcpo.md b/docs/modules/nixos/mcpo.md new file mode 100644 index 0000000..c5cb701 --- /dev/null +++ b/docs/modules/nixos/mcpo.md @@ -0,0 +1,48 @@ +# mcpo + +A simple MCP-to-OpenAPI proxy server. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/mcpo). + +## References + +- [GitHub](https://github.com/open-webui/mcpo) + +## Configuration + +You have to provide a package, for example from [synix](https://git.sid.ovh/sid/synix/tree/master/pkgs/mcpo/default.nix). + +Setting `mcpServers` is required. The following example runs a NixOS MCP server using [mcp-nixos](https://github.com/utensils/mcp-nixos). + +```nix +{ inputs, lib, pkgs, ... }: + +let + inherit (pkgs.stdenv.hostPlatform) system; +in +{ + imports = [ inputs.synix.nixosModules.mcpo ]; + + services.mcpo = { + enable = true; + package = inputs.synix.packages."${system}".mcpo; + settings = { + mcpServers = { + nixos = { + command = lib.getExe inputs.mcp-nixos.packages."${system}".mcp-nixos; + }; + }; + }; + }; +} +``` + +## Usage + +Each tool will be accessible under its own unique route `127.0.0.1:8000/`. Following the example from above, visit [127.0.0.1:8000/nixos/docs](http://127.0.0.1:8000/nixos/docs) to send requests manually. + +## Open WebUI Integration + +Follow the [official Open WebUI integration documentation starting at *Step 2*](https://docs.openwebui.com/openapi-servers/open-webui/#step-2-connect-tool-server-in-open-webui). + +In Open WebUI, users have to set *Function Calling* to *Native* in *Settings* > *General* > *Advanced Parameters*. Then, they can enable MCP servers in a chat by clicking *More* (the plus sign) in the bottom left of the prompt window. diff --git a/docs/modules/nixos/miniflux.md b/docs/modules/nixos/miniflux.md new file mode 100644 index 0000000..628bdcf --- /dev/null +++ b/docs/modules/nixos/miniflux.md @@ -0,0 +1,42 @@ +# Miniflux + + Miniflux is a minimalist and opinionated feed reader. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/miniflux). + +## References + +- [Website](https://miniflux.app/) +- [GitHub](https://github.com/miniflux/v2) +- [Configuration parameters](https://miniflux.app/docs/configuration.html) + +## Setup + +### DNS + +Make sure you have a CNAME record for Miniflux's subdomain (`rss` by default) pointing to your domain. + +### Sops + +Provide the following entries to your secrets.yaml: + +> Replace `abc123` with your actual secret(s) + +```yaml +miniflux: + admin-password: abc123 +``` + +## Config + +```nix +{ + imports = [inputs.synix.nixosModules.miniflux ]; + + services.miniflux = { + enable = true; + reverseProxy.enable = true; + reverseProxy.subdomain = "rss"; + }; +} +``` diff --git a/docs/modules/nixos/normalUsers.md b/docs/modules/nixos/normalUsers.md new file mode 100644 index 0000000..308841b --- /dev/null +++ b/docs/modules/nixos/normalUsers.md @@ -0,0 +1,20 @@ +# Normal Users + +This module automates user creation for normal users. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/normalUsers). + +## Config + +For example: + +```nix +imports = [ inputs.synix.nixosModules.normalUsers ] + +config.normalUsers = { + alice = { + extraGroups = [ "wheel" ]; + sshKeyFiles = [ ../../users/alice/pubkeys/id_rsa.pub ]; + }; +}; +``` diff --git a/docs/modules/nixos/nvidia.md b/docs/modules/nixos/nvidia.md new file mode 100644 index 0000000..7851abe --- /dev/null +++ b/docs/modules/nixos/nvidia.md @@ -0,0 +1,25 @@ +# Nvidia + +NixOS module that configures your Nvidia GPU with proprietary drivers. + +> Tested on Turing and Ampere. Should work with most modern Nvidia GPUs. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/nvidia). + +## Setup + +Import this module inside your NixOS configuration: + +``` +imports = [ inputs.synix.nixosModules.nvidia ]; +``` + +## Config + +Set the Nvidia package with `hardware.nvidia.package`. The default ist: + +```nix +imports = [ inputs.synix.nixosModules.nvidia ]; + +hardware.nvidia.package = config.boot.kernelPackages.nvidiaPackages.latest; +``` diff --git a/docs/modules/nixos/open-webui-oci.md b/docs/modules/nixos/open-webui-oci.md new file mode 100644 index 0000000..cc258b4 --- /dev/null +++ b/docs/modules/nixos/open-webui-oci.md @@ -0,0 +1,44 @@ +# Open WebUI OCI + +Open WebUI is an extensible, self-hosted AI interface that adapts to your workflow, all while operating entirely offline. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/open-webui-oci). + +## References + +- [Homepage](https://openwebui.com/) +- [GitHub](https://github.com/open-webui/open-webui) +- [Environment Configuration](https://docs.openwebui.com/getting-started/env-configuration/) + +## Configuration + +```nix +{ inputs, ... }: + +{ + imports = [ inputs.synix.nixosModules.open-webui-oci ]; + + services.open-webui-oci.enable = true; +} +``` + +## Usage + +Visit the web interface at your specified location to create an admin account. + +> The default location is `http://127.0.0.1:8080`. + +## Troubleshooting + +### JSON parse error + +If you get this error in the web interface: + +``` +SyntaxError: Unexpected token 'd', "data: {"id"... is not valid JSON category +``` + +Clear your browser cache. Steps on Chromium based browsers: + +1. Open DevTools (F12) → Right-click refresh button +1. Click "Empty Cache and Hard Reload" diff --git a/docs/modules/nixos/print-server.md b/docs/modules/nixos/print-server.md new file mode 100644 index 0000000..cc7c56d --- /dev/null +++ b/docs/modules/nixos/print-server.md @@ -0,0 +1,7 @@ +# Print server + +> Note: This module is not actively maintained. Expect things to break! + +This module sets up a printing server with a web interface. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/print-server). diff --git a/docs/modules/nixos/radicale.md b/docs/modules/nixos/radicale.md new file mode 100644 index 0000000..b0cc8c7 --- /dev/null +++ b/docs/modules/nixos/radicale.md @@ -0,0 +1,66 @@ +# Radicale + +A simple CalDAV and CardDAV server. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/radicale). + +## References + +- [Documentation](https://radicale.org/v3.html#documentation-1) +- [Wiki](https://github.com/Kozea/Radicale/wiki) +- [GitHub](https://github.com/Kozea/Radicale) + +## Sops + +Provide every user's SHA512 hashed password to your host's `secrets.yaml`: + +> Replace `abc123` with your actual secrets + +```yaml +radicale: + user1: abc123 + user2: abc123 + # ... +``` + +Generate hashed passwords with: + +```sh +nix-shell -p openssl --run 'openssl passwd -6 ' +``` + +## Setup + +Set a CNAME record for your Radicale subdomain (`dav` by default) pointing to your domain. + +Add two SRV records: + +Calendar: +- type: `SRV` +- name: `_caldavs._tcp` +- priority: `0` +- weight: `1` +- port: `443` +- target: `dav.domain.tld.` + +Contacts: +- name: `_carddavs._tcp` +> rest as above + +## Config + +```nix +{ inputs, ... }: + +{ + imports = [ inputs.synix.nixosModules.radicale ]; + + services.radicale = { + enable = true; + users = [ + "user1" + "user2" + ]; + }; +} +``` diff --git a/docs/modules/nixos/rss-bridge.md b/docs/modules/nixos/rss-bridge.md new file mode 100644 index 0000000..fc9d004 --- /dev/null +++ b/docs/modules/nixos/rss-bridge.md @@ -0,0 +1,11 @@ +# RSS-Bridge + +RSS-Bridge is a PHP web application. It generates web feeds for websites that don't have one. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/rss-bridge). + +## References + +- [docs](https://rss-bridge.github.io/rss-bridge/index.html) +- [repo](https://github.com/RSS-Bridge/rss-bridge) +- [bridges](https://github.com/RSS-Bridge/rss-bridge/tree/master/bridges) diff --git a/docs/modules/nixos/sops.md b/docs/modules/nixos/sops.md new file mode 100644 index 0000000..d37ae19 --- /dev/null +++ b/docs/modules/nixos/sops.md @@ -0,0 +1,58 @@ +# Sops + +Atomic secret provisioning for NixOS based on sops. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/sops). + +## References + +- [GitHub](https://github.com/Mic92/sops-nix) + +## Setup + +Generate an age key for your host from its ssh host key: + +```bash +nix-shell -p ssh-to-age --run 'cat /etc/ssh/ssh_host_ed25519_key.pub | ssh-to-age' +``` + +Then, add it to `.sops.yaml` (see [usage example](https://github.com/Mic92/sops-nix?tab=readme-ov-file#usage-example)). + +## Config + +### Flake + +```nix +# flake.nix +inputs = { + sops-nix.url = "github:Mic92/sops-nix"; + sops-nix.inputs.nixpkgs.follows = "nixpkgs"; +}; +``` + +### Host configuration + +Create a `secrets` directory in your hosts directory. Declare all your secrets in it: + +```nix +# hosts/YOUR_HOST/secrets/default.nix +{ inputs, ... }: + +{ + imports = [ inputs.synix.nixosModules.sops ]; + + sops.secrets.your-secret = { }; + sops.secrets.other-secret = { }; +``` + +## Usage + +For more information on how to use sops-nix, see the [Sops Home Manager module documentation](../home/sops.md). + +## Update Keys + +Update the keys of your SOPS files after making changes to `.sops.yaml`: + +```bash +sops --config PATH/TO/.sops.yaml updatekeys PATH/TO/secrets.yaml +``` diff --git a/docs/modules/nixos/tailscale.md b/docs/modules/nixos/tailscale.md new file mode 100644 index 0000000..e703fd6 --- /dev/null +++ b/docs/modules/nixos/tailscale.md @@ -0,0 +1,36 @@ +# Tailscale + +Private WireGuard networks made easy. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/tailscale). + +## References + +- [Website](https://tailscale.com/) +- [GitHub](https://github.com/tailscale/tailscale) +- [Documents](https://tailscale.com/kb/1017/install) + +## Sops + +Provide the following entries to your `secrets.yaml`: + +> Replace `abc123` with your actual secrets + +```yaml +tailscale: + auth-key: abc123 +``` + +## Config + +```nix +{ + imports = [ inputs.synix.nixosModules.tailscale ]; + + services.tailscale = { + enable = true; + enableSSH = true; + loginServer = ""; + }; +} +``` diff --git a/docs/modules/nixos/virtualisation.md b/docs/modules/nixos/virtualisation.md new file mode 100644 index 0000000..5ef0e0b --- /dev/null +++ b/docs/modules/nixos/virtualisation.md @@ -0,0 +1,172 @@ +# Virtualisation + +Virtualisation using QEMU via libvirt and managed through Virt-manager with VFIO support. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/virtualisation). + +## Overview + +1. **QEMU** is the hypervisor that provides the core virtualisation capabilities. +1. **libvirt** is a toolkit and API that manages virtualisation platforms, such as QEMU. +1. **Virt-manager** is a GUI tool that interacts with libvirt to manage VMs. +1. **virsh** is a CLI tool that interacts with libvirt to manage VMs. + +## Docs + +### QEMU + +- [Official docs](https://www.qemu.org/docs/master/) + +### libvirt + +- [Official docs](https://libvirt.org/docs.html) +- [Arch Wiki](https://wiki.archlinux.org/title/Libvirt) +- [virsh CLI](https://www.libvirt.org/manpages/virsh.html) + +> If you are using the [Home Manager module](../home/virtualisation.md) as well, then `virsh` is aliased to `virsh --connect qemu:///system` + +### Virt-manager + +- [GitHub Repository](https://github.com/virt-manager/virt-manager) +- [NixOS Official Wiki](https://wiki.nixos.org/wiki/Virt-manager) +- [NixOS Community Wiki](https://nixos.wiki/wiki/Virt-manager) +- [Arch Wiki](https://wiki.archlinux.org/title/Virt-manager) + +## Setup + +1. Import this module in your NixOS config. It is recommended to use the [Virtualisation Manager module](../home/virtualisation.md) as well. +1. Add your user to the `libvirtd`, `qemu-libvirtd` and `kvm` group: + ```nix + users.extraGroups.libvirtd.members = [ "" ]; + users.extraGroups.qemu-libvirtd.members = [ "" ]; + users.extraGroups.kvm.members = [ "" ]; + ``` +1. Rebuild and reboot: `rebuild all && sudo reboot now` +1. Enable and start the default network and reboot again: `virsh net-autostart default && virsh net-start default` + +## VFIO + +### Setup + +For successful PCI device passthrough, devices must be properly isolated by IOMMU groups. A device can be safely passed through if: +- It is the **only device** in its IOMMU group (recommended), OR +- **All devices** in its IOMMU group are passed through together + +This module includes an `iommu-groups` command to help identify IOMMU groups: + +```bash +iommu-groups +``` + +In this example, IOMMU group 9 contains only the Nvidia GPU which will get passed to the VM: + +``` +IOMMU Group 9 01:00.0 3D controller [0302]: NVIDIA Corporation TU117M [GeForce GTX 1650 Mobile / Max-Q] [10de:1f9d] (rev a1) +``` + +Take not of the PCI device ID. In this case: `10de:1f9d`. + +### Config + +This is an example with the Nvidia GPU above: + +```nix +{ inputs, ... }: + +{ + imports = [ inputs.synix.nixosModules.virtualisation ]; + + virtualisation = { + vfio = { + enable = true; + IOMMUType = "amd"; + devices = [ + "10de:1f9d" + ]; + blacklistNvidia = true; + }; + hugepages.enable = true; + }; +} +``` + +### Virt Manager + +#### 1. Open VM Hardware Settings + +- Select your VM in Virt Manager +- Click *"Show virtual hardware details"* + +#### 2. Add PCI Host Device + +- Click *"Add Hardware"* button at bottom +- Select *"PCI Host Device"* from the list +- Click *"Finish"* + +You may repeat this process for as many devices as you want to add to your VM. + +### Looking Glass with KVMFR + +*This has not yet been tested.* + +### Troubleshooting + +#### Check Kernel Parameters + +View current kernel parameters: + +```bash +cat /proc/cmdline +``` + +Check VFIO-related parameters: + +```bash +dmesg | grep -i vfio +``` + +Verify IOMMU is enabled: + +```bash +dmesg | grep -i iommu +``` + +#### Verify device binding + +```bash +lscpi -k +``` + +Look for your device you want to pass through. It should say: + +``` +Kernel driver in use: vfio-pci +``` + +For example: + +``` +01:00.0 3D controller: NVIDIA Corporation TU117M [GeForce GTX 1650 Mobile / Max-Q] (rev a1) + Subsystem: Lenovo Device 380d + Kernel driver in use: vfio-pci + Kernel modules: nvidiafb, nouveau +``` + +#### Verify module status + +Ensure blacklisted modules are not loaded: + +```bash +lsmod | grep nvidia +lsmod | grep nouveau +``` + +These should return nothing. + +#### `vfio-pci.ids` not appearing + +Check generated bootloader config: + +```bash +cat /boot/loader/entries/nixos-*.conf +``` diff --git a/docs/modules/nixos/webpage.md b/docs/modules/nixos/webpage.md new file mode 100644 index 0000000..1a79e73 --- /dev/null +++ b/docs/modules/nixos/webpage.md @@ -0,0 +1,5 @@ +# Web Page + +A very simple module to serve a static web page behind a reverse proxy using nginx. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/webPage). diff --git a/docs/modules/nixos/windows-oci.md b/docs/modules/nixos/windows-oci.md new file mode 100644 index 0000000..388d6f5 --- /dev/null +++ b/docs/modules/nixos/windows-oci.md @@ -0,0 +1,33 @@ +# Windows OCI + +Windows inside a Docker container. + +View the [*synix* NixOS module on Forgejo](https://git.sid.ovh/sid/synix/tree/master/modules/nixos/windows-oci). + +## References + +- [dockur on GitHub](https://github.com/dockur/windows) + +## Config + +```nix +imports = [ inputs.synix.nixosModule.windows-oci ]; + +services.windows-oci.enable = true; +``` + +## Setup + +You can monitor the installation process with: + +```bash +journalctl -u podman-windows.service -f +``` + +The first-time setup may fail. Rebooting should resolve the issue. + +## Usage + +Access the VNC web interface at `http://127.0.0.1:8006`. Or connect via RDP at `127.0.0.1`. + +TODO: Setup Windows RemoteApp diff --git a/docs/tips/dependency-tracing.md b/docs/tips/dependency-tracing.md new file mode 100644 index 0000000..6ff2105 --- /dev/null +++ b/docs/tips/dependency-tracing.md @@ -0,0 +1,49 @@ +# Dependency Tracing + +Dependency tracing in Nix allows you to understand the relationships between packages in your system configuration. + +## Forward Path Tracing + +This section answers the question: "*What are the dependencies of an installed package?*" + +Print a store path's dependency tree with: + +```bash +nix-store --query --tree /nix/store/... +``` + +Get a package's store path with: + +> Replace `YOUR_CONFIG` with the name of your NixOS or Home Manager configuration, and `PACKAGE` with the name of the package you want to analyze. + +##### NixOS + +```bash +nix path-info ~/.config/nixos#nixosConfigurations.YOUR_CONFIG.pkgs.PACKAGE +``` + +##### Home Manager + +```bash +nix path-info ~/.config/nixos#homeConfigurations.YOUR_CONFIG.pkgs.PACKAGE +``` + +## Backward Path Tracing + +This section answers the question: "*What are parents of an installed package?*" or "*Why is a certain package installed?*" + +Print a package's dependency path with: + +> Replace `YOUR_CONFIG` with the name of your NixOS or Home Manager configuration, and `PACKAGE` with the name of the package you want to analyze. + +##### NixOS + +```bash +nix why-depends --derivation ~/.config/nixos#nixosConfigurations.YOUR_CONFIG.config.system.build.toplevel ~/.config/nixos#nixosConfigurations.YOUR_CONFIG.pkgs.PACKAGE +``` + +##### Home Manager + +```bash +nix why-depends --derivation ~/.config/nixos#homeConfigurations.YOUR_CONFIG.activationPackage ~/.config/nixos#homeConfigurations.YOUR_CONFIG.pkgs.PACKAGE +``` diff --git a/docs/tips/useful-links.md b/docs/tips/useful-links.md new file mode 100644 index 0000000..3659bee --- /dev/null +++ b/docs/tips/useful-links.md @@ -0,0 +1,23 @@ +# Useful Links + +A collection of links regarding Nix/NixOS. + +## Documentation + +- [Nix Pills](https://nixos.org/guides/nix-pills/): An introduction to Nix, ported to the current format. +- [NixOS & Flakes Book](https://nixos-and-flakes.thiscute.world/): An unofficial book for beginners. +- [Noogle](https://noogle.dev/): Search Nix functions. +- [nix-lib](https://teu5us.github.io/nix-lib.html): Nix (builtins) & Nixpkgs (lib) functions. +- [nix.dev](https://nix.dev/): Official documentation for the Nix ecosystem. + +## Tools + +- [NüschtOS search](https://github.com/NuschtOS/search): Simple and fast static-page NixOS option search. +- [compose2nix](https://github.com/aksiksi/compose2nix): Generate a NixOS config from a Docker Compose project. +- [manix](https://github.com/mlvzk/manix): A fast CLI documentation searcher for Nix. +- [nix-tree](https://github.com/utdemir/nix-tree): Interactively browse dependency graphs of Nix derivations. +- [tex2nix](https://github.com/rgri/tex2nix): Generate texlive nix expressions for documents. + +## NixOS configurations + +- [srvos](https://github.com/nix-community/srvos): NixOS profiles for servers. diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..24cb53e --- /dev/null +++ b/flake.lock @@ -0,0 +1,751 @@ +{ + "nodes": { + "base16": { + "inputs": { + "fromYaml": "fromYaml" + }, + "locked": { + "lastModified": 1755819240, + "narHash": "sha256-qcMhnL7aGAuFuutH4rq9fvAhCpJWVHLcHVZLtPctPlo=", + "owner": "SenchoPens", + "repo": "base16.nix", + "rev": "75ed5e5e3fce37df22e49125181fa37899c3ccd6", + "type": "github" + }, + "original": { + "owner": "SenchoPens", + "repo": "base16.nix", + "type": "github" + } + }, + "base16-fish": { + "flake": false, + "locked": { + "lastModified": 1765809053, + "narHash": "sha256-XCUQLoLfBJ8saWms2HCIj4NEN+xNsWBlU1NrEPcQG4s=", + "owner": "tomyun", + "repo": "base16-fish", + "rev": "86cbea4dca62e08fb7fd83a70e96472f92574782", + "type": "github" + }, + "original": { + "owner": "tomyun", + "repo": "base16-fish", + "rev": "86cbea4dca62e08fb7fd83a70e96472f92574782", + "type": "github" + } + }, + "base16-helix": { + "flake": false, + "locked": { + "lastModified": 1760703920, + "narHash": "sha256-m82fGUYns4uHd+ZTdoLX2vlHikzwzdu2s2rYM2bNwzw=", + "owner": "tinted-theming", + "repo": "base16-helix", + "rev": "d646af9b7d14bff08824538164af99d0c521b185", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-helix", + "type": "github" + } + }, + "base16-vim": { + "flake": false, + "locked": { + "lastModified": 1732806396, + "narHash": "sha256-e0bpPySdJf0F68Ndanwm+KWHgQiZ0s7liLhvJSWDNsA=", + "owner": "tinted-theming", + "repo": "base16-vim", + "rev": "577fe8125d74ff456cf942c733a85d769afe58b7", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-vim", + "rev": "577fe8125d74ff456cf942c733a85d769afe58b7", + "type": "github" + } + }, + "firefox-gnome-theme": { + "flake": false, + "locked": { + "lastModified": 1764873433, + "narHash": "sha256-1XPewtGMi+9wN9Ispoluxunw/RwozuTRVuuQOmxzt+A=", + "owner": "rafaelmardojai", + "repo": "firefox-gnome-theme", + "rev": "f7ffd917ac0d253dbd6a3bf3da06888f57c69f92", + "type": "github" + }, + "original": { + "owner": "rafaelmardojai", + "repo": "firefox-gnome-theme", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-compat_2": { + "flake": false, + "locked": { + "lastModified": 1733328505, + "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_2": { + "inputs": { + "nixpkgs-lib": [ + "nixvim", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1765835352, + "narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "a34fae9c08a15ad73f295041fec82323541400a9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_3": { + "inputs": { + "nixpkgs-lib": [ + "nur", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1733312601, + "narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-parts_4": { + "inputs": { + "nixpkgs-lib": [ + "stylix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1767609335, + "narHash": "sha256-feveD98mQpptwrAEggBQKJTYbvwwglSbOv53uCfH9PY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "250481aafeb741edfe23d29195671c19b36b6dca", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "flake-schemas": { + "locked": { + "lastModified": 1765542151, + "narHash": "sha256-rzv+NVnOcr9pzd8RnvTscwAHAZmD8FLgxEEmHP1xGTA=", + "owner": "DeterminateSystems", + "repo": "flake-schemas", + "rev": "6f53c45897ef6d9e1f39e8ca9611571ac4aa4f17", + "type": "github" + }, + "original": { + "owner": "DeterminateSystems", + "repo": "flake-schemas", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "fromYaml": { + "flake": false, + "locked": { + "lastModified": 1731966426, + "narHash": "sha256-lq95WydhbUTWig/JpqiB7oViTcHFP8Lv41IGtayokA8=", + "owner": "SenchoPens", + "repo": "fromYaml", + "rev": "106af9e2f715e2d828df706c386a685698f3223b", + "type": "github" + }, + "original": { + "owner": "SenchoPens", + "repo": "fromYaml", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768935149, + "narHash": "sha256-S5/BZo4X1D9+U/yJ6xCJyUkXZ8y261q2gPP5Xsq8RPU=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "18cbede9ff6da05b911c5c4802a397c2686ac8fa", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "git-hooks-nix": { + "inputs": { + "flake-compat": [ + "nix" + ], + "gitignore": [ + "nix" + ], + "nixpkgs": [ + "nix", + "nixpkgs" + ], + "nixpkgs-stable": [ + "nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1734279981, + "narHash": "sha256-NdaCraHPp8iYMWzdXAt5Nv6sA3MUzlCiGiR586TCwo0=", + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "aa9f40c906904ebd83da78e7f328cd8aeaeae785", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "gnome-shell": { + "flake": false, + "locked": { + "host": "gitlab.gnome.org", + "lastModified": 1767737596, + "narHash": "sha256-eFujfIUQDgWnSJBablOuG+32hCai192yRdrNHTv0a+s=", + "owner": "GNOME", + "repo": "gnome-shell", + "rev": "ef02db02bf0ff342734d525b5767814770d85b49", + "type": "gitlab" + }, + "original": { + "host": "gitlab.gnome.org", + "owner": "GNOME", + "ref": "gnome-49", + "repo": "gnome-shell", + "type": "gitlab" + } + }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1768949235, + "narHash": "sha256-TtjKgXyg1lMfh374w5uxutd6Vx2P/hU81aEhTxrO2cg=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "75ed713570ca17427119e7e204ab3590cc3bf2a5", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-25.11", + "repo": "home-manager", + "type": "github" + } + }, + "ixx": { + "inputs": { + "flake-utils": [ + "nixvim", + "nuschtosSearch", + "flake-utils" + ], + "nixpkgs": [ + "nixvim", + "nuschtosSearch", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1754860581, + "narHash": "sha256-EM0IE63OHxXCOpDHXaTyHIOk2cNvMCGPqLt/IdtVxgk=", + "owner": "NuschtOS", + "repo": "ixx", + "rev": "babfe85a876162c4acc9ab6fb4483df88fa1f281", + "type": "github" + }, + "original": { + "owner": "NuschtOS", + "ref": "v0.1.1", + "repo": "ixx", + "type": "github" + } + }, + "nix": { + "inputs": { + "flake-compat": "flake-compat_2", + "flake-parts": "flake-parts", + "git-hooks-nix": "git-hooks-nix", + "nixpkgs": "nixpkgs", + "nixpkgs-23-11": "nixpkgs-23-11", + "nixpkgs-regression": "nixpkgs-regression" + }, + "locked": { + "lastModified": 1741125032, + "narHash": "sha256-Yy1Cd3Xm4UJTctYsVQfD5jY5z7pVncvLu8cq0cjjYT4=", + "owner": "DeterminateSystems", + "repo": "nix-src", + "rev": "271926aa5997c3120c8ef0962ce1c7f29fee1a05", + "type": "github" + }, + "original": { + "owner": "DeterminateSystems", + "ref": "flake-schemas", + "repo": "nix-src", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1734359947, + "narHash": "sha256-1Noao/H+N8nFB4Beoy8fgwrcOQLVm9o4zKW1ODaqK9E=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "48d12d5e70ee91fe8481378e540433a7303dbf6a", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-23-11": { + "locked": { + "lastModified": 1717159533, + "narHash": "sha256-oamiKNfr2MS6yH64rUn99mIZjc45nGJlj9eGth/3Xuw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "a62e6edd6d5e1fa0329b8653c801147986f8d446", + "type": "github" + } + }, + "nixpkgs-regression": { + "locked": { + "lastModified": 1643052045, + "narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1768773494, + "narHash": "sha256-XsM7GP3jHlephymxhDE+/TKKO1Q16phz/vQiLBGhpF4=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "77ef7a29d276c6d8303aece3444d61118ef71ac2", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixvim": { + "inputs": { + "flake-parts": "flake-parts_2", + "nixpkgs": [ + "nixpkgs" + ], + "nuschtosSearch": "nuschtosSearch", + "systems": "systems_2" + }, + "locked": { + "lastModified": 1768486829, + "narHash": "sha256-G621Q9cB1roQxK0C6guNjmWX0CmPA5xN46VD2kTdDEk=", + "owner": "nix-community", + "repo": "nixvim", + "rev": "503259b749971f431cb4aca7099cd60eadd7a613", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "nixos-25.11", + "repo": "nixvim", + "type": "github" + } + }, + "nur": { + "inputs": { + "flake-parts": "flake-parts_3", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1769002807, + "narHash": "sha256-27JgCsWRnWsI1ZMnrIbmyLm+GCoyDTYILcAVI75SN6g=", + "owner": "nix-community", + "repo": "NUR", + "rev": "818b545699f32a1058961604b4a2783875fe8cde", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "NUR", + "type": "github" + } + }, + "nur_2": { + "inputs": { + "flake-parts": [ + "stylix", + "flake-parts" + ], + "nixpkgs": [ + "stylix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1767886815, + "narHash": "sha256-pB2BBv6X9cVGydEV/9Y8+uGCvuYJAlsprs1v1QHjccA=", + "owner": "nix-community", + "repo": "NUR", + "rev": "4ff84374d77ff62e2e13a46c33bfeb73590f9fef", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "NUR", + "type": "github" + } + }, + "nuschtosSearch": { + "inputs": { + "flake-utils": "flake-utils", + "ixx": "ixx", + "nixpkgs": [ + "nixvim", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1761730856, + "narHash": "sha256-t1i5p/vSWwueZSC0Z2BImxx3BjoUDNKyC2mk24krcMY=", + "owner": "NuschtOS", + "repo": "search", + "rev": "e29de6db0cb3182e9aee75a3b1fd1919d995d85b", + "type": "github" + }, + "original": { + "owner": "NuschtOS", + "repo": "search", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-schemas": "flake-schemas", + "git-hooks": "git-hooks", + "home-manager": "home-manager", + "nix": "nix", + "nixpkgs": "nixpkgs_2", + "nixvim": "nixvim", + "nur": "nur", + "stylix": "stylix" + } + }, + "stylix": { + "inputs": { + "base16": "base16", + "base16-fish": "base16-fish", + "base16-helix": "base16-helix", + "base16-vim": "base16-vim", + "firefox-gnome-theme": "firefox-gnome-theme", + "flake-parts": "flake-parts_4", + "gnome-shell": "gnome-shell", + "nixpkgs": [ + "nixpkgs" + ], + "nur": "nur_2", + "systems": "systems_3", + "tinted-foot": "tinted-foot", + "tinted-kitty": "tinted-kitty", + "tinted-schemes": "tinted-schemes", + "tinted-tmux": "tinted-tmux", + "tinted-zed": "tinted-zed" + }, + "locked": { + "lastModified": 1768493544, + "narHash": "sha256-9qk2W/6GJWLAFXNruK/zdJ0bm3bfP50vJFbtuAjQpa4=", + "owner": "nix-community", + "repo": "stylix", + "rev": "362306faaa7459bebf8eabf135879785f3da9bd2", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "release-25.11", + "repo": "stylix", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_3": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "tinted-foot": { + "flake": false, + "locked": { + "lastModified": 1726913040, + "narHash": "sha256-+eDZPkw7efMNUf3/Pv0EmsidqdwNJ1TaOum6k7lngDQ=", + "owner": "tinted-theming", + "repo": "tinted-foot", + "rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-foot", + "rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4", + "type": "github" + } + }, + "tinted-kitty": { + "flake": false, + "locked": { + "lastModified": 1735730497, + "narHash": "sha256-4KtB+FiUzIeK/4aHCKce3V9HwRvYaxX+F1edUrfgzb8=", + "owner": "tinted-theming", + "repo": "tinted-kitty", + "rev": "de6f888497f2c6b2279361bfc790f164bfd0f3fa", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-kitty", + "type": "github" + } + }, + "tinted-schemes": { + "flake": false, + "locked": { + "lastModified": 1767817087, + "narHash": "sha256-eGE8OYoK6HzhJt/7bOiNV2cx01IdIrHL7gXgjkHRdNo=", + "owner": "tinted-theming", + "repo": "schemes", + "rev": "bd99656235aab343e3d597bf196df9bc67429507", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "schemes", + "type": "github" + } + }, + "tinted-tmux": { + "flake": false, + "locked": { + "lastModified": 1767489635, + "narHash": "sha256-e6nnFnWXKBCJjCv4QG4bbcouJ6y3yeT70V9MofL32lU=", + "owner": "tinted-theming", + "repo": "tinted-tmux", + "rev": "3c32729ccae99be44fe8a125d20be06f8d7d8184", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-tmux", + "type": "github" + } + }, + "tinted-zed": { + "flake": false, + "locked": { + "lastModified": 1767488740, + "narHash": "sha256-wVOj0qyil8m+ouSsVZcNjl5ZR+1GdOOAooAatQXHbuU=", + "owner": "tinted-theming", + "repo": "base16-zed", + "rev": "11abb0b282ad3786a2aae088d3a01c60916f2e40", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-zed", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..20467b0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,285 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + + nix.url = "github:DeterminateSystems/nix-src/flake-schemas"; + + flake-schemas.url = "github:DeterminateSystems/flake-schemas"; + + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + + home-manager.url = "github:nix-community/home-manager/release-25.11"; + home-manager.inputs.nixpkgs.follows = "nixpkgs"; + + nixvim.url = "github:nix-community/nixvim/nixos-25.11"; + nixvim.inputs.nixpkgs.follows = "nixpkgs"; + + nur.url = "github:nix-community/NUR"; + nur.inputs.nixpkgs.follows = "nixpkgs"; + + stylix.url = "github:nix-community/stylix/release-25.11"; + stylix.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + ... + }@inputs: + let + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" # For testing only. Use at your own risk. + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + nixpkgsFor = forAllSystems ( + system: + import nixpkgs { + inherit system; + overlays = [ + self.overlays.default + inputs.nix.overlays.default + ]; + } + ); + + test = { + system = "x86_64-linux"; + lib = nixpkgs.lib.extend (final: prev: self.outputs.lib or { }); + inputs = inputs // { + synix = self; + }; + outputs = { }; + overlays = [ + self.overlays.default + self.overlays.additions + self.overlays.modifications + (final: prev: { synix = self.packages."${final.system}"; }) + ]; + }; + in + { + inherit (inputs.flake-schemas) schemas; + + apps = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + mkApp = name: desc: { + type = "app"; + program = pkgs.lib.getExe (pkgs.callPackage ./apps/${name} { }); + meta.description = desc; + }; + in + { + create = mkApp "create" "Create a new NixOS configuration."; + deploy = mkApp "deploy" "Deploy NixOS configurations in your flake."; + install = mkApp "install" "Install a NixOS configuration."; + rebuild = mkApp "rebuild" "Wrapper script for 'nixos-rebuild switch' and 'home-manager switch' commands."; + update-packages = mkApp "update-packages" "Update all packages in this flake."; + wake-host = mkApp "wake-host" "Wake a host with WakeOnLan."; + } + ); + + lib = { + utils = import ./lib/utils.nix { lib = nixpkgs.lib; }; + }; + + packages = forAllSystems ( + system: + let + allArchs = import ./pkgs { pkgs = nixpkgs.legacyPackages.${system}; }; + x64only = + if system == "x86_64-linux" then + { + } + else + { }; + in + allArchs // x64only + ); + + overlays = import ./overlays { inherit inputs; }; + + nixosModules = import ./modules/nixos; + + homeModules = import ./modules/home; + + # test configs + nixosConfigurations = { + nixos-hyprland = nixpkgs.lib.nixosSystem { + inherit (test) system; + modules = [ + ./tests/build/nixos-hyprland + { nixpkgs.overlays = test.overlays; } + ]; + specialArgs = { + inherit (test) inputs outputs lib; + }; + }; + nixos-server = nixpkgs.lib.nixosSystem { + inherit (test) system; + modules = [ + ./tests/build/nixos-server + { nixpkgs.overlays = test.overlays; } + ]; + specialArgs = { + inherit (test) inputs outputs lib; + }; + }; + }; + homeConfigurations = { + hm-hyprland = inputs.home-manager.lib.homeManagerConfiguration { + pkgs = import nixpkgs { + inherit (test) overlays system; + }; + extraSpecialArgs = { + inherit (test) inputs outputs; + }; + modules = [ + ./tests/build/hm-hyprland + ]; + }; + }; + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + in + { + default = + let + inherit (self.checks.${system}.pre-commit-check) shellHook enabledPackages; + in + pkgs.mkShell { + inherit shellHook; + nativeBuildInputs = [ + enabledPackages + pkgs.nix + ] + ++ (with pkgs; [ + (python313.withPackages ( + p: with p; [ + mkdocs + mkdocs-material + mkdocs-material-extensions + pygments + ] + )) + ]); + }; + } + ); + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + flakePkgs = self.packages.${system}; + overlaidPkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.modifications ]; + }; + in + { + pre-commit-check = inputs.git-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt.enable = true; + }; + }; + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + build-overlays = pkgs.linkFarm "flake-overlays-${system}" { + kicad = overlaidPkgs.kicad; + }; + + synapse-test = + let + testPkgs = import nixpkgs { + inherit system; + config.permittedInsecurePackages = [ "olm-3.2.16" ]; + }; + in + testPkgs.testers.runNixOSTest ./tests/run/synapse.nix; + } + + ); + + hydraJobs = { + inherit (self) + packages + ; + }; + + templates = { + hetzner-amd = { + path = ./templates/nix-configs/hetzner-amd; + description = "Basic NixOS configuration for AMD based Hetzner VPS."; + }; + hyprland = { + path = ./templates/nix-configs/hyprland; + description = "Basic NixOS configuration for clients running Hyprland with standalone Home Manager."; + }; + pi4 = { + path = ./templates/nix-configs/pi4; + description = "Basic NixOS configuration for Raspberry Pi 4."; + }; + server = { + path = ./templates/nix-configs/server; + description = "Basic NixOS configuration for servers."; + }; + vm-uefi = { + path = ./templates/nix-configs/vm-uefi; + description = "Basic NixOS configuration for VMs (UEFI)."; + }; + + microvm = { + path = ./templates/microvm; + description = "MicroVM NixOS configurations"; + }; + container = { + path = ./templates/container; + description = "Container NixOS configurations"; + }; + + c-hello = { + path = ./templates/dev/c-hello; + description = "C hello world template."; + }; + esp-blink = { + path = ./templates/dev/esp-blink; + description = "ESP32 blink template."; + }; + flask-hello = { + path = ./templates/dev/flask-hello; + description = "Python Flask hello template."; + }; + py-hello = { + path = ./templates/dev/py-hello; + description = "Python hello world template."; + }; + rs-hello = { + path = ./templates/dev/rs-hello; + description = "Rust hello world template."; + }; + }; + }; +} diff --git a/lib/utils.nix b/lib/utils.nix new file mode 100644 index 0000000..bfff062 --- /dev/null +++ b/lib/utils.nix @@ -0,0 +1,85 @@ +{ lib, ... }: + +let + inherit (lib) + mkDefault + mkEnableOption + mkIf + mkOption + types + ; +in +{ + isNotEmptyStr = str: builtins.isString str && str != ""; + + mkMailIntegrationOption = service: { + enable = mkEnableOption "Mail integration for ${service}."; + smtpHost = mkOption { + type = types.str; + default = "localhost"; + description = "SMTP host for sending emails."; + }; + }; + + mkReverseProxyOption = service: subdomain: { + enable = mkEnableOption "Nginx reverse proxy for ${service}."; + subdomain = mkOption { + type = types.str; + default = subdomain; + description = "Subdomain for Nginx virtual host. Leave empty for root domain."; + }; + forceSSL = mkOption { + type = types.bool; + default = true; + description = "Force SSL for Nginx virtual host."; + }; + }; + + mkUrl = + { + fqdn, + ssl ? false, + port ? null, + path ? "", + ... + }: + let + protocol = if ssl then "https" else "http"; + portPart = if port != null then ":${toString port}" else ""; + pathPart = if path != "" then "/${path}" else ""; + in + "${protocol}://${fqdn}${portPart}${pathPart}"; + + mkVirtualHost = + { + address ? "127.0.0.1", + port ? null, + socketPath ? null, + location ? "/", + ssl ? false, + proxyWebsockets ? true, + recommendedProxySettings ? true, + extraConfig ? "", + ... + }: + let + target = + if port != null then + "http://${address}:${builtins.toString port}" + else if socketPath != null then + "http://unix:${socketPath}" + else + null; + in + { + enableACME = ssl; + forceSSL = ssl; + + locations = mkIf (target != null) { + "${location}" = { + proxyPass = mkDefault target; + inherit proxyWebsockets recommendedProxySettings extraConfig; + }; + }; + }; +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..8a18b20 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,81 @@ +site_name: synix docs +repo_url: https://git.sid.ovh/sid/synix +site_url: https://doc.sid.ovh/synix +site_dir: site +edit_uri: -/tree/master/docs/ +repo_name: Git +docs_dir: docs +theme: + name: material + features: + - content.code.select +markdown_extensions: + - pymdownx.highlight: + use_pygments: true + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.snippets + - pymdownx.superfences + +nav: + - Home: index.md # do not change + - Introduction to Nix: + - Overview: introduction-to-nix/overview.md + - Install Nix: introduction-to-nix/install-nix.md + - Nix Speedrun: introduction-to-nix/nix-speedrun.md + - Derivations: introduction-to-nix/derivations.md + - Nix Store: introduction-to-nix/nix-store.md + - Nixpkgs: introduction-to-nix/nixpkgs.md + - Flakes: introduction-to-nix/flakes.md + - NixOS: introduction-to-nix/nixos.md + - Getting Started: + - Create your nix-config flake: getting-started/create-nix-config.md + - Add configs to your flake: getting-started/add-configs.md + - Install instructions: getting-started/install-instructions.md + - Modules: + - NixOS: + - audio: modules/nixos/audio.md + - baibot: modules/nixos/baibot.md + - cifsMount: modules/nixos/cifsmount.md + - common: modules/nixos/common.md + - device: modules/nixos/device.md + - ftp-webserver: modules/nixos/ftp-webserver.md + - headplane: modules/nixos/headplane.md + - headscale: modules/nixos/headscale.md + - i2pd: modules/nixos/i2pd.md + - jellyfin: modules/nixos/jellyfin.md + - jirafeau: modules/nixos/jirafeau.md + - mailserver: modules/nixos/mailserver.md + - matrix-synapse: modules/nixos/matrix-synapse.md + - mcpo: modules/nixos/mcpo.md + - miniflux: modules/nixos/miniflux.md + - normalUsers: modules/nixos/normalUsers.md + - nvidia: modules/nixos/nvidia.md + - open-webui-oci: modules/nixos/open-webui-oci.md + - print-server: modules/nixos/print-server.md + - radicale: modules/nixos/radicale.md + - rss-bridge: modules/nixos/rss-bridge.md + - sops: modules/nixos/sops.md + - tailscale: modules/nixos/tailscale.md + - virtualisation: modules/nixos/virtualisation.md + - webPage: modules/nixos/webpage.md + - Home Manager: + - bemenu: modules/home/bemenu.md + - common: modules/home/common.md + - gpg: modules/home/gpg.md + - hyprland: modules/home/hyprland.md + - kitty: modules/home/kitty.md + - networkmanager-dmenu: modules/home/networkmanager-dmenu.md + - nextcloud-sync: modules/home/nextcloud-sync.md + - nixvim: modules/home/nixvim.md + - password-manager: modules/home/password-manager.md + - sops: modules/home/sops.md + - stylix: modules/home/stylix.md + - virtualisation: modules/home/virtualisation.md + - waybar: modules/home/waybar.md + - yazi: modules/home/yazi.md + - Tips: + - Useful Links: tips/useful-links.md + - Dependency Tracing: tips/dependency-tracing.md diff --git a/modules/home/bemenu/default.nix b/modules/home/bemenu/default.nix new file mode 100644 index 0000000..5e21df8 --- /dev/null +++ b/modules/home/bemenu/default.nix @@ -0,0 +1,21 @@ +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + programs.bemenu = { + settings = { + border = mkDefault 2; + border-radius = mkDefault 10; + center = mkDefault true; + ignorecase = mkDefault true; + list = mkDefault "20 down"; + margin = mkDefault 5; + prompt = mkDefault ""; + scrollbar = mkDefault "none"; + width-factor = mkDefault 0.3; + wrap = mkDefault true; + }; + }; +} diff --git a/modules/home/bitwarden/default.nix b/modules/home/bitwarden/default.nix new file mode 100644 index 0000000..e46e962 --- /dev/null +++ b/modules/home/bitwarden/default.nix @@ -0,0 +1,32 @@ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.bitwarden; + + inherit (lib) mkEnableOption mkIf; +in +{ + options.programs.bitwarden = { + enable = mkEnableOption "Bitwarden password manager integration"; + }; + + config = mkIf cfg.enable { + home.packages = [ + pkgs.bitwarden-menu + pkgs.bitwarden-cli + ]; + + programs.librewolf = mkIf config.programs.librewolf.enable { + profiles.default.extensions.packages = + with inputs.nur.legacyPackages."${pkgs.stdenv.hostPlatform.system}".repos.rycee.firefox-addons; [ + bitwarden + ]; + }; + }; +} diff --git a/modules/home/common/cdf.sh b/modules/home/common/cdf.sh new file mode 100644 index 0000000..6e89ef8 --- /dev/null +++ b/modules/home/common/cdf.sh @@ -0,0 +1,77 @@ +# change directory with fzf +# Usage: cdf [optional_relative_path] +# - If no argument, searches from $HOME. +# - If a relative path (e.g., "projects/my_app") is provided, searches only within that path relative to $HOME. +# - If an absolute path (e.g., "/mnt/data") is provided, searches only within that path. +function cdf() { + local exclude_names=( + ".cache" + ".cargo" + ".git" + ".npm" + ".rustup" + ".venv" + "Library" + "__pycache__" + "build" + "cache" + "dist" + "neorv32" + "nixpkgs" + "node_modules" + "octave" + "snap" + "target" + "venv" + ) + + local dir="$HOME" + + if [[ -n "$1" ]]; then + if [[ "$1" == /* ]]; then + dir="$1" + else + dir="$HOME/$1" + fi + + if [[ ! -d "$dir" ]]; then + echo "Error: '$dir' does not exist or is not a directory." + return 1 + fi + fi + + local find_args=("$dir") + find_args+=(-path "$dir/.*" -prune -o) + + local prune_exprs=() + local has_prunes=false + for name in "${exclude_names[@]}"; do + if $has_prunes; then + prune_exprs+=(-o) + fi + prune_exprs+=(-name "$name") + has_prunes=true + done + + if $has_prunes; then + find_args+=(\( "${prune_exprs[@]}" \) -prune -o) + fi + + find_args+=(-type d -print) + + local fzf_args=( + "-i" + "--height=40%" + "--reverse" + "--prompt=Select directory: " + "--preview=tree -C {} | head -50" + "--preview-window=right:50%:wrap" + ) + local selected=$(find "${find_args[@]}" 2>/dev/null | fzf "${fzf_args[@]}") + + if [[ -n "$selected" ]]; then + cd "$selected" || echo "Failed to cd into '$selected'" + pwd + ls -lAh + fi +} diff --git a/modules/home/common/default.nix b/modules/home/common/default.nix new file mode 100644 index 0000000..dbc55ed --- /dev/null +++ b/modules/home/common/default.nix @@ -0,0 +1,30 @@ +{ + config, + lib, + ... +}: + +let + inherit (lib) mkDefault; +in +{ + imports = [ + ./packages.nix + ./shellAliases.nix + ./zsh.nix + + ../../shared/common + ]; + + programs.home-manager.enable = true; + + home.homeDirectory = mkDefault "/home/${config.home.username}"; + + # Nicely reload system units when changing configs + systemd.user.startServices = "sd-switch"; + + # JSON formatted list of Home Manager options + manual.json.enable = true; + + news.display = "silent"; +} diff --git a/modules/home/common/packages.nix b/modules/home/common/packages.nix new file mode 100644 index 0000000..f66ec4b --- /dev/null +++ b/modules/home/common/packages.nix @@ -0,0 +1,11 @@ +{ pkgs, ... }: + +{ + home.packages = with pkgs; [ + hydra-check + nix-init + tldr + + (callPackage ../../../apps/rebuild { }) + ]; +} diff --git a/modules/home/common/shellAliases.nix b/modules/home/common/shellAliases.nix new file mode 100644 index 0000000..c1058d1 --- /dev/null +++ b/modules/home/common/shellAliases.nix @@ -0,0 +1,33 @@ +{ + home.shellAliases = { + l = "ls -lh"; + ll = "ls -lAh"; + ports = "ss -tulpn"; + publicip = "curl ifconfig.me/all"; + sudo = "sudo "; # make aliases work with `sudo` + + # systemd + userctl = "systemctl --user"; + enable = "systemctl --user enable"; + disable = "systemctl --user disable"; + start = "systemctl --user start"; + stop = "systemctl --user stop"; + journal = "journalctl --user"; + + # git + ga = "git add"; + gb = "git branch"; + gc = "git commit"; + gcl = "git clone"; + gco = "git checkout"; + gcp = "git cherry-pick -x"; + gd = "git diff"; + gf = "git fetch --all"; + gl = "git log"; + gm = "git merge"; + gp = "git push"; + gpl = "git pull"; + gr = "git remote"; + gs = "git status"; + }; +} diff --git a/modules/home/common/zsh.nix b/modules/home/common/zsh.nix new file mode 100644 index 0000000..5141aa9 --- /dev/null +++ b/modules/home/common/zsh.nix @@ -0,0 +1,17 @@ +{ config, lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + programs.zsh = { + enable = mkDefault true; + dotDir = "${config.xdg.configHome}/zsh"; + defaultKeymap = mkDefault "emacs"; + initContent = '' + PROMPT='%F{green}%n%f@%F{blue}%m%f %B%1~%b > ' + RPROMPT='[%F{yellow}%?%f]' + '' + + builtins.readFile ./cdf.sh; + }; +} diff --git a/modules/home/default.nix b/modules/home/default.nix new file mode 100644 index 0000000..d98e16e --- /dev/null +++ b/modules/home/default.nix @@ -0,0 +1,17 @@ +{ + bemenu = import ./bemenu; + common = import ./common; + gpg = import ./gpg; + hyprland = import ./hyprland; + kitty = import ./kitty; + lf = import ./lf; + librewolf = import ./librewolf; + networkmanager-dmenu = import ./networkmanager-dmenu; + nixvim = import ./nixvim; + passwordManager = import ./password-manager; + rofi-rbw = import ./rofi-rbw; + sops = import ./sops; + stylix = import ./stylix; + virtualisation = import ./virtualisation; + waybar = import ./waybar; +} diff --git a/modules/home/gpg/default.nix b/modules/home/gpg/default.nix new file mode 100644 index 0000000..f4f17a6 --- /dev/null +++ b/modules/home/gpg/default.nix @@ -0,0 +1,31 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.gpg; + + inherit (lib) mkDefault mkIf; +in +{ + programs.gpg = { + enable = mkDefault true; + }; + services.gpg-agent = mkIf cfg.enable { + defaultCacheTtl = mkDefault 600; + defaultCacheTtlSsh = mkDefault 600; + enable = mkDefault true; + enableScDaemon = mkDefault false; + enableSshSupport = mkDefault true; + maxCacheTtl = mkDefault 7200; + maxCacheTtlSsh = mkDefault 7200; + pinentry.package = mkDefault pkgs.pinentry-qt; + verbose = mkDefault true; + }; + programs.ssh = { + enable = mkDefault true; + }; +} diff --git a/modules/home/hyprland/applications/bemenu/default.nix b/modules/home/hyprland/applications/bemenu/default.nix new file mode 100644 index 0000000..d77b774 --- /dev/null +++ b/modules/home/hyprland/applications/bemenu/default.nix @@ -0,0 +1,21 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.applauncher.default; + + inherit (lib) mkIf; +in +{ + imports = [ ../../../bemenu ]; + + config = mkIf (cfg.enable && app == "bemenu") { + programs.bemenu = { + enable = true; + }; + }; +} diff --git a/modules/home/hyprland/applications/default.nix b/modules/home/hyprland/applications/default.nix new file mode 100644 index 0000000..193c696 --- /dev/null +++ b/modules/home/hyprland/applications/default.nix @@ -0,0 +1,202 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + apps = cfg.applications; + + # dynamically create a set of default app assignments + defaultApps = mapAttrs (name: app: app.default) apps; + + # function to generate the attribute set for each application + mkAppAttrs = + { + default, + bind ? [ "" ], + windowrule ? [ "" ], + }: + { + default = mkOption { + type = types.str; + default = default; + description = "The default application to use for the ${default}."; + }; + bind = mkOption { + type = types.listOf types.str; + default = bind; + description = "The keybinding to use for the ${default}."; + }; + windowrule = mkOption { + type = types.listOf types.str; + default = windowrule; + description = "The window rule to use for the ${default}."; + }; + }; + + # generate lists of all binds and window rules and remove empty strings + binds = filter (s: s != "") ( + builtins.concatLists (map (app: app.bind or [ "" ]) (attrValues apps)) + ); + windowrules = filter (s: s != "") ( + builtins.concatLists (map (app: app.windowrule or [ "" ]) (attrValues apps)) + ); + + inherit (lib) + attrValues + filter + getExe + mapAttrs + mkOption + types + ; +in +{ + imports = [ + ./bemenu + ./dmenu-bluetooth + ./element-desktop + ./feh + ./kitty + ./libreoffice + ./librewolf + ./mpv + ./ncmpcpp + ./networkmanager_dmenu + ./newsboat + ./passwordmanager + ./powermenu-bemenu + ./presentation-mode-bemenu + ./qbittorrent + ./screenshot + ./thunderbird + ./yazi + ./zathura + # add your application directories here + ]; + + options.wayland.windowManager.hyprland.applications = with defaultApps; { + applauncher = mkAppAttrs { + default = "bemenu"; + bind = [ "$mod, d, exec, ${applauncher}-run" ]; + }; + + audiomixer = mkAppAttrs { + default = "pulsemixer"; + bind = [ "$mod, a, exec, ${terminal} -T ${audiomixer} -e ${pkgs.pulsemixer}/bin/pulsemixer" ]; + windowrule = [ + "float, title:^${audiomixer}$" + "size 50% 50%, title:^${audiomixer}$" + ]; + }; + + bluetoothsettings = mkAppAttrs { + default = "dmenu-bluetooth"; + bind = [ "$mod SHIFT, b, exec, ${bluetoothsettings}" ]; + }; + + browser = mkAppAttrs { + default = "librewolf"; + bind = [ "$mod, b, exec, ${browser}" ]; + }; + + calculator = mkAppAttrs { + default = "octave"; + bind = [ + ", XF86Calculator, exec, ${terminal} -T ${calculator} -e ${pkgs.octave}/bin/octave" + ]; + }; + + emailclient = mkAppAttrs { + default = "thunderbird"; + bind = [ "$mod, m, exec, ${emailclient}" ]; + }; + + equalizer = mkAppAttrs { + default = "easyeffects"; + bind = [ "$mod CTRL, e, exec, ${getExe pkgs.easyeffects}" ]; + }; + + filemanager = mkAppAttrs { + default = "yazi"; + bind = [ "$mod, e, exec, ${terminal} -T ${filemanager} -e ${filemanager}" ]; + }; + + matrix-client = mkAppAttrs { + default = "element-desktop"; + bind = [ "$mod SHIFT, e, exec, ${matrix-client}" ]; + }; + + musicplayer = mkAppAttrs { + default = "ncmpcpp"; + bind = [ "$mod SHIFT, m, exec, ${terminal} -T ${musicplayer} -e ${musicplayer}" ]; + }; + + networksettings = mkAppAttrs { + default = "networkmanager_dmenu"; + bind = [ "$mod SHIFT, n, exec, ${networksettings}" ]; + }; + + notes = mkAppAttrs { + default = "quicknote"; + bind = [ "$mod CTRL, n, exec, ${terminal} -T ${notes} -e ${getExe pkgs.synix.quicknote}" ]; + }; + + office = mkAppAttrs { + default = "libreoffice"; + bind = [ "$mod SHIFT, o, exec, ${office}" ]; + }; + + password-manager = mkAppAttrs { + default = "passmenu-bemenu"; + bind = [ "$mod, p, exec, ${password-manager}" ]; + }; + + imageviewer = mkAppAttrs { default = "feh"; }; + + pdfviewer = mkAppAttrs { default = "zathura"; }; + + powermenu = mkAppAttrs { + default = "powermenu-bemenu"; + bind = [ "$mod SHIFT, q, exec, ${powermenu}" ]; + }; + + presentation-mode = mkAppAttrs { + default = "presentation-mode-bemenu"; + bind = [ "$mod SHIFT, p, exec, ${presentation-mode}" ]; + }; + + rssreader = mkAppAttrs { + default = "newsboat"; + bind = [ "$mod, n, exec, ${terminal} -T ${rssreader} -e ${rssreader}" ]; + }; + + screenshotter = mkAppAttrs { + default = "screenshot"; + bind = [ + "$mod, Print, exec, ${screenshotter} output" # select monitor + "$mod SHIFT, Print, exec, ${screenshotter} region" # select region + "$mod CTRL, Print, exec, ${screenshotter} window" # select window + ]; + }; + + terminal = mkAppAttrs { + default = "kitty"; + bind = [ "$mod, Return, exec, ${terminal}" ]; + }; + + torrent-client = mkAppAttrs { default = "qbittorrent"; }; + + videoplayer = mkAppAttrs { default = "mpv"; }; + }; + + config = { + wayland.windowManager.hyprland.settings = { + bind = binds; + windowrule = windowrules; + }; + }; +} diff --git a/modules/home/hyprland/applications/dmenu-bluetooth/default.nix b/modules/home/hyprland/applications/dmenu-bluetooth/default.nix new file mode 100644 index 0000000..4245e1e --- /dev/null +++ b/modules/home/hyprland/applications/dmenu-bluetooth/default.nix @@ -0,0 +1,22 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.bluetoothsettings.default; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "dmenu-bluetooth") { + home.packages = with pkgs; [ dmenu-bluetooth ]; + + home.sessionVariables = { + DMENU_BLUETOOTH_LAUNCHER = cfg.applications.applauncher.default or "bemenu"; + }; + }; +} diff --git a/modules/home/hyprland/applications/element-desktop/default.nix b/modules/home/hyprland/applications/element-desktop/default.nix new file mode 100644 index 0000000..d5cf3b8 --- /dev/null +++ b/modules/home/hyprland/applications/element-desktop/default.nix @@ -0,0 +1,25 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.matrix-client.default; + + inherit (lib) mkDefault mkIf; +in +{ + config = mkIf (cfg.enable && app == "element-desktop") { + # FIXME: screen sharing does not work + programs.element-desktop = { + enable = true; + settings = { + # Use Chromium for screen sharing + default_theme = mkDefault "dark"; + }; + }; + }; +} diff --git a/modules/home/hyprland/applications/feh/default.nix b/modules/home/hyprland/applications/feh/default.nix new file mode 100644 index 0000000..9f2b1c1 --- /dev/null +++ b/modules/home/hyprland/applications/feh/default.nix @@ -0,0 +1,45 @@ +{ config, lib, ... }: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.imageviewer.default; + desktop = "feh.desktop"; + mimeTypes = [ + "image/gif" + "image/heic" + "image/jpeg" + "image/jpg" + "image/pjpeg" + "image/png" + "image/tiff" + "image/webp" + "image/x-bmp" + "image/x-pcx" + "image/x-png" + "image/x-portable-anymap" + "image/x-portable-bitmap" + "image/x-portable-graymap" + "image/x-portable-pixmap" + "image/x-tga" + "image/x-xbitmap" + ]; + associations = + let + genMimeAssociations = import ../genMimeAssociations.nix; + in + genMimeAssociations desktop mimeTypes; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "feh") { + programs.feh = { + enable = true; + }; + + xdg.mimeApps = { + defaultApplications = associations; + associations.added = associations; + }; + }; +} diff --git a/modules/home/hyprland/applications/genMimeAssociations.nix b/modules/home/hyprland/applications/genMimeAssociations.nix new file mode 100644 index 0000000..bd9cae6 --- /dev/null +++ b/modules/home/hyprland/applications/genMimeAssociations.nix @@ -0,0 +1,8 @@ +# Generate a list of mime associations for a desktop file +desktop: mimeTypes: +builtins.listToAttrs ( + map (mimeType: { + name = mimeType; + value = desktop; + }) mimeTypes +) diff --git a/modules/home/hyprland/applications/kitty/default.nix b/modules/home/hyprland/applications/kitty/default.nix new file mode 100644 index 0000000..ee20fe0 --- /dev/null +++ b/modules/home/hyprland/applications/kitty/default.nix @@ -0,0 +1,21 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.terminal.default; + + inherit (lib) mkIf; +in +{ + imports = [ ../../../kitty ]; + + config = mkIf (cfg.enable && app == "kitty") { + programs.kitty = { + enable = true; + }; + }; +} diff --git a/modules/home/hyprland/applications/libreoffice/default.nix b/modules/home/hyprland/applications/libreoffice/default.nix new file mode 100644 index 0000000..94ce71f --- /dev/null +++ b/modules/home/hyprland/applications/libreoffice/default.nix @@ -0,0 +1,20 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.office.default; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "libreoffice") { + home.packages = [ pkgs.libreoffice ]; + + # TODO: set Tools > Options > Application Colors > Automatic = Dark + }; +} diff --git a/modules/home/hyprland/applications/librewolf/default.nix b/modules/home/hyprland/applications/librewolf/default.nix new file mode 100644 index 0000000..08d2f90 --- /dev/null +++ b/modules/home/hyprland/applications/librewolf/default.nix @@ -0,0 +1,41 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.browser.default; + + desktop = "librewolf.desktop"; + mimeTypes = [ + "text/html" + "text/xml" + "application/xhtml+xml" + "application/vnd.mozilla.xul+xml" + "x-scheme-handler/http" + "x-scheme-handler/https" + ]; + associations = + let + genMimeAssociations = import ../genMimeAssociations.nix; + in + genMimeAssociations desktop mimeTypes; + + inherit (lib) mkIf; +in +{ + imports = [ ../../../librewolf ]; + + config = mkIf (cfg.enable && app == "librewolf") { + programs.librewolf = { + enable = true; + }; + + xdg.mimeApps = { + associations.added = associations; + defaultApplications = associations; + }; + }; +} diff --git a/modules/home/hyprland/applications/mpv/default.nix b/modules/home/hyprland/applications/mpv/default.nix new file mode 100644 index 0000000..73daac8 --- /dev/null +++ b/modules/home/hyprland/applications/mpv/default.nix @@ -0,0 +1,15 @@ +{ config, lib, ... }: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.videoplayer.default; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "mpv") { + programs.mpv = { + enable = true; + }; + }; +} diff --git a/modules/home/hyprland/applications/ncmpcpp/default.nix b/modules/home/hyprland/applications/ncmpcpp/default.nix new file mode 100644 index 0000000..0f8cdfe --- /dev/null +++ b/modules/home/hyprland/applications/ncmpcpp/default.nix @@ -0,0 +1,22 @@ +{ config, lib, ... }: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.musicplayer.default; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "ncmpcpp") { + programs = { + ncmpcpp = { + enable = true; + }; + }; + services = { + mpd = { + enable = true; + }; + }; + }; +} diff --git a/modules/home/hyprland/applications/networkmanager_dmenu/default.nix b/modules/home/hyprland/applications/networkmanager_dmenu/default.nix new file mode 100644 index 0000000..750b279 --- /dev/null +++ b/modules/home/hyprland/applications/networkmanager_dmenu/default.nix @@ -0,0 +1,22 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.networksettings.default; + + inherit (lib) mkIf; +in +{ + imports = [ ../../../networkmanager-dmenu ]; + + config = mkIf (cfg.enable && app == "networkmanager_dmenu") { + programs.networkmanager-dmenu = { + enable = true; + config.dmenu.dmenu_command = "bemenu"; + }; + }; +} diff --git a/modules/home/hyprland/applications/newsboat/default.nix b/modules/home/hyprland/applications/newsboat/default.nix new file mode 100644 index 0000000..0268df6 --- /dev/null +++ b/modules/home/hyprland/applications/newsboat/default.nix @@ -0,0 +1,47 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.rssreader.default; + reloadTime = "${toString config.programs.newsboat.reloadTime}"; + newsboat-reload = (import ./newsboat-reload.nix { inherit config pkgs; }); + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "newsboat") { + programs.newsboat = { + enable = true; + extraConfig = builtins.readFile ./extra-config; + }; + + home.packages = [ newsboat-reload ]; # newsboat's waybar module executes newsboat-reload on click + + # Automatically reload newsboat on timer + systemd.user = { + timers.newsboat-reload = { + Unit.Description = "Reload newsboat every ${reloadTime} minutes"; + + Timer.OnBootSec = "10sec"; + Timer.OnUnitActiveSec = "${reloadTime}min"; + Timer.Unit = "newsboat-reload.service"; + + Install.WantedBy = [ "timers.target" ]; + }; + + services.newsboat-reload = { + Unit.Description = "Reload newsboat"; + + Service.Type = "oneshot"; + Service.ExecStart = "${newsboat-reload}/bin/newsboat-reload"; + + Install.WantedBy = [ "multi-user.target" ]; + }; + }; + }; +} diff --git a/modules/home/hyprland/applications/newsboat/extra-config b/modules/home/hyprland/applications/newsboat/extra-config new file mode 100644 index 0000000..687a040 --- /dev/null +++ b/modules/home/hyprland/applications/newsboat/extra-config @@ -0,0 +1,35 @@ +# Format +feedlist-title-format "Your feeds (%u unread, %t total)%?T? - tag ‘%T’&?" +articlelist-title-format "Articles in feed %T (%u unread, %t total) - %U" +searchresult-title-format "Search result (%u unread, %t total)" +filebrowser-title-format "%?O?Open File&Save File? - %f" +help-title-format "Help" +selecttag-title-format "Select Tag" +selectfilter-title-format "Select Filter" +itemview-title-format "%T (%u unread, %t total) " +urlview-title-format "URLs" +dialogs-title-format "Dialogs" +feedlist-format "%4i %n %11u %t" +articlelist-format "%4i %f %D %?T?|%-17T| ?%t" +notify-format "%d new articles (%n unread articles, %f unread feeds)" + +# Colors +color background white default +color listnormal white default +color listfocus white black bold +color listnormal_unread magenta default +color listfocus_unread magenta black bold +color info blue black bold +color article white default + +# Highlight +highlight article "(^Feed:.*|^Title:.*|^Author:.*)" blue default bold +highlight article "(^Link:.*|^Date:.*)" default default +highlight article "https?://[^ ]+" green default +highlight article "^(Title):.*$" blue default +highlight article "\\[[0-9][0-9]*\\]" magenta default bold +highlight article "\\[image\\ [0-9]+\\]" green default bold +highlight article "\\[embedded flash: [0-9][0-9]*\\]" green default bold +highlight article ":.*\\(link\\)$" cyan default +highlight article ":.*\\(image\\)$" blue default +highlight article ":.*\\(embedded flash\\)$" magenta default diff --git a/modules/home/hyprland/applications/newsboat/newsboat-reload.nix b/modules/home/hyprland/applications/newsboat/newsboat-reload.nix new file mode 100644 index 0000000..76b493d --- /dev/null +++ b/modules/home/hyprland/applications/newsboat/newsboat-reload.nix @@ -0,0 +1,10 @@ +{ config, pkgs, ... }: + +let + newsboat = "${pkgs.newsboat}/bin/newsboat"; + notify = "${pkgs.libnotify}/bin/notify-send"; + signal = "${toString config.programs.waybar.settings.mainBar."custom/newsboat".signal}"; +in +(pkgs.writeShellScriptBin "newsboat-reload" '' + ${notify} -u low 'Newsboat' 'Reloading RSS feeds...' && ${newsboat} -x reload && ${notify} -u low 'Newsboat' 'RSS feeds reloaded.' && pkill -RTMIN+${signal} waybar +'') diff --git a/modules/home/hyprland/applications/passwordmanager/default.nix b/modules/home/hyprland/applications/passwordmanager/default.nix new file mode 100644 index 0000000..09da309 --- /dev/null +++ b/modules/home/hyprland/applications/passwordmanager/default.nix @@ -0,0 +1,22 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.password-manager.default; + + inherit (lib) mkIf; +in +{ + imports = [ ../../../password-manager ]; + + config = mkIf (cfg.enable && app == "passmenu-bemenu") { + programs.passwordManager = { + enable = true; + wayland = true; + }; + }; +} diff --git a/modules/home/hyprland/applications/powermenu-bemenu/default.nix b/modules/home/hyprland/applications/powermenu-bemenu/default.nix new file mode 100644 index 0000000..117737b --- /dev/null +++ b/modules/home/hyprland/applications/powermenu-bemenu/default.nix @@ -0,0 +1,18 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.powermenu.default; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "powermenu-bemenu") { + home.packages = [ (import ./powermenu-bemenu.nix { inherit config pkgs; }) ]; + }; +} diff --git a/modules/home/hyprland/applications/powermenu-bemenu/powermenu-bemenu.nix b/modules/home/hyprland/applications/powermenu-bemenu/powermenu-bemenu.nix new file mode 100644 index 0000000..e19537f --- /dev/null +++ b/modules/home/hyprland/applications/powermenu-bemenu/powermenu-bemenu.nix @@ -0,0 +1,10 @@ +{ config, pkgs, ... }: + +(pkgs.writeShellScriptBin "powermenu-bemenu" '' + case $(echo -e "shutdown\nreboot\nlock\nkill" | bemenu -p "") in + shutdown) systemctl poweroff ;; + reboot) systemctl reboot ;; + lock) "${config.programs.hyprlock.package}/bin/hyprlock" ;; + kill) hyprctl dispatch exit ;; + esac +'') diff --git a/modules/home/hyprland/applications/presentation-mode-bemenu/default.nix b/modules/home/hyprland/applications/presentation-mode-bemenu/default.nix new file mode 100644 index 0000000..9ffefdf --- /dev/null +++ b/modules/home/hyprland/applications/presentation-mode-bemenu/default.nix @@ -0,0 +1,22 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.presentation-mode.default; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "presentation-mode-bemenu") { + home.packages = [ + (pkgs.writeShellScriptBin "presentation-mode-bemenu" ( + builtins.readFile ./presentation-mode-bemenu.sh + )) + ]; + }; +} diff --git a/modules/home/hyprland/applications/presentation-mode-bemenu/presentation-mode-bemenu.sh b/modules/home/hyprland/applications/presentation-mode-bemenu/presentation-mode-bemenu.sh new file mode 100644 index 0000000..8180c83 --- /dev/null +++ b/modules/home/hyprland/applications/presentation-mode-bemenu/presentation-mode-bemenu.sh @@ -0,0 +1,32 @@ +# Variables +DISPLAYS=( $(hyprctl monitors | grep -E '^Monitor' | awk '{print $2}') ) +EXTEND_RIGHT="Extend to right of main" +EXTEND_LEFT="Extend to left of main" +MIRROR="Mirror main" +DISABLE="Disable main" + +# Exit if only one display is available +[[ "${#DISPLAYS[@]}" -eq 1 ]] && echo "Only one display available." && exit 0 + +MAIN_DISPLAY=${DISPLAYS[0]} +SECOND_DISPLAY=${DISPLAYS[1]} # TODO: Add support for more than two displays + +# Select action +ACTIONS="$EXTEND_RIGHT\n$EXTEND_LEFT\n$MIRROR\n$DISABLE" +ACTIONS_CHOICE=$(echo -e "$ACTIONS" | bemenu -p "Select action") + +# Handle actions that do not need a mode +case "$ACTIONS_CHOICE" in + "$MIRROR") hyprctl keyword monitor "$SECOND_DISPLAY", preferred, auto, 1, mirror, "$MAIN_DISPLAY" && exit 0;; + "$DISABLE") hyprctl keyword monitor "$MAIN_DISPLAY", disable && exit 0;; +esac + +# Select mode +MODES=$( hyprctl monitors | awk '/^Monitor/{flag=1; next} /^$/{flag=0} flag' | awk -F "availableModes: " '{print $2}' | sed 's/ /\\n/g' | awk NF ) +MODES_CHOICE=$(echo -e "$MODES" | bemenu -p "Select mode") + +# Handle actions that need a mode +case "$ACTIONS_CHOICE" in + "$EXTEND_RIGHT") hyprctl keyword monitor "$SECOND_DISPLAY", "$MODES_CHOICE", auto-right, 1 ;; + "$EXTEND_LEFT") hyprctl keyword monitor "$SECOND_DISPLAY", "$MODES_CHOICE", auto-left, 1 ;; +esac diff --git a/modules/home/hyprland/applications/qbittorrent/default.nix b/modules/home/hyprland/applications/qbittorrent/default.nix new file mode 100644 index 0000000..bac33e0 --- /dev/null +++ b/modules/home/hyprland/applications/qbittorrent/default.nix @@ -0,0 +1,34 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.torrent-client.default; + desktop = "org.qbittorrent.qBittorrent.desktop"; + mimeTypes = [ + "application/x-bittorrent" + "x-scheme-handler/magnet" + ]; + associations = + let + genMimeAssociations = import ../genMimeAssociations.nix; + in + genMimeAssociations desktop mimeTypes; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "qbittorrent") { + home.packages = [ pkgs.qbittorrent ]; + # TODO: automatically apply dark theme + + xdg.mimeApps = { + defaultApplications = associations; + associations.added = associations; + }; + }; +} diff --git a/modules/home/hyprland/applications/screenshot/default.nix b/modules/home/hyprland/applications/screenshot/default.nix new file mode 100644 index 0000000..bf2c023 --- /dev/null +++ b/modules/home/hyprland/applications/screenshot/default.nix @@ -0,0 +1,18 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.screenshotter.default; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "screenshot") { + home.packages = [ (import ./screenshot.nix { inherit config pkgs; }) ]; + }; +} diff --git a/modules/home/hyprland/applications/screenshot/screenshot.nix b/modules/home/hyprland/applications/screenshot/screenshot.nix new file mode 100644 index 0000000..4288fe2 --- /dev/null +++ b/modules/home/hyprland/applications/screenshot/screenshot.nix @@ -0,0 +1,11 @@ +{ config, pkgs, ... }: + +# pass the mode (output, window, region) as a command line argument + +let + screenshotDir = "${config.xdg.userDirs.pictures}/screenshots"; +in +(pkgs.writeShellScriptBin "screenshot" '' + mkdir -p ${screenshotDir} + ${pkgs.hyprshot}/bin/hyprshot --mode $1 --output-folder ${screenshotDir} --filename screenshot_$(date +"%Y-%m-%d_%H-%M-%S").png +'') diff --git a/modules/home/hyprland/applications/thunderbird/default.nix b/modules/home/hyprland/applications/thunderbird/default.nix new file mode 100644 index 0000000..430f1f4 --- /dev/null +++ b/modules/home/hyprland/applications/thunderbird/default.nix @@ -0,0 +1,43 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.emailclient.default; + desktop = "thunderbird.desktop"; + mimeTypes = [ + "message/rfc822" + "x-scheme-handler/mailto" + "text/calendar" + "text/x-vcard" + ]; + associations = + let + genMimeAssociations = import ../genMimeAssociations.nix; + in + genMimeAssociations desktop mimeTypes; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "thunderbird") { + home.packages = [ pkgs.thunderbird ]; + + # programs.thunderbird = { + # enable = true; + # profiles.default = { + # isDefault = mkDefault true; + # withExternalGnupg = mkDefault true; + # }; + # }; + + xdg.mimeApps = { + defaultApplications = associations; + associations.added = associations; + }; + }; +} diff --git a/modules/home/hyprland/applications/yazi/default.nix b/modules/home/hyprland/applications/yazi/default.nix new file mode 100644 index 0000000..2b9f10d --- /dev/null +++ b/modules/home/hyprland/applications/yazi/default.nix @@ -0,0 +1,20 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.filemanager.default; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "yazi") { + programs.yazi = { + enable = true; + enableZshIntegration = config.programs.zsh.enable; + }; + }; +} diff --git a/modules/home/hyprland/applications/zathura/default.nix b/modules/home/hyprland/applications/zathura/default.nix new file mode 100644 index 0000000..7d23639 --- /dev/null +++ b/modules/home/hyprland/applications/zathura/default.nix @@ -0,0 +1,33 @@ +{ config, lib, ... }: + +let + cfg = config.wayland.windowManager.hyprland; + app = cfg.applications.pdfviewer.default; + desktop = "org.pwmt.zathura.desktop"; + mimeTypes = [ + "application/pdf" + ]; + associations = + let + genMimeAssociations = import ../genMimeAssociations.nix; + in + genMimeAssociations desktop mimeTypes; + + inherit (lib) mkIf; +in +{ + config = mkIf (cfg.enable && app == "zathura") { + programs.zathura = { + enable = true; + options = { + guiptions = "none"; + selection-clipboard = "clipboard"; + }; + }; + + xdg.mimeApps = { + defaultApplications = associations; + associations.added = associations; + }; + }; +} diff --git a/modules/home/hyprland/binds/default.nix b/modules/home/hyprland/binds/default.nix new file mode 100644 index 0000000..47be7ee --- /dev/null +++ b/modules/home/hyprland/binds/default.nix @@ -0,0 +1,34 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + + hyprland = import ./hyprland.nix; + mediakeys = import ./mediakeys.nix { inherit pkgs; }; + windows = import ./windows.nix; + workspaces = import ./workspaces.nix; + + binds = builtins.concatLists [ + hyprland + mediakeys + windows + workspaces + ]; + + inherit (lib) mkIf; +in +{ + config = mkIf cfg.enable { + wayland.windowManager.hyprland = { + settings = { + bind = binds; + bindm = (import ./mouse.nix); + }; + }; + }; +} diff --git a/modules/home/hyprland/binds/hyprland.nix b/modules/home/hyprland/binds/hyprland.nix new file mode 100644 index 0000000..552d8e3 --- /dev/null +++ b/modules/home/hyprland/binds/hyprland.nix @@ -0,0 +1,3 @@ +[ + # "$mod CTRL, b, exec, killall -SIGUSR1 waybar" # toggle bar # FIXME: waybar: no process found +] diff --git a/modules/home/hyprland/binds/mediakeys.nix b/modules/home/hyprland/binds/mediakeys.nix new file mode 100644 index 0000000..75eaa91 --- /dev/null +++ b/modules/home/hyprland/binds/mediakeys.nix @@ -0,0 +1,22 @@ +{ pkgs, ... }: + +let + brightness = "${pkgs.brightnessctl}/bin/brightnessctl"; + player = "${pkgs.playerctl}/bin/playerctl"; + volume = "${pkgs.pulseaudio}/bin/pactl"; +in +[ + ", XF86MonBrightnessUp, exec, ${brightness} s +5%" # increase screen brightness + ", XF86MonBrightnessDown, exec, ${brightness} s 5%-" # decrease screen brightness + ", XF86AudioRaiseVolume, exec, ${volume} set-sink-volume 0 +5%" # raise speaker volume + ", XF86AudioLowerVolume, exec, ${volume} set-sink-volume 0 -5%" # lower speaker volume + "SHIFT, XF86AudioRaiseVolume, exec, ${volume} set-source-volume 0 +1%" # raise mic volume + "SHIFT, XF86AudioLowerVolume, exec, ${volume} set-source-volume 0 -1%" # lower mic volume + ", XF86AudioMute, exec, ${volume} set-sink-mute 0 toggle" # mute/unmute speaker + "SHIFT, XF86AudioMute, exec, ${volume} set-source-mute 0 toggle" # mute/unmute mic + ", XF86AudioMicMute, exec, ${volume} set-source-mute 0 toggle" # mute/unmute mic + ", XF86AudioPlay, exec, ${player} play-pause" # toggle between play and pause music + ", XF86AudioStop, exec, ${player} stop" # stop music + ", XF86AudioPrev, exec, ${player} previous" # play previous + ", XF86AudioNext, exec, ${player} next" # play next +] diff --git a/modules/home/hyprland/binds/mouse.nix b/modules/home/hyprland/binds/mouse.nix new file mode 100644 index 0000000..6b224dd --- /dev/null +++ b/modules/home/hyprland/binds/mouse.nix @@ -0,0 +1,4 @@ +[ + "$mod, mouse:272, movewindow" # left mouse button + "$mod, mouse:273, resizewindow" # right mouse button +] diff --git a/modules/home/hyprland/binds/windows.nix b/modules/home/hyprland/binds/windows.nix new file mode 100644 index 0000000..74d7ee2 --- /dev/null +++ b/modules/home/hyprland/binds/windows.nix @@ -0,0 +1,27 @@ +[ + "$mod SHIFT, c, killactive" # kill active window + "$mod SHIFT, Return, layoutmsg, swapwithmaster master" # make active window master + "$mod CTRL, Return, layoutmsg, focusmaster" # focus master + "$mod, j, layoutmsg, cyclenext" # focus next window + "$mod, k, layoutmsg, cycleprev" # focus previous window + "$mod SHIFT, j, layoutmsg, swapnext" # swaps window with next + "$mod SHIFT, k, layoutmsg, swapprev" # swaps window with previous + "$mod, h, splitratio, -0.05" # decrease horizontal space of master stack + "$mod, l, splitratio, +0.05" # increase horizontal space of master stack + "$mod SHIFT, h, resizeactive, 0 -40" # shrink active window vertically + "$mod SHIFT, l, resizeactive, 0 40" # expand active window vertically + "$mod, i, layoutmsg, addmaster" # add active window to master stack + "$mod SHIFT, i, layoutmsg, removemaster" # remove active window from master stack + "$mod, o, layoutmsg, orientationcycle left top" # toggle between left and top orientation + "$mod, left, movefocus, l" # focus window to the left + "$mod, right, movefocus, r" # focus window to the right + "$mod, up, movefocus, u" # focus upper window + "$mod, down, movefocus, d" # focus lower window + "$mod SHIFT, left, swapwindow, l" # swap active window with window to the left + "$mod SHIFT, right, swapwindow, r" # swap active window with window to the right + "$mod SHIFT, up, swapwindow, u" # swap active window with upper window + "$mod SHIFT, down, swapwindow, d" # swap active window with lower window + "$mod, f, togglefloating" # toggle floating for active window + "$mod CTRL, f, workspaceopt, allfloat" # toggle floating for all windows on workspace + "$mod SHIFT, f, fullscreen" # toggle fullscreen for active window +] diff --git a/modules/home/hyprland/binds/workspaces.nix b/modules/home/hyprland/binds/workspaces.nix new file mode 100644 index 0000000..da6dbf0 --- /dev/null +++ b/modules/home/hyprland/binds/workspaces.nix @@ -0,0 +1,38 @@ +[ + "$mod, 1, workspace, 1" # go to workspace 1 + "$mod, 2, workspace, 2" # ... + "$mod, 3, workspace, 3" + "$mod, 4, workspace, 4" + "$mod, 5, workspace, 5" + "$mod, 6, workspace, 6" + "$mod, 7, workspace, 7" + "$mod, 8, workspace, 8" + "$mod, 9, workspace, 9" + "$mod, 0, workspace, 10" + "$mod SHIFT, 1, movetoworkspacesilent, 1" # move active window to workspace 1 + "$mod SHIFT, 2, movetoworkspacesilent, 2" # ... + "$mod SHIFT, 3, movetoworkspacesilent, 3" + "$mod SHIFT, 4, movetoworkspacesilent, 4" + "$mod SHIFT, 5, movetoworkspacesilent, 5" + "$mod SHIFT, 6, movetoworkspacesilent, 6" + "$mod SHIFT, 7, movetoworkspacesilent, 7" + "$mod SHIFT, 8, movetoworkspacesilent, 8" + "$mod SHIFT, 9, movetoworkspacesilent, 9" + "$mod SHIFT, 0, movetoworkspacesilent, 10" + "$mod CTRL, 1, focusworkspaceoncurrentmonitor, 1" # focus workspace 1 on current monitor + "$mod CTRL, 2, focusworkspaceoncurrentmonitor, 2" # ... + "$mod CTRL, 3, focusworkspaceoncurrentmonitor, 3" + "$mod CTRL, 4, focusworkspaceoncurrentmonitor, 4" + "$mod CTRL, 5, focusworkspaceoncurrentmonitor, 5" + "$mod CTRL, 6, focusworkspaceoncurrentmonitor, 6" + "$mod CTRL, 7, focusworkspaceoncurrentmonitor, 7" + "$mod CTRL, 8, focusworkspaceoncurrentmonitor, 8" + "$mod CTRL, 9, focusworkspaceoncurrentmonitor, 9" + "$mod CTRL, 0, focusworkspaceoncurrentmonitor, 10" + "$mod, Tab, workspace, previous_per_monitor" # go to previous workspace + "$mod SHIFT, Tab, movetoworkspace, previous_per_monitor" # move active window to previous workspace + "$mod, comma, focusmonitor, l" # go to left monitor + "$mod, period, focusmonitor, r" # go to right monitor + "$mod SHIFT, comma, movecurrentworkspacetomonitor, l" # move current workspace to left monitor + "$mod SHIFT, period, movecurrentworkspacetomonitor, r" # move current workspace to right monitor +] diff --git a/modules/home/hyprland/chromium.nix b/modules/home/hyprland/chromium.nix new file mode 100644 index 0000000..9a334b0 --- /dev/null +++ b/modules/home/hyprland/chromium.nix @@ -0,0 +1,46 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + desktop = "chromium-browser.desktop"; + + inherit (lib) + mkDefault + mkIf + ; +in +{ + config = mkIf cfg.enable { + programs.chromium = { + enable = mkDefault true; + package = mkDefault pkgs.ungoogled-chromium; + }; + + # Never open chromium by default + xdg.mimeApps.associations.removed = mkIf config.programs.chromium.enable { + "application/pdf" = desktop; + "application/rdf+xml" = desktop; + "application/rss+xml" = desktop; + "application/xhtml+xml" = desktop; + "application/xhtml_xml" = desktop; + "application/xml" = desktop; + "image/gif" = desktop; + "image/jpeg" = desktop; + "image/png" = desktop; + "image/webp" = desktop; + "text/html" = desktop; + "text/xml" = desktop; + "x-scheme-handler/http" = desktop; + "x-scheme-handler/https" = desktop; + "x-scheme-handler/webcal" = desktop; + "x-scheme-handler/mailto" = desktop; + "x-scheme-handler/about" = desktop; + "x-scheme-handler/unknown" = desktop; + }; + }; +} diff --git a/modules/home/hyprland/cursor.nix b/modules/home/hyprland/cursor.nix new file mode 100644 index 0000000..4a9b55c --- /dev/null +++ b/modules/home/hyprland/cursor.nix @@ -0,0 +1,26 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) mkForce; +in +{ + home.pointerCursor = { + name = mkForce "Bibata-Original-Ice"; + size = mkForce 24; + package = mkForce pkgs.bibata-cursors; + }; + + home.packages = [ pkgs.hyprcursor ]; + + home.sessionVariables = { + HYPRCURSOR_THEME = config.home.pointerCursor.name; + HYPRCURSOR_SIZE = toString config.home.pointerCursor.size; + }; + + # wayland.windowManager.hyprland.cursor.no_hardware_cursors = true; +} diff --git a/modules/home/hyprland/default.nix b/modules/home/hyprland/default.nix new file mode 100644 index 0000000..cc6b91e --- /dev/null +++ b/modules/home/hyprland/default.nix @@ -0,0 +1,104 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + + inherit (lib) + mkDefault + mkOption + mkIf + types + ; +in +{ + imports = [ + ../waybar + + ./applications + ./binds + ./chromium.nix + ./cursor.nix + ./hyprlock.nix + ./xdg + ]; + + options.wayland.windowManager.hyprland = { + autostart = mkOption { + type = types.bool; + default = false; + description = "Whether to automatically start Hyprland after login. Only supported for ZSH."; + }; + modifier = mkOption { + type = types.str; + default = "SUPER"; + description = "The modifier key to use."; + }; + }; + + config = { + wayland.windowManager.hyprland = { + systemd = { + enable = mkDefault true; + enableXdgAutostart = mkDefault false; + variables = mkDefault [ "--all" ]; + # extraCommands = [ # fix for dunst + # "${pkgs.dbus}/bin/dbus-update-activation-environment WAYLAND_DISPLAY" + # "systemctl --user restart dunst.service" + # ]; + }; + xwayland.enable = mkDefault true; + settings = import ./settings.nix { inherit config lib pkgs; }; + }; + + # Set some environment variables that hopefully fix some Wayland stuff + home.sessionVariables = { + CLUTTER_BACKEND = mkDefault "wayland"; + GDK_BACKEND = mkDefault "wayland"; + MOZ_ENABLE_WAYLAND = mkDefault 1; + NIXOS_OZONE_WL = mkDefault 1; + QT_AUTO_SCREEN_SCALE_FACTOR = mkDefault 1; + QT_QPA_PLATFORM = mkDefault "wayland"; + QT_WAYLAND_DISABLE_WINDOWDECORATION = mkDefault 1; + WAYLAND_DISPLAY = mkDefault "wayland-1"; + XDG_CURRENT_DESKTOP = mkDefault "Hyprland"; + XDG_SESSION_DESKTOP = mkDefault "Hyprland"; + XDG_SESSION_TYPE = mkDefault "wayland"; + + # WLR_RENDERER_ALLOW_SOFTWARE = mkDefault 1; # TODO: For VMs only? + }; + + # waybar + programs.waybar.enable = mkIf cfg.enable (mkDefault true); + + # auto discover fonts in `home.packages` + fonts.fontconfig.enable = true; + + # notifications + services.dunst = { + enable = mkDefault true; + waylandDisplay = config.home.sessionVariables.WAYLAND_DISPLAY; + }; + + # install some applications + home.packages = import ./packages.nix { inherit pkgs; }; # use programs.PACKAGE or services.SERVICE when possible + + # autostart + programs.zsh.loginExtra = mkIf cfg.autostart '' + if [ -z "$DISPLAY" ] && [ "$TTY" = "/dev/tty1" ]; then + exec Hyprland + fi + ''; + + services.udiskie = { + enable = mkDefault true; + tray = mkDefault "never"; + }; + + services.network-manager-applet.enable = mkDefault true; + }; +} diff --git a/modules/home/hyprland/hyprlock.nix b/modules/home/hyprland/hyprlock.nix new file mode 100644 index 0000000..9ddbb07 --- /dev/null +++ b/modules/home/hyprland/hyprlock.nix @@ -0,0 +1,38 @@ +{ config, lib, ... }: + +let + cfg = config.wayland.windowManager.hyprland; + + inherit (lib) mkIf; +in +{ + programs.hyprlock = mkIf cfg.enable { + enable = true; + settings = { + general = { + disable_loading_bar = true; + grace = 0; + hide_cursor = true; + no_fade_in = true; + }; + + animations.enabled = false; + + label = { + text = "$TIME"; + font_size = 100; + position = "0, 50"; + halign = "center"; + valign = "center"; + }; + + input-field = { + size = "200, 50"; + position = "0, -80"; + dots_center = true; + fade_on_empty = false; + outline_thickness = 3; + }; + }; + }; +} diff --git a/modules/home/hyprland/packages.nix b/modules/home/hyprland/packages.nix new file mode 100644 index 0000000..0beb5fb --- /dev/null +++ b/modules/home/hyprland/packages.nix @@ -0,0 +1,16 @@ +{ pkgs, ... }: + +# use `config.programs` or `config.services` when available + +with pkgs; +[ + file + grim + helvum + libnotify + slurp + udiskie + udisks + wev + wl-clipboard +] diff --git a/modules/home/hyprland/settings.nix b/modules/home/hyprland/settings.nix new file mode 100644 index 0000000..2c61471 --- /dev/null +++ b/modules/home/hyprland/settings.nix @@ -0,0 +1,56 @@ +{ config, lib, ... }: + +let + cfg = config.wayland.windowManager.hyprland; + + inherit (builtins) toString; + inherit (lib) mkDefault; +in +{ + # Do not add binds here. Use `./binds/default.nix` instead. + "$mod" = cfg.modifier; + + windowrule = [ + "center, floating:1, not class:^(Gimp)$, not class:^(steam)$" + "float, title:^(Open|Save) Files?$" + "noborder, onworkspace:w[t1]" + "bordersize ${toString cfg.settings.general.border_size}, floating:1" + + # https://wiki.hyprland.org/Useful-Utilities/Screen-Sharing/#xwayland + "opacity 0.0 override, class:^(xwaylandvideobridge)$" + "noanim, class:^(xwaylandvideobridge)$" + "noinitialfocus, class:^(xwaylandvideobridge)$" + "maxsize 1 1, class:^(xwaylandvideobridge)$" + "noblur, class:^(xwaylandvideobridge)$" + ]; + + # Layouts + general.layout = mkDefault "master"; + master = { + mfact = mkDefault 0.5; + new_status = mkDefault "master"; + new_on_top = mkDefault true; + }; + + input.kb_layout = mkDefault "de"; + + xwayland = { + force_zero_scaling = mkDefault true; + }; + + misc = { + disable_hyprland_logo = mkDefault true; + force_default_wallpaper = mkDefault 0; + }; + + # Styling + animations.enabled = mkDefault false; + decoration = { + blur.enabled = mkDefault false; + shadow.enabled = mkDefault false; + }; + general = { + resize_on_border = mkDefault true; + border_size = mkDefault 2; + }; +} diff --git a/modules/home/hyprland/xdg/default.nix b/modules/home/hyprland/xdg/default.nix new file mode 100644 index 0000000..ce49803 --- /dev/null +++ b/modules/home/hyprland/xdg/default.nix @@ -0,0 +1,27 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.wayland.windowManager.hyprland; + portal = pkgs.xdg-desktop-portal-hyprland; + + inherit (lib) mkDefault mkIf; +in +{ + config.xdg = mkIf cfg.enable { + enable = mkDefault true; + mime.enable = mkDefault true; + mimeApps.enable = mkDefault true; + userDirs = { + enable = mkDefault true; + createDirectories = mkDefault true; + }; + portal.enable = mkDefault true; + portal.extraPortals = [ portal ]; + portal.configPackages = [ portal ]; + }; +} diff --git a/modules/home/kitty/default.nix b/modules/home/kitty/default.nix new file mode 100644 index 0000000..6032894 --- /dev/null +++ b/modules/home/kitty/default.nix @@ -0,0 +1,22 @@ +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + programs.kitty = { + settings = { + confirm_os_window_close = mkDefault 0; + enable_audio_bell = mkDefault "no"; + window_margin_width = mkDefault 5; + }; + }; + + home.sessionVariables = { + TERMINAL = "kitty"; + }; + + home.shellAliases = { + s = "kitten ssh"; # copy terminfo + }; +} diff --git a/modules/home/librewolf/default.nix b/modules/home/librewolf/default.nix new file mode 100644 index 0000000..97dd43f --- /dev/null +++ b/modules/home/librewolf/default.nix @@ -0,0 +1,51 @@ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.librewolf; + + inherit (lib) mkDefault mkIf; +in +{ + imports = [ ./search ]; + + config = { + programs.librewolf = { + policies.Homepage.StartPage = mkDefault "previous-session"; + profiles.default = { + extensions.packages = import ./extensions.nix { inherit inputs pkgs; }; + settings = import ./settings.nix; + search = { + force = true; + default = "Startpage"; + privateDefault = "Startpage"; + order = [ "Startpage" ]; + engines = { + Startpage = { + urls = [ { template = "https://www.startpage.com/do/dsearch?q={searchTerms}"; } ]; + icon = "https://www.startpage.com/sp/cdn/favicons/favicon--default.ico"; + updateInterval = 24 * 60 * 60 * 1000; # every day + }; + # engines below are disabled + bing.metaData.hidden = true; + ddg.metaData.hidden = true; + google.metaData.hidden = true; + }; + }; + }; + }; + + home.sessionVariables = mkIf cfg.enable ( + with cfg; + { + DEFAULT_BROWSER = "${package}/bin/librewolf"; + BROWSER = "${package}/bin/librewolf"; + } + ); + }; +} diff --git a/modules/home/librewolf/extensions.nix b/modules/home/librewolf/extensions.nix new file mode 100644 index 0000000..e1d95d1 --- /dev/null +++ b/modules/home/librewolf/extensions.nix @@ -0,0 +1,9 @@ +{ inputs, pkgs, ... }: + +with inputs.nur.legacyPackages."${pkgs.stdenv.hostPlatform.system}".repos.rycee.firefox-addons; + +[ + darkreader + floccus + ublock-origin +] diff --git a/modules/home/librewolf/search/default.nix b/modules/home/librewolf/search/default.nix new file mode 100644 index 0000000..74e1d8c --- /dev/null +++ b/modules/home/librewolf/search/default.nix @@ -0,0 +1,88 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.librewolf; + engines = import ./engines.nix pkgs; + + urlRegex = "^(http|https|ftp)://"; + isUrl = s: (match urlRegex s) != null; + + transformEngine = + engine: + let + every_day = 24 * 60 * 60 * 1000; + in + { + urls = [ { template = engine.url; } ]; + icon = engine.icon; + updateInterval = if (isUrl engine.icon) then every_day else null; + definedAliases = optional (engine ? alias) engine.alias; + }; + + transformedEngines = mapAttrs' (name: engine: { + name = name; + value = transformEngine engine; + }) engines; + + inherit (lib) + listToAttrs + mapAttrs + mapAttrs' + mkOption + optional + types + ; + inherit (lib.strings) match; +in +{ + options.programs.librewolf = { + searchEngines = mkOption { + type = types.listOf types.str; + default = [ + "github" + "home-manager-options" + "nixos-options" + "nixos-wiki" + "nixpkgs" + "nixpkgs-issues" + "noogle" + "nuschtos" + "wikiless" + "youtube" + ]; + example = [ + "github" + "home-manager-options" + "howlongtobeat" + "keyforsteam" + "nixos-options" + "nixos-wiki" + "nixpkgs" + "nixpkgs-issues" + "noogle" + "nuschtos" + "protondb" + "steamdb" + "wikiless" + "youtube" + ]; + description = "Additional search engines for LibreWolf."; + }; + }; + + config.programs.librewolf = { + profiles.default.search.engines = mapAttrs (_: name: transformedEngines.${name}) ( + listToAttrs ( + map (name: { + name = name; + value = name; + }) cfg.searchEngines + ) + ); + }; +} diff --git a/modules/home/librewolf/search/engines.nix b/modules/home/librewolf/search/engines.nix new file mode 100644 index 0000000..4190bb6 --- /dev/null +++ b/modules/home/librewolf/search/engines.nix @@ -0,0 +1,87 @@ +pkgs: + +{ + github = { + url = "https://github.com/search?q={searchTerms}"; + icon = "https://github.com/favicon.ico"; + alias = "@gh"; + }; + + home-manager-options = { + url = "https://home-manager-options.extranix.com/?query={searchTerms}&release=release-25.11"; + icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; + alias = "@hm"; + }; + + nixpkgs = { + url = "https://search.nixos.org/packages?channel=25.11&query={searchTerms}"; + icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; + alias = "@np"; + }; + + nixos-options = { + url = "https://search.nixos.org/options?channel=25.11&query={searchTerms}"; + icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; + alias = "@no"; + }; + + nixos-wiki = { + url = "https://wiki.nixos.org/w/index.php?search={searchTerms}"; + icon = "https://wiki.nixos.org/favicon.png"; + alias = "@nw"; + }; + + noogle = { + url = "https://noogle.dev/q?term={searchTerms}"; + icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; + alias = "@nf"; + }; + + nuschtos = { + url = "https://search.xn--nschtos-n2a.de/?query={searchTerms}"; + icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; + alias = "@nu"; + }; + + nixpkgs-issues = { + url = "https://github.com/NixOS/nixpkgs/issues?q=is%3Aissue%20state%3Aopen%20{searchTerms}"; + icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; + alias = "@ni"; + }; + + wikiless = { + url = "https://wikiless.metastem.su/wiki/{searchTerms}"; + icon = "https://wikiless.metastem.su/wikiless-favicon.ico"; + alias = "@wiki"; + }; + + youtube = { + url = "https://www.youtube.com/results?search_query={searchTerms}"; + icon = "https://www.youtube.com/favicon.ico"; + alias = "@yt"; + }; + + protondb = { + url = "https://www.protondb.com/search?q={searchTerms}"; + icon = "https://www.protondb.com/favicon.ico"; + alias = "@pdb"; + }; + + steamdb = { + url = "https://steamdb.info/search/?a=all&q={searchTerms}"; + icon = "https://steamdb.info/favicon.ico"; + alias = "@stdb"; + }; + + keyforsteam = { + url = "https://www.keyforsteam.de/katalog/?search_name={searchTerms}"; + icon = "https://www.keyforsteam.de/favicon.ico"; + alias = "@k4s"; + }; + + howlongtobeat = { + url = "https://howlongtobeat.com/?q={searchTerms}"; + icon = "https://howlongtobeat.com/favicon.ico"; + alias = "@hltb"; + }; +} diff --git a/modules/home/librewolf/settings.nix b/modules/home/librewolf/settings.nix new file mode 100644 index 0000000..f8390fb --- /dev/null +++ b/modules/home/librewolf/settings.nix @@ -0,0 +1,20 @@ +{ + "browser.cache.disk.enable" = false; + "browser.cache.disk_cache_ssl" = false; + "browser.cache.insecure.enable" = false; + "browser.cache.memory.enable" = false; + "browser.cache.offline.enable" = false; + "browser.newtabpage.enabled" = false; + "browser.startup.homepage" = "chrome://browser/content/blanktab.html"; + "browser.toolbars.bookmarks.visibility" = "never"; + "browser.urlbar.shortcuts.history" = false; + "browser.urlbar.shortcuts.tabs" = false; + "browser.urlbar.suggest.history" = false; + "browser.urlbar.suggest.openpage" = false; + "browser.urlbar.suggest.recentsearches" = false; + "places.history.enabled" = false; + "plugin.scan.plid.all" = false; + "privacy.donottrackheader.enabled" = true; + "sidebar.revmap" = true; + "sidebar.verticalTabs" = true; +} diff --git a/modules/home/networkmanager-dmenu/default.nix b/modules/home/networkmanager-dmenu/default.nix new file mode 100644 index 0000000..41a9ea9 --- /dev/null +++ b/modules/home/networkmanager-dmenu/default.nix @@ -0,0 +1,55 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.networkmanager-dmenu; + + inherit (lib) + mkEnableOption + mkIf + mkOption + types + ; +in +{ + options.programs.networkmanager-dmenu = { + enable = mkEnableOption "networkmanager-dmenu."; + + package = mkOption { + type = types.package; + default = pkgs.networkmanager_dmenu; + description = "The package to use for networkmanager-dmenu."; + }; + + config = { + dmenu = { + dmenu_command = mkOption { + type = types.str; + default = "dmenu"; + description = "Command for dmenu, can include arguments."; + }; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ + cfg.package + pkgs.modemmanager # for "Enable WWAN" + pkgs.networkmanager # for `nmcli` + pkgs.networkmanagerapplet # for "Launch Connection Manager" + + pkgs.networkmanager-openconnect # VPN support + pkgs.openconnect # VPN support + ]; + + xdg.configFile."networkmanager-dmenu/config.ini".text = '' + [dmenu] + dmenu_command=${cfg.config.dmenu.dmenu_command} + ''; + }; +} diff --git a/modules/home/nixvim/default.nix b/modules/home/nixvim/default.nix new file mode 100644 index 0000000..07ec4fb --- /dev/null +++ b/modules/home/nixvim/default.nix @@ -0,0 +1,104 @@ +{ + inputs, + config, + lib, + ... +}: + +let + cfg = config.programs.nixvim; + + inherit (lib) mkDefault mkIf; +in +{ + imports = [ + inputs.nixvim.homeModules.nixvim + ./plugins + + ./spellfiles.nix + ]; + + config = { + programs.nixvim = { + clipboard.providers.wl-copy.enable = mkDefault true; + defaultEditor = mkDefault true; + enableMan = mkDefault true; + viAlias = mkDefault true; + vimAlias = mkDefault true; + vimdiffAlias = mkDefault true; + + # vim.g.* + globals = { + mapleader = mkDefault " "; + }; + + # vim.opt.* + opts = { + # behavior + cursorline = mkDefault true; # highlights the line under the cursor + mouse = mkDefault "a"; # enable mouse support + nu = mkDefault true; # line numbers + relativenumber = mkDefault true; # relative line numbers + scrolloff = mkDefault 20; # keeps some context above/below cursor + signcolumn = mkDefault "yes"; # reserve space for signs (e.g., GitGutter) + undofile = mkDefault true; # persistent undo + updatetime = mkDefault 500; # ms to wait for trigger an event (default 4000ms) + wrap = mkDefault true; # wraps text if it exceeds the width of the window + + # search + ignorecase = mkDefault true; # ignore case in search patterns + smartcase = mkDefault true; # smart case + incsearch = mkDefault true; # incremental search + hlsearch = mkDefault true; # highlight search + + # windows + splitbelow = mkDefault true; # new windows are created below current + splitright = mkDefault true; # new windows are created to the right of current + equalalways = mkDefault true; # window sizes are automatically updated. + + # tabs + expandtab = mkDefault true; # convert tabs into spaces + shiftwidth = mkDefault 2; # number of spaces to use for each step of (auto)indent + smartindent = mkDefault true; # smart autoindenting on new lines + softtabstop = mkDefault 2; # number of spaces in tab when editing + tabstop = mkDefault 2; # number of visual spaces per tab + + # spell checking + spell = mkDefault true; + spelllang = mkDefault [ + "en_us" + "de_20" + ]; + + }; + + # vim.diagnostic.config.* + diagnostic.settings = { + virtual_text = { + spacing = 4; + prefix = "●"; + severity_sort = true; + }; + signs = true; + underline = true; + update_in_insert = false; + }; + + extraConfigLua = '' + vim.cmd "set noshowmode" -- Hides "--INSERT--" mode indicator + ''; + + keymaps = import ./keymaps.nix; + }; + + home = { + sessionVariables = { + EDITOR = mkIf cfg.enable "nvim"; + VISUAL = mkIf cfg.enable "nvim"; + }; + shellAliases = { + v = mkIf cfg.enable "nvim"; + }; + }; + }; +} diff --git a/modules/home/nixvim/keymaps.nix b/modules/home/nixvim/keymaps.nix new file mode 100644 index 0000000..aaefaa0 --- /dev/null +++ b/modules/home/nixvim/keymaps.nix @@ -0,0 +1,347 @@ +[ + # cursor navigation + { + # scroll down, recenter + key = ""; + action = "zz"; + mode = "n"; + } + { + # scroll up, recenter + key = ""; + action = "zz"; + mode = "n"; + } + + # searching + { + # center cursor after search next + key = "n"; + action = "nzzzv"; + mode = "n"; + } + { + # center cursor after search previous + key = "N"; + action = "Nzzzv"; + mode = "n"; + } + { + # ex command + key = "pv"; + action = "Ex"; + mode = "n"; + } + + # search and replace + { + # search and replace word under cursor + key = "s"; + action = ":%s///gI"; + mode = "n"; + } + # search and replace selected text + { + key = "s"; + action = "y:%s/0/0/gI"; + mode = "v"; + } + + # clipboard operations + { + # copy to system clipboard in visual mode + key = ""; + action = ''"+y ''; + mode = "v"; + } + { + # paste from system clipboard in visual mode + key = ""; + action = ''"+p ''; + mode = "v"; + } + { + # yank to system clipboard + key = "Y"; + action = "+Y"; + mode = "n"; + } + { + # replace selected text with clipboard content + key = "p"; + action = "_dP"; + mode = "x"; + } + { + # delete without copying to clipboard + key = "d"; + action = "_d"; + mode = [ + "n" + "v" + ]; + } + + # line operations + { + # move lines down in visual mode + key = "J"; + action = ":m '>+1gv=gv"; + mode = "v"; + } + { + # move lines up in visual mode + key = "K"; + action = ":m '<-2gv=gv"; + mode = "v"; + } + { + # join lines + key = "J"; + action = "mzJ`z"; + mode = "n"; + } + + # quickfix + { + # Run make command + key = "m"; + action = ":make"; + mode = "n"; + } + { + # previous quickfix item + key = ""; + action = "cprevzz"; + mode = "n"; + } + { + # next quickfix item + key = ""; + action = "cnextzz"; + mode = "n"; + } + + # location list navigation + { + # previous location list item + key = "j"; + action = "lprevzz"; + mode = "n"; + } + { + # next location list item + key = "k"; + action = "lnextzz"; + mode = "n"; + } + + # disabling keys + { + # disable the 'Q' key + key = "Q"; + action = ""; + mode = "n"; + } + + # text selection + { + # select whole buffer + key = ""; + action = "ggVG"; + mode = "n"; + } + + # window operations + { + # focus next window + key = ""; + action = ":wincmd W"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # focus previous window + key = ""; + action = ":wincmd w"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # window size adjustments + { + # increase window width + key = ""; + action = ":vertical resize +5"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # decrease window width + key = ""; + action = ":vertical resize -5"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # window closing and opening + { + # close current window + key = "c"; + action = ":q"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # new vertical split at $HOME + key = "n"; + action = ":vsp $HOME"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # window split orientation toggling + { + # toggle split orientation + key = "t"; + action = ":wincmd T"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # spell checking + { + # toggle spell checking + key = "ss"; + action = ":setlocal spell!"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # switch to english spell checking + key = "se"; + action = ":setlocal spelllang=en_us"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # switch to german spell checking + key = "sg"; + action = ":setlocal spelllang=de_20"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # move to next misspelling + key = "]s"; + action = "]szz"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # move to previous misspelling + key = "[s"; + action = "[szz"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # correction suggestions for a misspelled word + key = "z="; + action = "z="; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # adding words to the dictionary + key = "zg"; + action = "zg"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + # buffer navigation + { + # next buffer + key = ""; + action = ":bnext"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # previous buffer + key = ""; + action = ":bprevious"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + { + # close current buffer + key = "bd"; + action = ":bdelete"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } + + { + # apply code action + key = "ca"; + action = ":lua vim.lsp.buf.code_action()"; + options = { + noremap = true; + silent = true; + }; + mode = "n"; + } +] diff --git a/modules/home/nixvim/plugins/cmp.nix b/modules/home/nixvim/plugins/cmp.nix new file mode 100644 index 0000000..b93fcb6 --- /dev/null +++ b/modules/home/nixvim/plugins/cmp.nix @@ -0,0 +1,54 @@ +{ config, lib, ... }: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.cmp; + + inherit (lib) mkDefault mkIf; +in +{ + programs.nixvim = { + plugins = { + cmp = { + enable = mkDefault true; + settings = { + autoEnableSources = mkDefault true; + experimental.ghost_text = mkDefault true; + snippet.expand = mkDefault "luasnip"; + formatting.fields = mkDefault [ + "kind" + "abbr" + "menu" + ]; + sources = [ + { name = "git"; } + { name = "nvim_lsp"; } + { + name = "buffer"; + option.get_bufnrs.__raw = "vim.api.nvim_list_bufs"; + keywordLength = 3; + } + { + name = "path"; + keywordLength = 3; + } + { name = "luasnip"; } + ]; + mapping = { + "" = "cmp.mapping.complete()"; + "" = "cmp.mapping.scroll_docs(-4)"; + "" = "cmp.mapping.close()"; + "" = "cmp.mapping.scroll_docs(4)"; + "" = "cmp.mapping.confirm({ select = true })"; + "" = "cmp.mapping(cmp.mapping.select_prev_item(), {'i', 's'})"; + "" = "cmp.mapping(cmp.mapping.select_next_item(), {'i', 's'})"; + }; + }; + }; + cmp-cmdline = mkIf plugin.enable { enable = mkDefault false; }; # autocomplete for cmdline + cmp_luasnip = mkIf plugin.enable { enable = mkDefault true; }; + luasnip = mkIf plugin.enable { enable = mkDefault true; }; + cmp-treesitter = mkIf (plugin.enable && cfg.plugins.treesitter.enable) { enable = mkDefault true; }; + }; + }; +} diff --git a/modules/home/nixvim/plugins/default.nix b/modules/home/nixvim/plugins/default.nix new file mode 100644 index 0000000..1941f43 --- /dev/null +++ b/modules/home/nixvim/plugins/default.nix @@ -0,0 +1,18 @@ +{ lib, ... }: + +{ + imports = [ + ./cmp.nix + ./lsp.nix + ./lualine.nix + ./telescope.nix + # ./treesitter.nix # HOTFIX: does not build + ./trouble.nix + ]; + + config.programs.nixvim.plugins = { + markdown-preview.enable = lib.mkDefault true; + # warning: Nixvim: `plugins.web-devicons` was enabled automatically because the following plugins are enabled. This behaviour is deprecated. Please explicitly define `plugins.web-devicons.enable` + web-devicons.enable = true; + }; +} diff --git a/modules/home/nixvim/plugins/lsp.nix b/modules/home/nixvim/plugins/lsp.nix new file mode 100644 index 0000000..b3c70a8 --- /dev/null +++ b/modules/home/nixvim/plugins/lsp.nix @@ -0,0 +1,69 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.lsp; + + inherit (lib) mkDefault mkIf optional; +in +{ + config = { + programs.nixvim = { + plugins = { + lsp-format = mkIf plugin.enable { enable = mkDefault true; }; + + lsp = { + enable = mkDefault true; + postConfig = ""; + keymaps = { + silent = mkDefault true; + diagnostic = mkDefault { + # Navigate in diagnostics + "k" = "goto_prev"; + "j" = "goto_next"; + }; + + lspBuf = mkDefault { + gd = "definition"; + gD = "references"; + gt = "type_definition"; + gi = "implementation"; + K = "hover"; + "" = "rename"; + }; + }; + + servers = { + bashls.enable = mkDefault true; + clangd.enable = mkDefault true; + cssls.enable = mkDefault true; + dockerls.enable = mkDefault true; + gopls.enable = mkDefault true; + html.enable = mkDefault true; + jsonls.enable = mkDefault true; + nixd.enable = mkDefault true; + pyright.enable = mkDefault true; + rust_analyzer = { + enable = mkDefault true; + installCargo = mkDefault true; + installRustc = mkDefault true; + settings.rustfmt.overrideCommand = mkDefault [ + "${pkgs.rustfmt}/bin/rustfmt --edition 2021" # --config tab_spaces=2" + ]; + }; + texlab.enable = mkDefault true; + vhdl_ls.enable = mkDefault true; + yamlls.enable = mkDefault true; + }; + }; + }; + }; + + home.packages = optional (cfg.enable && plugin.servers.nixd.enable) pkgs.nixfmt; + }; +} diff --git a/modules/home/nixvim/plugins/lualine.nix b/modules/home/nixvim/plugins/lualine.nix new file mode 100644 index 0000000..72d2238 --- /dev/null +++ b/modules/home/nixvim/plugins/lualine.nix @@ -0,0 +1,18 @@ +{ config, lib, ... }: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.lualine; + + inherit (lib) mkDefault; +in +{ + config = { + programs.nixvim = { + plugins.lualine = { + enable = mkDefault true; + settings.options.icons_enabled = mkDefault false; + }; + }; + }; +} diff --git a/modules/home/nixvim/plugins/telescope.nix b/modules/home/nixvim/plugins/telescope.nix new file mode 100644 index 0000000..6e1dabc --- /dev/null +++ b/modules/home/nixvim/plugins/telescope.nix @@ -0,0 +1,52 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.telescope; + + inherit (lib) mkDefault optionals; +in +{ + config = { + programs.nixvim = { + plugins.telescope = { + enable = mkDefault true; + extensions = { + file-browser.enable = mkDefault true; + fzf-native.enable = mkDefault true; + live-grep-args.enable = mkDefault true; + manix.enable = mkDefault true; + }; + keymaps = mkDefault { + "" = "file_browser"; + "" = "git_files"; + "bl" = "buffers"; + "fd" = "diagnostics"; + "ff" = "find_files"; + "fg" = "live_grep"; + "fh" = "help_tags"; + "fm" = "man_pages"; + "fn" = "manix"; + "fo" = "oldfiles"; + "fb" = "file_browser"; + }; + }; + keymaps = optionals plugin.enable [ + { + key = ""; + action = ":lua require('telescope').extensions.live_grep_args.live_grep_args()"; + mode = "n"; + } + ]; + }; + + home.packages = optionals plugin.enable [ + pkgs.ripgrep # for "live_grep" + ]; + }; +} diff --git a/modules/home/nixvim/plugins/treesitter.nix b/modules/home/nixvim/plugins/treesitter.nix new file mode 100644 index 0000000..22b7040 --- /dev/null +++ b/modules/home/nixvim/plugins/treesitter.nix @@ -0,0 +1,42 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.treesitter; + + cc = "${pkgs.gcc}/bin/gcc"; + + inherit (lib) mkDefault mkIf; +in +{ + config = { + programs.nixvim = { + plugins.treesitter = { + enable = mkDefault true; + nixvimInjections = mkDefault true; + settings = { + folding.enable = mkDefault true; + highlight.enable = mkDefault true; + indent.enable = mkDefault true; + }; + }; + plugins.treesitter-context = mkIf plugin.enable { enable = mkDefault true; }; + plugins.treesitter-textobjects = mkIf plugin.enable { enable = mkDefault true; }; + }; + + # Fix for: ERROR `cc` executable not found. + home.sessionVariables = mkIf plugin.enable { + CC = mkDefault cc; + }; + + # Fix for: WARNING `tree-sitter` executable not found + home.packages = mkIf plugin.enable [ + plugin.package + ]; + }; +} diff --git a/modules/home/nixvim/plugins/trouble.nix b/modules/home/nixvim/plugins/trouble.nix new file mode 100644 index 0000000..139576d --- /dev/null +++ b/modules/home/nixvim/plugins/trouble.nix @@ -0,0 +1,43 @@ +{ config, lib, ... }: + +let + cfg = config.programs.nixvim; + plugin = cfg.plugins.trouble; + + inherit (lib) mkDefault mkIf; +in +{ + config = { + programs.nixvim = { + plugins.trouble = { + enable = mkDefault true; + }; + keymaps = mkIf plugin.enable [ + { + mode = "n"; + key = "xq"; + action = "Trouble qflist toggle"; + options = { + desc = "Trouble quifick toggle"; + }; + } + { + mode = "n"; + key = "xl"; + action = "Trouble loclist toggle"; + options = { + desc = "Trouble loclist toggle"; + }; + } + { + mode = "n"; + key = "xx"; + action = "Trouble diagnostics toggle"; + options = { + desc = "Trouble diagnostics toggle"; + }; + } + ]; + }; + }; +} diff --git a/modules/home/nixvim/spellfiles.nix b/modules/home/nixvim/spellfiles.nix new file mode 100644 index 0000000..35b4a6e --- /dev/null +++ b/modules/home/nixvim/spellfiles.nix @@ -0,0 +1,26 @@ +{ config, pkgs, ... }: + +let + spellDir = config.xdg.dataHome + "/nvim/site/spell"; + baseUrl = "http://ftp.de.vim.org/runtime/spell"; +in +{ + home.file = { + de-spl = { + enable = true; + source = pkgs.fetchurl { + url = baseUrl + "/de.utf-8.spl"; + sha256 = "sha256-c8cQfqM5hWzb6SHeuSpFk5xN5uucByYdobndGfaDo9E="; + }; + target = spellDir + "/de.utf8.spl"; + }; + de-sug = { + enable = true; + source = pkgs.fetchurl { + url = baseUrl + "/de.utf-8.sug"; + sha256 = "sha256-E9Ds+Shj2J72DNSopesqWhOg6Pm6jRxqvkerqFcUqUg="; + }; + target = spellDir + "/de.utf8.sug"; + }; + }; +} diff --git a/modules/home/password-manager/default.nix b/modules/home/password-manager/default.nix new file mode 100644 index 0000000..4e0e4f9 --- /dev/null +++ b/modules/home/password-manager/default.nix @@ -0,0 +1,115 @@ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.passwordManager; + passmenuScript = pkgs.writeShellScriptBin "passmenu-bemenu" (builtins.readFile ./passmenu); # TODO: override original passmenu script coming from pass itself + passff-host = pkgs.passff-host; + + inherit (lib) + mkDefault + mkEnableOption + mkIf + mkOption + mkOverride + types + ; +in +{ + options.programs.passwordManager = { + enable = mkEnableOption "password manager using pass, passmenu, and passff."; + length = mkOption { + type = types.str; + default = "20"; + description = "Default length for generated passwords."; + }; + charset = mkOption { + type = types.enum [ + "alphanumerical" + "ascii" + "custom" + ]; + default = "alphanumerical"; + description = '' + Character set for generated passwords. "alphanumerical" and "ascii" will get overwritten by the corresponding charset. Anything else will get parsed as the charset directly. + ''; + }; + editor = mkOption { + type = types.str; + default = "nvim"; + description = "Editor to use for editing passwords. Make sure it is installed on your system."; + }; + wayland = mkOption { + type = types.bool; + default = false; + description = "If true, bemenu and ydotool will be used instead of dmenu and xdotool."; + }; + key = mkOption { + type = types.str; + default = ""; + description = "GPG key for password store."; + }; + }; + + config = mkIf cfg.enable { + programs.password-store = { + enable = true; + package = pkgs.pass.withExtensions (exts: [ exts.pass-otp ]); + settings = { + PASSWORD_STORE_DIR = mkDefault "${config.xdg.dataHome}/password-store"; + PASSWORD_STORE_KEY = mkIf (cfg.key != "") cfg.key; + PASSWORD_STORE_GENERATED_LENGTH = cfg.length; + PASSWORD_STORE_CHARACTER_SET = + if cfg.charset == "alphanumerical" then + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + else if cfg.charset == "ascii" then + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:',.<>/?" + else + cfg.charset; + PASSWORD_STORE_ENABLE_EXTENSIONS = "true"; + EDITOR = cfg.editor; + }; + }; + + services.gpg-agent.pinentry.package = mkOverride 1001 pkgs.pinentry-qt; # mkDefault collides with gpg home module + + home.packages = [ + passmenuScript + pkgs.zbar + ] + ++ ( + if cfg.wayland then + [ + pkgs.bemenu + pkgs.ydotool + ] + else + [ + pkgs.dmenu + pkgs.xdotool + ] + ); + + home.sessionVariables.PASSWORD_STORE_MENU = if cfg.wayland then "bemenu" else "dmenu"; + + # FIXME: passff does not autofill OTPs + programs.librewolf = mkIf config.programs.librewolf.enable { + package = pkgs.librewolf.override { + nativeMessagingHosts = [ + passff-host + ]; + hasMozSystemDirPatch = true; + }; + nativeMessagingHosts = [ passff-host ]; + profiles.default.extensions.packages = + with inputs.nur.legacyPackages."${pkgs.stdenv.hostPlatform.system}".repos.rycee.firefox-addons; [ + passff + ]; + }; + }; +} diff --git a/modules/home/password-manager/passmenu b/modules/home/password-manager/passmenu new file mode 100644 index 0000000..cd392a0 --- /dev/null +++ b/modules/home/password-manager/passmenu @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +shopt -s nullglob globstar + +typeit=0 +if [[ $1 == "--type" ]]; then + typeit=1 + shift +fi + +if [[ -n $WAYLAND_DISPLAY ]]; then + dmenu="dmenu-wl" + xdotool="ydotool type --file -" +elif [[ -n $DISPLAY ]]; then + dmenu="dmenu" + xdotool="xdotool type --clearmodifiers --file -" +else + echo "Error: No Wayland or X11 display detected" >&2 + exit 1 +fi + +# Overwrite dmenu if PASSMENU_SCRIPT_MENU is set and not empty +dmenu=${PASSWORD_STORE_MENU:-$dmenu} + +prefix=${PASSWORD_STORE_DIR-~/.password-store} +password_files=( "$prefix"/**/*.gpg ) +password_files=( "${password_files[@]#"$prefix"/}" ) +password_files=( "${password_files[@]%.gpg}" ) + +password=$(printf '%s\n' "${password_files[@]}" | "$dmenu" "$@") + +[[ -n $password ]] || exit + +if [[ $typeit -eq 0 ]]; then + pass show -c "$password" 2>/dev/null +else + pass show "$password" | { IFS= read -r pass; printf %s "$pass"; } | $xdotool +fi diff --git a/modules/home/rofi-rbw/default.nix b/modules/home/rofi-rbw/default.nix new file mode 100644 index 0000000..30ee756 --- /dev/null +++ b/modules/home/rofi-rbw/default.nix @@ -0,0 +1,55 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.rofi-rbw; + + inherit (lib) + generators + mkEnableOption + mkIf + mkOption + mkPackageOption + types + ; +in +{ + options = { + programs.rofi-rbw = { + enable = mkEnableOption "rofi-rbw"; + package = mkPackageOption pkgs "rofi-rbw" { }; + settings = mkOption { + type = types.attrsOf ( + types.oneOf [ + types.str + types.int + types.bool + ] + ); + default = { }; + example = { + action = "copy"; + prompt = "Bitwarden"; + selector = "rofi"; + selector-args = "-i"; + }; + description = '' + Configuration settings for rofi-rbw. + See https://github.com/fdw/rofi-rbw/blob/main/docs/rofi-rbw.1.md + ''; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ cfg.package ]; + + xdg.configFile."rofi-rbw.rc".text = generators.toKeyValue { + mkKeyValue = key: value: "${key} = ${toString value}"; + } cfg.settings; + }; +} diff --git a/modules/home/sops/default.nix b/modules/home/sops/default.nix new file mode 100644 index 0000000..e35b1f1 --- /dev/null +++ b/modules/home/sops/default.nix @@ -0,0 +1,22 @@ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + secrets = "${toString inputs.self}/users/${config.home.username}/home/secrets/secrets.yaml"; +in +{ + imports = [ inputs.sops-nix.homeManagerModules.sops ]; + + home.packages = with pkgs; [ + age + sops + ]; + + sops.defaultSopsFile = lib.mkIf (builtins.pathExists secrets) (lib.mkDefault secrets); + sops.age.keyFile = lib.mkDefault "${config.home.homeDirectory}/.config/sops/age/keys.txt"; +} diff --git a/modules/home/stylix/default.nix b/modules/home/stylix/default.nix new file mode 100644 index 0000000..34fd04f --- /dev/null +++ b/modules/home/stylix/default.nix @@ -0,0 +1,91 @@ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + cfg = config.stylix; + + # all valid options for `cfg.scheme` + validSchemes = [ + "ayu" + "dracula" + "gruvbox" + "moonfly" + "nord" + "oxocarbon" + ]; + # schemes names in `pkgs.base16-schemes` that need a suffix + needsSuffix = [ + "ayu" + "gruvbox" + ]; + # schemes in ./schemes + customSchemes = [ + "moonfly" + "oxocarbon" + ]; + schemeName = + if builtins.elem cfg.scheme needsSuffix then "${cfg.scheme}-${cfg.polarity}" else cfg.scheme; + scheme = + if builtins.elem cfg.scheme customSchemes then + ./schemes/${schemeName}.yaml + else + "${pkgs.base16-schemes}/share/themes/${schemeName}.yaml"; + + inherit (lib) + mkDefault + mkIf + types + ; +in +{ + imports = [ + inputs.stylix.homeModules.stylix + + ./targets + ]; + + options.stylix = { + scheme = lib.mkOption { + type = types.str; + default = "dracula"; + description = '' + Base16 color scheme name. Available options are: + ${toString validSchemes} + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = builtins.elem cfg.scheme validSchemes; + message = "Stylix: Invalid colorscheme '${cfg.scheme}'. Available options: ${toString validSchemes}"; + } + ]; + + stylix = { + autoEnable = mkDefault true; + base16Scheme = scheme; + fonts = { + monospace = mkDefault { + package = pkgs.hack-font; + name = "Hack"; + }; + }; + polarity = mkDefault "dark"; + + targets = { + librewolf.profileNames = [ "default" ]; + }; + }; + + home.packages = [ + (pkgs.callPackage ./print-colors { }) + ]; + }; +} diff --git a/modules/home/stylix/print-colors/default.nix b/modules/home/stylix/print-colors/default.nix new file mode 100644 index 0000000..11db15a --- /dev/null +++ b/modules/home/stylix/print-colors/default.nix @@ -0,0 +1,22 @@ +{ + python3Packages, + ... +}: + +python3Packages.buildPythonApplication { + pname = "print-colors"; + version = "1.0.0"; + + src = ./.; + pyproject = true; + + build-system = [ python3Packages.setuptools ]; + + propagatedBuildInputs = [ python3Packages.pyyaml ]; + + doCheck = false; + + meta = { + description = "Display colors from a YAML color palette file in the terminal."; + }; +} diff --git a/modules/home/stylix/print-colors/print-colors b/modules/home/stylix/print-colors/print-colors new file mode 100644 index 0000000..8851351 --- /dev/null +++ b/modules/home/stylix/print-colors/print-colors @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 + +import yaml +import argparse + +def hex_to_rgb(hex_color): + """Converts a hex color string (e.g., 'RRGGBB') to an RGB tuple.""" + hex_color = hex_color.lstrip('#') + + if len(hex_color) != 6: + raise ValueError(f"Invalid hex color format: {hex_color}. Expected 6 characters.") + + return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + +def print_color_block(hex_color, label): + """Prints a colored block with a label using ANSI escape codes.""" + try: + r, g, b = hex_to_rgb(hex_color) + except ValueError as e: + print(f"Error converting hex '{hex_color}' for '{label}': {e}") + return + + luminosity = (r * 0.2126 + g * 0.7152 + b * 0.0722) + + if luminosity > 186: + text_color_code = ";38;2;0;0;0" # black + else: + text_color_code = ";38;2;255;255;255" # white + + print(f"\x1b[48;2;{r};{g};{b}{text_color_code}m {label.ljust(15)} \x1b[0m") + +def main(): + parser = argparse.ArgumentParser( + description="Display colors from a YAML color palette file in the terminal." + ) + parser.add_argument( + "yaml_file", + metavar="YAML_FILE", + type=str, + help="Path to the YAML file containing the color palette." + ) + + args = parser.parse_args() + + yaml_file_path = args.yaml_file + + try: + with open(yaml_file_path, 'r') as f: + data = yaml.safe_load(f) + except FileNotFoundError: + print(f"Error: '{yaml_file_path}' not found.") + return + except yaml.YAMLError as e: + print(f"Error parsing YAML file '{yaml_file_path}': {e}") + return + except Exception as e: + print(f"An unexpected error occurred while reading '{yaml_file_path}': {e}") + return + + system_name = data.get('system', 'N/A') + theme_name = data.get('name', 'N/A') + description = data.get('description', 'N/A') + palette = data.get('palette', {}) + + print(f"\x1b[1mSystem:\x1b[0m {system_name}") + print(f"\x1b[1mTheme Name:\x1b[0m {theme_name}") + print(f"\x1b[1mDescription:\x1b[0m {description}\n") + + print("\x1b[1mPalette Colors:\x1b[0m") + + sorted_palette_keys = sorted(palette.keys(), key=lambda x: (x.startswith('base'), x)) + + for key in sorted_palette_keys: + hex_color = palette.get(key) + if hex_color: + print_color_block(hex_color, key) + else: + print(f"Warning: Key '{key}' has no hexadecimal color value.") + +if __name__ == "__main__": + main() diff --git a/modules/home/stylix/print-colors/setup.py b/modules/home/stylix/print-colors/setup.py new file mode 100644 index 0000000..3f36b52 --- /dev/null +++ b/modules/home/stylix/print-colors/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup + +setup( + name='print-colors', + version='1.0.0', + scripts=['print-colors'], +) diff --git a/modules/home/stylix/schemes/moonfly.yaml b/modules/home/stylix/schemes/moonfly.yaml new file mode 100644 index 0000000..122f700 --- /dev/null +++ b/modules/home/stylix/schemes/moonfly.yaml @@ -0,0 +1,22 @@ +system: "base16" +name: "Moonfly" +description: "A dark theme inspired by the Moonfly color scheme." +slug: "moonfly-theme" +variant: "dark" +palette: + base00: "080808" + base01: "323437" + base02: "9e9e9e" + base03: "bdbdbd" + base04: "b2ceee" + base05: "c6c6c6" + base06: "e4e4e4" + base07: "eeeeee" + base08: "ff5454" + base09: "cf87e8" + base0A: "8cc85f" + base0B: "e3c78a" + base0C: "79dac8" + base0D: "80a0ff" + base0E: "36c692" + base0F: "74b2ff" diff --git a/modules/home/stylix/schemes/oxocarbon.yaml b/modules/home/stylix/schemes/oxocarbon.yaml new file mode 100644 index 0000000..b278c55 --- /dev/null +++ b/modules/home/stylix/schemes/oxocarbon.yaml @@ -0,0 +1,22 @@ +system: "base16" +name: "Oxocarbon" +description: "A dark theme inspired by the Oxocarbon Dark color scheme." +slug: "oxocarbon-theme" +variant: "dark" +palette: + base00: "161616" + base01: "262626" + base02: "393939" + base03: "525252" + base04: "dde1e6" + base05: "f2f4f8" + base06: "ffffff" + base07: "08bdba" + base08: "ee5396" + base09: "ff7eb6" + base0A: "3ddbd9" + base0B: "42be65" + base0C: "82cfff" + base0D: "33b1ff" + base0E: "be95ff" + base0F: "78a9ff" diff --git a/modules/home/stylix/targets/bemenu.nix b/modules/home/stylix/targets/bemenu.nix new file mode 100644 index 0000000..0882c42 --- /dev/null +++ b/modules/home/stylix/targets/bemenu.nix @@ -0,0 +1,55 @@ +{ config, lib, ... }: + +let + cfg = config.stylix; + target = cfg.targets.bemenu'; + + colors = config.lib.stylix.colors.withHashtag; + + inherit (lib) + mkEnableOption + mkIf + mkOption + types + ; +in +{ + options.stylix.targets.bemenu' = { + enable = mkEnableOption "bemenu' target for Stylix."; + radius = mkOption { + type = types.int; + default = cfg.targets.hyprland.radius; + description = "Window corner radius in pixels."; + }; + }; + + config = mkIf (cfg.enable && target.enable) { + stylix.targets.bemenu.enable = false; + + programs.bemenu = mkIf (cfg.enable && target.enable) { + settings = { + border-radius = target.radius; + + bdr = colors.blue; # Border + tb = colors.base00; # Title background + tf = colors.green; # Title foreground + fb = colors.base00; # Filter background + ff = colors.base05; # Filter foreground + cb = colors.base00; # Cursor background + cf = colors.base02; # Cursor foreground + nb = colors.base00; # Normal background + nf = colors.base05; # Normal foreground + hb = colors.base01; # Highlighted background + hf = colors.blue; # Highlighted foreground + fbb = colors.base00; # Feedback background + fbf = colors.base05; # Feedback foreground + sb = colors.base01; # Selected background + sf = colors.base05; # Selected foreground + ab = colors.base00; # Alternating background + af = colors.base05; # Alternating foreground + scb = colors.base00; # Scrollbar background + scf = colors.blue; # Scrollbar foreground + }; + }; + }; +} diff --git a/modules/home/stylix/targets/default.nix b/modules/home/stylix/targets/default.nix new file mode 100644 index 0000000..09b2519 --- /dev/null +++ b/modules/home/stylix/targets/default.nix @@ -0,0 +1,8 @@ +{ + imports = [ + ./bemenu.nix + ./hyprland.nix + ./nixvim.nix + ./waybar.nix + ]; +} diff --git a/modules/home/stylix/targets/hyprland.nix b/modules/home/stylix/targets/hyprland.nix new file mode 100644 index 0000000..37770b1 --- /dev/null +++ b/modules/home/stylix/targets/hyprland.nix @@ -0,0 +1,40 @@ +{ config, lib, ... }: + +let + cfg = config.stylix; + target = cfg.targets.hyprland; + + inherit (lib) + mkIf + mkOption + types + ; +in +{ + options.stylix.targets.hyprland = { + gaps = mkOption { + type = types.int; + default = 0; + description = "Window gaps in pixels."; + }; + radius = mkOption { + type = types.int; + default = 0; + description = "Window corner radius in pixels."; + }; + }; + + config = mkIf (cfg.enable && target.enable) { + wayland.windowManager.hyprland = { + settings = { + general = { + gaps_in = target.gaps / 2; + gaps_out = target.gaps; + }; + decoration = { + rounding = target.radius; + }; + }; + }; + }; +} diff --git a/modules/home/stylix/targets/nixvim.nix b/modules/home/stylix/targets/nixvim.nix new file mode 100644 index 0000000..7fcdd33 --- /dev/null +++ b/modules/home/stylix/targets/nixvim.nix @@ -0,0 +1,14 @@ +{ config, lib, ... }: + +let + cfg = config.stylix; + target = cfg.targets.nixvim; + + inherit (lib) mkIf; +in +{ + config = mkIf cfg.enable { + stylix.targets.nixvim.enable = false; + programs.nixvim.colorschemes."${cfg.scheme}".enable = !target.enable; + }; +} diff --git a/modules/home/stylix/targets/waybar.nix b/modules/home/stylix/targets/waybar.nix new file mode 100644 index 0000000..a533515 --- /dev/null +++ b/modules/home/stylix/targets/waybar.nix @@ -0,0 +1,130 @@ +{ config, lib, ... }: + +let + cfg = config.stylix; + target = cfg.targets.waybar'; + + colors = config.lib.stylix.colors.withHashtag; + gaps = toString (4 + target.gaps); + halfgaps = toString (2 + target.gaps / 2); + radius = toString target.radius; + + bar = { + margin = "0 ${gaps} 0 ${gaps}"; + tray = { + spacing = target.gaps; + }; + + # calendar has no css + clock.calendar.format = { + months = "{}"; + weeks = "W{}"; + weekdays = "{}"; + today = "{}"; + }; + }; + + inherit (lib) + mkEnableOption + mkIf + mkOption + types + ; +in +{ + options.stylix.targets.waybar' = { + enable = mkEnableOption "waybar' target for Stylix."; + gaps = mkOption { + type = types.int; + default = cfg.targets.hyprland.gaps; + description = "Widget gaps in pixels."; + }; + radius = mkOption { + type = types.int; + default = cfg.targets.hyprland.radius; + description = "Widget corner radius in pixels."; + }; + }; + + config = mkIf (cfg.enable && target.enable) { + stylix.targets.waybar.enable = false; + + programs.waybar = { + settings = { + mainBar = bar; + otherBar = bar; + }; + + style = '' + * { + border-radius: ${radius}px; + border: none; + font-family: monospace; + font-size: 15px; + min-height: 5px; + transition: none; + } + + window#waybar { + background: transparent; + } + + #workspaces { + color: ${colors.base05}; + background: ${colors.base00}; + } + + #workspaces button { + padding: ${halfgaps}px; + } + + #workspaces button.active { + color: ${colors.blue}; + } + + #workspaces button.urgent { + color: ${colors.orange}; + } + + #workspaces button:hover { + color: ${colors.base00}; + background: ${colors.base02}; + } + + #clock { + padding: ${gaps}px; + } + + #cpu, #memory, #network, #battery, #keyboard-state, #disk, #bluetooth, #wireplumber, #pulseaudio, #language, #custom-newsboat, #custom-timer, #tray { + margin-left: ${gaps}px; + padding: ${gaps}px; + } + + #keyboard-state label.locked { + background-color: ${colors.base00}; + color: ${colors.blue}; + } + + #battery.charging { + background-color: ${colors.green}; + color: ${colors.base00}; + } + + #battery.warning:not(.charging) { + background-color: ${colors.yellow}; + color: ${colors.base00}; + } + + #battery.critical:not(.charging) { + background-color: ${colors.red}; + color: ${colors.base00}; + } + + #bluetooth.discovering { + background-color: ${colors.blue}; + color: ${colors.base00}; + } + ''; + }; + }; +} diff --git a/modules/home/virtualisation/default.nix b/modules/home/virtualisation/default.nix new file mode 100644 index 0000000..c83be51 --- /dev/null +++ b/modules/home/virtualisation/default.nix @@ -0,0 +1,15 @@ +let + uri = "qemu:///system"; +in +{ + dconf.settings = { + "org/virt-manager/virt-manager/connections" = { + autoconnect = [ uri ]; + uris = [ uri ]; + }; + }; + + home.shellAliases = { + virsh = "virsh --connect ${uri}"; + }; +} diff --git a/modules/home/waybar/default.nix b/modules/home/waybar/default.nix new file mode 100644 index 0000000..ea794ce --- /dev/null +++ b/modules/home/waybar/default.nix @@ -0,0 +1,133 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.waybar; + + clock = { + format = mkDefault "{:%a %d %b %H:%M}"; + tooltip-format = mkDefault "{calendar}"; + timezone = mkDefault "Europe/Berlin"; + locale = mkDefault "en_US.UTF-8"; + calendar = { + mode = mkDefault "year"; + mode-mon-col = mkDefault 3; + weeks-pos = mkDefault "right"; + on-scroll = mkDefault 1; + }; + actions = { + on-click-right = mkDefault "mode"; + on-click-forward = mkDefault "tz_up"; + on-click-backward = mkDefault "tz_down"; + on-scroll-up = mkDefault "shift_up"; + on-scroll-down = mkDefault "shift_down"; + }; + }; + + "hyprland/workspaces" = { + active-only = mkDefault false; + all-outputs = mkDefault false; + format = mkDefault "{name}"; + on-click = mkDefault "activate"; + on-scroll-up = mkDefault "hyprctl dispatch workspace e-1"; + on-scroll-down = mkDefault "hyprctl dispatch workspace e+1"; + }; + + keyboard-state = { + numlock = mkDefault true; + capslock = mkDefault true; + scrolllock = mkDefault true; + format = { + scrolllock = mkDefault "S "; + capslock = mkDefault "C "; + numlock = mkDefault "N"; + }; + }; + + "hyprland/language" = { + format = mkDefault "{}"; + format-en = mkDefault "us"; + format-de = mkDefault "de"; + # TODO: switch language on click + }; + + # Add your custom modules here + "custom/newsboat" = import ./modules/newsboat.nix { inherit lib pkgs; }; + "pulseaudio#input" = import ./modules/pulseaudio/input.nix { inherit lib pkgs; }; + "pulseaudio#output" = import ./modules/pulseaudio/output.nix { inherit lib pkgs; }; + battery = import ./modules/battery.nix { inherit lib; }; + bluetooth = import ./modules/bluetooth.nix { inherit lib; }; + cpu = import ./modules/cpu.nix { inherit lib; }; + disk = import ./modules/disk.nix { inherit lib; }; + memory = import ./modules/memory.nix { inherit lib; }; + network = import ./modules/network.nix { inherit lib; }; + wireplumber = import ./modules/wireplumber.nix { inherit lib; }; + + inherit (lib) mkDefault mkIf; +in +{ + imports = [ + ./modules/timer # TODO: Do not use imports. Move to let-in-block. + ]; + + config = mkIf cfg.enable { + home.packages = with pkgs; [ font-awesome ]; + + programs.waybar = { + systemd.enable = mkDefault true; + settings = { + mainBar = { + output = mkDefault ""; + modules-left = mkDefault [ + "hyprland/workspaces" + "keyboard-state" + "hyprland/language" + ]; + modules-center = mkDefault [ "clock" ]; + modules-right = mkDefault [ + "network" + "cpu" + "memory" + "disk" + "pulseaudio#input" + "pulseaudio#output" + "tray" + ]; + + inherit + "custom/newsboat" + "hyprland/language" + "hyprland/workspaces" + "pulseaudio#input" + "pulseaudio#output" + battery + bluetooth + clock + cpu + disk + keyboard-state + memory + network + wireplumber + ; + }; + otherBar = { + output = with cfg.settings.mainBar; if (output != "") then "!${output}" else "nowhere"; + modules-left = mkDefault [ + "hyprland/workspaces" + ]; + modules-center = mkDefault [ "clock" ]; + + inherit + "hyprland/workspaces" + clock + ; + }; + }; + }; + }; +} diff --git a/modules/home/waybar/modules/battery.nix b/modules/home/waybar/modules/battery.nix new file mode 100644 index 0000000..d0c4df4 --- /dev/null +++ b/modules/home/waybar/modules/battery.nix @@ -0,0 +1,28 @@ +# battery +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + interval = mkDefault 10; + states = { + warning = mkDefault 20; + critical = mkDefault 10; + }; + format = mkDefault "{icon} {capacity}"; + format-icons = mkDefault [ + "" + "" + "" + "" + "" + ]; + tooltip-format = mkDefault '' + {timeTo} + Capacity: {capacity}% + Power Draw: {power} W + Charge Cycles: {cycles} + Health: {health}% + ''; +} diff --git a/modules/home/waybar/modules/bluetooth.nix b/modules/home/waybar/modules/bluetooth.nix new file mode 100644 index 0000000..b1e1c4f --- /dev/null +++ b/modules/home/waybar/modules/bluetooth.nix @@ -0,0 +1,20 @@ +# bluetooth +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + format = mkDefault " {status}"; + format-connected = mkDefault " {device_alias}"; + format-connected-battery = mkDefault " {device_alias} {device_battery_percentage}%"; + format-disabled = mkDefault ""; + format-off = mkDefault ""; + format-on = mkDefault ""; + max-length = mkDefault 12; + on-click = "bluetoothctl power off"; + tooltip-format = mkDefault "{controller_alias}\t{controller_address}\n\n{num_connections} connected"; + tooltip-format-connected = mkDefault "{controller_alias}\t{controller_address}\n\n{num_connections} connected\n\n{device_enumerate}"; + tooltip-format-enumerate-connected = mkDefault "{device_alias}\t{device_address}"; + tooltip-format-enumerate-connected-battery = mkDefault "{device_alias}\t{device_address}\t{device_battery_percentage}%"; +} diff --git a/modules/home/waybar/modules/cpu.nix b/modules/home/waybar/modules/cpu.nix new file mode 100644 index 0000000..ba061b7 --- /dev/null +++ b/modules/home/waybar/modules/cpu.nix @@ -0,0 +1,25 @@ +# cpu +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + interval = mkDefault 10; + format = mkDefault " {usage}"; + tooltip-format = mkDefault '' + CPU Load: {load} + CPU Usage: {usage}% + Core 0 Usage: {usage0}% + Core 1 Usage: {usage1}% + Core 2 Usage: {usage2}% + Core 3 Usage: {usage3}% + Core 4 Usage: {usage4}% + Core 5 Usage: {usage5}% + Core 6 Usage: {usage6}% + Core 7 Usage: {usage7}% + Avg Frequency: {avg_frequency} GHz + Max Frequency: {max_frequency} GHz + Min Frequency: {min_frequency} GHz + ''; +} diff --git a/modules/home/waybar/modules/disk.nix b/modules/home/waybar/modules/disk.nix new file mode 100644 index 0000000..98e2162 --- /dev/null +++ b/modules/home/waybar/modules/disk.nix @@ -0,0 +1,11 @@ +# disk +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + interval = mkDefault 3600; + format = mkDefault " {percentage_used}"; + path = mkDefault "/"; +} diff --git a/modules/home/waybar/modules/memory.nix b/modules/home/waybar/modules/memory.nix new file mode 100644 index 0000000..f8fabbc --- /dev/null +++ b/modules/home/waybar/modules/memory.nix @@ -0,0 +1,18 @@ +# memory +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + interval = mkDefault 10; + format = mkDefault " {percentage}"; + tooltip-format = mkDefault '' + Total Memory: {total} GiB + Used Memory: {used} GiB ({percentage}%) + Available Memory: {avail} GiB + Total Swap: {swapTotal} GiB + Used Swap: {swapUsed} GiB ({swapPercentage}%) + Available Swap: {swapAvail} GiB + ''; +} diff --git a/modules/home/waybar/modules/network.nix b/modules/home/waybar/modules/network.nix new file mode 100644 index 0000000..e1e38f2 --- /dev/null +++ b/modules/home/waybar/modules/network.nix @@ -0,0 +1,48 @@ +# network +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + interval = mkDefault 10; + format-wifi = mkDefault " {signalStrength}"; + format-ethernet = mkDefault ""; + format-disconnected = mkDefault ""; # An empty format will hide the module. + tooltip-format = mkDefault '' + Interface: {ifname} + IP Address: {ipaddr} + Gateway: {gwaddr} + Netmask: {netmask} + CIDR: {cidr} + ESSID: {essid} + Signal: {signaldBm} dBm / {signalStrength}% + Frequency: {frequency} GHz + Bandwidth Up: {bandwidthUpBits} / {bandwidthUpBytes} + Bandwidth Down: {bandwidthDownBits} / {bandwidthDownBytes} + Total Bandwidth: {bandwidthTotalBits} / {bandwidthTotalBytes} + Icon: {icon} + ''; + tooltip-format-wifi = mkDefault '' + Interface: {ifname} + IP Address: {ipaddr}/{cidr} + Gateway: {gwaddr} + Netmask: {netmask} + ESSID: {essid} + Signal: {signaldBm} dBm / {signalStrength}% + Frequency: {frequency} GHz + Bandwidth Up: {bandwidthUpBits} / {bandwidthUpBytes} + Bandwidth Down: {bandwidthDownBits} / {bandwidthDownBytes} + Total Bandwidth: {bandwidthTotalBits} / {bandwidthTotalBytes} + ''; + tooltip-format-ethernet = mkDefault '' + Interface: {ifname} + IP Address: {ipaddr}/{cidr} + Gateway: {gwaddr} + Netmask: {netmask} + Bandwidth Up: {bandwidthUpBits} / {bandwidthUpBytes} + Bandwidth Down: {bandwidthDownBits} / {bandwidthDownBytes} + Total Bandwidth: {bandwidthTotalBits} / {bandwidthTotalBytes} + ''; + tooltip-format-disconnected = mkDefault "Disconnected"; +} diff --git a/modules/home/waybar/modules/newsboat.nix b/modules/home/waybar/modules/newsboat.nix new file mode 100644 index 0000000..e7222bb --- /dev/null +++ b/modules/home/waybar/modules/newsboat.nix @@ -0,0 +1,29 @@ +# custom/newsboat +{ + lib, + pkgs, + ... +}: + +let + newsboat-print-unread = + let + newsboat = "${pkgs.newsboat}/bin/newsboat"; + in + (pkgs.writeShellScriptBin "newsboat-print-unread" '' + UNREAD=$(${newsboat} -x print-unread | awk '{print $1}') + + if [[ $UNREAD -gt 0 ]]; then + printf " %i" "$UNREAD" + fi + ''); + + inherit (lib) mkDefault; +in +{ + exec = mkDefault "${newsboat-print-unread}/bin/newsboat-print-unread"; + format = mkDefault "{}"; + hide-empty-text = mkDefault true; # disable module when output is empty + signal = mkDefault 10; + on-click = mkDefault "newsboat-reload"; +} diff --git a/modules/home/waybar/modules/pulseaudio/input.nix b/modules/home/waybar/modules/pulseaudio/input.nix new file mode 100644 index 0000000..3563e37 --- /dev/null +++ b/modules/home/waybar/modules/pulseaudio/input.nix @@ -0,0 +1,20 @@ +# pulseaudio input +{ lib, pkgs, ... }: + +let + helvum = "${pkgs.helvum}/bin/helvum"; + pactl = "${pkgs.pulseaudio}/bin/pactl"; + + inherit (lib) mkDefault; +in +{ + format = mkDefault "{format_source}"; + format-source = mkDefault " {volume}"; + format-source-muted = mkDefault ""; + on-click = mkDefault "${pactl} set-source-mute 0 toggle"; + on-click-middle = mkDefault helvum; + on-scroll-down = mkDefault "${pactl} set-source-volume 0 -5%"; + on-scroll-up = mkDefault "${pactl} set-source-volume 0 +5%"; + scroll-step = mkDefault 1; + smooth-scrolling-threshold = mkDefault 1; +} diff --git a/modules/home/waybar/modules/pulseaudio/output.nix b/modules/home/waybar/modules/pulseaudio/output.nix new file mode 100644 index 0000000..c3b6670 --- /dev/null +++ b/modules/home/waybar/modules/pulseaudio/output.nix @@ -0,0 +1,27 @@ +# pulseaudio output +{ lib, pkgs, ... }: + +let + helvum = "${pkgs.helvum}/bin/helvum"; + pactl = "${pkgs.pulseaudio}/bin/pactl"; + + inherit (lib) mkDefault; +in +{ + format = mkDefault "{icon} {volume}"; + format-muted = mkDefault "x"; + format-icons = mkDefault { + default = mkDefault [ + "" + "" + "" + ]; + }; + max-volume = mkDefault 150; + on-click = mkDefault "${pactl} set-sink-mute 0 toggle"; + on-click-middle = mkDefault helvum; + on-scroll-down = mkDefault "${pactl} set-sink-volume 0 -5%"; + on-scroll-up = mkDefault "${pactl} set-sink-volume 0 +5%"; + scroll-step = mkDefault 5; + smooth-scrolling-threshold = mkDefault 1; +} diff --git a/modules/home/waybar/modules/timer/default.nix b/modules/home/waybar/modules/timer/default.nix new file mode 100644 index 0000000..d3ae919 --- /dev/null +++ b/modules/home/waybar/modules/timer/default.nix @@ -0,0 +1,28 @@ +# custom/timer +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.programs.waybar; + timer = pkgs.writeShellScriptBin "timer" (builtins.readFile ./timer.sh); + + inherit (lib) mkDefault mkIf; +in +{ + config = mkIf cfg.enable { + programs.waybar.settings.mainBar."custom/timer" = { + exec = mkDefault "${timer}/bin/timer print"; + # `interval` is not needed since timer script will update the status bar + format = mkDefault "{}"; + hide-empty-text = mkDefault true; # disable module when output is empty + signal = mkDefault 11; + on-click = mkDefault "${timer}/bin/timer stop"; + }; + + home.packages = [ timer ]; # Add an additional check if the widget is enabled? Currently, the waybar module installs this package regardless of the config. + }; +} diff --git a/modules/home/waybar/modules/timer/timer.sh b/modules/home/waybar/modules/timer/timer.sh new file mode 100644 index 0000000..9215aa0 --- /dev/null +++ b/modules/home/waybar/modules/timer/timer.sh @@ -0,0 +1,78 @@ +TIMER_FILE="/tmp/timer" # file to store the current time +SIGNAL=11 # signal number to send to status bar +STATUS_BAR="waybar" # Support for more status bars? + +help() { + echo "Usage: $0 {start -h H -m M -s S|stop|print}" +} + +start_timer() { + local hours=$1 + local minutes=$2 + local seconds=$3 + local total_seconds=$((hours * 3600 + minutes * 60 + seconds)) + + ( + notify-send "Timer Started" "Your countdown timer has been started." + + trap "exit" INT TERM + trap "rm -f -- '$TIMER_FILE'" EXIT + + while [ $total_seconds -gt 0 ]; do + hours=$(( total_seconds / 3600 )) + minutes=$(( (total_seconds % 3600) / 60 )) + seconds=$(( total_seconds % 60 )) + + printf "%02d:%02d:%02d\n" $hours $minutes $seconds > $TIMER_FILE + pkill -RTMIN+$SIGNAL $STATUS_BAR + sleep 1 + total_seconds=$(( total_seconds - 1 )) + done + + notify-send "Timer Finished" "Your countdown timer has ended." + stop_timer + ) & +} + +stop_timer() { + rm -f $TIMER_FILE + pkill -RTMIN+$SIGNAL $STATUS_BAR + exit 0 +} + +print_time() { + if [[ -f $TIMER_FILE ]]; then + printf " %s" "$(cat $TIMER_FILE)" + else + echo "" # waybar widget will be hidden if stdout is empty + fi +} + +if [ "$1" = "start" ]; then + shift + while getopts "h:m:s:" opt; do + case $opt in + h) HOURS=$OPTARG ;; + m) MINUTES=$OPTARG ;; + s) SECONDS=$OPTARG ;; + *) echo "Invalid option"; exit 1 ;; + esac + done + HOURS=${HOURS:-0} + MINUTES=${MINUTES:-0} + SECONDS=${SECONDS:-0} + + start_timer $HOURS $MINUTES $SECONDS + +elif [ "$1" = "stop" ]; then + notify-send "Timer Stopped" "Your countdown timer has been stopped." + stop_timer + +elif [ "$1" = "print" ]; then + print_time + +else + echo "Invalid command $1" + help + exit 1 +fi diff --git a/modules/home/waybar/modules/wireplumber.nix b/modules/home/waybar/modules/wireplumber.nix new file mode 100644 index 0000000..ff4b0ef --- /dev/null +++ b/modules/home/waybar/modules/wireplumber.nix @@ -0,0 +1,18 @@ +# wireplumber +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + format = mkDefault "{icon} {volume}"; + format-muted = mkDefault "mute"; # does not render. See issue #144 + format-icons = mkDefault [ + "" + "" + "" + ]; + on-click = mkDefault "helvum"; + max-volume = mkDefault 150; + scroll-step = mkDefault 1; +} diff --git a/modules/nixos/amd/default.nix b/modules/nixos/amd/default.nix new file mode 100644 index 0000000..252d991 --- /dev/null +++ b/modules/nixos/amd/default.nix @@ -0,0 +1,24 @@ +{ pkgs, ... }: + +{ + boot.initrd.kernelModules = [ "amdgpu" ]; + + environment.systemPackages = with pkgs; [ + lact + nvtopPackages.amd + rocmPackages.clr.icd + rocmPackages.hipcc + rocmPackages.miopen + rocmPackages.rocm-runtime + rocmPackages.rocm-smi + rocmPackages.rocminfo + + ]; + + # environment.variables.ROC_ENABLE_PRE_VEGA = "1"; # for Polaris + + hardware.amdgpu.opencl.enable = true; + + systemd.packages = with pkgs; [ lact ]; + systemd.services.lactd.wantedBy = [ "multi-user.target" ]; +} diff --git a/modules/nixos/audio/default.nix b/modules/nixos/audio/default.nix new file mode 100644 index 0000000..50a76f3 --- /dev/null +++ b/modules/nixos/audio/default.nix @@ -0,0 +1,28 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) mkDefault; +in +{ + services.pipewire = { + enable = mkDefault true; + wireplumber.enable = mkDefault true; + alsa.enable = mkDefault true; + pulse.enable = mkDefault true; + jack.enable = mkDefault true; + audio.enable = mkDefault true; + }; + + services.pulseaudio.enable = false; + + security.rtkit.enable = mkDefault config.services.pipewire.enable; + + environment.systemPackages = with pkgs; [ + pulseaudioFull + ]; +} diff --git a/modules/nixos/baibot/default.nix b/modules/nixos/baibot/default.nix new file mode 100644 index 0000000..af66779 --- /dev/null +++ b/modules/nixos/baibot/default.nix @@ -0,0 +1,98 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.baibot; + homeDir = "/var/lib/baibot"; + + inherit (lib) + mkEnableOption + mkIf + mkOption + optional + types + ; +in +{ + options = { + services.baibot = { + enable = mkEnableOption "Baibot, a Matrix AI bot."; + + configFile = mkOption { + type = types.nullOr types.path; + default = "${homeDir}/config.yml"; + description = '' + Path to the baibot configuration file. Use the template for reference: + https://github.com/etkecc/baibot/blob/main/etc/app/config.yml.dist + ''; + }; + + persistenceDataDirPath = mkOption { + type = types.nullOr types.path; + default = "${homeDir}/data"; + description = '' + Path to the directory where baibot will store its persistent data. + ''; + }; + + environmentFile = lib.mkOption { + description = '' + Path to an environment file that is passed to the systemd service. + ''; + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/secrets/baibot"; + }; + + package = mkOption { + type = types.nullOr types.package; + default = null; + description = '' + The baibot package to use for the service. This must be set by the user, + as there is no default baibot package available in Nixpkgs. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.package != null; + message = "The baibot package is not specified. Please set the services.baibot.package option to a valid baibot package."; + } + ]; + + users = { + users.baibot = { + isSystemUser = true; + description = "Baibot system user"; + home = homeDir; + createHome = true; + group = "baibot"; + }; + groups.baibot = { }; + }; + + systemd.services.baibot = { + description = "Baibot Service"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + BAIBOT_CONFIG_FILE_PATH = cfg.configFile; + BAIBOT_PERSISTENCE_DATA_DIR_PATH = cfg.persistenceDataDirPath; + }; + serviceConfig = { + ExecStart = "${cfg.package}/bin/baibot"; + EnvironmentFile = optional (cfg.environmentFile != null) cfg.environmentFile; + Restart = "always"; + User = "baibot"; + Group = "baibot"; + }; + }; + }; +} diff --git a/modules/nixos/bluetooth/default.nix b/modules/nixos/bluetooth/default.nix new file mode 100644 index 0000000..cff6e80 --- /dev/null +++ b/modules/nixos/bluetooth/default.nix @@ -0,0 +1,22 @@ +{ lib, pkgs, ... }: + +let + inherit (lib) mkDefault; +in +{ + hardware.bluetooth.enable = mkDefault true; + hardware.bluetooth.powerOnBoot = mkDefault false; + hardware.bluetooth.settings.General.Enable = mkDefault "Source,Sink,Media,Socket"; + hardware.bluetooth.settings.General.Experimental = mkDefault true; + + environment.systemPackages = with pkgs; [ + blueman + bluez + bluez-tools + ]; + + boot.kernelModules = [ + "btusb" + "bluetooth" + ]; +} diff --git a/modules/nixos/cifsMount/default.nix b/modules/nixos/cifsMount/default.nix new file mode 100644 index 0000000..dc6541f --- /dev/null +++ b/modules/nixos/cifsMount/default.nix @@ -0,0 +1,91 @@ +{ + config, + lib, + pkgs, + ... +}: + +with lib; + +let + cfg = config.services.cifsMount; +in +{ + + options.services.cifsMount = { + enable = mkEnableOption "CIFS mounting of predefined remote directories."; + + # List of predefined remote CIFS shares to mount + remotes = mkOption { + type = with types; listOf (attrsOf str); + default = [ ]; + description = "List of predefined remote CIFS shares to mount."; + example = [ + { + # Example entry for a CIFS mount. + host = "remotehost"; + shareName = "share"; + mountPoint = "/local/mountpoint"; + credentialsFile = "/path/to/credentials"; + user = "yourusername"; + } + ]; + }; + }; + + config = mkIf cfg.enable { + environment.systemPackages = with pkgs; [ cifs-utils ]; + + # Define /etc/fstab entries for each remote CIFS share + fileSystems = fold ( + remote: fs: + let + mountOption = "credentials=${remote.credentialsFile},uid=${remote.user},gid=${remote.user},user,noauto"; + in + fs + // { + "${toString remote.mountPoint}" = { + device = "//${remote.host}/${remote.shareName}"; + fsType = "cifs"; + options = [ + (optionalString (remote.credentialsFile != null) "credentials=${remote.credentialsFile}") + "uid=${remote.user}" + "gid=${remote.user}" + "user" + "noauto" + ]; + }; + } + ) { } cfg.remotes; + + # Create systemd user services for each remote CIFS share + systemd.user.services = fold ( + remote: services: + let + serviceName = "cifs-mount-${remote.shareName}"; + in + { + ${serviceName} = { + description = "Mount remote share: //${remote.host}/${remote.shareName} to ${remote.mountPoint}"; + wantedBy = [ "graphical-session.target" ]; + after = [ "graphical-session.target" ]; + + # Service configuration to mount and unmount CIFS share + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "mount //${remote.host}/${remote.shareName}"; + ExecStop = "umount //${remote.host}/${remote.shareName}"; + Restart = "on-failure"; + }; + }; + } + // services + ) { } cfg.remotes; + + # Ensure that all cifs-mount services are started with the graphical session + systemd.user.targets.graphical-session.wants = map ( + remote: "cifs-mount-${remote.shareName}.service" + ) cfg.remotes; + }; +} diff --git a/modules/nixos/common/default.nix b/modules/nixos/common/default.nix new file mode 100644 index 0000000..9847a07 --- /dev/null +++ b/modules/nixos/common/default.nix @@ -0,0 +1,14 @@ +{ + imports = [ + ./environment.nix + ./htop.nix + ./nationalization.nix + ./networking.nix + ./nix.nix + ./sudo.nix + ./well-known.nix + ./zsh.nix + + ../../shared/common + ]; +} diff --git a/modules/nixos/common/environment.nix b/modules/nixos/common/environment.nix new file mode 100644 index 0000000..6921f2c --- /dev/null +++ b/modules/nixos/common/environment.nix @@ -0,0 +1,63 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) mkDefault optionals; +in +{ + environment.systemPackages = + with pkgs; + [ + cryptsetup + curl + dig + dnsutils + fzf + gptfdisk + iproute2 + jq + lm_sensors + lsof + netcat-openbsd + nettools + nixos-container + nmap + nurl + p7zip + pciutils + psmisc + rclone + rsync + tcpdump + tmux + tree + unzip + usbutils + wget + xxd + zip + + (callPackage ../../../apps/rebuild { }) + ] + ++ optionals (pkgs.stdenv.hostPlatform == pkgs.stdenv.buildPlatform) [ + pkgs.kitty.terminfo + ]; + + environment.shellAliases = { + l = "ls -lh"; + ll = "ls -lAh"; + ports = "ss -tulpn"; + publicip = "curl ifconfig.me/all"; + sudo = "sudo "; # make aliases work with `sudo` + }; + + # saves one instance of nixpkgs. + environment.ldso32 = null; + + boot.tmp.cleanOnBoot = mkDefault true; + boot.initrd.systemd.enable = mkDefault (!config.boot.swraid.enable && !config.boot.isContainer); +} diff --git a/modules/nixos/common/htop.nix b/modules/nixos/common/htop.nix new file mode 100644 index 0000000..c95c3fc --- /dev/null +++ b/modules/nixos/common/htop.nix @@ -0,0 +1,8 @@ +{ + programs.htop = { + enable = true; + settings = { + highlight_base_name = 1; + }; + }; +} diff --git a/modules/nixos/common/nationalization.nix b/modules/nixos/common/nationalization.nix new file mode 100644 index 0000000..1d35507 --- /dev/null +++ b/modules/nixos/common/nationalization.nix @@ -0,0 +1,31 @@ +{ lib, ... }: + +let + de = "de_DE.UTF-8"; + en = "en_US.UTF-8"; + + inherit (lib) mkDefault; +in +{ + i18n = { + defaultLocale = mkDefault en; + extraLocaleSettings = { + LC_ADDRESS = mkDefault de; + LC_IDENTIFICATION = mkDefault de; + LC_MEASUREMENT = mkDefault de; + LC_MONETARY = mkDefault de; + LC_NAME = mkDefault de; + LC_NUMERIC = mkDefault de; + LC_PAPER = mkDefault de; + LC_TELEPHONE = mkDefault de; + LC_TIME = mkDefault en; + }; + }; + + console = { + font = mkDefault "Lat2-Terminus16"; + keyMap = mkDefault "de"; + }; + + time.timeZone = mkDefault "Europe/Berlin"; +} diff --git a/modules/nixos/common/networking.nix b/modules/nixos/common/networking.nix new file mode 100644 index 0000000..15d1b1f --- /dev/null +++ b/modules/nixos/common/networking.nix @@ -0,0 +1,40 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) mkDefault; + inherit (lib.utils) isNotEmptyStr; +in +{ + config = { + assertions = [ + { + assertion = isNotEmptyStr config.networking.domain; + message = "synix/nixos/common: config.networking.domain cannot be empty."; + } + { + assertion = isNotEmptyStr config.networking.hostName; + message = "synix/nixos/common: config.networking.hostName cannot be empty."; + } + ]; + + networking = { + domain = mkDefault "${config.networking.hostName}.local"; + hostId = mkDefault "8425e349"; # same as NixOS install ISO and nixos-anywhere + + # NetworkManager + useDHCP = false; + networkmanager = { + enable = true; + plugins = with pkgs; [ + networkmanager-openconnect + networkmanager-openvpn + ]; + }; + }; + }; +} diff --git a/modules/nixos/common/nix.nix b/modules/nixos/common/nix.nix new file mode 100644 index 0000000..e793401 --- /dev/null +++ b/modules/nixos/common/nix.nix @@ -0,0 +1,19 @@ +{ + config, + lib, + ... +}: + +let + inherit (lib) mkDefault; +in +{ + nix = { + # use flakes + channel.enable = mkDefault false; + + # De-duplicate store paths using hardlinks except in containers + # where the store is host-managed. + optimise.automatic = mkDefault (!config.boot.isContainer); + }; +} diff --git a/modules/nixos/common/sudo.nix b/modules/nixos/common/sudo.nix new file mode 100644 index 0000000..3459e93 --- /dev/null +++ b/modules/nixos/common/sudo.nix @@ -0,0 +1,26 @@ +{ config, ... }: + +{ + security.sudo = { + enable = true; + execWheelOnly = true; + extraConfig = '' + Defaults lecture = never + ''; + }; + + assertions = + let + validUsers = users: users == [ ] || users == [ "root" ]; + validGroups = groups: groups == [ ] || groups == [ "wheel" ]; + validUserGroups = builtins.all ( + r: validUsers (r.users or [ ]) && validGroups (r.groups or [ ]) + ) config.security.sudo.extraRules; + in + [ + { + assertion = config.security.sudo.execWheelOnly -> validUserGroups; + message = "Some definitions in `security.sudo.extraRules` refer to users other than 'root' or groups other than 'wheel'. Disable `config.security.sudo.execWheelOnly`, or adjust the rules."; + } + ]; +} diff --git a/modules/nixos/common/well-known.nix b/modules/nixos/common/well-known.nix new file mode 100644 index 0000000..07a938c --- /dev/null +++ b/modules/nixos/common/well-known.nix @@ -0,0 +1,17 @@ +{ + # avoid TOFU MITM + programs.ssh.knownHosts = { + "github.com".hostNames = [ "github.com" ]; + "github.com".publicKey = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl"; + + "gitlab.com".hostNames = [ "gitlab.com" ]; + "gitlab.com".publicKey = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf"; + + "git.sr.ht".hostNames = [ "git.sr.ht" ]; + "git.sr.ht".publicKey = + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMZvRd4EtM7R+IHVMWmDkVU3VLQTSwQDSAvW0t2Tkj60"; + }; + # TODO: add synix +} diff --git a/modules/nixos/common/zsh.nix b/modules/nixos/common/zsh.nix new file mode 100644 index 0000000..a0ff7fb --- /dev/null +++ b/modules/nixos/common/zsh.nix @@ -0,0 +1,26 @@ +{ + programs.zsh = { + enable = true; + syntaxHighlighting = { + enable = true; + highlighters = [ + "main" + "brackets" + "cursor" + "pattern" + ]; + patterns = { + "rm -rf" = "fg=white,bold,bg=red"; + "rm -fr" = "fg=white,bold,bg=red"; + }; + }; + autosuggestions = { + enable = true; + strategy = [ + "completion" + "history" + ]; + }; + enableLsColors = true; + }; +} diff --git a/modules/nixos/coturn/default.nix b/modules/nixos/coturn/default.nix new file mode 100644 index 0000000..0ed379b --- /dev/null +++ b/modules/nixos/coturn/default.nix @@ -0,0 +1,125 @@ +{ config, lib, ... }: + +let + cfg = config.services.coturn; + + inherit (lib) + mkDefault + mkEnableOption + mkIf + mkOption + optionalString + optionals + types + ; +in +{ + options.services.coturn = { + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the firewall for coturn."; + }; + debug = mkOption { + type = types.bool; + default = true; + description = "Enable debug logging for coturn."; + }; + sops = mkEnableOption "SOPS integration"; + }; + + config = mkIf cfg.enable { + services.coturn = { + no-cli = mkDefault true; + no-tcp-relay = mkDefault true; + no-tls = mkDefault false; + min-port = mkDefault 49000; + max-port = mkDefault 50000; + listening-port = mkDefault 3478; + tls-listening-port = mkDefault 5349; + + use-auth-secret = true; + static-auth-secret-file = mkIf cfg.sops config.sops.secrets."coturn/static-auth-secret".path; + realm = mkDefault "turn.${config.networking.domain}"; + + cert = + mkIf (!cfg.no-tls && cfg.sops) + "${config.security.acme.certs.${cfg.realm}.directory}/full.pem"; + pkey = + mkIf (!cfg.no-tls && cfg.sops) + "${config.security.acme.certs.${cfg.realm}.directory}/key.pem"; + + extraConfig = '' + # ban private IP ranges + no-multicast-peers + denied-peer-ip=0.0.0.0-0.255.255.255 + denied-peer-ip=10.0.0.0-10.255.255.255 + denied-peer-ip=100.64.0.0-100.127.255.255 + denied-peer-ip=127.0.0.0-127.255.255.255 + denied-peer-ip=169.254.0.0-169.254.255.255 + denied-peer-ip=172.16.0.0-172.31.255.255 + denied-peer-ip=192.0.0.0-192.0.0.255 + denied-peer-ip=192.0.2.0-192.0.2.255 + denied-peer-ip=192.88.99.0-192.88.99.255 + denied-peer-ip=192.168.0.0-192.168.255.255 + denied-peer-ip=198.18.0.0-198.19.255.255 + denied-peer-ip=198.51.100.0-198.51.100.255 + denied-peer-ip=203.0.113.0-203.0.113.255 + denied-peer-ip=240.0.0.0-255.255.255.255 + denied-peer-ip=::1 + denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff + denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255 + denied-peer-ip=100::-100::ffff:ffff:ffff:ffff + denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff + denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff + denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff + denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff + '' + + optionalString cfg.debug '' + # debugging + syslog + verbose + ''; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedUDPPortRanges = [ + { + from = cfg.min-port; + to = cfg.max-port; + } + ]; + allowedUDPPorts = [ + cfg.listening-port + cfg.alt-listening-port + ] + ++ optionals (!cfg.no-tls) [ + cfg.tls-listening-port + cfg.alt-tls-listening-port + ]; + allowedTCPPorts = [ + cfg.listening-port + cfg.alt-listening-port + ] + ++ optionals (!cfg.no-tls) [ + cfg.tls-listening-port + cfg.alt-tls-listening-port + ]; + }; + + security.acme.certs = mkIf (!cfg.no-tls) { + ${cfg.realm} = { + postRun = "systemctl restart coturn.service"; + group = "turnserver"; + }; + }; + + sops = mkIf cfg.sops { + secrets."coturn/static-auth-secret" = { + owner = "turnserver"; + group = "turnserver"; + mode = "0400"; + }; + }; + }; +} diff --git a/modules/nixos/default.nix b/modules/nixos/default.nix new file mode 100644 index 0000000..094f15e --- /dev/null +++ b/modules/nixos/default.nix @@ -0,0 +1,36 @@ +{ + amd = import ./amd; + audio = import ./audio; + baibot = import ./baibot; + bluetooth = import ./bluetooth; + cifsMount = import ./cifsMount; + common = import ./common; + coturn = import ./coturn; + device = import ./device; + ftp-webserver = import ./ftp-webserver; + headplane = import ./headplane; + headscale = import ./headscale; + hyprland = import ./hyprland; + i2pd = import ./i2pd; + jellyfin = import ./jellyfin; + jirafeau = import ./jirafeau; + mailserver = import ./mailserver; + matrix-synapse = import ./matrix-synapse; + maubot = import ./maubot; + mcpo = import ./mcpo; + miniflux = import ./miniflux; + nginx = import ./nginx; + normalUsers = import ./normalUsers; + nvidia = import ./nvidia; + ollama = import ./ollama; + open-webui-oci = import ./open-webui-oci; + openssh = import ./openssh; + print-server = import ./print-server; + radicale = import ./radicale; + rss-bridge = import ./rss-bridge; + sops = import ./sops; + tailscale = import ./tailscale; + virtualisation = import ./virtualisation; + webPage = import ./webPage; + windows-oci = import ./windows-oci; +} diff --git a/modules/nixos/device/default.nix b/modules/nixos/device/default.nix new file mode 100644 index 0000000..6186fa2 --- /dev/null +++ b/modules/nixos/device/default.nix @@ -0,0 +1,6 @@ +{ + desktop = import ./desktop.nix; + laptop = import ./laptop.nix; + server = import ./server.nix; + vm = import ./vm.nix; +} diff --git a/modules/nixos/device/desktop.nix b/modules/nixos/device/desktop.nix new file mode 100644 index 0000000..dd2784f --- /dev/null +++ b/modules/nixos/device/desktop.nix @@ -0,0 +1,8 @@ +{ + imports = [ + ../audio + ]; + + # improve desktop responsiveness when updating the system + nix.daemonCPUSchedPolicy = "idle"; +} diff --git a/modules/nixos/device/laptop.nix b/modules/nixos/device/laptop.nix new file mode 100644 index 0000000..b007204 --- /dev/null +++ b/modules/nixos/device/laptop.nix @@ -0,0 +1,33 @@ +{ pkgs, lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + imports = [ + ./desktop.nix + ../bluetooth + ]; + + services.tlp = { + enable = mkDefault true; + settings = { + CPU_SCALING_GOVERNOR_ON_AC = mkDefault "performance"; + CPU_SCALING_GOVERNOR_ON_BAT = mkDefault "powersave"; + + CPU_ENERGY_PERF_POLICY_ON_BAT = mkDefault "power"; + CPU_ENERGY_PERF_POLICY_ON_AC = mkDefault "performance"; + + PLATFORM_PROFILE_ON_AC = mkDefault "performance"; + PLATFORM_PROFILE_ON_BAT = mkDefault "low-power"; + + START_CHARGE_THRESH_BAT0 = mkDefault 75; + STOP_CHARGE_THRESH_BAT0 = mkDefault 80; + }; + }; + + # avoid conflicts with tlp + services.power-profiles-daemon.enable = mkDefault false; + + environment.systemPackages = [ pkgs.powertop ]; +} diff --git a/modules/nixos/device/server.nix b/modules/nixos/device/server.nix new file mode 100644 index 0000000..d8d1b7d --- /dev/null +++ b/modules/nixos/device/server.nix @@ -0,0 +1,69 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (config.programs) neovim; + inherit (lib) mkDefault mkIf; +in +{ + environment = { + variables = { + BROWSER = "echo"; + EDITOR = mkIf neovim.enable "nvim"; + VISUAL = mkIf neovim.enable "nvim"; + }; + shellAliases = { + v = mkIf neovim.enable "nvim"; + }; + # do not install /lib/ld-linux.so.2 and /lib64/ld-linux-x86-64.so.2 + stub-ld.enable = mkDefault false; + }; + + documentation = { + enable = mkDefault false; + nixos.enable = mkDefault false; + doc.enable = mkDefault false; + info.enable = mkDefault false; + man.enable = mkDefault false; + }; + + fonts.fontconfig.enable = mkDefault false; + + xdg.autostart.enable = mkDefault false; + xdg.icons.enable = mkDefault false; + xdg.menus.enable = mkDefault false; + xdg.mime.enable = mkDefault false; + xdg.sounds.enable = mkDefault false; + + programs.git.package = mkDefault pkgs.gitMinimal; + + programs.neovim = { + enable = mkDefault true; + defaultEditor = mkDefault true; + vimAlias = mkDefault true; + viAlias = mkDefault true; + }; + + # emergency mode is useless on headless machines + systemd.enableEmergencyMode = false; + boot.initrd.systemd.suppressedUnits = mkIf config.systemd.enableEmergencyMode [ + "emergency.service" + "emergency.target" + ]; + + systemd.sleep.extraConfig = '' + AllowSuspend=no + AllowHibernation=no + ''; + + # force reboots + systemd.settings.Manager = { + RuntimeWatchdogSec = mkDefault "15s"; + RebootWatchdogSec = mkDefault "30s"; + KExecWatchdogSec = mkDefault "1m"; + }; +} diff --git a/modules/nixos/device/vm.nix b/modules/nixos/device/vm.nix new file mode 100644 index 0000000..c76b5a3 --- /dev/null +++ b/modules/nixos/device/vm.nix @@ -0,0 +1,10 @@ +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + services.qemuGuest.enable = mkDefault true; + services.spice-vdagentd.enable = mkDefault true; + services.spice-webdavd.enable = mkDefault true; +} diff --git a/modules/nixos/ftp-webserver/default.nix b/modules/nixos/ftp-webserver/default.nix new file mode 100644 index 0000000..8a7753c --- /dev/null +++ b/modules/nixos/ftp-webserver/default.nix @@ -0,0 +1,54 @@ +{ config, lib, ... }: + +let + cfg = config.services.ftp-webserver; + domain = config.networking.domain; + fqdn = if (cfg.subdomain != "") then "${cfg.subdomain}.${domain}" else domain; + nginx = config.services.nginx; + + inherit (lib) + mkEnableOption + mkIf + mkOption + types + ; +in +{ + options.services.ftp-webserver = { + enable = mkEnableOption "FTP webserver."; + subdomain = mkOption { + type = types.str; + default = "ftp"; + description = "Subdomain for Nginx virtual host. Leave empty for root domain."; + }; + forceSSL = mkOption { + type = types.bool; + default = true; + description = "Force SSL for Nginx virtual host."; + }; + root = mkOption { + type = types.str; + default = "/srv/www"; + description = "Root directory for the FTP webserver."; + }; + }; + + config = mkIf cfg.enable { + services.nginx.virtualHosts."${fqdn}" = { + root = cfg.root; + locations."/" = { + extraConfig = '' + autoindex on; + autoindex_exact_size off; + autoindex_localtime on; + ''; + }; + forceSSL = cfg.forceSSL; + enableACME = cfg.forceSSL; + sslCertificate = mkIf cfg.forceSSL "${config.security.acme.certs."${fqdn}".directory}/cert.pem"; + sslCertificateKey = mkIf cfg.forceSSL "${config.security.acme.certs."${fqdn}".directory}/key.pem"; + }; + + systemd.tmpfiles.rules = [ "d ${cfg.root} 0755 ${nginx.user} ${nginx.group}" ]; + }; +} diff --git a/modules/nixos/headplane/default.nix b/modules/nixos/headplane/default.nix new file mode 100644 index 0000000..017c4e3 --- /dev/null +++ b/modules/nixos/headplane/default.nix @@ -0,0 +1,78 @@ +{ + inputs, + config, + lib, + ... +}: + +let + cfg = config.services.headplane; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + headscale = config.services.headscale; + + inherit (lib) + mkDefault + mkIf + ; + + inherit (lib.utils) + mkReverseProxyOption + mkVirtualHost + ; +in +{ + imports = [ inputs.headplane.nixosModules.headplane ]; + + options.services.headplane = { + reverseProxy = mkReverseProxyOption "Headplane" "hp"; + }; + + config = mkIf cfg.enable { + nixpkgs.overlays = [ + inputs.headplane.overlays.default + ]; + + services.headplane = { + settings = { + server = { + host = mkDefault (if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0"); + port = mkDefault 3000; + cookie_secret_path = config.sops.secrets."headplane/cookie_secret".path; + }; + headscale = { + url = "http://127.0.0.1:${toString headscale.port}"; + public_url = headscale.settings.server_url; + config_path = "/etc/headscale/config.yaml"; + }; + integration.agent = { + enabled = mkDefault true; + pre_authkey_path = config.sops.secrets."headplane/agent_pre_authkey".path; + }; + }; + }; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${fqdn}" = mkVirtualHost { + port = cfg.settings.server.port; + ssl = cfg.reverseProxy.forceSSL; + }; + }; + + sops.secrets = + let + owner = headscale.user; + group = headscale.group; + mode = "0400"; + in + { + "headplane/cookie_secret" = { + inherit owner group mode; + }; + "headplane/agent_pre_authkey" = { + inherit owner group mode; + }; + }; + }; +} diff --git a/modules/nixos/headscale/acl.hujson b/modules/nixos/headscale/acl.hujson new file mode 100644 index 0000000..e975c9e --- /dev/null +++ b/modules/nixos/headscale/acl.hujson @@ -0,0 +1,17 @@ +{ + "acls": [ + { + "action": "accept", + "src": ["*"], + "dst": ["*:*"] + } + ], + "ssh": [ + { + "action": "accept", + "src": ["autogroup:member"], + "dst": ["autogroup:member"], + "users": ["autogroup:nonroot", "root"] + } + ] +} diff --git a/modules/nixos/headscale/default.nix b/modules/nixos/headscale/default.nix new file mode 100644 index 0000000..d1591e9 --- /dev/null +++ b/modules/nixos/headscale/default.nix @@ -0,0 +1,105 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.services.headscale; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + acl = "headscale/acl.hujson"; + + inherit (lib) + mkDefault + mkIf + mkOption + optional + optionals + types + ; + + inherit (lib.utils) + mkReverseProxyOption + mkUrl + mkVirtualHost + ; +in +{ + options.services.headscale = { + reverseProxy = mkReverseProxyOption "Headscale" "hs"; + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to automatically open firewall ports. TCP: 80, 443; UDP: 3478."; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = !cfg.settings.derp.server.enable || cfg.reverseProxy.forceSSL; + message = "synix/nixos/headscale: DERP requires TLS"; + } + { + assertion = fqdn != cfg.settings.dns.base_domain; + message = "synix/nixos/headscale: `settings.server_url` must be different from `settings.dns.base_domain`"; + } + { + assertion = !cfg.settings.dns.override_local_dns || cfg.settings.dns.nameservers.global != [ ]; + message = "synix/nixos/headscale: `settings.dns.nameservers.global` must be set when `settings.dns.override_local_dns` is true"; + } + ]; + + environment.etc.${acl} = { + inherit (config.services.headscale) user group; + source = ./acl.hujson; + }; + + environment.shellAliases = { + hs = "${cfg.package}/bin/headscale"; + }; + + services.headscale = { + address = mkDefault (if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0"); + port = mkDefault 8077; + settings = { + policy.path = "/etc/${acl}"; + database.type = "sqlite"; # postgres is highly discouraged as it is only supported for legacy reasons + server_url = mkUrl { + inherit fqdn; + ssl = with cfg.reverseProxy; enable && forceSSL; + }; + derp.server.enable = cfg.reverseProxy.forceSSL; + dns = { + magic_dns = mkDefault true; + base_domain = mkDefault "tail"; + search_domains = [ cfg.settings.dns.base_domain ]; + override_local_dns = mkDefault true; + nameservers.global = optionals cfg.settings.dns.override_local_dns [ + "1.1.1.1" + "1.0.0.1" + "2606:4700:4700::1111" + "2606:4700:4700::1001" + ]; + }; + }; + }; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${fqdn}" = mkVirtualHost { + inherit (cfg) address port; + ssl = cfg.reverseProxy.forceSSL; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ + 80 + 443 + ]; + allowedUDPPorts = optional cfg.settings.derp.server.enable 3478; + }; + }; +} diff --git a/modules/nixos/hyprland/default.nix b/modules/nixos/hyprland/default.nix new file mode 100644 index 0000000..9b33ce5 --- /dev/null +++ b/modules/nixos/hyprland/default.nix @@ -0,0 +1,27 @@ +{ pkgs, lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + programs.hyprland.enable = mkDefault true; + + programs.dconf.enable = true; # fixes nixvim hm module + + xdg.portal = { + enable = true; + extraPortals = with pkgs; [ + xdg-desktop-portal-gtk + ]; + }; + + services.gnome.gnome-keyring.enable = true; + security.pam.services = { + login = { + enableGnomeKeyring = true; + }; + hyprlock = { }; + }; + + services.udisks2.enable = mkDefault true; +} diff --git a/modules/nixos/i2pd/default.nix b/modules/nixos/i2pd/default.nix new file mode 100644 index 0000000..c9f3e46 --- /dev/null +++ b/modules/nixos/i2pd/default.nix @@ -0,0 +1,48 @@ +# TODO: HM config for i2p profile in LibreWolf + +{ config, lib, ... }: + +let + cfg = config.services.i2pd; + + inherit (lib) + mkDefault + mkIf + mkOption + optional + types + ; +in +{ + options.services.i2pd = { + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the necessary firewall ports for enabled protoclos of i2pd."; + }; + }; + + config = mkIf cfg.enable { + services.i2pd = { + address = mkDefault "127.0.0.1"; + proto = { + http.enable = mkDefault true; + socksProxy.enable = mkDefault true; + httpProxy.enable = mkDefault true; + sam.enable = mkDefault true; + i2cp.enable = mkDefault true; + }; + }; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall ( + with cfg.proto; + optional bob.enable bob.port + ++ optional http.enable http.port + ++ optional httpProxy.enable httpProxy.port + ++ optional i2cp.enable i2cp.port + ++ optional i2pControl.enable i2pControl.port + ++ optional sam.enable sam.port + ++ optional socksProxy.enable socksProxy.port + ); + }; +} diff --git a/modules/nixos/jellyfin/default.nix b/modules/nixos/jellyfin/default.nix new file mode 100644 index 0000000..afe0d1b --- /dev/null +++ b/modules/nixos/jellyfin/default.nix @@ -0,0 +1,66 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.jellyfin; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + + inherit (lib) + mkDefault + mkIf + mkOption + types + ; + + inherit (lib.utils) + mkReverseProxyOption + mkVirtualHost + ; +in +{ + options.services.jellyfin = { + reverseProxy = mkReverseProxyOption "Jellyfin" "jf"; + libraries = mkOption { + type = types.listOf types.str; + default = [ + "movies" + "music" + "shows" + ]; + description = "A list of library names. Directories for these will be created under ${cfg.dataDir}/libraries."; + }; + }; + + config = mkIf cfg.enable { + services.jellyfin = { + openFirewall = mkDefault false; + }; + + environment.systemPackages = with pkgs; [ + jellyfin-web + jellyfin-ffmpeg + ]; + + systemd.tmpfiles.rules = + (map ( + library: "d ${cfg.dataDir}/libraries/${library} 0770 ${cfg.user} ${cfg.group} -" + ) cfg.libraries) + ++ [ + "z ${cfg.dataDir} 0770 ${cfg.user} ${cfg.group} -" + "Z ${cfg.dataDir}/libraries 0770 ${cfg.user} ${cfg.group} -" + ]; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${fqdn}" = mkVirtualHost { + port = 8096; + ssl = cfg.reverseProxy.forceSSL; + }; + }; + }; +} diff --git a/modules/nixos/jirafeau/default.nix b/modules/nixos/jirafeau/default.nix new file mode 100644 index 0000000..498c9a3 --- /dev/null +++ b/modules/nixos/jirafeau/default.nix @@ -0,0 +1,44 @@ +{ config, lib, ... }: + +let + cfg = config.services.jirafeau; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + + inherit (lib) + mkDefault + mkIf + ; + + inherit (lib.utils) + mkReverseProxyOption + ; +in +{ + options.services.jirafeau = { + reverseProxy = mkReverseProxyOption "Jirafeau" "share"; + }; + + config = mkIf cfg.enable { + services.jirafeau = { + hostName = fqdn; + extraConfig = mkDefault '' + $cfg['style'] = 'dark-courgette'; + $cfg['maximal_upload_size'] = 4096; + ''; + nginxConfig = { + enableACME = if cfg.reverseProxy.enable then cfg.reverseProxy.forceSSL else mkDefault false; + forceSSL = if cfg.reverseProxy.enable then cfg.reverseProxy.forceSSL else mkDefault false; + listenAddresses = mkDefault [ "0.0.0.0" ]; # FIXME: 127.0.0.1 does not work + serverName = fqdn; + sslCertificate = + mkIf (with cfg.reverseProxy; enable && forceSSL) + "${config.security.acme.certs."${fqdn}".directory}/cert.pem"; + sslCertificateKey = + mkIf (with cfg.reverseProxy; enable && forceSSL) + "${config.security.acme.certs."${fqdn}".directory}/key.pem"; + }; + }; + }; +} diff --git a/modules/nixos/mailserver/default.nix b/modules/nixos/mailserver/default.nix new file mode 100644 index 0000000..0ad65bd --- /dev/null +++ b/modules/nixos/mailserver/default.nix @@ -0,0 +1,104 @@ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + cfg = config.mailserver; + domain = config.networking.domain; + fqdn = "${cfg.subdomain}.${domain}"; + + inherit (lib) + mapAttrs' + mkDefault + mkIf + mkOption + nameValuePair + types + ; +in +{ + imports = [ inputs.nixos-mailserver.nixosModules.mailserver ]; + + options.mailserver = { + subdomain = mkOption { + type = types.str; + default = "mail"; + description = "Subdomain for rDNS"; + }; + accounts = mkOption { + type = types.attrsOf ( + types.submodule { + options = { + aliases = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "A list of aliases of this account. `@domain` will be appended automatically."; + }; + sendOnly = mkOption { + type = types.bool; + default = false; + description = "Specifies if the account should be a send-only account."; + }; + }; + } + ); + default = { }; + description = '' + This options wraps `loginAccounts`. + `loginAccounts..name` will be automatically set to `@`. + ''; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.subdomain != ""; + message = "synix/nixos/mailserver: config.mailserver.subdomain cannot be empty."; + } + ]; + + mailserver = { + inherit fqdn; + + domains = mkDefault [ domain ]; + certificateScheme = mkDefault "acme-nginx"; + stateVersion = mkDefault 1; + + loginAccounts = mapAttrs' ( + user: accConf: + nameValuePair "${user}@${domain}" { + name = "${user}@${domain}"; + aliases = map (alias: "${alias}@${domain}") (accConf.aliases or [ ]); + sendOnly = accConf.sendOnly; + quota = mkDefault "5G"; + hashedPasswordFile = config.sops.secrets."mailserver/accounts/${user}".path; + } + ) cfg.accounts; + }; + + security.acme = { + acceptTerms = true; + defaults.email = "postmaster@${domain}"; + defaults.webroot = "/var/lib/acme/acme-challenge"; + }; + + environment.systemPackages = [ pkgs.mailutils ]; + + sops = { + secrets = mapAttrs' ( + user: _config: + nameValuePair "mailserver/accounts/${user}" { + restartUnits = [ + "postfix.service" + "dovecot.service" + ]; + } + ) cfg.accounts; + }; + }; +} diff --git a/modules/nixos/matrix-synapse/bridges.nix b/modules/nixos/matrix-synapse/bridges.nix new file mode 100644 index 0000000..b2d48db --- /dev/null +++ b/modules/nixos/matrix-synapse/bridges.nix @@ -0,0 +1,151 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.matrix-synapse; + + mkMautrixBridgeOptions = name: pkgName: { + enable = mkEnableOption "Mautrix-${name} for your Matrix-Synapse instance."; + package = mkPackageOption pkgs pkgName { }; + admin = mkOption { + type = types.str; + description = "The user to give admin permissions to."; + example = "@admin:example.com"; + }; + }; + + mkMautrixBridge = name: port: { + environment.systemPackages = [ cfg.bridges.${name}.package ]; + + services."mautrix-${name}" = { + enable = true; + package = cfg.bridges.${name}.package; + environmentFile = mkIf cfg.sops config.sops.templates."mautrix-${name}/env-file".path; + settings = { + bridge = { + permissions = { + "*" = "relay"; + "${cfg.settings.server_name}" = "user"; + "${cfg.bridges.${name}.admin}" = "admin"; + }; + }; + homeserver = { + address = "http://localhost:${toString cfg.port}"; + domain = cfg.settings.server_name; + }; + appservice = { + address = "http://localhost:${toString port}"; + public_address = cfg.settings.public_baseurl; + hostname = "localhost"; + inherit port; + }; + provisioning.shared_secret = "$MAUTRIX_${toUpper name}_PROVISIONING_SHARED_SECRET"; + public_media = { + enabled = false; + signing_key = "$MAUTRIX_${toUpper name}_PUBLIC_MEDIA_SIGNING_KEY"; + }; + direct_media = { + enabled = false; + server_key = "$MAUTRIX_${toUpper name}_DIRECT_MEDIA_SERVER_KEY"; + }; + backfill = { + enabled = true; + }; + encryption = { + allow = true; + default = true; + require = false; + pickle_key = "$MAUTRIX_${toUpper name}_ENCRYPTION_PICKLE_KEY"; + }; + }; + }; + + sops = mkIf cfg.sops ( + let + owner = "mautrix-${name}"; + group = "mautrix-${name}"; + mode = "0400"; + in + { + secrets."mautrix-${name}/encryption-pickle-key" = { + inherit owner group mode; + }; + secrets."mautrix-${name}/provisioning-shared-secret" = { + inherit owner group mode; + }; + secrets."mautrix-${name}/public-media-signing-key" = { + inherit owner group mode; + }; + secrets."mautrix-${name}/direct-media-server-key" = { + inherit owner group mode; + }; + templates."mautrix-${name}/env-file" = { + inherit owner group mode; + content = '' + MAUTRIX_${toUpper name}_ENCRYPTION_PICKLE_KEY=${ + config.sops.placeholder."mautrix-${name}/encryption-pickle-key" + } + MAUTRIX_${toUpper name}_PROVISIONING_SHARED_SECRET=${ + config.sops.placeholder."mautrix-${name}/provisioning-shared-secret" + } + MAUTRIX_${toUpper name}_PUBLIC_MEDIA_SIGNING_KEY=${ + config.sops.placeholder."mautrix-${name}/public-media-signing-key" + } + MAUTRIX_${toUpper name}_DIRECT_MEDIA_SERVER_KEY=${ + config.sops.placeholder."mautrix-${name}/direct-media-server-key" + } + ''; + }; + } + ); + + }; + + inherit (lib) + mkEnableOption + mkIf + mkMerge + mkOption + mkPackageOption + toUpper + types + ; + + inherit (builtins) toString; +in +{ + options.services.matrix-synapse = { + bridges = { + whatsapp = mkMautrixBridgeOptions "WhatsApp" "mautrix-whatsapp"; + signal = mkMautrixBridgeOptions "Signal" "mautrix-signal"; + }; + }; + + config = mkMerge [ + (mkIf cfg.bridges.whatsapp.enable (mkMautrixBridge "whatsapp" 29318)) + (mkIf cfg.bridges.whatsapp.enable { + services.mautrix-whatsapp = { + settings = { + network = { + displayname_template = "{{or .BusinessName .PushName .Phone}} (WA)"; + history_sync.request_full_sync = true; + }; + }; + }; + }) + (mkIf cfg.bridges.signal.enable (mkMautrixBridge "signal" 29328)) + (mkIf cfg.bridges.signal.enable { + services.mautrix-signal = { + settings = { + network = { + displayname_template = "{{or .ProfileName .PhoneNumber \"Unknown user\" }} (S)"; + }; + }; + }; + }) + ]; +} diff --git a/modules/nixos/matrix-synapse/default.nix b/modules/nixos/matrix-synapse/default.nix new file mode 100644 index 0000000..542094f --- /dev/null +++ b/modules/nixos/matrix-synapse/default.nix @@ -0,0 +1,189 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.matrix-synapse; + baseUrl = "https://${cfg.settings.server_name}"; + + baseClientConfig = { + "m.homeserver".base_url = baseUrl; + "m.identity_server".base_url = "https://vector.im"; + }; + + livekitConfig = optionalAttrs config.services.livekit.enable { + "org.matrix.msc3575.proxy".url = baseUrl; + "org.matrix.msc4143.rtc_foci" = [ + { + type = "livekit"; + livekit_service_url = baseUrl + "/livekit/jwt"; + } + ]; + }; + + clientConfig = baseClientConfig // livekitConfig; + + serverConfig."m.server" = "${cfg.settings.server_name}:443"; + + mkWellKnown = data: '' + default_type application/json; + add_header Access-Control-Allow-Origin *; + return 200 '${builtins.toJSON data}'; + ''; + + inherit (lib) + mkEnableOption + mkIf + mkMerge + mkOption + optionalAttrs + types + ; +in +{ + imports = [ + ./bridges.nix + ./livekit.nix + ]; + + options.services.matrix-synapse = { + port = mkOption { + type = types.nullOr types.port; + default = 8008; + description = '' + The port to listen for HTTP(S) requests on (will be applied to a listener). + ''; + }; + coturn = { + enable = mkEnableOption "Coturn integration for Matrix Synapse."; + realm = mkOption { + type = types.str; + default = "turn.${config.networking.domain}"; + description = "Realm for the coturn server used by Matrix Synapse."; + }; + listening-port = mkOption { + type = types.port; + default = 3478; + }; + tls-listening-port = mkOption { + type = types.port; + default = 5349; + }; + alt-listening-port = mkOption { + type = types.port; + default = 3479; + }; + alt-tls-listening-port = mkOption { + type = types.port; + default = 5350; + }; + }; + sops = mkEnableOption "SOPS integration"; + }; + + config = mkIf cfg.enable { + services.postgresql = { + enable = true; + initialScript = pkgs.writeText "synapse-init.sql" '' + CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse'; + CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse" + TEMPLATE template0 + LC_COLLATE = 'C' + LC_CTYPE = 'C'; + ''; + }; + + services.matrix-synapse = mkMerge [ + { + settings = { + registration_shared_secret_path = + mkIf cfg.sops + config.sops.secrets."matrix/registration-shared-secret".path; + server_name = config.networking.domain; + public_baseurl = baseUrl; + listeners = [ + { + inherit (cfg) port; + bind_addresses = [ "127.0.0.1" ]; + resources = [ + { + compress = true; + names = [ "client" ]; + } + { + compress = false; + names = [ "federation" ]; + } + ]; + tls = false; + type = "http"; + x_forwarded = true; + } + ]; + }; + } + (mkIf cfg.coturn.enable { + settings = { + turn_uris = with cfg.coturn; [ + "turn:${realm}:${toString listening-port}?transport=udp" + "turn:${realm}:${toString listening-port}?transport=tcp" + "turn:${realm}:${toString tls-listening-port}?transport=udp" + "turn:${realm}:${toString tls-listening-port}?transport=tcp" + "turn:${realm}:${toString alt-listening-port}?transport=udp" + "turn:${realm}:${toString alt-listening-port}?transport=tcp" + "turn:${realm}:${toString alt-tls-listening-port}?transport=udp" + "turn:${realm}:${toString alt-tls-listening-port}?transport=tcp" + ]; + extraConfigFiles = mkIf cfg.sops [ config.sops.templates."coturn/static-auth-secret.env".path ]; + turn_user_lifetime = "1h"; + }; + }) + ]; + + environment.shellAliases = mkIf cfg.sops { + register_new_matrix_user = "${cfg.package}/bin/register_new_matrix_user -k $(sudo cat ${cfg.settings.registration_shared_secret_path})"; + }; + + services.nginx.virtualHosts."${cfg.settings.server_name}" = { + enableACME = true; + forceSSL = true; + + locations."= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig; + locations."= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig; + + locations."/_matrix".proxyPass = "http://127.0.0.1:${toString cfg.port}"; + locations."/_synapse".proxyPass = "http://127.0.0.1:${toString cfg.port}"; + }; + + sops = mkIf cfg.sops { + secrets = mkMerge [ + { + "matrix/registration-shared-secret" = { + owner = "matrix-synapse"; + group = "matrix-synapse"; + mode = "0400"; + }; + } + (mkIf cfg.coturn.enable { + "coturn/static-auth-secret" = { + owner = "turnserver"; + group = "turnserver"; + mode = "0400"; + }; + }) + ]; + templates = mkIf cfg.coturn.enable { + "coturn/static-auth-secret.env" = { + owner = "matrix-synapse"; + group = "matrix-synapse"; + mode = "0400"; + content = '' + static-auth-secret=${config.sops.placeholder."coturn/static-auth-secret"} + ''; + }; + }; + }; + }; +} diff --git a/modules/nixos/matrix-synapse/livekit.nix b/modules/nixos/matrix-synapse/livekit.nix new file mode 100644 index 0000000..c4850c6 --- /dev/null +++ b/modules/nixos/matrix-synapse/livekit.nix @@ -0,0 +1,61 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.services.matrix-synapse; + domain = config.networking.domain; + + inherit (lib) mkIf mkDefault; +in +{ + config = mkIf cfg.enable { + services.livekit = { + enable = true; + settings.port = mkDefault 7880; + settings.room.auto_create = mkDefault false; + openFirewall = mkDefault true; + keyFile = mkIf cfg.sops config.sops.templates."livekit/key".path; + }; + + services.lk-jwt-service = { + enable = true; + port = mkDefault 8080; + livekitUrl = "wss://${domain}/livekit/sfu"; + keyFile = mkIf cfg.sops config.sops.templates."livekit/key".path; + }; + + systemd.services.lk-jwt-service.environment.LIVEKIT_FULL_ACCESS_HOMESERVERS = domain; + + services.nginx.virtualHosts = { + "${domain}".locations = { + "^~ /livekit/jwt/" = { + priority = 400; + proxyPass = "http://127.0.0.1:${toString config.services.lk-jwt-service.port}/"; + }; + "^~ /livekit/sfu/" = { + priority = 400; + proxyPass = "http://127.0.0.1:${toString config.services.livekit.settings.port}/"; + proxyWebsockets = true; + extraConfig = '' + proxy_send_timeout 120; + proxy_read_timeout 120; + proxy_buffering off; + proxy_set_header Accept-Encoding gzip; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + ''; + }; + }; + }; + + sops = mkIf cfg.sops { + secrets."livekit/key" = { }; + templates."livekit/key".content = '' + API Secret: ${config.sops.placeholder."livekit/key"} + ''; + }; + }; +} diff --git a/modules/nixos/maubot/default.nix b/modules/nixos/maubot/default.nix new file mode 100644 index 0000000..cfbd96c --- /dev/null +++ b/modules/nixos/maubot/default.nix @@ -0,0 +1,110 @@ +{ config, lib, ... }: + +let + cfg = config.services.maubot; + user = config.users.users.maubot; + synapse = config.services.matrix-synapse; + + inherit (lib) + concatLines + mkEnableOption + mkIf + mkOption + optionalString + types + ; + inherit (builtins) toString listToAttrs; +in +{ + options.services.maubot = { + admins = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "alice" + "bob" + ]; + description = "List of admin users for Maubot. Each admin must have a corresponding entry in the SOPS file under 'maubot/admins/' containing their password"; + }; + sops = mkEnableOption "SOPS integration"; + }; + + config = mkIf cfg.enable { + services.maubot = { + extraConfigFile = mkIf cfg.sops config.sops.templates."maubot/extra-config-file".path; + settings = { + server = { + port = 29316; + public_url = synapse.settings.public_baseurl; + }; + plugin_directories = with user; { + upload = home + "/plugins"; + load = [ (home + "/plugins") ]; + trash = home + "/trash"; + }; + plugin_databases = with user; { + sqlite = home + "/plugins"; + }; + # FIXME: ValueError: dictionary doesn't specify a version + # logging = with user; { + # handlers.file.filename = home + "/maubot.log"; + # }; + }; + }; + + environment.systemPackages = [ + cfg.package + ]; + + systemd.tmpfiles.rules = [ + "d ${cfg.dataDir} 0755 ${user.name} ${user.group} -" + "d ${cfg.settings.plugin_directories.upload} 0755 ${user.name} ${user.group} -" + "d ${cfg.settings.plugin_directories.trash} 0755 ${user.name} ${user.group} -" + ]; + + services.nginx.virtualHosts."${synapse.settings.server_name}".locations = { + "^~ /_matrix/maubot/" = { + proxyPass = with cfg.settings.server; "http://${hostname}:${toString port}"; + proxyWebsockets = true; + }; + "^~ /_matrix/maubot/v1/logs" = { + proxyPass = with cfg.settings.server; "http://${hostname}:${toString port}"; + proxyWebsockets = true; + }; + }; + + sops = mkIf cfg.sops ( + let + owner = user.name; + group = user.group; + mode = "0400"; + in + { + secrets = listToAttrs ( + map (admin: { + name = "maubot/admins/${admin}"; + value = { inherit owner group mode; }; + }) cfg.admins + ); + templates."maubot/extra-config-file" = { + inherit owner group mode; + content = '' + homeservers: + ${synapse.settings.server_name}: + url: http://127.0.0.1:${toString synapse.port} + secret: ${config.sops.placeholder."matrix/registration-shared-secret"} + '' + + optionalString (cfg.admins != [ ]) ( + '' + admins: + '' + + concatLines ( + map (admin: " ${admin}: ${config.sops.placeholder."maubot/admins/${admin}"}") cfg.admins + ) + ); + }; + } + ); + }; + +} diff --git a/modules/nixos/mcpo/default.nix b/modules/nixos/mcpo/default.nix new file mode 100644 index 0000000..89e0e29 --- /dev/null +++ b/modules/nixos/mcpo/default.nix @@ -0,0 +1,111 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.mcpo; + + configFile = pkgs.writeText "mcpo-config.json" (builtins.toJSON cfg.settings); + + inherit (lib) + getExe + mkEnableOption + mkIf + mkOption + types + ; +in +{ + options.services.mcpo = { + enable = mkEnableOption "mcpo, an MCP-to-OpenAPI proxy server."; + + package = mkOption { + type = types.nullOr types.package; + description = "The package to use for mcpo. You have to specify this manually."; + default = null; + }; + + user = mkOption { + type = types.str; + description = "The user the mcpo service will run as."; + default = "mcpo"; + }; + + group = mkOption { + type = types.str; + description = "The group the mcpo service will run as."; + default = "mcpo"; + }; + + workDir = mkOption { + type = types.str; + description = "The working directory for the mcpo service."; + default = "/var/lib/mcpo"; + }; + + settings = mkOption { + type = types.attrs; + description = "A set of attributes that will be translated into the JSON configuration file for mcpo. It follows the Claude Desktop format."; + default = { }; + example = { + mcpServers = { + memory = { + command = "npx"; + args = [ + "-y" + "@modelcontextprotocol/server-memory" + ]; + }; + time = { + command = "uvx"; + args = [ + "mcp-server-time" + "--local-timezone=America/New_York" + ]; + }; + mcp_sse = { + type = "sse"; + url = "http://127.0.0.1:8001/sse"; + headers = { + Authorization = "Bearer token"; + X-Custom-Header = "value"; + }; + }; + mcp_streamable_http = { + type = "streamable_http"; + url = "http://127.0.0.1:8002/mcp"; + }; + }; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.tmpfiles.rules = [ + "d ${cfg.workDir} - ${cfg.user} ${cfg.group} - -" + ]; + + users.users."${cfg.user}" = { + isSystemUser = true; + group = cfg.group; + }; + + users.groups."${cfg.group}" = { }; + + systemd.services.mcpo = { + description = "Service for mcpo, an MCP-to-OpenAPI proxy server."; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = "${getExe cfg.package} --config ${configFile}"; + Restart = "on-failure"; + User = cfg.user; + Group = cfg.group; + WorkingDirectory = cfg.workDir; + }; + }; + }; +} diff --git a/modules/nixos/miniflux/default.nix b/modules/nixos/miniflux/default.nix new file mode 100644 index 0000000..be6a260 --- /dev/null +++ b/modules/nixos/miniflux/default.nix @@ -0,0 +1,57 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.services.miniflux; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + port = 8085; + + inherit (lib) + mkDefault + mkIf + ; + + inherit (lib.utils) + mkReverseProxyOption + mkVirtualHost + ; +in +{ + options.services.miniflux = { + reverseProxy = mkReverseProxyOption "Miniflux" "rss"; + }; + + config = mkIf cfg.enable { + services.miniflux = { + adminCredentialsFile = config.sops.templates."miniflux/admin-credentials".path; + createDatabaseLocally = mkDefault true; + config = { + ADMIN_USERNAME = mkDefault "admin"; + CREATE_ADMIN = mkDefault 1; + PORT = mkDefault port; # overrides LISTEN_ADDR to "0.0.0.0:${PORT}" + }; + }; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${fqdn}" = mkVirtualHost { + port = cfg.config.PORT; + ssl = cfg.reverseProxy.forceSSL; + }; + }; + + sops = { + secrets."miniflux/admin-password" = { }; + templates."miniflux/admin-credentials" = { + content = '' + ADMIN_USERNAME=${cfg.config.ADMIN_USERNAME} + ADMIN_PASSWORD=${config.sops.placeholder."miniflux/admin-password"} + ''; + }; + }; + }; +} diff --git a/modules/nixos/nginx/default.nix b/modules/nixos/nginx/default.nix new file mode 100644 index 0000000..acffbed --- /dev/null +++ b/modules/nixos/nginx/default.nix @@ -0,0 +1,80 @@ +{ config, lib, ... }: + +let + cfg = config.services.nginx; + + inherit (lib) + mkDefault + mkIf + mkOption + optional + optionals + types + ; +in +{ + options.services.nginx = { + forceSSL = mkOption { + type = types.bool; + default = false; + description = "Force SSL for Nginx virtual host."; + }; + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the firewall for HTTP (and HTTPS if forceSSL is enabled)."; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = optionals cfg.openFirewall ( + [ + 80 + ] + ++ optional cfg.forceSSL 443 + ); + + services.nginx = { + recommendedOptimisation = mkDefault true; + recommendedGzipSettings = mkDefault true; + recommendedProxySettings = mkDefault true; + recommendedTlsSettings = cfg.forceSSL; + + commonHttpConfig = "access_log syslog:server=unix:/dev/log;"; + + resolver.addresses = + let + isIPv6 = addr: builtins.match ".*:.*:.*" addr != null; + escapeIPv6 = addr: if isIPv6 addr then "[${addr}]" else addr; + cloudflare = [ + "1.1.1.1" + "2606:4700:4700::1111" + ]; + resolvers = + if config.networking.nameservers == [ ] then cloudflare else config.networking.nameservers; + in + map escapeIPv6 resolvers; + + sslDhparam = mkIf cfg.forceSSL config.security.dhparams.params.nginx.path; + + virtualHosts = { + "${config.networking.domain}" = mkDefault { + enableACME = cfg.forceSSL; + forceSSL = cfg.forceSSL; + }; + }; + }; + + security.acme = mkIf cfg.forceSSL { + acceptTerms = true; + defaults.email = mkDefault "postmaster@${config.networking.domain}"; + defaults.webroot = mkDefault "/var/lib/acme/acme-challenge"; + certs."${config.networking.domain}".postRun = "systemctl reload nginx.service"; + }; + + security.dhparams = mkIf cfg.forceSSL { + enable = true; + params.nginx = { }; + }; + }; +} diff --git a/modules/nixos/normalUsers/default.nix b/modules/nixos/normalUsers/default.nix new file mode 100644 index 0000000..7f769bb --- /dev/null +++ b/modules/nixos/normalUsers/default.nix @@ -0,0 +1,69 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.normalUsers; + + inherit (lib) + attrNames + genAttrs + mkOption + types + ; +in +{ + options.normalUsers = mkOption { + type = types.attrsOf ( + types.submodule { + options = { + extraGroups = mkOption { + type = (types.listOf types.str); + default = [ ]; + description = "Extra groups for the user"; + example = [ "wheel" ]; + }; + shell = mkOption { + type = types.path; + default = pkgs.zsh; + description = "Shell for the user"; + }; + initialPassword = mkOption { + type = types.str; + default = "changeme"; + description = "Initial password for the user"; + }; + sshKeyFiles = mkOption { + type = (types.listOf types.path); + default = [ ]; + description = "SSH key files for the user"; + example = [ "/path/to/id_rsa.pub" ]; + }; + }; + } + ); + default = { }; + description = "Users to create. The usernames are the attribute names."; + }; + + config = { + # Create user groups + users.groups = genAttrs (attrNames cfg) (userName: { + name = userName; + }); + + # Create users + users.users = genAttrs (attrNames cfg) (userName: { + name = userName; + inherit (cfg.${userName}) extraGroups shell initialPassword; + + isNormalUser = true; + group = "${userName}"; + home = "/home/${userName}"; + openssh.authorizedKeys.keyFiles = cfg.${userName}.sshKeyFiles; + }); + }; +} diff --git a/modules/nixos/nvidia/default.nix b/modules/nixos/nvidia/default.nix new file mode 100644 index 0000000..4d95ea2 --- /dev/null +++ b/modules/nixos/nvidia/default.nix @@ -0,0 +1,30 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) mkDefault; +in +{ + boot.blacklistedKernelModules = [ "nouveau" ]; + boot.extraModulePackages = [ config.hardware.nvidia.package ]; + boot.initrd.kernelModules = [ "nvidia" ]; + + environment.systemPackages = with pkgs; [ + nvtopPackages.nvidia + ]; + + hardware.enableRedistributableFirmware = true; + hardware.graphics.enable = true; + hardware.nvidia.modesetting.enable = true; + hardware.nvidia.nvidiaSettings = true; + hardware.nvidia.open = false; + hardware.nvidia.package = mkDefault config.boot.kernelPackages.nvidiaPackages.latest; + + nixpkgs.config.cudaSupport = true; + + services.xserver.videoDrivers = [ "nvidia" ]; +} diff --git a/modules/nixos/ollama/default.nix b/modules/nixos/ollama/default.nix new file mode 100644 index 0000000..54b2dad --- /dev/null +++ b/modules/nixos/ollama/default.nix @@ -0,0 +1,47 @@ +{ config, lib, ... }: + +let + cfg = config.services.ollama; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + + inherit (lib) + mkDefault + mkForce + mkIf + ; + + inherit (lib.utils) + mkReverseProxyOption + mkVirtualHost + ; +in +{ + options.services.ollama = { + reverseProxy = mkReverseProxyOption "Ollama" "ollama"; + }; + + config = mkIf cfg.enable { + services.ollama = { + host = mkDefault (if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0"); + user = mkDefault "ollama"; + group = mkDefault "ollama"; + }; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${fqdn}" = mkVirtualHost { + port = cfg.port; + ssl = cfg.reverseProxy.forceSSL; + recommendedProxySettings = mkForce false; + extraConfig = '' + proxy_set_header Host ${cfg.host}:${toString cfg.port}; + ''; + }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.home} 0755 ${cfg.user} ${cfg.group} -" + ]; + }; +} diff --git a/modules/nixos/open-webui-oci/default.nix b/modules/nixos/open-webui-oci/default.nix new file mode 100644 index 0000000..89df77c --- /dev/null +++ b/modules/nixos/open-webui-oci/default.nix @@ -0,0 +1,168 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.open-webui-oci; + + defaultEnv = { + ANONYMIZED_TELEMETRY = "False"; + BYPASS_MODEL_ACCESS_CONTROL = "True"; + DEFAULT_LOCALE = "en"; + DEFAULT_USER_ROLE = "user"; + DO_NOT_TRACK = "True"; + ENABLE_DIRECT_CONNECTIONS = "True"; + ENABLE_IMAGE_GENERATION = "True"; + ENABLE_PERSISTENT_CONFIG = "False"; + ENABLE_SIGNUP = "False"; + ENABLE_SIGNUP_PASSWORD_CONFIRMATION = "True"; + ENABLE_USER_WEBHOOKS = "True"; + ENABLE_WEBSEARCH = "True"; + ENV = "prod"; + SCARF_NO_ANALYTICS = "True"; + USER_AGENT = "OpenWebUI"; + USER_PERMISSIONS_FEATURES_DIRECT_TOOL_SERVERS = "True"; + WEB_SEARCH_ENGINE = "DuckDuckGo"; + }; + + inherit (lib) + concatStringsSep + literalExpression + mkEnableOption + mkIf + mkOption + mkOverride + optional + types + ; +in +{ + options.services.open-webui-oci = { + enable = mkEnableOption "Open WebUI container with Podman."; + externalUrl = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression "http://${config.networking.domain}"; + description = "Public URL to add to CORS."; + }; + port = mkOption { + type = types.port; + default = 3000; + description = "Which port the Open-WebUI server listens to."; + }; + environment = mkOption { + default = { }; + type = types.attrsOf types.str; + description = '' + Extra environment variables for Open-WebUI. + For more details see + ''; + }; + environmentFile = mkOption { + description = "Environment file to be passed to the Open WebUI container."; + type = types.nullOr types.path; + default = null; + example = "config.sops.templates.open-webui-env.path"; + }; + }; + + config = mkIf cfg.enable { + virtualisation.podman = { + enable = true; + autoPrune.enable = true; + dockerCompat = true; + }; + + networking.firewall.interfaces = + let + matchAll = if !config.networking.nftables.enable then "podman+" else "podman*"; + in + { + "${matchAll}".allowedUDPPorts = [ 53 ]; + }; + + virtualisation.oci-containers.backend = "podman"; + + virtualisation.oci-containers.containers."open-webui" = { + image = "ghcr.io/open-webui/open-webui:main"; + environment = + defaultEnv + // cfg.environment + // { + PORT = "${toString cfg.port}"; + CORS_ALLOW_ORIGIN = concatStringsSep ";" ( + [ + "http://localhost:${toString cfg.port}" + "http://127.0.0.1:${toString cfg.port}" + "http://0.0.0.0:${toString cfg.port}" + ] + ++ optional (cfg.externalUrl != null) cfg.externalUrl + ); + }; + environmentFiles = optional (cfg.environmentFile != null) cfg.environmentFile; + volumes = [ + "open-webui_open-webui:/app/backend/data:rw" + ]; + log-driver = "journald"; + extraOptions = [ + "--network=host" + ]; + }; + systemd.services."podman-open-webui" = { + serviceConfig = { + Restart = mkOverride 90 "always"; + }; + after = [ + "podman-network-open-webui_default.service" + "podman-volume-open-webui_open-webui.service" + ]; + requires = [ + "podman-network-open-webui_default.service" + "podman-volume-open-webui_open-webui.service" + ]; + partOf = [ + "podman-compose-open-webui-root.target" + ]; + wantedBy = [ + "podman-compose-open-webui-root.target" + ]; + }; + + systemd.services."podman-network-open-webui_default" = { + path = [ pkgs.podman ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStop = "podman network rm -f open-webui_default"; + }; + script = '' + podman network inspect open-webui_default || podman network create open-webui_default + ''; + partOf = [ "podman-compose-open-webui-root.target" ]; + wantedBy = [ "podman-compose-open-webui-root.target" ]; + }; + + systemd.services."podman-volume-open-webui_open-webui" = { + path = [ pkgs.podman ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + podman volume inspect open-webui_open-webui || podman volume create open-webui_open-webui + ''; + partOf = [ "podman-compose-open-webui-root.target" ]; + wantedBy = [ "podman-compose-open-webui-root.target" ]; + }; + + systemd.targets."podman-compose-open-webui-root" = { + unitConfig = { + Description = "Root target generated by compose2nix."; + }; + wantedBy = [ "multi-user.target" ]; + }; + }; +} diff --git a/modules/nixos/openssh/default.nix b/modules/nixos/openssh/default.nix new file mode 100644 index 0000000..0958445 --- /dev/null +++ b/modules/nixos/openssh/default.nix @@ -0,0 +1,16 @@ +{ lib, ... }: + +let + inherit (lib) mkDefault; +in +{ + services.openssh = { + enable = mkDefault true; + ports = mkDefault [ 2299 ]; + openFirewall = mkDefault true; + settings = { + PermitRootLogin = mkDefault "no"; + PasswordAuthentication = mkDefault false; + }; + }; +} diff --git a/modules/nixos/print-server/default.nix b/modules/nixos/print-server/default.nix new file mode 100644 index 0000000..f96650d --- /dev/null +++ b/modules/nixos/print-server/default.nix @@ -0,0 +1,83 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.print-server; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + port = 631; + + inherit (lib) + mkEnableOption + mkIf + mkOption + types + ; + + inherit (lib.utils) + mkReverseProxyOption + mkVirtualHost + ; +in +{ + options.services.print-server = { + enable = mkEnableOption "print server"; + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Open firewall for printing and avahi service."; + }; + reverseProxy = mkReverseProxyOption "print-server" "print"; + }; + + config = mkIf cfg.enable { + services.printing = { + enable = true; + listenAddresses = [ "*:${builtins.toString port}" ]; + webInterface = true; + tempDir = "/tmp/cups"; + allowFrom = [ "all" ]; + snmpConf = '' + Address @LOCAL + ''; + clientConf = ""; + openFirewall = cfg.openFirewall; + drivers = with pkgs; [ + brlaser + brgenml1lpr + brgenml1cupswrapper # Brother + postscript-lexmark # Lexmark + hplip + hplipWithPlugin # HP + splix + samsung-unified-linux-driver # Samsung + gutenprint + gutenprintBin # different vendors + ]; + defaultShared = true; + browsing = true; + browsedConf = '' + BrowsePoll ${fqdn} + ''; + }; + + # autodiscovery of network printers + services.avahi = { + enable = true; + nssmdns4 = true; + openFirewall = cfg.openFirewall; + }; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + ${fqdn} = mkVirtualHost { + inherit port; + ssl = cfg.reverseProxy.forceSSL; + }; + }; + }; +} diff --git a/modules/nixos/radicale/default.nix b/modules/nixos/radicale/default.nix new file mode 100644 index 0000000..8b16ad3 --- /dev/null +++ b/modules/nixos/radicale/default.nix @@ -0,0 +1,116 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.radicale; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + port = 5232; + + inherit (lib) + concatLines + mkDefault + mkIf + mkOption + types + ; + + inherit (lib.utils) + mkReverseProxyOption + ; +in +{ + options.services.radicale = { + users = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "alice" + "bob" + ]; + description = "List of users for Radicale. Each user must have a corresponding entry in the SOPS file under 'radicale/'"; + }; + reverseProxy = mkReverseProxyOption "Radicale" "dav"; + }; + + config = mkIf cfg.enable { + services.radicale = { + settings = { + server = { + hosts = [ + "${if cfg.reverseProxy.enable then "127.0.0.1" else "0.0.0.0"}:${builtins.toString port}" + ]; + max_connections = mkDefault 20; + max_content_length = mkDefault 500000000; + timeout = mkDefault 30; + }; + auth = { + type = "htpasswd"; + delay = mkDefault 1; + htpasswd_filename = config.sops.templates."radicale/users".path; + htpasswd_encryption = mkDefault "sha512"; + }; + storage = { + filesystem_folder = mkDefault "/var/lib/radicale/collections"; + }; + }; + }; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${fqdn}" = { + forceSSL = cfg.reverseProxy.forceSSL; + enableACME = cfg.reverseProxy.forceSSL; + locations = { + "/" = { + proxyPass = "http://localhost:${builtins.toString port}/"; + extraConfig = '' + proxy_set_header X-Script-Name /; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_pass_header Authorization; + ''; + }; + "/.well-known/caldav" = { + return = "301 $scheme://$host/"; + }; + "/.well-known/carddav" = { + return = "301 $scheme://$host/"; + }; + }; + }; + }; + + environment.systemPackages = [ + pkgs.openssl + ]; + + sops = + let + owner = "radicale"; + group = "radicale"; + mode = "0440"; + + mkSecrets = + users: + builtins.listToAttrs ( + map (user: { + name = "radicale/${user}"; + value = { inherit owner group mode; }; + }) users + ); + + mkTemplate = users: { + inherit owner group mode; + content = concatLines (map (user: "${user}:${config.sops.placeholder."radicale/${user}"}") users); + }; + in + { + secrets = mkSecrets cfg.users; + templates."radicale/users" = mkTemplate cfg.users; + }; + }; +} diff --git a/modules/nixos/rss-bridge/default.nix b/modules/nixos/rss-bridge/default.nix new file mode 100644 index 0000000..9c7757b --- /dev/null +++ b/modules/nixos/rss-bridge/default.nix @@ -0,0 +1,40 @@ +{ config, lib, ... }: + +let + cfg = config.services.rss-bridge; + domain = config.networking.domain; + subdomain = cfg.reverseProxy.subdomain; + fqdn = if (cfg.reverseProxy.enable && subdomain != "") then "${subdomain}.${domain}" else domain; + + inherit (lib) + mkIf + ; + + inherit (lib.utils) + mkReverseProxyOption + mkVirtualHost + ; +in +{ + options.services.rss-bridge = { + reverseProxy = mkReverseProxyOption "RSS-Bridge" "rss-bridge"; + }; + + config = mkIf cfg.enable { + services.rss-bridge = { + webserver = if cfg.reverseProxy.enable then "nginx" else null; + virtualHost = if cfg.reverseProxy.enable then fqdn else null; + config = { + system.enabled_bridges = [ "*" ]; + }; + }; + + systemd.tmpfiles.rules = [ "d ${cfg.dataDir} 0755 ${cfg.user} ${cfg.group} -" ]; + + services.nginx.virtualHosts = mkIf cfg.reverseProxy.enable { + "${cfg.virtualHost}" = mkVirtualHost { + ssl = cfg.reverseProxy.forceSSL; + }; + }; + }; +} diff --git a/modules/nixos/sops/default.nix b/modules/nixos/sops/default.nix new file mode 100644 index 0000000..627014b --- /dev/null +++ b/modules/nixos/sops/default.nix @@ -0,0 +1,21 @@ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + secrets = "${toString inputs.self}/hosts/${config.networking.hostName}/secrets/secrets.yaml"; +in +{ + imports = [ inputs.sops-nix.nixosModules.sops ]; + + environment.systemPackages = with pkgs; [ + age + sops + ]; + + sops.defaultSopsFile = lib.mkIf (builtins.pathExists secrets) (lib.mkDefault secrets); +} diff --git a/modules/nixos/tailscale/default.nix b/modules/nixos/tailscale/default.nix new file mode 100644 index 0000000..3e43963 --- /dev/null +++ b/modules/nixos/tailscale/default.nix @@ -0,0 +1,50 @@ +{ config, lib, ... }: + +let + cfg = config.services.tailscale; + + inherit (lib) + mkIf + mkOption + optional + types + ; +in +{ + options.services.tailscale = { + loginServer = mkOption { + type = types.str; + description = "The Tailscale login server to use."; + }; + enableSSH = mkOption { + type = types.bool; + default = false; + description = "Enable Tailscale SSH functionality."; + }; + acceptDNS = mkOption { + type = types.bool; + default = true; + description = "Enable Tailscale's MagicDNS and custom DNS configuration."; + }; + }; + + config = mkIf cfg.enable { + services.tailscale = { + authKeyFile = config.sops.secrets."tailscale/auth-key".path; + extraSetFlags = optional cfg.enableSSH "--ssh" ++ optional cfg.acceptDNS "--accept-dns"; + extraUpFlags = [ + "--login-server=${cfg.loginServer}" + ] + ++ optional cfg.enableSSH "--ssh" + ++ optional cfg.acceptDNS "--accept-dns"; + }; + + environment.shellAliases = { + ts = "${cfg.package}/bin/tailscale"; + }; + + networking.firewall.trustedInterfaces = [ cfg.interfaceName ]; + + sops.secrets."tailscale/auth-key" = { }; + }; +} diff --git a/modules/nixos/virtualisation/default.nix b/modules/nixos/virtualisation/default.nix new file mode 100644 index 0000000..fb62b0f --- /dev/null +++ b/modules/nixos/virtualisation/default.nix @@ -0,0 +1,92 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.virtualisation; + + boolToZeroOne = x: if x then "1" else "0"; + + aclString = strings.concatMapStringsSep '' + , + '' strings.escapeNixString cfg.libvirtd.deviceACL; + + inherit (lib) + mkDefault + mkOption + optionalString + optionals + strings + types + ; +in +{ + imports = [ + ./hugepages.nix + ./kvmfr.nix + ./quickemu.nix + ./vfio.nix + ]; + + options.virtualisation = { + libvirtd = { + deviceACL = mkOption { + type = types.listOf types.str; + default = [ ]; + example = [ + "/dev/kvm" + "/dev/net/tun" + "/dev/vfio/vfio" + ]; + description = "List of device paths that QEMU processes are allowed to access."; + }; + + clearEmulationCapabilities = mkOption { + type = types.bool; + default = true; + description = "Whether to remove privileged Linux capabilities from QEMU processes after they start."; + }; + }; + }; + + config = { + virtualisation = { + libvirtd = { + enable = mkDefault true; + onBoot = mkDefault "ignore"; + onShutdown = mkDefault "shutdown"; + qemu.runAsRoot = mkDefault false; + qemu.verbatimConfig = '' + clear_emulation_capabilities = ${boolToZeroOne cfg.libvirtd.clearEmulationCapabilities} + '' + + optionalString (cfg.libvirtd.deviceACL != [ ]) '' + cgroup_device_acl = [ + ${aclString} + ] + ''; + qemu.swtpm.enable = mkDefault true; # TPM 2.0 + }; + spiceUSBRedirection.enable = mkDefault true; + }; + + users.users."qemu-libvirtd" = { + extraGroups = optionals (!cfg.libvirtd.qemu.runAsRoot) [ + "kvm" + "input" + ]; + isSystemUser = true; + }; + + programs.virt-manager.enable = mkDefault true; + + environment.systemPackages = [ + (pkgs.writeShellScriptBin "iommu-groups" (builtins.readFile ./iommu-groups.sh)) + pkgs.dnsmasq + pkgs.qemu_full + pkgs.virtio-win + ]; + }; +} diff --git a/modules/nixos/virtualisation/hugepages.nix b/modules/nixos/virtualisation/hugepages.nix new file mode 100644 index 0000000..a05b914 --- /dev/null +++ b/modules/nixos/virtualisation/hugepages.nix @@ -0,0 +1,45 @@ +{ + config, + lib, + ... +}: + +let + cfg = config.virtualisation.hugepages; + + inherit (lib) + mkEnableOption + mkOption + optionals + types + ; +in +{ + options.virtualisation.hugepages = { + enable = mkEnableOption "huge pages."; + + defaultPageSize = mkOption { + type = types.strMatching "[0-9]*[kKmMgG]"; + default = "1M"; + description = "Default size of huge pages. You can use suffixes K, M, and G to specify KB, MB, and GB."; + }; + + pageSize = mkOption { + type = types.strMatching "[0-9]*[kKmMgG]"; + default = "1M"; + description = "Size of huge pages that are allocated at boot. You can use suffixes K, M, and G to specify KB, MB, and GB."; + }; + + numPages = mkOption { + type = types.ints.positive; + default = 1; + description = "Number of huge pages to allocate at boot."; + }; + }; + + config.boot.kernelParams = optionals cfg.enable [ + "default_hugepagesz=${cfg.defaultPageSize}" + "hugepagesz=${cfg.pageSize}" + "hugepages=${toString cfg.numPages}" + ]; +} diff --git a/modules/nixos/virtualisation/iommu-groups.sh b/modules/nixos/virtualisation/iommu-groups.sh new file mode 100644 index 0000000..3d2e4f3 --- /dev/null +++ b/modules/nixos/virtualisation/iommu-groups.sh @@ -0,0 +1,6 @@ +shopt -s nullglob +for d in /sys/kernel/iommu_groups/*/devices/*; do + n=${d#*/iommu_groups/*}; n=${n%%/*} + printf 'IOMMU Group %s ' "$n" + lspci -nns "${d##*/}" +done; diff --git a/modules/nixos/virtualisation/kvmfr.nix b/modules/nixos/virtualisation/kvmfr.nix new file mode 100644 index 0000000..ad7bfe6 --- /dev/null +++ b/modules/nixos/virtualisation/kvmfr.nix @@ -0,0 +1,157 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.virtualisation.kvmfr; + + sizeFromResolution = + resolution: + let + ceilToPowerOf2 = + let + findNextPower = target: power: if power >= target then power else findNextPower target (power * 2); + in + n: if n <= 1 then 1 else findNextPower n 1; + pixelSize = if resolution.pixelFormat == "rgb24" then 3 else 4; + bytes = resolution.width * resolution.height * pixelSize * 2; + in + ceilToPowerOf2 (bytes / 1024 / 1024 + 10); + + deviceSizes = map (device: device.size) cfg.devices; + + devices = imap (index: _deviceConfig: "/dev/kvmfr${toString index}") cfg.devices; + + udevPackage = pkgs.writeTextDir "/lib/udev/rules.d/99-kvmfr.rules" ( + concatStringsSep "\n" ( + imap0 (index: deviceConfig: '' + SUBSYSTEM=="kvmfr", KERNEL=="kvmfr${toString index}", OWNER="${deviceConfig.permissions.user}", GROUP="${deviceConfig.permissions.group}", MODE="${deviceConfig.permissions.mode}", TAG+="systemd" + '') cfg.devices + ) + ); + + apparmorAbstraction = concatStringsSep "\n" (map (device: "${device} rw") devices); + + permissionsType = types.submodule { + options = { + user = mkOption { + type = types.str; + default = "root"; + description = "Owner of the shared memory device."; + }; + group = mkOption { + type = types.str; + default = "root"; + description = "Group of the shared memory device."; + }; + mode = mkOption { + type = types.str; + default = "0600"; + description = "Mode of the shared memory device."; + }; + }; + }; + + resolutionType = types.submodule { + options = { + width = mkOption { + type = types.number; + description = "Maximum horizontal video size that should be supported by this device."; + }; + + height = mkOption { + type = types.number; + description = "Maximum vertical video size that should be supported by this device."; + }; + + pixelFormat = mkOption { + type = types.enum [ + "rgba32" + "rgb24" + ]; + description = "Pixel format to use."; + default = "rgba32"; + }; + }; + }; + + deviceType = ( + types.submodule ( + { config, options, ... }: + { + options = { + resolution = mkOption { + type = types.nullOr resolutionType; + default = null; + description = "Automatically calculate the minimum device size for a specific resolution. Overrides `size` if set."; + }; + + size = mkOption { + type = types.number; + description = "Size for the kvmfr device in megabytes."; + }; + + permissions = mkOption { + type = permissionsType; + default = { }; + description = "Permissions of the kvmfr device."; + }; + }; + + config = { + size = mkIf (config.resolution != null) (sizeFromResolution config.resolution); + }; + } + ) + ); + + inherit (lib) + concatStringsSep + imap + imap0 + mkIf + mkOption + optionals + types + ; +in +{ + options.virtualisation.kvmfr = { + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the kvmfr kernel module."; + }; + + devices = mkOption { + type = types.listOf deviceType; + default = [ ]; + description = "List of devices to create."; + }; + }; + + config = mkIf cfg.enable { + boot.extraModulePackages = with config.boot.kernelPackages; [ kvmfr ]; + services.udev.packages = optionals (cfg.devices != [ ]) [ udevPackage ]; + + environment.systemPackages = with pkgs; [ looking-glass-client ]; + environment.etc = { + "modules-load.d/kvmfr.conf".text = '' + kvmfr + ''; + + "modprobe.d/kvmfr.conf".text = '' + options kvmfr static_size_mb=${concatStringsSep "," (map (size: toString size) deviceSizes)} + ''; + + "apparmor.d/local/abstractions/libvirt-qemu" = mkIf config.security.apparmor.enable { + text = apparmorAbstraction; + }; + }; + + virtualisation.libvirtd.deviceACL = devices; + }; +} diff --git a/modules/nixos/virtualisation/quickemu.nix b/modules/nixos/virtualisation/quickemu.nix new file mode 100644 index 0000000..f82ae9c --- /dev/null +++ b/modules/nixos/virtualisation/quickemu.nix @@ -0,0 +1,28 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.virtualisation.quickemu; + + inherit (lib) mkEnableOption mkIf; +in +{ + options.virtualisation.quickemu = { + enable = mkEnableOption "quickemu"; + }; + + config = mkIf cfg.enable { + environment.systemPackages = with pkgs; [ + quickemu + ]; + + boot.extraModprobeConfig = '' + options kvm_amd nested=1 + options kvm ignore_msrs=1 report_ignored_msrs=0 + ''; + }; +} diff --git a/modules/nixos/virtualisation/vfio.nix b/modules/nixos/virtualisation/vfio.nix new file mode 100644 index 0000000..36df31e --- /dev/null +++ b/modules/nixos/virtualisation/vfio.nix @@ -0,0 +1,103 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.virtualisation.vfio; + + inherit (lib) + mkEnableOption + mkIf + mkOption + optional + optionals + types + versionOlder + ; +in +{ + options.virtualisation.vfio = { + enable = mkEnableOption "VFIO Configuration"; + + IOMMUType = mkOption { + type = types.enum [ + "intel" + "amd" + ]; + example = "intel"; + description = "Type of the IOMMU used"; + }; + + devices = mkOption { + type = types.listOf (types.strMatching "[0-9a-f]{4}:[0-9a-f]{4}"); + default = [ ]; + example = [ + "10de:1b80" + "10de:10f0" + ]; + description = "PCI IDs of devices to bind to vfio-pci"; + }; + + disableEFIfb = mkOption { + type = types.bool; + default = false; + example = true; + description = "Disables the usage of the EFI framebuffer on boot."; + }; + + blacklistNvidia = mkOption { + type = types.bool; + default = false; + description = "Add Nvidia GPU modules to blacklist"; + }; + + ignoreMSRs = mkOption { + type = types.bool; + default = false; + example = true; + description = "Enables or disables kvm guest access to model-specific registers"; + }; + }; + + config = mkIf cfg.enable { + services.udev.extraRules = '' + SUBSYSTEM=="vfio", OWNER="root", GROUP="kvm" + ''; + + boot.kernelParams = + ( + if cfg.IOMMUType == "intel" then + [ + "intel_iommu=on" + "intel_iommu=igfx_off" + ] + else + [ "amd_iommu=on" ] + ) + ++ optional (cfg.devices != [ ]) ("vfio-pci.ids=" + builtins.concatStringsSep "," cfg.devices) + ++ (optional cfg.disableEFIfb "video=efifb:off") + ++ ( + optionals cfg.ignoreMSRs [ + "kvm.ignore_msrs=1" + "kvm.report_ignored_msrs=0" + ] + ++ optional cfg.blacklistNvidia "modprobe.blacklist=nouveau,nvidia,nvidia_drm,nvidia" + ); + + boot.initrd.kernelModules = [ + "vfio_pci" + "vfio_iommu_type1" + "vfio" + ] + ++ optionals (versionOlder pkgs.linux.version "6.2") [ "vfio_virqfd" ]; + + # boot.blacklistedKernelModules = optionals cfg.blacklistNvidia [ + # "nouveau" + # "nvidia" + # "nvidia_drm" + # ]; + }; +} diff --git a/modules/nixos/webPage/default.nix b/modules/nixos/webPage/default.nix new file mode 100644 index 0000000..32d58b1 --- /dev/null +++ b/modules/nixos/webPage/default.nix @@ -0,0 +1,66 @@ +{ config, lib, ... }: + +let + cfg = config.services.webPage; + domain = config.networking.domain; + fqdn = if (cfg.subdomain != "") then "${cfg.subdomain}.${domain}" else domain; + nginxUser = config.services.nginx.user; + + inherit (lib) + mkEnableOption + mkIf + mkOption + types + ; +in +{ + options.services.webPage = { + enable = mkEnableOption "static web page hosting"; + subdomain = mkOption { + type = types.str; + default = "www"; + description = "The subdomain to serve the web page on. Leave empty for root domain."; + }; + forceSSL = mkOption { + type = types.bool; + default = true; + description = "Force SSL for Nginx virtual host."; + }; + webRoot = mkOption { + type = types.str; + description = "The root directory of the web page."; + example = "/var/www"; + }; + test = mkOption { + type = types.bool; + default = false; + description = "If true, a sample index.html will be placed in the web root for testing purposes."; + }; + }; + + config = mkIf cfg.enable { + services.nginx.virtualHosts."${fqdn}" = { + enableACME = cfg.forceSSL; + forceSSL = cfg.forceSSL; + root = cfg.webRoot; + locations."/".index = "index.html"; + sslCertificate = mkIf cfg.forceSSL "${config.security.acme.certs."${fqdn}".directory}/cert.pem"; + sslCertificateKey = mkIf cfg.forceSSL "${config.security.acme.certs."${fqdn}".directory}/key.pem"; + }; + + systemd.tmpfiles.rules = [ "d ${cfg.webRoot} 0755 ${nginxUser} ${nginxUser} -" ]; + + system.activationScripts.webPagePermissions = '' + chown -R ${nginxUser}:${nginxUser} ${cfg.webRoot} + chmod -R 0755 ${cfg.webRoot} + ''; + + # test page + environment.etc."sample.html" = mkIf cfg.test { source = ./sample.html; }; + system.activationScripts.moveSampleHtmlToWebRoot = mkIf cfg.test '' + mv /etc/sample.html ${cfg.webRoot}/index.html + chown -R ${nginxUser}:${nginxUser} ${cfg.webRoot} + chmod -R 0755 ${cfg.webRoot} + ''; + }; +} diff --git a/modules/nixos/webPage/sample.html b/modules/nixos/webPage/sample.html new file mode 100644 index 0000000..878ebc1 --- /dev/null +++ b/modules/nixos/webPage/sample.html @@ -0,0 +1,12 @@ + + + + + + Test Page + + +

Test Page

+

This is a sample index.html file for testing purposes.

+ + diff --git a/modules/nixos/windows-oci/default.nix b/modules/nixos/windows-oci/default.nix new file mode 100644 index 0000000..b050daa --- /dev/null +++ b/modules/nixos/windows-oci/default.nix @@ -0,0 +1,170 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.windows-oci; + + inherit (lib) + mkEnableOption + mkIf + mkOption + mkOverride + optional + types + ; +in +{ + options.services.windows-oci = { + enable = mkEnableOption "Windows in an OCI container using Podman"; + volume = mkOption { + type = types.str; + default = "/opt/windows"; + description = "Path to the volume for Windows data."; + }; + sharedVolume = mkOption { + type = types.nullOr types.str; + default = null; + description = "Path to a shared volume to mount inside the Windows container. You have to create this directory manually."; + }; + settings = { + version = mkOption { + type = types.str; + default = "11"; + example = "2025"; + description = "Windows version to use."; + }; + ramSize = mkOption { + type = types.str; + default = "8G"; + description = "Amount of RAM to allocate to the Windows container."; + }; + cpuCores = mkOption { + type = types.str; + default = "4"; + description = "Number of CPU cores to allocate to the Windows container."; + }; + diskSize = mkOption { + type = types.str; + default = "64G"; + description = "Size of the virtual disk for the Windows container."; + }; + username = mkOption { + type = types.str; + default = "admin"; + description = "Username for the Windows installation."; + }; + password = mkOption { + type = types.str; + default = "admin"; + description = "Password for the Windows installation."; + }; + language = mkOption { + type = types.str; + default = "English"; + description = "Language for the Windows installation."; + }; + region = mkOption { + type = types.str; + default = "en-DE"; + description = "Region for the Windows installation."; + }; + keyboard = mkOption { + type = types.str; + default = "de-DE"; + description = "Keyboard layout for the Windows installation."; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.tmpfiles.rules = [ "d ${cfg.volume} 0755 root podman -" ]; + + virtualisation.podman = { + enable = true; + autoPrune.enable = true; + dockerCompat = true; + defaultNetwork.settings = { + dns_enabled = true; + }; + }; + + # https://github.com/NixOS/nixpkgs/issues/226365 + networking.firewall.interfaces."podman+".allowedUDPPorts = [ 53 ]; + + virtualisation.oci-containers.backend = "podman"; + + virtualisation.oci-containers.containers."windows" = { + image = "dockurr/windows"; + environment = with cfg.settings; { + "VERSION" = version; + "RAM_SIZE" = ramSize; + "CPU_CORES" = cpuCores; + "DISK_SIZE" = diskSize; + "USERNAME" = username; + "PASSWORD" = password; + "LANGUAGE" = language; + "REGION" = region; + "KEYBOARD" = keyboard; + }; + volumes = [ + "${cfg.volume}:/storage:rw" + ] + ++ optional (cfg.sharedVolume != null) "${cfg.sharedVolume}:/shared:rw"; + ports = [ + "8006:8006/tcp" + "3389:3389/tcp" + "3389:3389/udp" + ]; + log-driver = "journald"; + extraOptions = [ + "--cap-add=NET_ADMIN" + "--device=/dev/kvm:/dev/kvm:rwm" + "--device=/dev/net/tun:/dev/net/tun:rwm" + "--network-alias=windows" + "--network=windows_default" + ]; + }; + systemd.services."podman-windows" = { + serviceConfig = { + Restart = mkOverride 90 "always"; + }; + after = [ + "podman-network-windows_default.service" + ]; + requires = [ + "podman-network-windows_default.service" + ]; + partOf = [ + "podman-compose-windows-root.target" + ]; + wantedBy = [ + "podman-compose-windows-root.target" + ]; + }; + + systemd.services."podman-network-windows_default" = { + path = [ pkgs.podman ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStop = "podman network rm -f windows_default"; + }; + script = '' + podman network inspect windows_default || podman network create windows_default + ''; + partOf = [ "podman-compose-windows-root.target" ]; + wantedBy = [ "podman-compose-windows-root.target" ]; + }; + + systemd.targets."podman-compose-windows-root" = { + unitConfig = { + Description = "Root target generated by compose2nix."; + }; + wantedBy = [ "multi-user.target" ]; + }; + }; +} diff --git a/modules/shared/common/default.nix b/modules/shared/common/default.nix new file mode 100644 index 0000000..3925d83 --- /dev/null +++ b/modules/shared/common/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./nix.nix + ]; +} diff --git a/modules/shared/common/nix.nix b/modules/shared/common/nix.nix new file mode 100644 index 0000000..2d0daa3 --- /dev/null +++ b/modules/shared/common/nix.nix @@ -0,0 +1,85 @@ +{ + config, + lib, + pkgs, + ... +}: + +let + inherit (lib) + mkDefault + optional + versionOlder + versions + ; +in +{ + nix.package = mkDefault pkgs.nix; + + # for `nix run synix#foo`, `nix build synix#bar`, etc + nix.registry = { + synix = { + from = { + id = "synix"; + type = "indirect"; + }; + to = { + owner = "sid"; + repo = "synix"; + host = "git.sid.ovh"; + type = "gitea"; + }; + }; + }; + + # fallback quickly if substituters are not available. + nix.settings.connect-timeout = mkDefault 5; + nix.settings.fallback = true; + + nix.settings.experimental-features = [ + "nix-command" + "flakes" + ] + ++ optional ( + config.nix.package != null && versionOlder (versions.majorMinor config.nix.package.version) "2.22" + ) "repl-flake"; + + nix.settings.log-lines = mkDefault 25; + + # avoid disk full issues + nix.settings.max-free = mkDefault (3000 * 1024 * 1024); + nix.settings.min-free = mkDefault (512 * 1024 * 1024); + + # avoid copying unnecessary stuff over SSH + nix.settings.builders-use-substitutes = true; + + # workaround for https://github.com/NixOS/nix/issues/9574 + nix.settings.nix-path = config.nix.nixPath; + + nix.settings.download-buffer-size = 524288000; # 500 MiB + + # add all wheel users to the trusted-users group + nix.settings.trusted-users = [ + "@wheel" + ]; + + # binary caches + nix.settings.substituters = [ + "https://cache.nixos.org" + "https://nix-community.cachix.org" + "https://cache.garnix.io" + "https://numtide.cachix.org" + ]; + nix.settings.trusted-public-keys = [ + "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + "cache.garnix.io:CTFPyKSLcx5RMJKfLo5EEPUObbA78b0YQ2DTCJXqr9g=" + "numtide.cachix.org-1:2ps1kLBUWjxIneOy1Ik6cQjb41X0iXVXeHigGmycPPE=" + ]; + + nix.gc = { + automatic = true; + dates = "weekly"; + options = "--delete-older-than 30d"; + }; +} diff --git a/overlays/default.nix b/overlays/default.nix new file mode 100644 index 0000000..c4a695d --- /dev/null +++ b/overlays/default.nix @@ -0,0 +1,29 @@ +{ inputs, ... }: + +{ + default = final: prev: { + lib = prev.lib // inputs.self.lib; + }; + + additions = final: _prev: import ../pkgs { pkgs = final; }; + + modifications = final: prev: { + # https://github.com/NixOS/nixpkgs/issues/335003#issuecomment-2755803376 + kicad = ( + prev.kicad.override { + stable = true; + } + ); + + # bemenu is not a valid selector in v1.5.1 + rofi-rbw-wayland = prev.rofi-rbw-wayland.overrideAttrs (oldAttrs: { + version = "unstable-2026-01-05"; + src = prev.fetchFromGitHub { + owner = "fdw"; + repo = "rofi-rbw"; + rev = "2cfbbadf7f585bc0163bb84cd6a205a2bceafab4"; + hash = "sha256-2gSZniHv/sT3eR1BThxbO2MqnCIfkaKqvFYoqdOk3xY="; + }; + }); + }; +} diff --git a/pkgs/arxiv-mcp-server/default.nix b/pkgs/arxiv-mcp-server/default.nix new file mode 100644 index 0000000..8e8f08a --- /dev/null +++ b/pkgs/arxiv-mcp-server/default.nix @@ -0,0 +1,66 @@ +{ + lib, + python3, + fetchPypi, +}: + +python3.pkgs.buildPythonApplication rec { + pname = "arxiv-mcp-server"; + version = "0.3.1"; + pyproject = true; + + src = fetchPypi { + pname = "arxiv_mcp_server"; + inherit version; + hash = "sha256-yGNetU7el6ZXsavD8uvO17OZtaPuYgzkxiVEk402GUs="; + }; + + build-system = [ + python3.pkgs.hatchling + ]; + + dependencies = with python3.pkgs; [ + aiofiles + aiohttp + anyio + arxiv + httpx + mcp + pydantic + pydantic-settings + pymupdf4llm + python-dateutil + python-dotenv + sse-starlette + uvicorn + ]; + + optional-dependencies = with python3.pkgs; { + test = [ + aioresponses + pytest + pytest-asyncio + pytest-cov + pytest-mock + ]; + }; + + pythonRemoveDeps = [ + "black" + ]; + + pythonImportsCheck = [ + "arxiv_mcp_server" + ]; + + meta = { + description = "A flexible arXiv search and analysis service with MCP protocol support"; + homepage = "https://pypi.org/project/arxiv-mcp-server"; + license = with lib.licenses; [ + asl20 + mit + ]; + maintainers = with lib.maintainers; [ ]; + mainProgram = "arxiv-mcp-server"; + }; +} diff --git a/pkgs/baibot/default.nix b/pkgs/baibot/default.nix new file mode 100644 index 0000000..519be4c --- /dev/null +++ b/pkgs/baibot/default.nix @@ -0,0 +1,46 @@ +{ + lib, + rustPlatform, + fetchFromGitHub, + pkg-config, + openssl, + sqlite, + stdenv, + darwin, +}: + +rustPlatform.buildRustPackage rec { + pname = "baibot"; + version = "1.10.0"; + + src = fetchFromGitHub { + owner = "etkecc"; + repo = "baibot"; + rev = "v${version}"; + hash = "sha256-8QKYDk7Q4oiqyf3vwziu1+A5T2soKuKC5MxPlsOWTM4="; + }; + + useFetchCargoVendor = true; + cargoHash = "sha256-jLZcjvkNj9FzSCQbQ9aqiV42a6OXyqm/FfbTIJX03x4="; + + nativeBuildInputs = [ + pkg-config + ]; + + buildInputs = [ + openssl + sqlite + ] + ++ lib.optionals stdenv.isDarwin [ + darwin.apple_sdk.frameworks.Security + darwin.apple_sdk.frameworks.SystemConfiguration + ]; + + meta = { + description = "A Matrix bot for using diffent capabilities (text-generation, text-to-speech, speech-to-text, image-generation, etc.) of AI / Large Language Models (OpenAI, Anthropic, etc"; + homepage = "https://github.com/etkecc/baibot"; + changelog = "https://github.com/etkecc/baibot/blob/${src.rev}/CHANGELOG.md"; + license = lib.licenses.agpl3Only; + mainProgram = "baibot"; + }; +} diff --git a/pkgs/blender-mcp/default.nix b/pkgs/blender-mcp/default.nix new file mode 100644 index 0000000..7b6abc0 --- /dev/null +++ b/pkgs/blender-mcp/default.nix @@ -0,0 +1,40 @@ +{ + lib, + python3, + fetchPypi, +}: + +python3.pkgs.buildPythonApplication rec { + pname = "blender-mcp"; + version = "1.4.0"; + pyproject = true; + + src = fetchPypi { + pname = "blender_mcp"; + inherit version; + hash = "sha256-0+bWXhw8/DXC6aFQJiSwU7BqsfhoY+pUdIfOEVMStqQ="; + }; + + build-system = [ + python3.pkgs.setuptools + python3.pkgs.wheel + ]; + + dependencies = with python3.pkgs; [ + mcp + supabase + tomli + ]; + + pythonImportsCheck = [ + "blender_mcp" + ]; + + meta = { + description = "Blender integration through the Model Context Protocol"; + homepage = "https://pypi.org/project/blender-mcp"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ ]; + mainProgram = "blender-mcp"; + }; +} diff --git a/pkgs/bulk-rename/bulk-rename.sh b/pkgs/bulk-rename/bulk-rename.sh new file mode 100644 index 0000000..b63a059 --- /dev/null +++ b/pkgs/bulk-rename/bulk-rename.sh @@ -0,0 +1,137 @@ +#!/bin/bash + +# Function to display usage +usage() { + echo "Usage: bulk-rename [directory]" + echo " bulk-rename [file1 file2 ...]" + echo "" + echo "If files are provided as arguments, they will be renamed." + echo "If a directory is provided, all files in that directory will be renamed." + echo "If no arguments are provided, files in current directory will be renamed." +} + +# Function to cleanup temporary files +cleanup() { + [ -n "$index" ] && [ -f "$index" ] && rm -f "$index" + [ -n "$index_edit" ] && [ -f "$index_edit" ] && rm -f "$index_edit" +} + +# Set trap to ensure cleanup on exit +trap cleanup EXIT + +# Check for help flag +if [[ "$1" == "-h" || "$1" == "--help" ]]; then + usage + exit 0 +fi + +# Create temporary files +index=$(mktemp /tmp/bulk-rename-index.XXXXXXXXXX) || exit 1 +index_edit=$(mktemp /tmp/bulk-rename.XXXXXXXXXX) || exit 1 + +# Determine target directory and what files to rename +target_dir="." +if [ $# -eq 0 ]; then + # No arguments - use current directory + shopt -s nullglob dotglob + files=(*) + if [ ${#files[@]} -gt 0 ]; then + printf '%s\n' "${files[@]}" > "$index" + fi +elif [ -d "$1" ] && [ $# -eq 1 ]; then + # Single directory argument + target_dir="$1" + if [ "$target_dir" = "." ]; then + target_dir="$(pwd)" + fi + shopt -s nullglob dotglob + files=("$target_dir"/*) + # Extract just the filenames + for file in "${files[@]}"; do + [ -e "$file" ] && basename "$file" + done | sort > "$index" +else + # File arguments - assume current directory for now + # TODO: Handle files from different directories properly + for file in "$@"; do + if [ -e "$file" ]; then + basename "$file" + else + echo "Warning: $file does not exist" >&2 + fi + done > "$index" +fi + +# Check if we have any files to rename +if [ ! -s "$index" ]; then + echo "No files to rename" + exit 0 +fi + +# Copy index to edit file +cat "$index" > "$index_edit" + +# Get editor (same priority as lf) +EDITOR="${EDITOR:-${VISUAL:-vi}}" + +# Open editor +"$EDITOR" "$index_edit" + +# Check if line counts match +original_lines=$(wc -l < "$index") +edited_lines=$(wc -l < "$index_edit") + +if [ "$original_lines" -ne "$edited_lines" ]; then + echo "Error: Number of lines must stay the same ($original_lines -> $edited_lines)" >&2 + exit 1 +fi + +# Process the renames +echo "Processing renames in $target_dir..." +success_count=0 +error_count=0 + +max=$((original_lines + 1)) +counter=1 + +while [ $counter -le $max ]; do + a=$(sed "${counter}q;d" "$index") + b=$(sed "${counter}q;d" "$index_edit") + + counter=$((counter + 1)) + + # Skip if names are the same + [ "$a" = "$b" ] && continue + + # Full paths for source and destination + source_path="$target_dir/$a" + dest_path="$target_dir/$b" + + # Check if destination already exists + if [ -e "$dest_path" ]; then + echo "Error: File exists: $b" >&2 + error_count=$((error_count + 1)) + continue + fi + + # Check if source exists + if [ ! -e "$source_path" ]; then + echo "Error: Source file does not exist: $a" >&2 + error_count=$((error_count + 1)) + continue + fi + + # Perform rename + if mv "$source_path" "$dest_path"; then + echo "Renamed: $a -> $b" + success_count=$((success_count + 1)) + else + echo "Error: Failed to rename $a -> $b" >&2 + error_count=$((error_count + 1)) + fi +done + +echo "Summary: $success_count successful, $error_count errors" + +# Exit with error code if there were errors +[ $error_count -gt 0 ] && exit 1 || exit 0 diff --git a/pkgs/bulk-rename/default.nix b/pkgs/bulk-rename/default.nix new file mode 100644 index 0000000..2c591e4 --- /dev/null +++ b/pkgs/bulk-rename/default.nix @@ -0,0 +1,16 @@ +{ + writeShellApplication, + ... +}: + +let + name = "bulk-rename"; + text = builtins.readFile ./${name}.sh; +in +writeShellApplication { + inherit name text; + meta.mainProgram = name; + + runtimeInputs = [ + ]; +} diff --git a/pkgs/cppman/default.nix b/pkgs/cppman/default.nix new file mode 100644 index 0000000..de4820a --- /dev/null +++ b/pkgs/cppman/default.nix @@ -0,0 +1,55 @@ +{ + lib, + python3, + fetchPypi, + groff, +}: + +python3.pkgs.buildPythonApplication rec { + pname = "cppman"; + version = "0.5.9"; + pyproject = true; + + src = fetchPypi { + inherit pname version; + hash = "sha256-FaTkCrAltNzsWnOlDfJrfdrvfBSPyxl5QP/ySE+emQM="; + }; + + build-system = [ + python3.pkgs.setuptools + python3.pkgs.wheel + ]; + + dependencies = with python3.pkgs; [ + beautifulsoup4 + distutils + html5lib + lxml + six + soupsieve + typing-extensions + webencodings + ]; + + postInstall = '' + wrapProgram $out/bin/cppman --prefix PATH : "${groff}/bin" + ''; + + pythonRelaxDeps = true; + pythonRemoveDeps = true; # for bs4 + + pythonImportsCheck = [ + "cppman" + ]; + + meta = { + description = "C++ 98/11/14/17/20 manual pages for Linux/MacOS"; + homepage = "https://pypi.org/project/cppman"; + license = with lib.licenses; [ + gpl3Only + gpl2Only + ]; + maintainers = with lib.maintainers; [ ]; + mainProgram = "cppman"; + }; +} diff --git a/pkgs/default.nix b/pkgs/default.nix new file mode 100644 index 0000000..57ee78b --- /dev/null +++ b/pkgs/default.nix @@ -0,0 +1,24 @@ +{ + pkgs ? import , + ... +}: + +{ + arxiv-mcp-server = pkgs.callPackage ./arxiv-mcp-server { }; + baibot = pkgs.callPackage ./baibot { }; + blender-mcp = pkgs.callPackage ./blender-mcp { }; + bulk-rename = pkgs.callPackage ./bulk-rename { }; + cppman = pkgs.callPackage ./cppman { }; + fetcher-mcp = pkgs.callPackage ./fetcher-mcp { }; + freecad-mcp = pkgs.callPackage ./freecad-mcp { }; + kicad-mcp = pkgs.callPackage ./kicad-mcp { }; + mcpo = pkgs.callPackage ./mcpo { }; + pass2bw = pkgs.callPackage ./pass2bw { }; + pyman = pkgs.callPackage ./pyman { }; + quicknote = pkgs.callPackage ./quicknote { }; + synapse_change_display_name = pkgs.callPackage ./synapse_change_display_name { }; + synix-docs = pkgs.callPackage ./synix-docs { }; + trelis-gitingest-mcp = pkgs.callPackage ./trelis-gitingest-mcp { }; + + # marker-pdf = pkgs.callPackage ./marker-pdf { }; # FIXME +} diff --git a/pkgs/fetcher-mcp/default.nix b/pkgs/fetcher-mcp/default.nix new file mode 100644 index 0000000..d774592 --- /dev/null +++ b/pkgs/fetcher-mcp/default.nix @@ -0,0 +1,69 @@ +{ + lib, + buildNpmPackage, + fetchFromGitHub, + makeWrapper, + playwright-driver, + linkFarm, + jq, + ... +}: + +let + revision = "1161"; + + chromium-headless-shell = + playwright-driver.passthru.components."chromium-headless-shell".overrideAttrs + (old: { + inherit revision; + }); + + browsers-headless-only = linkFarm "playwright-browsers-headless-only" [ + { + name = "chromium-${revision}"; + path = chromium-headless-shell; + } + ]; +in +buildNpmPackage rec { + pname = "fetcher-mcp"; + version = "0.3.6"; + + src = fetchFromGitHub { + owner = "jae-jae"; + repo = "fetcher-mcp"; + rev = "4f4ad0f723367a7b0d3215c01d04282d573e6980"; + hash = "sha256-4Hh2H2ANBHOYYl3I1BqrkdCPNF/1hgv649CqAy7aiYw="; + }; + + env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD = "1"; + + nativeBuildInputs = [ + makeWrapper + jq + ]; + + npmDepsHash = "sha256-a56gDzZCo95vQUO57uFwMc9g/7jweYdCKqx64W8D1T8="; + + postPatch = '' + ${jq}/bin/jq 'del(.scripts.postinstall) | del(.scripts."install-browser")' package.json > package.json.tmp && mv package.json.tmp package.json + ''; + + makeWrapperArgs = [ + "--set" + "PLAYWRIGHT_BROWSERS_PATH" + "${browsers-headless-only}" + # "--set" + # "PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS" + # "true" + ]; + + meta = { + description = "MCP server for fetch web page content using Playwright headless browser"; + homepage = "https://github.com/jae-jae/fetcher-mcp"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ ]; + mainProgram = "fetcher-mcp"; + platforms = lib.platforms.all; + }; +} diff --git a/pkgs/freecad-mcp/default.nix b/pkgs/freecad-mcp/default.nix new file mode 100644 index 0000000..68997c4 --- /dev/null +++ b/pkgs/freecad-mcp/default.nix @@ -0,0 +1,36 @@ +{ + lib, + python3, + fetchPypi, +}: + +python3.pkgs.buildPythonApplication rec { + pname = "freecad-mcp"; + version = "0.1.13"; + pyproject = true; + + src = fetchPypi { + pname = "freecad_mcp"; + inherit version; + hash = "sha256-/CCMTyaDt6XsG+mok12pIM0TwG86Vs4pxq/Zd5Ol6wg="; + }; + + build-system = [ + python3.pkgs.hatchling + ]; + + dependencies = with python3.pkgs; [ + mcp + ]; + + pythonImportsCheck = [ + "freecad_mcp" + ]; + + meta = { + description = "Add your description here"; + homepage = "https://pypi.org/project/freecad-mcp/"; + license = lib.licenses.mit; + mainProgram = "freecad-mcp"; + }; +} diff --git a/pkgs/kicad-mcp/default.nix b/pkgs/kicad-mcp/default.nix new file mode 100644 index 0000000..e08c324 --- /dev/null +++ b/pkgs/kicad-mcp/default.nix @@ -0,0 +1,69 @@ +{ + lib, + buildNpmPackage, + fetchFromGitHub, + python3, + fetchPypi, + makeWrapper, + nodejs, + kicad, + ... +}: + +let + kicad-skip = import ./kicad-skip.nix { inherit lib python3 fetchPypi; }; + + pythonEnv = python3.withPackages ( + ps: + [ + kicad-skip + ] + ++ (with ps; [ + cairosvg + colorlog + pillow + pydantic + python-dotenv + requests + ]) + ); +in +buildNpmPackage rec { + pname = "kicad-mcp"; + version = "2.1.0-alpha"; + + src = fetchFromGitHub { + owner = "mixelpixx"; + repo = "KiCAD-MCP-Server"; + rev = "d2723bc29207c942bc93083131e2ac2a8d1a82d5"; + hash = "sha256-WvEd+tSjAdc+qjFh4kUaWGnd9pEiJb6Og7i1viUqh94="; + }; + + npmDepsHash = "sha256-nz73qj8CK2LyFixoF14ET2wq407YyuJUw/4VTDc80cQ="; + + buildScript = "build"; + + nativeBuildInputs = [ makeWrapper ]; + + buildInputs = [ + pythonEnv + ]; + + postInstall = '' + mkdir -p $out/bin + + makeWrapper ${nodejs}/bin/node $out/bin/${pname} \ + --add-flags "$out/lib/node_modules/${pname}/dist/index.js" \ + --prefix PATH : ${ + lib.makeBinPath [ + pythonEnv + ] + } \ + --prefix PYTHONPATH : "${kicad}/lib/python3/dist-packages" + ''; + + meta = { + description = "Model Context Protocol server for KiCAD"; + homepage = "https://github.com/mixelpixx/KiCAD-MCP-Server"; + }; +} diff --git a/pkgs/kicad-mcp/kicad-skip.nix b/pkgs/kicad-mcp/kicad-skip.nix new file mode 100644 index 0000000..c209104 --- /dev/null +++ b/pkgs/kicad-mcp/kicad-skip.nix @@ -0,0 +1,35 @@ +{ + lib, + python3, + fetchPypi, +}: + +python3.pkgs.buildPythonApplication rec { + pname = "kicad-skip"; + version = "0.2.5"; + pyproject = true; + + src = fetchPypi { + inherit pname version; + hash = "sha256-3GtHIV2h6C8syFZ/Dx6OWsgpf14ZQdlnGS4EM4DgjIM="; + }; + + build-system = [ + python3.pkgs.setuptools + ]; + + dependencies = with python3.pkgs; [ + sexpdata + ]; + + pythonImportsCheck = [ + "kicad_skip" + ]; + + meta = { + description = "A friendly way to skip the drudgery and manipulate kicad 7+ s-expression schematic, netlist and PCB files"; + homepage = "https://pypi.org/project/kicad-skip/"; + license = lib.licenses.lgpl21Only; + mainProgram = "kicad-skip"; + }; +} diff --git a/pkgs/marker-pdf/default.nix b/pkgs/marker-pdf/default.nix new file mode 100644 index 0000000..cb7d839 --- /dev/null +++ b/pkgs/marker-pdf/default.nix @@ -0,0 +1,106 @@ +{ + lib, + python3, + fetchPypi, + fetchurl, +}: + +let + fontFileName = "GoNotoCurrent-Regular.ttf"; + + fetchFont = fetchurl { + url = "https://models.datalab.to/artifacts/${fontFileName}"; + hash = "sha256-iCr7q5ZWCMLSvGJ/2AFrliqlpr4tNY+d4kp7WWfFYy4="; + }; + + python = python3; + + pdftext = import ./pdftext.nix { inherit lib python fetchPypi; }; + surya-ocr = import ./surya-ocr.nix { inherit lib python fetchPypi; }; +in + +python.pkgs.buildPythonApplication rec { + pname = "marker-pdf"; + version = "1.8.2"; + pyproject = true; + + src = fetchPypi { + pname = "marker_pdf"; + inherit version; + hash = "sha256-k2mxOpBBtXdCzxP4hqfXnCEqUF69hQZWr/d9V/tITZ4="; + }; + + patches = [ + ./skip-font-download.patch + ./fix-output-dir.patch + ]; + + pythonRelaxDeps = [ + "click" + "anthropic" + "markdownify" + "pillow" + ]; + + pythonRemoveDeps = [ + "pre-commit" + ]; + + postInstall = '' + FONT_DEST_DIR="$out/lib/${python.libPrefix}/site-packages/static/fonts" + mkdir -p $FONT_DEST_DIR + cp ${fetchFont} "$FONT_DEST_DIR/${fontFileName}" + echo "Installed font to $FONT_DEST_DIR/${fontFileName}" + ''; + + build-system = [ + python.pkgs.poetry-core + ]; + + dependencies = [ + pdftext + surya-ocr + ] + ++ (with python.pkgs; [ + anthropic + click + filetype + ftfy + google-genai + markdown2 + markdownify + openai + pillow + pydantic + pydantic-settings + python-dotenv + rapidfuzz + regex + scikit-learn + torch + tqdm + transformers + ]); + + optional-dependencies = with python.pkgs; { + full = [ + ebooklib + mammoth + openpyxl + python-pptx + weasyprint + ]; + }; + + pythonImportsCheck = [ + "marker" + ]; + + meta = { + description = "Convert documents to markdown with high speed and accuracy"; + homepage = "https://pypi.org/project/marker-pdf/"; + license = lib.licenses.gpl3Only; + maintainers = with lib.maintainers; [ ]; + }; + +} diff --git a/pkgs/marker-pdf/fix-output-dir.patch b/pkgs/marker-pdf/fix-output-dir.patch new file mode 100644 index 0000000..dd52173 --- /dev/null +++ b/pkgs/marker-pdf/fix-output-dir.patch @@ -0,0 +1,11 @@ +--- a/marker/settings.py ++++ b/marker/settings.py +@@ -6,7 +6,7 @@ + class Settings(BaseSettings): + # Paths + BASE_DIR: str = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +- OUTPUT_DIR: str = os.path.join(BASE_DIR, "conversion_results") ++ OUTPUT_DIR: str = "/tmp/marker_conversion_results" + FONT_DIR: str = os.path.join(BASE_DIR, "static", "fonts") + DEBUG_DATA_FOLDER: str = os.path.join(BASE_DIR, "debug_data") + ARTIFACT_URL: str = "https://models.datalab.to/artifacts" diff --git a/pkgs/marker-pdf/pdftext.nix b/pkgs/marker-pdf/pdftext.nix new file mode 100644 index 0000000..c1c4648 --- /dev/null +++ b/pkgs/marker-pdf/pdftext.nix @@ -0,0 +1,43 @@ +{ + lib, + python, + fetchPypi, +}: + +python.pkgs.buildPythonApplication rec { + pname = "pdftext"; + version = "0.6.3"; + pyproject = true; + + src = fetchPypi { + inherit pname version; + hash = "sha256-q1xd/g8ft43h24N8ytrB6kGwfOGJD+rZc8moTNr1Tew="; + }; + + pythonRelaxDeps = [ + "pypdfium2" + ]; + + build-system = [ + python.pkgs.poetry-core + ]; + + dependencies = with python.pkgs; [ + click + numpy + pydantic + pydantic-settings + pypdfium2 + ]; + + pythonImportsCheck = [ + "pdftext" + ]; + + meta = { + description = "Extract structured text from pdfs quickly"; + homepage = "https://pypi.org/project/pdftext/"; + license = lib.licenses.asl20; + maintainers = with lib.maintainers; [ ]; + }; +} diff --git a/pkgs/marker-pdf/skip-font-download.patch b/pkgs/marker-pdf/skip-font-download.patch new file mode 100644 index 0000000..a350fd1 --- /dev/null +++ b/pkgs/marker-pdf/skip-font-download.patch @@ -0,0 +1,24 @@ +--- a/marker/util.py ++++ b/marker/util.py +@@ -151,13 +151,7 @@ + return sorted_lines + + def download_font(): +- if not os.path.exists(settings.FONT_PATH): +- os.makedirs(os.path.dirname(settings.FONT_PATH), exist_ok=True) +- font_dl_path = f"{settings.ARTIFACT_URL}/{settings.FONT_NAME}" +- with requests.get(font_dl_path, stream=True) as r, open(settings.FONT_PATH, 'wb') as f: +- r.raise_for_status() +- for chunk in r.iter_content(chunk_size=8192): +- f.write(chunk) ++ pass + + def get_opening_tag_type(tag): + """ +@@ -195,4 +189,4 @@ + if tag_type in TAG_MAPPING: + return True, TAG_MAPPING[tag_type] + +- return False, None +\ No newline at end of file ++ return False, None diff --git a/pkgs/marker-pdf/surya-ocr.nix b/pkgs/marker-pdf/surya-ocr.nix new file mode 100644 index 0000000..ee7dbd0 --- /dev/null +++ b/pkgs/marker-pdf/surya-ocr.nix @@ -0,0 +1,58 @@ +{ + lib, + python, + fetchPypi, +}: + +python.pkgs.buildPythonApplication rec { + pname = "surya-ocr"; + version = "0.14.6"; + pyproject = true; + + src = fetchPypi { + pname = "surya_ocr"; + inherit version; + hash = "sha256-yFoL2d0AyGq0TtJlwO0VYBEG268tDQoGf6e7UzE31fA="; + }; + + pythonRelaxDeps = [ + "opencv-python-headless" + "pillow" + "pypdfium2" + "einops" + ]; + + pythonRemoveDeps = [ + "pre-commit" + ]; + + build-system = [ + python.pkgs.poetry-core + ]; + + dependencies = with python.pkgs; [ + click + einops + filetype + opencv-python-headless + pillow + platformdirs + pydantic + pydantic-settings + pypdfium2 + python-dotenv + torch + transformers + ]; + + pythonImportsCheck = [ + "surya" + ]; + + meta = { + description = "OCR, layout, reading order, and table recognition in 90+ languages"; + homepage = "https://pypi.org/project/surya-ocr/"; + license = lib.licenses.gpl3Only; + maintainers = with lib.maintainers; [ ]; + }; +} diff --git a/pkgs/mcpo/default.nix b/pkgs/mcpo/default.nix new file mode 100644 index 0000000..d4f3ee5 --- /dev/null +++ b/pkgs/mcpo/default.nix @@ -0,0 +1,53 @@ +# See https://github.com/NixOS/nixpkgs/pull/410836 +{ + lib, + python3, + fetchFromGitHub, +}: + +python3.pkgs.buildPythonApplication rec { + pname = "mcpo"; + version = "0.0.19"; + pyproject = true; + + src = fetchFromGitHub { + owner = "open-webui"; + repo = "mcpo"; + rev = "v${version}"; + hash = "sha256-ZfTVMrXXEsEKHmeG4850Hq3MEpQA/3WMpAVZS0zvp1I="; + }; + + build-system = [ + python3.pkgs.hatchling + ]; + + dependencies = with python3.pkgs; [ + click + fastapi + mcp + passlib + pydantic + pyjwt + python-dotenv + typer + uvicorn + watchdog + ]; + + pythonRelaxDeps = [ + "mcp" + ]; + + pythonImportsCheck = [ + "mcpo" + ]; + + meta = { + description = "A simple, secure MCP-to-OpenAPI proxy server"; + homepage = "https://github.com/open-webui/mcpo"; + changelog = "https://github.com/open-webui/mcpo/blob/${src.rev}/CHANGELOG.md"; + license = lib.licenses.mit; + maintainers = with lib.maintainers; [ ]; + mainProgram = "mcpo"; + }; +} diff --git a/pkgs/pass2bw/convert_csvs.py b/pkgs/pass2bw/convert_csvs.py new file mode 100644 index 0000000..4babb74 --- /dev/null +++ b/pkgs/pass2bw/convert_csvs.py @@ -0,0 +1,77 @@ +import csv +import re +import sys + +def process_csv(input_file, output_file): + column_mapping = { + 'Group(/)': 'folder', + 'Title': 'name', + 'Password': 'login_password', + 'Notes': 'notes' + } + + default_values = { + 'favorite': '', + 'type': 'login', + 'fields': '', + 'reprompt': '0', + 'login_uri': '', + 'login_username': '', + 'login_totp': '' + } + + target_columns = [ + 'folder', 'favorite', 'type', 'name', 'notes', 'fields', + 'reprompt', 'login_uri', 'login_username', 'login_password', 'login_totp' + ] + + with open(input_file, 'r', newline='', encoding='utf-8') as infile, \ + open(output_file, 'w', newline='', encoding='utf-8') as outfile: + + reader = csv.DictReader(infile) + writer = csv.DictWriter(outfile, fieldnames=target_columns) + + writer.writeheader() + + for row in reader: + new_row = {} + + for old_col, new_col in column_mapping.items(): + new_row[new_col] = row.get(old_col, '') + + for col, default_val in default_values.items(): + if col not in new_row: + new_row[col] = default_val + + if new_row['notes']: + new_row['notes'] = new_row['notes'].replace('\n', ' ').replace('\r', ' ') + new_row['notes'] = re.sub(r'\s+', ' ', new_row['notes']).strip() + + notes = new_row['notes'] + if notes: + # Look for pattern: "login: USERNAME" + match = re.search(r'login:\s*(\S+)', notes, re.IGNORECASE) + if match: + username = match.group(1) + new_row['login_username'] = username + new_row['notes'] = re.sub(r'login:\s*\S+', '', notes, flags=re.IGNORECASE).strip() + new_row['notes'] = re.sub(r'\s+', ' ', new_row['notes']).strip() + new_row['notes'] = new_row['notes'].strip(': ').strip() + + for key in new_row: + if new_row[key]: + new_row[key] = str(new_row[key]).replace('\n', ' ').replace('\r', ' ') + new_row[key] = re.sub(r'\s+', ' ', new_row[key]).strip() + + writer.writerow(new_row) + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python script.py ") + sys.exit(1) + + input_file = sys.argv[1] + output_file = sys.argv[2] + + process_csv(input_file, output_file) + print(f"Processing complete. Output saved to {output_file}") diff --git a/pkgs/pass2bw/default.nix b/pkgs/pass2bw/default.nix new file mode 100644 index 0000000..349d8d1 --- /dev/null +++ b/pkgs/pass2bw/default.nix @@ -0,0 +1,32 @@ +{ + stdenv, + lib, + makeWrapper, + python3, + ... +}: + +stdenv.mkDerivation rec { + pname = "pass2bw"; + version = "0.1"; + + src = ./.; + + dontBuild = true; + doCheck = false; + + nativeBuildInputs = [ makeWrapper ]; + + installPhase = '' + mkdir -p $out/bin + + sed -e "s|python ./convert_csvs.py|python $out/bin/convert_csvs.py|" \ + ${src}/${pname}.sh > $out/bin/${pname} + chmod +x $out/bin/${pname} + + cp ${src}/convert_csvs.py $out/bin/ + + wrapProgram $out/bin/${pname} \ + --prefix PATH : ${lib.makeBinPath [ python3 ]} + ''; +} diff --git a/pkgs/pass2bw/pass2bw.sh b/pkgs/pass2bw/pass2bw.sh new file mode 100644 index 0000000..95cb544 --- /dev/null +++ b/pkgs/pass2bw/pass2bw.sh @@ -0,0 +1,8 @@ +if [ "$#" -ne 2 ]; then + echo "Usage: $0 " + exit 1 +fi + +pass2csv "$1" /tmp/pass.csv +python ./convert_csvs.py /tmp/pass.csv "$2" +rm /tmp/pass.csv diff --git a/pkgs/pyman/default.nix b/pkgs/pyman/default.nix new file mode 100644 index 0000000..f822df5 --- /dev/null +++ b/pkgs/pyman/default.nix @@ -0,0 +1,22 @@ +{ + python3Packages, + ... +}: + +python3Packages.buildPythonApplication { + pname = "pyman"; + version = "1.0.0"; + + src = ./.; + pyproject = true; + + build-system = [ python3Packages.setuptools ]; + + propagatedBuildInputs = [ python3Packages.pyyaml ]; + + doCheck = false; + + meta = { + description = "Display colors from a YAML color palette file in the terminal."; + }; +} diff --git a/pkgs/pyman/pyman b/pkgs/pyman/pyman new file mode 100644 index 0000000..966fab9 --- /dev/null +++ b/pkgs/pyman/pyman @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +import sys +import io + +def main(): + if len(sys.argv) != 2: + print(f"Usage: {sys.argv[0]} ") + sys.exit(1) + + topic = sys.argv[1] + + # Capture the output of the help function + captured_output = io.StringIO() + sys.stdout = captured_output + try: + help(topic) + except Exception as e: + print(f"Error: {e}") + finally: + sys.stdout = sys.__stdout__ + + # Print the captured output + print(captured_output.getvalue()) + +if __name__ == "__main__": + main() diff --git a/pkgs/pyman/setup.py b/pkgs/pyman/setup.py new file mode 100644 index 0000000..6576262 --- /dev/null +++ b/pkgs/pyman/setup.py @@ -0,0 +1,7 @@ +from setuptools import setup + +setup( + name='pyman', + version='1.0.0', + scripts=['pyman'], +) diff --git a/pkgs/quicknote/default.nix b/pkgs/quicknote/default.nix new file mode 100644 index 0000000..c7b06d2 --- /dev/null +++ b/pkgs/quicknote/default.nix @@ -0,0 +1,16 @@ +{ + writeShellApplication, + ... +}: + +let + name = "quicknote"; + text = builtins.readFile ./${name}.sh; +in +writeShellApplication { + inherit name text; + meta.mainProgram = name; + + runtimeInputs = [ + ]; +} diff --git a/pkgs/quicknote/quicknote.sh b/pkgs/quicknote/quicknote.sh new file mode 100644 index 0000000..d24fbce --- /dev/null +++ b/pkgs/quicknote/quicknote.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +if [ -n "$XDG_DOCUMENTS_DIR" ]; then + NOTES_DIR="$XDG_DOCUMENTS_DIR/notes" +else + NOTES_DIR="/tmp/notes" +fi + +mkdir -p "$NOTES_DIR" + +TIMESTAMP=$(date +"%Y-%m-%d_%H-%M-%S") +NOTE_FILE="$NOTES_DIR/note_$TIMESTAMP.md" +touch "$NOTE_FILE" + +if [ -z "$EDITOR" ]; then + # Try common editors + for editor in vim vi nano; do + if command -v "$editor" &> /dev/null; then + EDITOR="$editor" + break + fi + done + + if [ -z "$EDITOR" ]; then + echo "Error: No editor found. Please set the EDITOR environment variable." + exit 1 + fi +fi + +"$EDITOR" "$NOTE_FILE" diff --git a/pkgs/synapse_change_display_name/default.nix b/pkgs/synapse_change_display_name/default.nix new file mode 100644 index 0000000..8e9322e --- /dev/null +++ b/pkgs/synapse_change_display_name/default.nix @@ -0,0 +1,26 @@ +{ stdenv, gcc, ... }: + +let + pname = "synapse_change_display_name"; + version = "1.0.0"; +in +stdenv.mkDerivation { + inherit pname version; + + src = ./.; + + buildInputs = [ gcc ]; + + buildPhase = '' + gcc -o ${pname} ${pname}.c + ''; + + installPhase = '' + mkdir -p $out/bin + cp ${pname} $out/bin/ + ''; + + meta = { + description = "A program for updating MatrixS-Synapse display names"; + }; +} diff --git a/pkgs/synapse_change_display_name/synapse_change_display_name.c b/pkgs/synapse_change_display_name/synapse_change_display_name.c new file mode 100644 index 0000000..0571467 --- /dev/null +++ b/pkgs/synapse_change_display_name/synapse_change_display_name.c @@ -0,0 +1,105 @@ +#include +#include +#include + +// Function to print the help page +void print_help() { + printf("Usage:\n"); + printf(" update_display_name [-t ACCESS_TOKEN] [-r REMOTE] [-u USER] [-d " + "DISPLAY_NAME] [-h]\n\n"); + printf("Options:\n"); + printf(" -h Show this help message and exit.\n"); + printf(" -t ACCESS_TOKEN Your access token for authentication.\n"); + printf(" -r REMOTE The remote server address.\n"); + printf(" -u USER The user identifier to update.\n"); + printf(" -d DISPLAY_NAME The new display name to set.\n\n"); + printf("Example:\n"); + printf(" update_display_name -t my_access_token -r my_remote_server -u " + "user123 -d \"My New Display Name\"\n"); +} + +int main(int argc, char *argv[]) { + char *access_token = NULL; + char *remote = NULL; + char *user = NULL; + char *display_name = NULL; + + for (int i = 1; i < argc; i++) { + if (argv[i][0] == '-' && + strlen(argv[i]) == 2) { // Ensure it is a single-character flag + switch (argv[i][1]) { + case 'h': + print_help(); + return 0; + case 't': + if (i + 1 < argc) { + access_token = argv[++i]; + } else { + printf("Error: Missing value for -t (ACCESS_TOKEN).\n\n"); + print_help(); + return 1; + } + break; + case 'r': + if (i + 1 < argc) { + remote = argv[++i]; + } else { + printf("Error: Missing value for -r (REMOTE).\n\n"); + print_help(); + return 1; + } + break; + case 'u': + if (i + 1 < argc) { + user = argv[++i]; + } else { + printf("Error: Missing value for -u (USER).\n\n"); + print_help(); + return 1; + } + break; + case 'd': + if (i + 1 < argc) { + display_name = argv[++i]; + } else { + printf("Error: Missing value for -d (DISPLAY_NAME).\n\n"); + print_help(); + return 1; + } + break; + default: + printf("Error: Unknown option -%c.\n\n", argv[i][1]); + print_help(); + return 1; + } + } else { + printf("Error: Invalid argument format: %s\n\n", argv[i]); + print_help(); + return 1; + } + } + + if (!access_token || !remote || !user || !display_name) { + printf("Error: Missing required arguments.\n\n"); + print_help(); + return 1; + } + + char command[1024]; + snprintf(command, sizeof(command), + "curl --header \"Authorization: Bearer %s\" " + "--location --request PUT " + "'https://%s/_matrix/client/v3/profile/%s/displayname' " + "--data '{ \"displayname\": \"%s\" }'", + access_token, remote, user, display_name); + + int result = system(command); + + if (result != 0) { + printf("\nError: Failed to execute the curl command.\n"); + return 1; + } + + printf("\nDisplay name updated successfully.\n"); + return 0; +} diff --git a/pkgs/synix-docs/default.nix b/pkgs/synix-docs/default.nix new file mode 100644 index 0000000..fb37d1b --- /dev/null +++ b/pkgs/synix-docs/default.nix @@ -0,0 +1,25 @@ +{ + stdenv, + python313, + ... +}: + +let + python = python313.withPackages ( + p: with p; [ + mkdocs + mkdocs-material + mkdocs-material-extensions + pygments + ] + ); +in +stdenv.mkDerivation { + name = "synix-docs"; + src = ./../../.; + nativeBuildInputs = [ + python + ]; + buildPhase = "mkdocs build --strict --verbose"; + installPhase = "cp -r site $out"; +} diff --git a/pkgs/trelis-gitingest-mcp/default.nix b/pkgs/trelis-gitingest-mcp/default.nix new file mode 100644 index 0000000..4a810ed --- /dev/null +++ b/pkgs/trelis-gitingest-mcp/default.nix @@ -0,0 +1,48 @@ +# See https://github.com/coderamp-labs/gitingest/issues/245 +{ + lib, + python3, + fetchPypi, + curl, + git, +}: + +python3.pkgs.buildPythonApplication rec { + pname = "trelis-gitingest-mcp"; + version = "1.1.2"; + pyproject = true; + + src = fetchPypi { + pname = "trelis_gitingest_mcp"; + inherit version; + hash = "sha256-ZOoG0nL0+XnyOUPo4qsGTYizAPzSECUr9eSHEp4Hmzc="; + }; + + build-system = [ + python3.pkgs.hatchling + ]; + + dependencies = + with python3.pkgs; + [ + gitingest + mcp + pathspec + ] + ++ [ + curl + git + ]; + + pythonImportsCheck = [ + "gitingest_mcp" + ]; + + meta = { + description = "An MCP server for gitingest"; + homepage = "https://pypi.org/project/trelis-gitingest-mcp/"; + license = lib.licenses.asl20; + maintainers = with lib.maintainers; [ ]; + mainProgram = "trelis-gitingest-mcp"; + }; +} diff --git a/templates/container/README.md b/templates/container/README.md new file mode 100644 index 0000000..9836c7d --- /dev/null +++ b/templates/container/README.md @@ -0,0 +1,70 @@ +# NixOS Container + +Imperative container NixOS configuration. + +## References: + +- [NixOS Manual](https://nixos.org/manual/nixos/stable/#sec-imperative-containers) +- [NixOS Wiki](https://wiki.nixos.org/wiki/NixOS_Containers#Define_and_create_nixos-container_from_a_Flake_file) + +## Setup + +In your host configuration, set: + +```nix +boot.enableContainers = true; +``` + +Create a new directory and initialize the template inside of it: + +> `nxc` is an arbitrary name + +```bash +mkdir -p nxc +cd nxc +nix flake init -t git+https://git.sid.ovh/sid/synix#container +``` + +## Usage + +Create the container: + +```bash +sudo nixos-container create nxc --flake . +``` + +Start the container: + +```bash +sudo nixos-container start nxc +``` + +Rebuild the container: + +```bash +sudo nixos-container update nxc --flake . +``` + +Log in as root: + +```bash +sudo nixos-container root-login nxc +``` + +Stop the container: + +```bash +sudo nixos-container stop nxc +``` + +Destroy the container: + +```bash +sudo nixos-container destroy nxc +``` + +For more, see the help page: + +```bash +nixos-container --help +``` diff --git a/templates/container/config/base.nix b/templates/container/config/base.nix new file mode 100644 index 0000000..1db4661 --- /dev/null +++ b/templates/container/config/base.nix @@ -0,0 +1,32 @@ +# Edit this only if you know what you're doing. +{ outputs, ... }: + +{ + boot = { + isContainer = true; + isNspawnContainer = true; + }; + + nix = { + channel.enable = false; + settings = { + experimental-features = "nix-command flakes"; + builders-use-substitutes = true; + substituters = [ + "https://cache.nixos.org" + "https://nix-community.cachix.org" + ]; + trusted-public-keys = [ + "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + ]; + }; + }; + + nixpkgs.overlays = [ + outputs.overlays.synix-packages + outputs.overlays.local-packages + ]; + + system.stateVersion = "25.11"; +} diff --git a/templates/container/config/configuration.nix b/templates/container/config/configuration.nix new file mode 100644 index 0000000..da1f3f2 --- /dev/null +++ b/templates/container/config/configuration.nix @@ -0,0 +1,5 @@ +{ + networking.hostName = "nxc"; + + # Add the rest of your configuration here +} diff --git a/templates/container/config/default.nix b/templates/container/config/default.nix new file mode 100644 index 0000000..19f200e --- /dev/null +++ b/templates/container/config/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./base.nix + ./configuration.nix + ]; +} diff --git a/templates/container/flake.nix b/templates/container/flake.nix new file mode 100644 index 0000000..e6c3b3b --- /dev/null +++ b/templates/container/flake.nix @@ -0,0 +1,58 @@ +{ + description = "Container NixOS configurations"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + synix.url = "git+https://git.sid.ovh/sid/synix.git?ref=release-25.11"; + synix.imputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + ... + }@inputs: + let + inherit (self) outputs; + + system = "x86_64-linux"; + + lib = nixpkgs.lib.extend (final: prev: inputs.synix.lib or { }); + in + { + packages = + let + pkgs = nixpkgs.legacyPackages.${system}; + in + import ./pkgs { inherit pkgs; }; + + overlays = import ./overlays { inherit (self) inputs; }; + + devShells = + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + nixos-container + tmux + ]; + }; + }; + + nixosModules = import ./modules; + + nixosConfigurations = { + container = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ ./config ]; + specialArgs = { + inherit inputs outputs lib; + }; + }; + }; + }; +} diff --git a/templates/container/modules/default.nix b/templates/container/modules/default.nix new file mode 100644 index 0000000..9b65824 --- /dev/null +++ b/templates/container/modules/default.nix @@ -0,0 +1,3 @@ +{ + # example = import ./example; +} diff --git a/templates/container/overlays/default.nix b/templates/container/overlays/default.nix new file mode 100644 index 0000000..0f5d0f3 --- /dev/null +++ b/templates/container/overlays/default.nix @@ -0,0 +1,7 @@ +{ inputs, ... }: + +{ + synix-packages = final: prev: { synix = inputs.synix.overlays.additions final prev; }; + + local-packages = final: prev: { local = import ../pkgs { pkgs = final; }; }; +} diff --git a/templates/container/pkgs/default.nix b/templates/container/pkgs/default.nix new file mode 100644 index 0000000..0df9091 --- /dev/null +++ b/templates/container/pkgs/default.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + # example = pkgs.callPackage ./example { }; +} diff --git a/templates/dev/c-hello/.envrc b/templates/dev/c-hello/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/templates/dev/c-hello/.envrc @@ -0,0 +1 @@ +use flake diff --git a/templates/dev/c-hello/.github/workflows/c-nix.yml b/templates/dev/c-hello/.github/workflows/c-nix.yml new file mode 100644 index 0000000..2895662 --- /dev/null +++ b/templates/dev/c-hello/.github/workflows/c-nix.yml @@ -0,0 +1,23 @@ +name: C Nix Pipeline + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Nix + uses: cachix/install-nix-action@v18 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Run nix flake check + run: nix flake check diff --git a/templates/dev/c-hello/.gitignore b/templates/dev/c-hello/.gitignore new file mode 100644 index 0000000..48c1ab2 --- /dev/null +++ b/templates/dev/c-hello/.gitignore @@ -0,0 +1,7 @@ +.cache/ +.direnv/ +.pre-commit-config.yaml +bin/ +build/ +compile_commands.json +result/ diff --git a/templates/dev/c-hello/Makefile b/templates/dev/c-hello/Makefile new file mode 100644 index 0000000..79c1dff --- /dev/null +++ b/templates/dev/c-hello/Makefile @@ -0,0 +1,51 @@ +PNAME = hello-world + +BUILD_DIR = build +INCLUDE_DIR = include +SRC_DIR = src + +CC = gcc +CFLAGS = -I$(INCLUDE_DIR) -Wall -Wextra -g + +TARGET = $(BUILD_DIR)/$(PNAME) +SRCS = $(wildcard $(SRC_DIR)/*.c) +OBJS = $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS)) + +# Default target +all: $(TARGET) + +# Build the executable +$(TARGET): $(OBJS) + $(CC) $(OBJS) -o $@ + +# Compile source files into object files +$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c | $(BUILD_DIR) + $(CC) $(CFLAGS) -c $< -o $@ + +# Create the build directory +$(BUILD_DIR): + mkdir -p $(BUILD_DIR) + +# Run the executable +run: $(TARGET) + @$(TARGET) + +# Clean built files +clean: + rm -rf $(BUILD_DIR) + +# Clean then build +rebuild: clean all + +# Display help information +help: + @echo "Makefile Usage:" + @echo "" + @echo " make - Builds the executable (default)" + @echo " make all - Same as 'make'" + @echo " make run - Builds the executable if needed and then runs it" + @echo " make clean - Removes the build directory and all built files" + @echo " make rebuild - Performs a clean first, then builds the project" + @echo " make help - Displays this help message" + +.PHONY: all clean run rebuild help diff --git a/templates/dev/c-hello/build.sh b/templates/dev/c-hello/build.sh new file mode 100644 index 0000000..f7f470c --- /dev/null +++ b/templates/dev/c-hello/build.sh @@ -0,0 +1,2 @@ +make clean +bear --output build/compile_commands.json -- make all diff --git a/templates/dev/c-hello/flake.nix b/templates/dev/c-hello/flake.nix new file mode 100644 index 0000000..86ce156 --- /dev/null +++ b/templates/dev/c-hello/flake.nix @@ -0,0 +1,144 @@ +{ + description = "A hello world template in C"; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + pre-commit-hooks = { + url = "github:cachix/pre-commit-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + pre-commit-hooks, + ... + }: + let + pname = "hello-world"; # Also change this in the Makefile + version = "0.1.0"; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + nixpkgsFor = forAllSystems ( + system: + import nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + } + ); + in + { + overlays.default = final: prev: { + "${pname}" = final.stdenv.mkDerivation rec { + inherit pname version; + src = ./.; + installPhase = '' + mkdir -p $out/bin + cp build/${pname} $out/bin/ + ''; + }; + }; + + packages = forAllSystems (system: { + default = nixpkgsFor.${system}."${pname}"; + "${pname}" = nixpkgsFor.${system}."${pname}"; + }); + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + in + { + default = pkgs.mkShell { + buildInputs = + self.checks.${system}.pre-commit-check.enabledPackages + ++ (with pkgs; [ + bear + coreutils + gcc + gdb + gnumake + ]); + shellHook = self.checks.${system}.pre-commit-check.shellHook + '' + export LD_LIBRARY_PATH=${pkgs.stdenv.cc.cc.lib}/lib:$LD_LIBRARY_PATH + ''; + }; + } + ); + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + flakePkgs = self.packages.${system}; + in + { + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + + integration-test = + let + exe = "${flakePkgs.${pname}}/bin/${pname}"; + in + pkgs.runCommand "${pname}-test" + { + nativeBuildInputs = [ + pkgs.synixutils + flakePkgs.${pname} + ]; + } + '' + assert_equal() { + if [[ "$1" != "$2" ]]; then + echo "Test failed: Expected '$1' but got '$2'" + exit 1 + fi + } + + exp1="Hello, world!" + out1="$(${exe})" + + assert_equal "$exp1" "$out1" + + echo "Test passed!" > $out + ''; + + pre-commit-check = pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt = { + enable = true; + }; + clang-format = { + enable = true; + types_or = nixpkgs.lib.mkForce [ "c" ]; + }; + }; + }; + } + ); + }; +} diff --git a/templates/dev/c-hello/src/main.c b/templates/dev/c-hello/src/main.c new file mode 100644 index 0000000..30feb49 --- /dev/null +++ b/templates/dev/c-hello/src/main.c @@ -0,0 +1,8 @@ +#include +#include + +int main(int argc, char **argv) { + printf("Hello, world!\n"); + + return EXIT_SUCCESS; +} diff --git a/templates/dev/esp-blink/.envrc b/templates/dev/esp-blink/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/templates/dev/esp-blink/.envrc @@ -0,0 +1 @@ +use flake diff --git a/templates/dev/esp-blink/.gitignore b/templates/dev/esp-blink/.gitignore new file mode 100644 index 0000000..c206ec4 --- /dev/null +++ b/templates/dev/esp-blink/.gitignore @@ -0,0 +1,6 @@ +.cache/ +.direnv/ +build/ +managed_components/ +sdkconfig +sdkconfig.old diff --git a/templates/dev/esp-blink/CMakeLists.txt b/templates/dev/esp-blink/CMakeLists.txt new file mode 100644 index 0000000..6405fb8 --- /dev/null +++ b/templates/dev/esp-blink/CMakeLists.txt @@ -0,0 +1,5 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +idf_build_set_property(MINIMAL_BUILD ON) +project(blink) diff --git a/templates/dev/esp-blink/README.md b/templates/dev/esp-blink/README.md new file mode 100644 index 0000000..d804413 --- /dev/null +++ b/templates/dev/esp-blink/README.md @@ -0,0 +1,33 @@ +# ESP32 blink template + +Set `BLINK_GPIO` to your LED pin in [`main/main.c`](./main/main.c). + +## Clean the build directory + +```bash +idf.py fullclean +``` + +## Set the build target + +```bash +idf.py set-target esp32s3 +``` + +## Open configuration menu + +```bash +idf.py menuconfig +``` + +## Build the project + +```bash +idf.py all +``` + +## Flash the binary + +```bash +idf.py flash +``` diff --git a/templates/dev/esp-blink/flake.nix b/templates/dev/esp-blink/flake.nix new file mode 100644 index 0000000..ba3b20e --- /dev/null +++ b/templates/dev/esp-blink/flake.nix @@ -0,0 +1,103 @@ +{ + description = "A blink template for ESP32"; + + inputs = { + nixpkgs.url = "nixpkgs/nixpkgs-unstable"; + esp = { + url = "github:mirrexagon/nixpkgs-esp-dev"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + pre-commit-hooks = { + url = "github:cachix/pre-commit-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + esp, + pre-commit-hooks, + ... + }: + let + pname = "blink"; # Also change this in CMakeLists.txt + version = "0.1.0"; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + nixpkgsFor = forAllSystems ( + system: + import nixpkgs { + inherit system; + overlays = [ + self.overlays.default + esp.overlays.default + ]; + } + ); + in + { + overlays.default = final: prev: { }; + + packages = forAllSystems (system: { }); + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + in + { + default = esp.devShells."${system}".default; + } + ); + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + flakePkgs = self.packages.${system}; + in + { + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + + pre-commit-check = pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt = { + enable = true; + }; + clang-format = { + enable = true; + types_or = nixpkgs.lib.mkForce [ + "c" + "cpp" + ]; + }; + }; + }; + } + ); + }; +} diff --git a/templates/dev/esp-blink/main/CMakeLists.txt b/templates/dev/esp-blink/main/CMakeLists.txt new file mode 100644 index 0000000..9aaa88c --- /dev/null +++ b/templates/dev/esp-blink/main/CMakeLists.txt @@ -0,0 +1,4 @@ +idf_component_register( + SRCS "main.c" + INCLUDE_DIRS "." +) diff --git a/templates/dev/esp-blink/main/idf_component.yml b/templates/dev/esp-blink/main/idf_component.yml new file mode 100644 index 0000000..d57b77d --- /dev/null +++ b/templates/dev/esp-blink/main/idf_component.yml @@ -0,0 +1,2 @@ +dependencies: + espressif/led_strip: "^3.0.0" diff --git a/templates/dev/esp-blink/main/main.c b/templates/dev/esp-blink/main/main.c new file mode 100644 index 0000000..9d38707 --- /dev/null +++ b/templates/dev/esp-blink/main/main.c @@ -0,0 +1,33 @@ +#include "driver/gpio.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include + +static const char *TAG = "BLINK"; + +#define BLINK_GPIO 38 +#define BLINK_PERIOD 1000 + +static uint8_t s_led_state = 0; + +static void blink_led(void) { gpio_set_level(BLINK_GPIO, s_led_state); } + +static void configure_led(void) { + ESP_LOGI(TAG, "Example configured to blink GPIO LED!"); + gpio_reset_pin(BLINK_GPIO); + gpio_set_direction(BLINK_GPIO, GPIO_MODE_OUTPUT); +} + +static void delay_ms(uint32_t ms) { vTaskDelay(pdMS_TO_TICKS(ms)); } + +void app_main(void) { + configure_led(); + + while (1) { + ESP_LOGI(TAG, "Turning the LED %s!", s_led_state == true ? "ON" : "OFF"); + blink_led(); + s_led_state = !s_led_state; + delay_ms(BLINK_PERIOD); + } +} diff --git a/templates/dev/flask-hello/.envrc b/templates/dev/flask-hello/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/templates/dev/flask-hello/.envrc @@ -0,0 +1 @@ +use flake diff --git a/templates/dev/flask-hello/.github/workflows/python-nix.yml b/templates/dev/flask-hello/.github/workflows/python-nix.yml new file mode 100644 index 0000000..a51c010 --- /dev/null +++ b/templates/dev/flask-hello/.github/workflows/python-nix.yml @@ -0,0 +1,23 @@ +name: Python Nix Pipeline + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Nix + uses: cachix/install-nix-action@v18 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Run nix flake check + run: nix flake check diff --git a/templates/dev/flask-hello/.gitignore b/templates/dev/flask-hello/.gitignore new file mode 100644 index 0000000..3f4095c --- /dev/null +++ b/templates/dev/flask-hello/.gitignore @@ -0,0 +1,30 @@ +# Byte-compiled Python files +*.py[cod] +__pycache__/ + +# Distribution / packaging +.Python +*.egg +*.egg-info/ +.coverage +.htmlcov/ +.pytest_cache/ +.tox/ +.venv/ +.direnv/ +ENV/ +build/ +dist/ +env.bak/ +env/ +venv.bak/ +venv/ + +# IDE/editor files +*.sublime-project +*.sublime-workspace +.idea/ +.vscode/ + +# Nix-related files +result diff --git a/templates/dev/flask-hello/app.py b/templates/dev/flask-hello/app.py new file mode 100644 index 0000000..1b4f5b6 --- /dev/null +++ b/templates/dev/flask-hello/app.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python3 + +from flask_hello import create_app + +app = create_app() diff --git a/templates/dev/flask-hello/flake.nix b/templates/dev/flask-hello/flake.nix new file mode 100644 index 0000000..39a9ce3 --- /dev/null +++ b/templates/dev/flask-hello/flake.nix @@ -0,0 +1,77 @@ +{ + description = "A hello world template for Python Flask"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + pre-commit-hooks = { + url = "github:cachix/pre-commit-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + ... + }: + let + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + nixpkgsFor = forAllSystems ( + system: + import nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + } + ); + in + { + overlays.default = final: _prev: { + flask_hello = self.packages.${final.system}.default; + }; + + packages = forAllSystems (system: { + default = nixpkgsFor.${system}.callPackage ./nix/package.nix { }; + }); + + devShells = forAllSystems (system: { + default = import ./nix/shell.nix { pkgs = nixpkgsFor.${system}; }; + }); + + nixosModules = { + flask_hello = import ./nix/module.nix; + }; + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems (system: { + build-packages = nixpkgsFor."${system}".linkFarm "flake-packages-${system}" self.packages.${system}; + pre-commit-check = self.inputs.pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt.enable = true; + black.enable = true; + }; + }; + }); + }; +} diff --git a/templates/dev/flask-hello/flask_hello/__init__.py b/templates/dev/flask-hello/flask_hello/__init__.py new file mode 100644 index 0000000..c655324 --- /dev/null +++ b/templates/dev/flask-hello/flask_hello/__init__.py @@ -0,0 +1,17 @@ +from flask import Flask + + +def create_app(): + app = Flask(__name__) + + from .blueprints.home import home_bp + + app.register_blueprint(home_bp) + + from flask import render_template + + @app.errorhandler(404) + def not_found_error(error): + return render_template("errors.html", error="Page not found"), 404 + + return app diff --git a/templates/dev/flask-hello/flask_hello/blueprints/__init__.py b/templates/dev/flask-hello/flask_hello/blueprints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/templates/dev/flask-hello/flask_hello/blueprints/home.py b/templates/dev/flask-hello/flask_hello/blueprints/home.py new file mode 100644 index 0000000..ab1caa5 --- /dev/null +++ b/templates/dev/flask-hello/flask_hello/blueprints/home.py @@ -0,0 +1,8 @@ +from flask import Blueprint, render_template + +home_bp = Blueprint("home", __name__) + + +@home_bp.route("/") +def index(): + return render_template("index.html") diff --git a/templates/dev/flask-hello/flask_hello/static/css/style.css b/templates/dev/flask-hello/flask_hello/static/css/style.css new file mode 100644 index 0000000..3a40e02 --- /dev/null +++ b/templates/dev/flask-hello/flask_hello/static/css/style.css @@ -0,0 +1,9 @@ +body { + font-family: Arial, sans-serif; + margin: 40px; + background-color: #f5f5f5; +} + +h1 { + color: #333; +} diff --git a/templates/dev/flask-hello/flask_hello/templates/base.html b/templates/dev/flask-hello/flask_hello/templates/base.html new file mode 100644 index 0000000..4a01c22 --- /dev/null +++ b/templates/dev/flask-hello/flask_hello/templates/base.html @@ -0,0 +1,12 @@ + + + + + + {% block title %}Flask App{% endblock %} + + + + {% block content %}{% endblock %} + + diff --git a/templates/dev/flask-hello/flask_hello/templates/errors.html b/templates/dev/flask-hello/flask_hello/templates/errors.html new file mode 100644 index 0000000..117fd32 --- /dev/null +++ b/templates/dev/flask-hello/flask_hello/templates/errors.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block content %} +

Error

+

{{ error }}

+
Go Home +{% endblock %} diff --git a/templates/dev/flask-hello/flask_hello/templates/index.html b/templates/dev/flask-hello/flask_hello/templates/index.html new file mode 100644 index 0000000..7bf5a2f --- /dev/null +++ b/templates/dev/flask-hello/flask_hello/templates/index.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} + +{% block content %} +

Hello, World!

+

Welcome to your Flask application.

+{% endblock %} diff --git a/templates/dev/flask-hello/nix/module.nix b/templates/dev/flask-hello/nix/module.nix new file mode 100644 index 0000000..46cb9b3 --- /dev/null +++ b/templates/dev/flask-hello/nix/module.nix @@ -0,0 +1,132 @@ +{ + inputs, + config, + lib, + pkgs, + ... +}: + +let + cfg = config.services.flask_hello; + domain = config.networking.domain; + fqdn = if (cfg.nginx.subdomain != "") then "${cfg.nginx.subdomain}.${domain}" else domain; + + python-with-packages = pkgs.python3.withPackages ( + p: with p; [ + flask + ] + ); + + inherit (lib) + concatStringsSep + getExe + mkDefault + mkEnableOption + mkIf + mkOption + mkPackageOption + types + ; +in +{ + options.services.flask_hello = { + enable = mkEnableOption "Flask Hello World service."; + + package = mkPackageOption pkgs "flask_hello" { }; + + port = mkOption { + type = types.port; + default = 5000; + description = "The port to listen on."; + }; + + user = mkOption { + type = types.str; + description = "The user the Flask service will run as."; + default = "flaskapp"; + }; + + group = mkOption { + type = types.str; + description = "The group the Flask service will run as."; + default = "flaskapp"; + }; + + nginx = { + enable = mkOption { + type = types.bool; + default = true; + description = "Enable Nginx as a reverse proxy for the Flask application."; + }; + subdomain = mkOption { + type = types.str; + default = "flask_hello"; + description = "Subdomain for the Nginx virtual host. Leave empty for root domain."; + }; + ssl = mkOption { + type = types.bool; + default = true; + description = "Enable SSL for the Nginx virtual host using ACME."; + }; + }; + + gunicorn.extraArgs = mkOption { + type = types.listOf types.str; + default = [ ]; + description = "Extra arguments for gunicorn."; + }; + }; + + config = mkIf cfg.enable { + nixpkgs.overlays = [ inputs.flask_hello.overlays.default ]; + + networking.firewall.allowedTCPPorts = [ + 80 # ACME challenge + 443 + ]; + + systemd.services.flask_hello = { + description = "Flask Hello World"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + PYTHONPATH = "${python-with-packages}/${python-with-packages.sitePackages}"; + }; + serviceConfig = { + ExecStart = '' + ${getExe pkgs.python3Packages.gunicorn} \ + --bind=127.0.0.1:${toString cfg.port} \ + ${concatStringsSep " " cfg.gunicorn.extraArgs} \ + app:app + ''; + WorkingDirectory = "${cfg.package}"; + Restart = "on-failure"; + User = cfg.user; + Group = cfg.group; + }; + }; + + users.users."${cfg.user}" = { + home = "/var/lib/${cfg.user}"; + isSystemUser = true; + group = cfg.group; + }; + users.groups."${cfg.group}" = { }; + + services.nginx = mkIf cfg.nginx.enable { + enable = mkDefault true; + virtualHosts."${fqdn}" = { + enableACME = cfg.nginx.ssl; + forceSSL = cfg.nginx.ssl; + locations."/".proxyPass = "http://127.0.0.1:${toString cfg.port}"; + }; + }; + + security.acme = mkIf (cfg.nginx.enable && cfg.nginx.ssl) { + acceptTerms = true; + defaults.email = mkDefault "postmaster@${domain}"; + defaults.webroot = mkDefault "/var/lib/acme/acme-challenge"; + certs."${domain}".postRun = "systemctl reload nginx.service"; + }; + }; +} diff --git a/templates/dev/flask-hello/nix/package.nix b/templates/dev/flask-hello/nix/package.nix new file mode 100644 index 0000000..b9d943d --- /dev/null +++ b/templates/dev/flask-hello/nix/package.nix @@ -0,0 +1,31 @@ +{ + python3, + ... +}: + +python3.pkgs.buildPythonApplication rec { + pname = "flask_hello"; + version = "0.1.0"; + pyproject = true; + + build-system = [ python3.pkgs.setuptools ]; + + dependencies = with python3.pkgs; [ + flask + ]; + + src = ../.; + + installPhase = '' + runHook preInstall + + mkdir -p $out + cp -r $src/${pname} $out/ + cp $src/app.py $out/ + chmod +x $out/app.py + + runHook postInstall + ''; + + doCheck = false; +} diff --git a/templates/dev/flask-hello/nix/shell.nix b/templates/dev/flask-hello/nix/shell.nix new file mode 100644 index 0000000..9503e21 --- /dev/null +++ b/templates/dev/flask-hello/nix/shell.nix @@ -0,0 +1,17 @@ +{ + pkgs ? import { }, + ... +}: + +pkgs.mkShell { + buildInputs = [ + (pkgs.python3.withPackages ( + p: with p; [ + flask + gunicorn + ] + )) + pkgs.nixfmt-tree + pkgs.black + ]; +} diff --git a/templates/dev/flask-hello/pyproject.toml b/templates/dev/flask-hello/pyproject.toml new file mode 100644 index 0000000..2210340 --- /dev/null +++ b/templates/dev/flask-hello/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "flask_hello" +version = "0.1.0" +dependencies = [ + "flask", +] + +[tool.setuptools.packages.find] +include = ["flask_hello*"] diff --git a/templates/dev/py-hello/.github/workflows/python-nix.yml b/templates/dev/py-hello/.github/workflows/python-nix.yml new file mode 100644 index 0000000..a51c010 --- /dev/null +++ b/templates/dev/py-hello/.github/workflows/python-nix.yml @@ -0,0 +1,23 @@ +name: Python Nix Pipeline + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Nix + uses: cachix/install-nix-action@v18 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Run nix flake check + run: nix flake check diff --git a/templates/dev/py-hello/.gitignore b/templates/dev/py-hello/.gitignore new file mode 100644 index 0000000..8e4be95 --- /dev/null +++ b/templates/dev/py-hello/.gitignore @@ -0,0 +1,29 @@ +# Byte-compiled Python files +*.py[cod] +__pycache__/ + +# Distribution / packaging +.Python +*.egg +*.egg-info/ +.coverage +.htmlcov/ +.pytest_cache/ +.tox/ +.venv/ +ENV/ +build/ +dist/ +env.bak/ +env/ +venv.bak/ +venv/ + +# IDE/editor files +*.sublime-project +*.sublime-workspace +.idea/ +.vscode/ + +# Nix-related files +result diff --git a/templates/dev/py-hello/flake.nix b/templates/dev/py-hello/flake.nix new file mode 100644 index 0000000..f39af78 --- /dev/null +++ b/templates/dev/py-hello/flake.nix @@ -0,0 +1,131 @@ +{ + description = "A hello world template in Python"; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + pre-commit-hooks = { + url = "github:cachix/pre-commit-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + ... + }: + let + pname = "hello-world"; + version = "0.1.0"; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + nixpkgsFor = forAllSystems ( + system: + import nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + } + ); + in + { + overlays.default = + final: prev: + let + python = final.python312; + in + { + "${pname}" = python.pkgs.buildPythonApplication { + inherit pname version; + pyproject = true; + src = ./.; + build-system = [ + python.pkgs.setuptools + python.pkgs.wheel + ]; + dependencies = with python.pkgs; [ + ]; + pythonImportsCheck = [ + "hello_world" + ]; + }; + }; + + packages = forAllSystems (system: { + default = nixpkgsFor.${system}."${pname}"; + "${pname}" = nixpkgsFor.${system}."${pname}"; + }); + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + python = pkgs.python312; + in + { + default = pkgs.mkShell { + inherit (self.checks.${system}.pre-commit-check) shellHook; + buildInputs = self.checks.${system}.pre-commit-check.enabledPackages ++ [ + (python.withPackages ( + p: with p; [ + ] + )) + ]; + }; + + venv = pkgs.mkShell { + buildInputs = [ + python + ] + ++ [ + (python.withPackages ( + p: with p; [ + pip + ] + )) + ]; + shellHook = '' + python -m venv .venv + source .venv/bin/activate + pip install . + ''; + }; + } + ); + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems (system: { + # TODO: Add integration test + + pre-commit-check = self.inputs.pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt = { + enable = true; + }; + # TODO: Add Python format check + }; + }; + }); + }; +} diff --git a/templates/dev/py-hello/pyproject.toml b/templates/dev/py-hello/pyproject.toml new file mode 100644 index 0000000..8f5e727 --- /dev/null +++ b/templates/dev/py-hello/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["setuptools>=75", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "hello-world" +version = "0.1.0" +requires-python = ">=3.12" + +dependencies = [ +] + +[project.scripts] +hello-world = "hello_world.__main__:main" diff --git a/templates/dev/py-hello/src/hello_world/__init__.py b/templates/dev/py-hello/src/hello_world/__init__.py new file mode 100644 index 0000000..75f17d4 --- /dev/null +++ b/templates/dev/py-hello/src/hello_world/__init__.py @@ -0,0 +1 @@ +# This file is intentionally empty. diff --git a/templates/dev/py-hello/src/hello_world/__main__.py b/templates/dev/py-hello/src/hello_world/__main__.py new file mode 100644 index 0000000..cdff27e --- /dev/null +++ b/templates/dev/py-hello/src/hello_world/__main__.py @@ -0,0 +1,5 @@ +def main(): + print("Hello, world!") + +if __name__ == "__main__": + main() diff --git a/templates/dev/rs-hello/.github/workflows/rust-nix.yml b/templates/dev/rs-hello/.github/workflows/rust-nix.yml new file mode 100644 index 0000000..702e503 --- /dev/null +++ b/templates/dev/rs-hello/.github/workflows/rust-nix.yml @@ -0,0 +1,26 @@ +name: Rust Nix Pipeline + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build-and-test: + name: Build and Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up Nix + uses: cachix/install-nix-action@v18 + with: + nix_path: nixpkgs=channel:nixos-unstable + + - name: Run cargo tests in dev shell + run: nix develop --command bash -c "cargo test" + + - name: Run nix flake check + run: nix flake check diff --git a/templates/dev/rs-hello/.gitignore b/templates/dev/rs-hello/.gitignore new file mode 100644 index 0000000..290f9ef --- /dev/null +++ b/templates/dev/rs-hello/.gitignore @@ -0,0 +1,3 @@ +.pre-commit-config.yaml +result/ +target/ diff --git a/templates/dev/rs-hello/Cargo.toml b/templates/dev/rs-hello/Cargo.toml new file mode 100644 index 0000000..9aa83d7 --- /dev/null +++ b/templates/dev/rs-hello/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "hello-world" +version = "0.1.0" +edition = "2021" +authors = ["Your Name "] +description = "A simple Hello World program" + +[dependencies] diff --git a/templates/dev/rs-hello/flake.nix b/templates/dev/rs-hello/flake.nix new file mode 100644 index 0000000..390194a --- /dev/null +++ b/templates/dev/rs-hello/flake.nix @@ -0,0 +1,127 @@ +{ + description = "A hello world template in Rust"; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + pre-commit-hooks = { + url = "github:cachix/pre-commit-hooks.nix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + ... + }: + let + pname = "hello-world"; + version = "0.1.0"; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + nixpkgsFor = forAllSystems ( + system: + import nixpkgs { + inherit system; + overlays = [ self.overlays.default ]; + } + ); + in + { + overlays.default = final: prev: { + "${pname}" = final.rustPlatform.buildRustPackage { + inherit pname version; + src = ./.; + cargoLock.lockFile = ./Cargo.lock; + nativeBuildInputs = with final; [ pkg-config ]; + }; + }; + + packages = forAllSystems (system: { + default = nixpkgsFor.${system}."${pname}"; + "${pname}" = nixpkgsFor.${system}."${pname}"; + }); + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + in + { + default = pkgs.mkShell { + inherit (self.checks.${system}.pre-commit-check) shellHook; + buildInputs = + self.checks.${system}.pre-commit-check.enabledPackages + ++ (with pkgs; [ + cargo + pkg-config + pre-commit + rust-analyzer + rustc + ]); + }; + } + ); + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgsFor.${system}; + in + { + integration-test = + pkgs.runCommand "hello-world-test" + { + nativeBuildInputs = [ + pkgs.synixutils + self.packages.${system}.${pname} + ]; + } + '' + output=$(hello-world) + + echo "$output" | grep -q "Hello, World!" || { + echo "Test failed: Expected 'Hello, World!' but got: $output" + exit 1 + } + + echo "Hello World test passed!" > $out + ''; + + pre-commit-check = self.inputs.pre-commit-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt = { + enable = true; + }; + rustfmt = { + enable = true; + }; + }; + }; + } + ); + }; +} diff --git a/templates/dev/rs-hello/src/main.rs b/templates/dev/rs-hello/src/main.rs new file mode 100644 index 0000000..c5a79cf --- /dev/null +++ b/templates/dev/rs-hello/src/main.rs @@ -0,0 +1,11 @@ +fn main() { + println!("Hello, world!"); +} + +#[cfg(test)] +mod tests { + #[test] + fn test_hello_world() { + assert!(true); + } +} diff --git a/templates/microvm/.envrc b/templates/microvm/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/templates/microvm/.envrc @@ -0,0 +1 @@ +use flake diff --git a/templates/microvm/.gitignore b/templates/microvm/.gitignore new file mode 100644 index 0000000..5534200 --- /dev/null +++ b/templates/microvm/.gitignore @@ -0,0 +1,2 @@ +*.img +.direnv/ diff --git a/templates/microvm/README.md b/templates/microvm/README.md new file mode 100644 index 0000000..ef93eac --- /dev/null +++ b/templates/microvm/README.md @@ -0,0 +1,67 @@ +# microvm + +[microvm](https://github.com/microvm-nix/microvm.nix) NixOS configuration. + +## Setup + +To be able to rebuild remotely and for convenient ssh access, add the uvm host to your Home Manager ssh configuration: + +```nix +programs.ssh.matchBlocks = { + uvm = { + host = "uvm"; + hostname = "localhost"; + port = 2222; + user = "root"; + checkHostIP = false; + }; +}; +``` + +Create a new directory and initialize the template inside of it: + +```bash +mkdir -p microvm +cd microvm +nix flake init -t git+https://git.sid.ovh/sid/synix#microvm +``` + +Add your public key to the NixOS configuration. See [`config/configuration.nix`](./config/configuration.nix). + +## Usage + +Run VM: + +```bash +nix run .#microvm +``` + +Or with `tmux`: + +```bash +tmux new-session -s microvm 'nix run .#microvm' +``` + +> `tmux` is available in the Nix development shell. + +SSH into VM: + +```bash +ssh uvm +``` + +Remote rebuilding: + +```bash +nix run .#rebuild uvm +``` + +> Note: `` needs to be a remote host where you login as root via ssh with no password. + +If you need to use remote sudo, you can also use [synix's rebuild script](https://git.sid.ovh/sid/synix/blob/master/modules/nixos/common/rebuild.sh) for remote rebuilds. But then, the root user password cannot be empty: + +```bash +rebuild -p . -H uvm -T uvm -B +``` + +You might want to set up [PAM's SSH agent Auth](https://search.nixos.org/options?channel=unstable&query=sshAgentAuth) or use an [askpass helper](https://search.nixos.org/options?channel=unstable&query=askpass). diff --git a/templates/microvm/config/base.nix b/templates/microvm/config/base.nix new file mode 100644 index 0000000..10f624e --- /dev/null +++ b/templates/microvm/config/base.nix @@ -0,0 +1,84 @@ +# Edit this only if you know what you're doing. +{ inputs, outputs, ... }: + +{ + imports = [ + inputs.microvm.nixosModules.microvm + ]; + + networking.hostName = "uvm"; + + users.users.root = { + password = ""; + }; + services.getty.autologinUser = "root"; + + microvm = { + volumes = [ + { + mountPoint = "/var"; + image = "var.img"; + size = 256; + } + ]; + shares = [ + { + proto = "9p"; + tag = "ro-store"; + source = "/nix/store"; + mountPoint = "/nix/.ro-store"; + } + ]; + interfaces = [ + { + type = "user"; + id = "qemu"; + mac = "02:00:00:00:00:01"; + } + ]; + forwardPorts = [ + { + host.port = 2222; + guest.port = 22; + } + ]; + optimize.enable = true; + hypervisor = "qemu"; + socket = "control.socket"; + }; + + nix = { + channel.enable = false; + settings = { + experimental-features = "nix-command flakes"; + builders-use-substitutes = true; + substituters = [ + "https://cache.nixos.org" + "https://nix-community.cachix.org" + "https://microvm.cachix.org" + ]; + trusted-public-keys = [ + "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" + "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" + "microvm.cachix.org-1:oXnBc6hRE3eX5rSYdRyMYXnfzcCxC7yKPTbZXALsqys=" + ]; + }; + }; + + nixpkgs.overlays = [ + outputs.overlays.synix-packages + outputs.overlays.local-packages + ]; + + services.openssh = { + enable = true; + ports = [ 22 ]; + openFirewall = true; + settings = { + PermitRootLogin = "yes"; + PasswordAuthentication = false; + }; + }; + + system.stateVersion = "25.11"; +} diff --git a/templates/microvm/config/configuration.nix b/templates/microvm/config/configuration.nix new file mode 100644 index 0000000..3f95d2e --- /dev/null +++ b/templates/microvm/config/configuration.nix @@ -0,0 +1,10 @@ +{ + users.users.root = { + openssh.authorizedKeys.keyFiles = [ + # copy your public key here and point to it + # ./id_rsa.pub + ]; + }; + + # Add the rest of your configuration here +} diff --git a/templates/microvm/config/default.nix b/templates/microvm/config/default.nix new file mode 100644 index 0000000..19f200e --- /dev/null +++ b/templates/microvm/config/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./base.nix + ./configuration.nix + ]; +} diff --git a/templates/microvm/flake.nix b/templates/microvm/flake.nix new file mode 100644 index 0000000..c03bdb5 --- /dev/null +++ b/templates/microvm/flake.nix @@ -0,0 +1,96 @@ +{ + description = "MicroVM NixOS configurations"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + + microvm.url = "github:microvm-nix/microvm.nix"; + microvm.inputs.nixpkgs.follows = "nixpkgs"; + + synix.url = "git+https://git.sid.ovh/sid/synix.git?ref=release-25.11"; + synix.imputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + ... + }@inputs: + let + inherit (self) outputs; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + lib = nixpkgs.lib.extend (final: prev: inputs.synix.lib or { }); + + mkApp = program: description: { + type = "app"; + inherit program; + meta.description = description; + }; + + mkNixosConfiguration = + system: modules: + nixpkgs.lib.nixosSystem { + inherit system modules; + specialArgs = { + inherit inputs outputs lib; + }; + }; + in + { + apps = forAllSystems ( + system: + let + microvm = self.nixosConfigurations."microvm-${system}".config.microvm; + inherit (nixpkgs.lib) getExe; + in + { + rebuild = mkApp (getExe microvm.deploy.rebuild) "Rebuild the VM."; + microvm = mkApp (getExe microvm.declaredRunner) "Run the VM."; + } + ); + + packages = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + import ./pkgs { inherit pkgs; } + ); + + overlays = import ./overlays { inherit (self) inputs; }; + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = pkgs.mkShell { + buildInputs = with pkgs; [ + tmux + ]; + }; + # FIXME: `microvm.deploy.rebuild` does not seem to care about askpass + # shellHook = '' + # export SSH_ASKPASS="pass " + # export SSH_ASKPASS_REQUIRE="force" + # ''; + } + ); + + nixosModules = import ./modules; + + nixosConfigurations = { + microvm-x86_64-linux = mkNixosConfiguration "x86_64-linux" [ ./config ]; + microvm-aarch64-linux = mkNixosConfiguration "aarch64-linux" [ ./config ]; + }; + }; +} diff --git a/templates/microvm/modules/default.nix b/templates/microvm/modules/default.nix new file mode 100644 index 0000000..9b65824 --- /dev/null +++ b/templates/microvm/modules/default.nix @@ -0,0 +1,3 @@ +{ + # example = import ./example; +} diff --git a/templates/microvm/overlays/default.nix b/templates/microvm/overlays/default.nix new file mode 100644 index 0000000..0f5d0f3 --- /dev/null +++ b/templates/microvm/overlays/default.nix @@ -0,0 +1,7 @@ +{ inputs, ... }: + +{ + synix-packages = final: prev: { synix = inputs.synix.overlays.additions final prev; }; + + local-packages = final: prev: { local = import ../pkgs { pkgs = final; }; }; +} diff --git a/templates/microvm/pkgs/default.nix b/templates/microvm/pkgs/default.nix new file mode 100644 index 0000000..0df9091 --- /dev/null +++ b/templates/microvm/pkgs/default.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + # example = pkgs.callPackage ./example { }; +} diff --git a/templates/nix-configs/hetzner-amd/flake.nix b/templates/nix-configs/hetzner-amd/flake.nix new file mode 100644 index 0000000..4840249 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/flake.nix @@ -0,0 +1,89 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-old-stable.url = "github:nixos/nixpkgs/nixos-25.05"; + + synix.url = "git+https://git.sid.ovh/sid/synix.git?ref=release-25.11"; + synix.imputs.nixpkgs.follows = "nixpkgs"; + + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + ... + }@inputs: + let + inherit (self) outputs; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + lib = nixpkgs.lib.extend (final: prev: inputs.synix.lib or { }); + + mkNixosConfiguration = + system: modules: + nixpkgs.lib.nixosSystem { + inherit system modules; + specialArgs = { + inherit inputs outputs lib; + }; + }; + in + { + packages = forAllSystems (system: import ./pkgs nixpkgs.legacyPackages.${system}); + + overlays = import ./overlays { inherit inputs; }; + + nixosModules = import ./modules/nixos; + + nixosConfigurations = { + HOSTNAME = mkNixosConfiguration "x86_64-linux" [ ./hosts/HOSTNAME ]; + }; + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + flakePkgs = self.packages.${system}; + overlaidPkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.modifications ]; + }; + in + { + pre-commit-check = inputs.git-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt.enable = true; + }; + }; + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + build-overlays = pkgs.linkFarm "flake-overlays-${system}" { + # package = overlaidPkgs.package; + }; + } + ); + }; +} diff --git a/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/boot.nix b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/boot.nix new file mode 100644 index 0000000..53a9686 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/boot.nix @@ -0,0 +1,7 @@ +{ + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 10; + }; + boot.loader.efi.canTouchEfiVariables = true; +} diff --git a/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/default.nix b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/default.nix new file mode 100644 index 0000000..5fbf9d6 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/default.nix @@ -0,0 +1,22 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + inputs.synix.nixosModules.common + + outputs.nixosModules.common + ]; + + system.stateVersion = "25.11"; +} diff --git a/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/disks.sh b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/disks.sh new file mode 100644 index 0000000..5e06bc8 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/disks.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL diff --git a/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/hardware.nix b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/hardware.nix new file mode 100644 index 0000000..2bfd7b4 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/hardware.nix @@ -0,0 +1,48 @@ +{ + config, + lib, + pkgs, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "ahci" + "nvme" + "sd_mod" + "sdhci_pci" + "sr_mod" + "usb_storage" + "virtio_pci" + "virtio_scsi" + "xhci_pci" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0022" + "dmask=0022" + ]; + }; + + swapDevices = [ { device = "/dev/disk/by-label/SWAP"; } ]; + + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/networking.nix b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/networking.nix new file mode 100644 index 0000000..f96e974 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "HOSTNAME"; + networking.domain = "HOSTNAME.local"; +} diff --git a/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/packages.nix b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/packages.nix new file mode 100644 index 0000000..96cc691 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} diff --git a/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/default.nix b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/default.nix new file mode 100644 index 0000000..c8695e0 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ]; +} diff --git a/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/nginx.nix b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/nginx.nix new file mode 100644 index 0000000..04a2482 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/nginx.nix @@ -0,0 +1,14 @@ +{ + inputs, + ... +}: + +{ + imports = [ inputs.synix.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} diff --git a/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/openssh.nix b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/openssh.nix new file mode 100644 index 0000000..b851d18 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/services/openssh.nix @@ -0,0 +1,12 @@ +{ + inputs, + ... +}: + +{ + imports = [ + inputs.synix.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/users.nix b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/users.nix new file mode 100644 index 0000000..253394d --- /dev/null +++ b/templates/nix-configs/hetzner-amd/hosts/HOSTNAME/users.nix @@ -0,0 +1,9 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.synix.nixosModules.normalUsers + + ../../users/USERNAME + ]; +} diff --git a/templates/nix-configs/hetzner-amd/modules/nixos/common/default.nix b/templates/nix-configs/hetzner-amd/modules/nixos/common/default.nix new file mode 100644 index 0000000..aa96a5f --- /dev/null +++ b/templates/nix-configs/hetzner-amd/modules/nixos/common/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./overlays.nix + ]; +} diff --git a/templates/nix-configs/hetzner-amd/modules/nixos/common/overlays.nix b/templates/nix-configs/hetzner-amd/modules/nixos/common/overlays.nix new file mode 100644 index 0000000..348ae08 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/modules/nixos/common/overlays.nix @@ -0,0 +1,11 @@ +{ outputs, ... }: + +{ + nixpkgs.overlays = [ + outputs.overlays.synix-packages + outputs.overlays.local-packages + outputs.overlays.modifications + outputs.overlays.old-stable-packages + outputs.overlays.unstable-packages + ]; +} diff --git a/templates/nix-configs/hetzner-amd/modules/nixos/default.nix b/templates/nix-configs/hetzner-amd/modules/nixos/default.nix new file mode 100644 index 0000000..28a636c --- /dev/null +++ b/templates/nix-configs/hetzner-amd/modules/nixos/default.nix @@ -0,0 +1,3 @@ +{ + common = import ./common; +} diff --git a/templates/nix-configs/hetzner-amd/overlays/default.nix b/templates/nix-configs/hetzner-amd/overlays/default.nix new file mode 100644 index 0000000..23332b5 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/overlays/default.nix @@ -0,0 +1,35 @@ +{ inputs, ... }: + +{ + # synix packages accessible through 'pkgs.synix' + synix-packages = final: prev: { synix = inputs.synix.packages."${final.system}"; }; + + # packages in `pkgs/` accessible through 'pkgs.local' + local-packages = final: prev: { local = import ../pkgs { pkgs = final; }; }; + + # https://nixos.wiki/wiki/Overlays + modifications = + final: prev: + let + files = [ + ]; + imports = builtins.map (f: import f final prev) files; + in + builtins.foldl' (a: b: a // b) { } imports // inputs.synix.overlays.modifications final prev; + + # old-stable nixpkgs accessible through 'pkgs.old-stable' + old-stable-packages = final: prev: { + old-stable = import inputs.nixpkgs-old-stable { + inherit (final) system; + inherit (prev) config; + }; + }; + + # unstable nixpkgs accessible through 'pkgs.unstable' + unstable-packages = final: prev: { + unstable = import inputs.nixpkgs-unstable { + inherit (final) system; + inherit (prev) config; + }; + }; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/.sops.yaml b/templates/nix-configs/hetzner-amd/pi4/.sops.yaml new file mode 100644 index 0000000..e812787 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/.sops.yaml @@ -0,0 +1,18 @@ +keys: + - &host_portuus age1lghtkhxlz2tc5j9cjm6ancvz4a0mkgevjw4e2mhfar7cr5atl50snr5rs4 + - &host_edge age194tp22lgh6uw3lcg2u0j9ylllfvs6anjk4ns7prhy8e08k20q3jq439e6c + - &user_sid age19yeqvv28fgrtk6jsh3xyaf0lch86kna6rcz4dwe962yyyyevu30sx474xy + - &user_steffen age1e8p35795htf7twrejyugpzw0qja2v33awcw76y4gp6acnxnkzq0s935t4t +creation_rules: + - path_regex: hosts/portuus/secrets/secrets.yaml$ + key_groups: + - age: + - *user_sid + - *user_steffen + - *host_portuus + - path_regex: hosts/edge/secrets/secrets.yaml$ + key_groups: + - age: + - *user_sid + - *user_steffen + - *host_edge diff --git a/templates/nix-configs/hetzner-amd/pi4/flake.nix b/templates/nix-configs/hetzner-amd/pi4/flake.nix new file mode 100644 index 0000000..bf2f97d --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/flake.nix @@ -0,0 +1,93 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-old-stable.url = "github:nixos/nixpkgs/nixos-25.05"; + + synix.url = "git+https://git.sid.ovh/sid/synix.git?ref=release-25.11"; + synix.imputs.nixpkgs.follows = "nixpkgs"; + + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + ... + }@inputs: + let + inherit (self) outputs; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + overlays = [ inputs.synix.overlays.default ]; + + mkNixosConfiguration = + system: modules: + nixpkgs.lib.nixosSystem { + inherit system modules; + specialArgs = { + inherit inputs outputs; + lib = + (import nixpkgs { + inherit system overlays; + }).lib; + }; + }; + in + { + packages = forAllSystems (system: import ./pkgs nixpkgs.legacyPackages.${system}); + + overlays = import ./overlays { inherit inputs; }; + + nixosModules = import ./modules/nixos; + + nixosConfigurations = { + HOSTNAME = mkNixosConfiguration "x86_64-linux" [ ./hosts/HOSTNAME ]; + }; + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + flakePkgs = self.packages.${system}; + overlaidPkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.modifications ]; + }; + in + { + pre-commit-check = inputs.git-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt.enable = true; + }; + }; + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + build-overlays = pkgs.linkFarm "flake-overlays-${system}" { + # package = overlaidPkgs.package; + }; + } + ); + }; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/boot.nix b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/boot.nix new file mode 100644 index 0000000..53a9686 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/boot.nix @@ -0,0 +1,7 @@ +{ + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 10; + }; + boot.loader.efi.canTouchEfiVariables = true; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/default.nix b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/default.nix new file mode 100644 index 0000000..5fbf9d6 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/default.nix @@ -0,0 +1,22 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + inputs.synix.nixosModules.common + + outputs.nixosModules.common + ]; + + system.stateVersion = "25.11"; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/disks.sh b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/disks.sh new file mode 100644 index 0000000..3fca099 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/disks.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +SSD='/dev/sda' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +parted -s "$SSD" \ + mklabel gpt \ + mkpart ESP fat32 1MiB 513MiB \ + set 1 esp on \ + mkpart primary linux-swap 513MiB "$((513 + SWAP_GB*1024))"MiB \ + mkpart primary ext4 "$((513 + SWAP_GB*1024))"MiB 100% +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -n BOOT "${SSD}1" +mkswap -L SWAP "${SSD}2" +mkfs.ext4 -L ROOT "${SSD}3" + +echo "Mounting partitions..." +mount "${SSD}3" "$MNT" +mkdir -p "$MNT/boot" +mount "${SSD}1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL diff --git a/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/hardware.nix b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/hardware.nix new file mode 100644 index 0000000..aa13477 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/hardware.nix @@ -0,0 +1,41 @@ +{ + lib, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/profiles/qemu-guest.nix") + ]; + + boot.initrd.availableKernelModules = [ + "ahci" + "xhci_pci" + "virtio_pci" + "virtio_scsi" + "sd_mod" + "sr_mod" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + }; + + swapDevices = [ + { device = "/dev/disk/by-label/SWAP"; } + ]; + + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/networking.nix b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/networking.nix new file mode 100644 index 0000000..f96e974 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "HOSTNAME"; + networking.domain = "HOSTNAME.local"; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/packages.nix b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/packages.nix new file mode 100644 index 0000000..96cc691 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/default.nix b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/default.nix new file mode 100644 index 0000000..c8695e0 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ]; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/nginx.nix b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/nginx.nix new file mode 100644 index 0000000..04a2482 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/nginx.nix @@ -0,0 +1,14 @@ +{ + inputs, + ... +}: + +{ + imports = [ inputs.synix.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/openssh.nix b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/openssh.nix new file mode 100644 index 0000000..b851d18 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/services/openssh.nix @@ -0,0 +1,12 @@ +{ + inputs, + ... +}: + +{ + imports = [ + inputs.synix.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/users.nix b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/users.nix new file mode 100644 index 0000000..253394d --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/hosts/HOSTNAME/users.nix @@ -0,0 +1,9 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.synix.nixosModules.normalUsers + + ../../users/USERNAME + ]; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/modules/nixos/common/default.nix b/templates/nix-configs/hetzner-amd/pi4/modules/nixos/common/default.nix new file mode 100644 index 0000000..aa96a5f --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/modules/nixos/common/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./overlays.nix + ]; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/modules/nixos/common/overlays.nix b/templates/nix-configs/hetzner-amd/pi4/modules/nixos/common/overlays.nix new file mode 100644 index 0000000..348ae08 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/modules/nixos/common/overlays.nix @@ -0,0 +1,11 @@ +{ outputs, ... }: + +{ + nixpkgs.overlays = [ + outputs.overlays.synix-packages + outputs.overlays.local-packages + outputs.overlays.modifications + outputs.overlays.old-stable-packages + outputs.overlays.unstable-packages + ]; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/modules/nixos/default.nix b/templates/nix-configs/hetzner-amd/pi4/modules/nixos/default.nix new file mode 100644 index 0000000..28a636c --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/modules/nixos/default.nix @@ -0,0 +1,3 @@ +{ + common = import ./common; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/overlays/default.nix b/templates/nix-configs/hetzner-amd/pi4/overlays/default.nix new file mode 100644 index 0000000..23332b5 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/overlays/default.nix @@ -0,0 +1,35 @@ +{ inputs, ... }: + +{ + # synix packages accessible through 'pkgs.synix' + synix-packages = final: prev: { synix = inputs.synix.packages."${final.system}"; }; + + # packages in `pkgs/` accessible through 'pkgs.local' + local-packages = final: prev: { local = import ../pkgs { pkgs = final; }; }; + + # https://nixos.wiki/wiki/Overlays + modifications = + final: prev: + let + files = [ + ]; + imports = builtins.map (f: import f final prev) files; + in + builtins.foldl' (a: b: a // b) { } imports // inputs.synix.overlays.modifications final prev; + + # old-stable nixpkgs accessible through 'pkgs.old-stable' + old-stable-packages = final: prev: { + old-stable = import inputs.nixpkgs-old-stable { + inherit (final) system; + inherit (prev) config; + }; + }; + + # unstable nixpkgs accessible through 'pkgs.unstable' + unstable-packages = final: prev: { + unstable = import inputs.nixpkgs-unstable { + inherit (final) system; + inherit (prev) config; + }; + }; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/pkgs/default.nix b/templates/nix-configs/hetzner-amd/pi4/pkgs/default.nix new file mode 100644 index 0000000..2dadf8a --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/pkgs/default.nix @@ -0,0 +1,8 @@ +{ + pkgs ? import , + ... +}: + +{ + # example = pkgs.callPackage ./example { }; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/users/USERNAME/default.nix b/templates/nix-configs/hetzner-amd/pi4/users/USERNAME/default.nix new file mode 100644 index 0000000..9885271 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pi4/users/USERNAME/default.nix @@ -0,0 +1,8 @@ +{ + normalUsers.USERNAME = { + extraGroups = [ + "wheel" + ]; + # sshKeyFiles = [ ./pubkeys/YOUR_PUBKEY.pub ]; # FIXME + }; +} diff --git a/templates/nix-configs/hetzner-amd/pi4/users/USERNAME/pubkeys/YOUR_PUBKEY.pub b/templates/nix-configs/hetzner-amd/pi4/users/USERNAME/pubkeys/YOUR_PUBKEY.pub new file mode 100644 index 0000000..e69de29 diff --git a/templates/nix-configs/hetzner-amd/pkgs/default.nix b/templates/nix-configs/hetzner-amd/pkgs/default.nix new file mode 100644 index 0000000..2dadf8a --- /dev/null +++ b/templates/nix-configs/hetzner-amd/pkgs/default.nix @@ -0,0 +1,8 @@ +{ + pkgs ? import , + ... +}: + +{ + # example = pkgs.callPackage ./example { }; +} diff --git a/templates/nix-configs/hetzner-amd/users/USERNAME/default.nix b/templates/nix-configs/hetzner-amd/users/USERNAME/default.nix new file mode 100644 index 0000000..9885271 --- /dev/null +++ b/templates/nix-configs/hetzner-amd/users/USERNAME/default.nix @@ -0,0 +1,8 @@ +{ + normalUsers.USERNAME = { + extraGroups = [ + "wheel" + ]; + # sshKeyFiles = [ ./pubkeys/YOUR_PUBKEY.pub ]; # FIXME + }; +} diff --git a/templates/nix-configs/hetzner-amd/users/USERNAME/pubkeys/YOUR_PUBKEY.pub b/templates/nix-configs/hetzner-amd/users/USERNAME/pubkeys/YOUR_PUBKEY.pub new file mode 100644 index 0000000..e69de29 diff --git a/templates/nix-configs/hyprland/flake.nix b/templates/nix-configs/hyprland/flake.nix new file mode 100644 index 0000000..aedca40 --- /dev/null +++ b/templates/nix-configs/hyprland/flake.nix @@ -0,0 +1,125 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-old-stable.url = "github:nixos/nixpkgs/nixos-25.05"; + + synix.url = "git+https://git.sid.ovh/sid/synix.git?ref=release-25.11"; + synix.imputs.nixpkgs.follows = "nixpkgs"; + + home-manager.url = "github:nix-community/home-manager"; + home-manager.inputs.nixpkgs.follows = "nixpkgs"; + + nixvim.url = "github:nix-community/nixvim"; + nixvim.inputs.nixpkgs.follows = "nixpkgs"; + + nur.url = "github:nix-community/NUR"; + nur.inputs.nixpkgs.follows = "nixpkgs"; + + stylix.url = "github:danth/stylix"; + stylix.inputs.nixpkgs.follows = "nixpkgs"; + + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + home-manager, + ... + }@inputs: + let + inherit (self) outputs; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + lib = nixpkgs.lib.extend (final: prev: inputs.synix.lib or { }); + + mkNixosConfiguration = + system: modules: + nixpkgs.lib.nixosSystem { + inherit system modules; + specialArgs = { + inherit inputs outputs lib; + }; + }; + in + { + devShells = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + in + { + default = import ./shell.nix { inherit pkgs; }; + } + ); + + packages = forAllSystems (system: import ./pkgs nixpkgs.legacyPackages.${system}); + + overlays = import ./overlays { inherit inputs; }; + + nixosModules = import ./modules/nixos; + + nixosConfigurations = { + HOSTNAME = mkNixosConfiguration "x86_64-linux" [ ./hosts/HOSTNAME ]; + }; + + homeConfigurations = { + "USERNAME@HOSTNAME" = home-manager.lib.homeManagerConfiguration { + pkgs = nixpkgs.legacyPackages.x86_64-linux; # FIXME: Set architecture + extraSpecialArgs = { + inherit inputs outputs; + }; + modules = [ + ./users/USERNAME/home + ./users/USERNAME/home/hosts/HOSTNAME + ]; + }; + }; + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + flakePkgs = self.packages.${system}; + overlaidPkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.modifications ]; + }; + in + { + pre-commit-check = inputs.git-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt.enable = true; + }; + }; + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + build-overlays = pkgs.linkFarm "flake-overlays-${system}" { + # package = overlaidPkgs.package; + }; + } + ); + }; +} diff --git a/templates/nix-configs/hyprland/hosts/HOSTNAME/boot.nix b/templates/nix-configs/hyprland/hosts/HOSTNAME/boot.nix new file mode 100644 index 0000000..53a9686 --- /dev/null +++ b/templates/nix-configs/hyprland/hosts/HOSTNAME/boot.nix @@ -0,0 +1,7 @@ +{ + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 10; + }; + boot.loader.efi.canTouchEfiVariables = true; +} diff --git a/templates/nix-configs/hyprland/hosts/HOSTNAME/default.nix b/templates/nix-configs/hyprland/hosts/HOSTNAME/default.nix new file mode 100644 index 0000000..5fbf9d6 --- /dev/null +++ b/templates/nix-configs/hyprland/hosts/HOSTNAME/default.nix @@ -0,0 +1,22 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + inputs.synix.nixosModules.common + + outputs.nixosModules.common + ]; + + system.stateVersion = "25.11"; +} diff --git a/templates/nix-configs/hyprland/hosts/HOSTNAME/disks.sh b/templates/nix-configs/hyprland/hosts/HOSTNAME/disks.sh new file mode 100644 index 0000000..5e06bc8 --- /dev/null +++ b/templates/nix-configs/hyprland/hosts/HOSTNAME/disks.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL diff --git a/templates/nix-configs/hyprland/hosts/HOSTNAME/hardware.nix b/templates/nix-configs/hyprland/hosts/HOSTNAME/hardware.nix new file mode 100644 index 0000000..2bfd7b4 --- /dev/null +++ b/templates/nix-configs/hyprland/hosts/HOSTNAME/hardware.nix @@ -0,0 +1,48 @@ +{ + config, + lib, + pkgs, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "ahci" + "nvme" + "sd_mod" + "sdhci_pci" + "sr_mod" + "usb_storage" + "virtio_pci" + "virtio_scsi" + "xhci_pci" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0022" + "dmask=0022" + ]; + }; + + swapDevices = [ { device = "/dev/disk/by-label/SWAP"; } ]; + + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/templates/nix-configs/hyprland/hosts/HOSTNAME/networking.nix b/templates/nix-configs/hyprland/hosts/HOSTNAME/networking.nix new file mode 100644 index 0000000..f96e974 --- /dev/null +++ b/templates/nix-configs/hyprland/hosts/HOSTNAME/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "HOSTNAME"; + networking.domain = "HOSTNAME.local"; +} diff --git a/templates/nix-configs/hyprland/hosts/HOSTNAME/packages.nix b/templates/nix-configs/hyprland/hosts/HOSTNAME/packages.nix new file mode 100644 index 0000000..96cc691 --- /dev/null +++ b/templates/nix-configs/hyprland/hosts/HOSTNAME/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} diff --git a/templates/nix-configs/hyprland/hosts/HOSTNAME/services/default.nix b/templates/nix-configs/hyprland/hosts/HOSTNAME/services/default.nix new file mode 100644 index 0000000..c8695e0 --- /dev/null +++ b/templates/nix-configs/hyprland/hosts/HOSTNAME/services/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ]; +} diff --git a/templates/nix-configs/hyprland/hosts/HOSTNAME/services/nginx.nix b/templates/nix-configs/hyprland/hosts/HOSTNAME/services/nginx.nix new file mode 100644 index 0000000..04a2482 --- /dev/null +++ b/templates/nix-configs/hyprland/hosts/HOSTNAME/services/nginx.nix @@ -0,0 +1,14 @@ +{ + inputs, + ... +}: + +{ + imports = [ inputs.synix.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} diff --git a/templates/nix-configs/hyprland/hosts/HOSTNAME/services/openssh.nix b/templates/nix-configs/hyprland/hosts/HOSTNAME/services/openssh.nix new file mode 100644 index 0000000..b851d18 --- /dev/null +++ b/templates/nix-configs/hyprland/hosts/HOSTNAME/services/openssh.nix @@ -0,0 +1,12 @@ +{ + inputs, + ... +}: + +{ + imports = [ + inputs.synix.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/templates/nix-configs/hyprland/hosts/HOSTNAME/users.nix b/templates/nix-configs/hyprland/hosts/HOSTNAME/users.nix new file mode 100644 index 0000000..253394d --- /dev/null +++ b/templates/nix-configs/hyprland/hosts/HOSTNAME/users.nix @@ -0,0 +1,9 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.synix.nixosModules.normalUsers + + ../../users/USERNAME + ]; +} diff --git a/templates/nix-configs/hyprland/modules/nixos/common/default.nix b/templates/nix-configs/hyprland/modules/nixos/common/default.nix new file mode 100644 index 0000000..aa96a5f --- /dev/null +++ b/templates/nix-configs/hyprland/modules/nixos/common/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./overlays.nix + ]; +} diff --git a/templates/nix-configs/hyprland/modules/nixos/common/overlays.nix b/templates/nix-configs/hyprland/modules/nixos/common/overlays.nix new file mode 100644 index 0000000..348ae08 --- /dev/null +++ b/templates/nix-configs/hyprland/modules/nixos/common/overlays.nix @@ -0,0 +1,11 @@ +{ outputs, ... }: + +{ + nixpkgs.overlays = [ + outputs.overlays.synix-packages + outputs.overlays.local-packages + outputs.overlays.modifications + outputs.overlays.old-stable-packages + outputs.overlays.unstable-packages + ]; +} diff --git a/templates/nix-configs/hyprland/modules/nixos/default.nix b/templates/nix-configs/hyprland/modules/nixos/default.nix new file mode 100644 index 0000000..28a636c --- /dev/null +++ b/templates/nix-configs/hyprland/modules/nixos/default.nix @@ -0,0 +1,3 @@ +{ + common = import ./common; +} diff --git a/templates/nix-configs/hyprland/overlays/default.nix b/templates/nix-configs/hyprland/overlays/default.nix new file mode 100644 index 0000000..23332b5 --- /dev/null +++ b/templates/nix-configs/hyprland/overlays/default.nix @@ -0,0 +1,35 @@ +{ inputs, ... }: + +{ + # synix packages accessible through 'pkgs.synix' + synix-packages = final: prev: { synix = inputs.synix.packages."${final.system}"; }; + + # packages in `pkgs/` accessible through 'pkgs.local' + local-packages = final: prev: { local = import ../pkgs { pkgs = final; }; }; + + # https://nixos.wiki/wiki/Overlays + modifications = + final: prev: + let + files = [ + ]; + imports = builtins.map (f: import f final prev) files; + in + builtins.foldl' (a: b: a // b) { } imports // inputs.synix.overlays.modifications final prev; + + # old-stable nixpkgs accessible through 'pkgs.old-stable' + old-stable-packages = final: prev: { + old-stable = import inputs.nixpkgs-old-stable { + inherit (final) system; + inherit (prev) config; + }; + }; + + # unstable nixpkgs accessible through 'pkgs.unstable' + unstable-packages = final: prev: { + unstable = import inputs.nixpkgs-unstable { + inherit (final) system; + inherit (prev) config; + }; + }; +} diff --git a/templates/nix-configs/hyprland/pkgs/default.nix b/templates/nix-configs/hyprland/pkgs/default.nix new file mode 100644 index 0000000..2dadf8a --- /dev/null +++ b/templates/nix-configs/hyprland/pkgs/default.nix @@ -0,0 +1,8 @@ +{ + pkgs ? import , + ... +}: + +{ + # example = pkgs.callPackage ./example { }; +} diff --git a/templates/nix-configs/hyprland/shell.nix b/templates/nix-configs/hyprland/shell.nix new file mode 100644 index 0000000..a33eea0 --- /dev/null +++ b/templates/nix-configs/hyprland/shell.nix @@ -0,0 +1,9 @@ +{ + pkgs ? import { }, + ... +}: + +pkgs.mkShell { + NIX_CONFIG = "extra-experimental-features = nix-command flakes"; + nativeBuildInputs = with pkgs; [ home-manager ]; +} diff --git a/templates/nix-configs/hyprland/users/USERNAME/default.nix b/templates/nix-configs/hyprland/users/USERNAME/default.nix new file mode 100644 index 0000000..9885271 --- /dev/null +++ b/templates/nix-configs/hyprland/users/USERNAME/default.nix @@ -0,0 +1,8 @@ +{ + normalUsers.USERNAME = { + extraGroups = [ + "wheel" + ]; + # sshKeyFiles = [ ./pubkeys/YOUR_PUBKEY.pub ]; # FIXME + }; +} diff --git a/templates/nix-configs/hyprland/users/USERNAME/home/default.nix b/templates/nix-configs/hyprland/users/USERNAME/home/default.nix new file mode 100644 index 0000000..16fe6f8 --- /dev/null +++ b/templates/nix-configs/hyprland/users/USERNAME/home/default.nix @@ -0,0 +1,24 @@ +{ inputs, outputs, ... }: + +{ + imports = [ + inputs.synix.homeModules.common + inputs.synix.homeModules.nixvim + + outputs.nixosModules.common + ]; + + home.username = "USERNAME"; + + programs.git = { + enable = true; + settings.user = { + userName = "GIT_NAME"; + userEmail = "GIT_EMAIL"; + }; + }; + + programs.nixvim.enable = true; + + home.stateVersion = "25.11"; +} diff --git a/templates/nix-configs/hyprland/users/USERNAME/home/hosts/HOSTNAME/default.nix b/templates/nix-configs/hyprland/users/USERNAME/home/hosts/HOSTNAME/default.nix new file mode 100644 index 0000000..bca6d0f --- /dev/null +++ b/templates/nix-configs/hyprland/users/USERNAME/home/hosts/HOSTNAME/default.nix @@ -0,0 +1 @@ +{ imports = [ ../../hyprland ]; } diff --git a/templates/nix-configs/hyprland/users/USERNAME/home/hyprland/default.nix b/templates/nix-configs/hyprland/users/USERNAME/home/hyprland/default.nix new file mode 100644 index 0000000..276e1a7 --- /dev/null +++ b/templates/nix-configs/hyprland/users/USERNAME/home/hyprland/default.nix @@ -0,0 +1,17 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.synix.homeModules.hyprland + inputs.synix.homeModules.stylix + + ./packages.nix + ]; + + wayland.windowManager.hyprland = { + enable = true; + autostart = true; + }; + + stylix.enable = true; +} diff --git a/templates/nix-configs/hyprland/users/USERNAME/home/hyprland/packages.nix b/templates/nix-configs/hyprland/users/USERNAME/home/hyprland/packages.nix new file mode 100644 index 0000000..3b585b7 --- /dev/null +++ b/templates/nix-configs/hyprland/users/USERNAME/home/hyprland/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + home.packages = with pkgs; [ ]; +} diff --git a/templates/nix-configs/hyprland/users/USERNAME/pubkeys/YOUR_PUBKEY.pub b/templates/nix-configs/hyprland/users/USERNAME/pubkeys/YOUR_PUBKEY.pub new file mode 100644 index 0000000..e69de29 diff --git a/templates/nix-configs/pi4/flake.nix b/templates/nix-configs/pi4/flake.nix new file mode 100644 index 0000000..4840249 --- /dev/null +++ b/templates/nix-configs/pi4/flake.nix @@ -0,0 +1,89 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-old-stable.url = "github:nixos/nixpkgs/nixos-25.05"; + + synix.url = "git+https://git.sid.ovh/sid/synix.git?ref=release-25.11"; + synix.imputs.nixpkgs.follows = "nixpkgs"; + + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + ... + }@inputs: + let + inherit (self) outputs; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + lib = nixpkgs.lib.extend (final: prev: inputs.synix.lib or { }); + + mkNixosConfiguration = + system: modules: + nixpkgs.lib.nixosSystem { + inherit system modules; + specialArgs = { + inherit inputs outputs lib; + }; + }; + in + { + packages = forAllSystems (system: import ./pkgs nixpkgs.legacyPackages.${system}); + + overlays = import ./overlays { inherit inputs; }; + + nixosModules = import ./modules/nixos; + + nixosConfigurations = { + HOSTNAME = mkNixosConfiguration "x86_64-linux" [ ./hosts/HOSTNAME ]; + }; + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + flakePkgs = self.packages.${system}; + overlaidPkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.modifications ]; + }; + in + { + pre-commit-check = inputs.git-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt.enable = true; + }; + }; + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + build-overlays = pkgs.linkFarm "flake-overlays-${system}" { + # package = overlaidPkgs.package; + }; + } + ); + }; +} diff --git a/templates/nix-configs/pi4/hosts/HOSTNAME/boot.nix b/templates/nix-configs/pi4/hosts/HOSTNAME/boot.nix new file mode 100644 index 0000000..a459d88 --- /dev/null +++ b/templates/nix-configs/pi4/hosts/HOSTNAME/boot.nix @@ -0,0 +1,8 @@ +{ + boot = { + loader = { + grub.enable = false; + generic-extlinux-compatible.enable = true; + }; + }; +} diff --git a/templates/nix-configs/pi4/hosts/HOSTNAME/default.nix b/templates/nix-configs/pi4/hosts/HOSTNAME/default.nix new file mode 100644 index 0000000..5fbf9d6 --- /dev/null +++ b/templates/nix-configs/pi4/hosts/HOSTNAME/default.nix @@ -0,0 +1,22 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + inputs.synix.nixosModules.common + + outputs.nixosModules.common + ]; + + system.stateVersion = "25.11"; +} diff --git a/templates/nix-configs/pi4/hosts/HOSTNAME/disks.sh b/templates/nix-configs/pi4/hosts/HOSTNAME/disks.sh new file mode 100644 index 0000000..5e06bc8 --- /dev/null +++ b/templates/nix-configs/pi4/hosts/HOSTNAME/disks.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL diff --git a/templates/nix-configs/pi4/hosts/HOSTNAME/hardware.nix b/templates/nix-configs/pi4/hosts/HOSTNAME/hardware.nix new file mode 100644 index 0000000..a0d751a --- /dev/null +++ b/templates/nix-configs/pi4/hosts/HOSTNAME/hardware.nix @@ -0,0 +1,23 @@ +{ pkgs, lib, ... }: + +{ + boot = { + kernelPackages = pkgs.linuxKernel.packages.linux_rpi4; + initrd.availableKernelModules = [ + "xhci_pci" + "usbhid" + "usb_storage" + ]; + }; + + fileSystems = { + "/" = { + device = "/dev/disk/by-label/NIXOS_SD"; + fsType = "ext4"; + options = [ "noatime" ]; + }; + }; + + nixpkgs.hostPlatform = lib.mkDefault "aarch64-linux"; + hardware.enableRedistributableFirmware = true; +} diff --git a/templates/nix-configs/pi4/hosts/HOSTNAME/networking.nix b/templates/nix-configs/pi4/hosts/HOSTNAME/networking.nix new file mode 100644 index 0000000..f96e974 --- /dev/null +++ b/templates/nix-configs/pi4/hosts/HOSTNAME/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "HOSTNAME"; + networking.domain = "HOSTNAME.local"; +} diff --git a/templates/nix-configs/pi4/hosts/HOSTNAME/packages.nix b/templates/nix-configs/pi4/hosts/HOSTNAME/packages.nix new file mode 100644 index 0000000..96cc691 --- /dev/null +++ b/templates/nix-configs/pi4/hosts/HOSTNAME/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} diff --git a/templates/nix-configs/pi4/hosts/HOSTNAME/services/default.nix b/templates/nix-configs/pi4/hosts/HOSTNAME/services/default.nix new file mode 100644 index 0000000..c8695e0 --- /dev/null +++ b/templates/nix-configs/pi4/hosts/HOSTNAME/services/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ]; +} diff --git a/templates/nix-configs/pi4/hosts/HOSTNAME/services/nginx.nix b/templates/nix-configs/pi4/hosts/HOSTNAME/services/nginx.nix new file mode 100644 index 0000000..04a2482 --- /dev/null +++ b/templates/nix-configs/pi4/hosts/HOSTNAME/services/nginx.nix @@ -0,0 +1,14 @@ +{ + inputs, + ... +}: + +{ + imports = [ inputs.synix.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} diff --git a/templates/nix-configs/pi4/hosts/HOSTNAME/services/openssh.nix b/templates/nix-configs/pi4/hosts/HOSTNAME/services/openssh.nix new file mode 100644 index 0000000..b851d18 --- /dev/null +++ b/templates/nix-configs/pi4/hosts/HOSTNAME/services/openssh.nix @@ -0,0 +1,12 @@ +{ + inputs, + ... +}: + +{ + imports = [ + inputs.synix.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/templates/nix-configs/pi4/hosts/HOSTNAME/users.nix b/templates/nix-configs/pi4/hosts/HOSTNAME/users.nix new file mode 100644 index 0000000..253394d --- /dev/null +++ b/templates/nix-configs/pi4/hosts/HOSTNAME/users.nix @@ -0,0 +1,9 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.synix.nixosModules.normalUsers + + ../../users/USERNAME + ]; +} diff --git a/templates/nix-configs/pi4/modules/nixos/common/default.nix b/templates/nix-configs/pi4/modules/nixos/common/default.nix new file mode 100644 index 0000000..aa96a5f --- /dev/null +++ b/templates/nix-configs/pi4/modules/nixos/common/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./overlays.nix + ]; +} diff --git a/templates/nix-configs/pi4/modules/nixos/common/overlays.nix b/templates/nix-configs/pi4/modules/nixos/common/overlays.nix new file mode 100644 index 0000000..348ae08 --- /dev/null +++ b/templates/nix-configs/pi4/modules/nixos/common/overlays.nix @@ -0,0 +1,11 @@ +{ outputs, ... }: + +{ + nixpkgs.overlays = [ + outputs.overlays.synix-packages + outputs.overlays.local-packages + outputs.overlays.modifications + outputs.overlays.old-stable-packages + outputs.overlays.unstable-packages + ]; +} diff --git a/templates/nix-configs/pi4/modules/nixos/default.nix b/templates/nix-configs/pi4/modules/nixos/default.nix new file mode 100644 index 0000000..28a636c --- /dev/null +++ b/templates/nix-configs/pi4/modules/nixos/default.nix @@ -0,0 +1,3 @@ +{ + common = import ./common; +} diff --git a/templates/nix-configs/pi4/overlays/default.nix b/templates/nix-configs/pi4/overlays/default.nix new file mode 100644 index 0000000..23332b5 --- /dev/null +++ b/templates/nix-configs/pi4/overlays/default.nix @@ -0,0 +1,35 @@ +{ inputs, ... }: + +{ + # synix packages accessible through 'pkgs.synix' + synix-packages = final: prev: { synix = inputs.synix.packages."${final.system}"; }; + + # packages in `pkgs/` accessible through 'pkgs.local' + local-packages = final: prev: { local = import ../pkgs { pkgs = final; }; }; + + # https://nixos.wiki/wiki/Overlays + modifications = + final: prev: + let + files = [ + ]; + imports = builtins.map (f: import f final prev) files; + in + builtins.foldl' (a: b: a // b) { } imports // inputs.synix.overlays.modifications final prev; + + # old-stable nixpkgs accessible through 'pkgs.old-stable' + old-stable-packages = final: prev: { + old-stable = import inputs.nixpkgs-old-stable { + inherit (final) system; + inherit (prev) config; + }; + }; + + # unstable nixpkgs accessible through 'pkgs.unstable' + unstable-packages = final: prev: { + unstable = import inputs.nixpkgs-unstable { + inherit (final) system; + inherit (prev) config; + }; + }; +} diff --git a/templates/nix-configs/pi4/pkgs/default.nix b/templates/nix-configs/pi4/pkgs/default.nix new file mode 100644 index 0000000..2dadf8a --- /dev/null +++ b/templates/nix-configs/pi4/pkgs/default.nix @@ -0,0 +1,8 @@ +{ + pkgs ? import , + ... +}: + +{ + # example = pkgs.callPackage ./example { }; +} diff --git a/templates/nix-configs/pi4/users/USERNAME/default.nix b/templates/nix-configs/pi4/users/USERNAME/default.nix new file mode 100644 index 0000000..9885271 --- /dev/null +++ b/templates/nix-configs/pi4/users/USERNAME/default.nix @@ -0,0 +1,8 @@ +{ + normalUsers.USERNAME = { + extraGroups = [ + "wheel" + ]; + # sshKeyFiles = [ ./pubkeys/YOUR_PUBKEY.pub ]; # FIXME + }; +} diff --git a/templates/nix-configs/pi4/users/USERNAME/pubkeys/YOUR_PUBKEY.pub b/templates/nix-configs/pi4/users/USERNAME/pubkeys/YOUR_PUBKEY.pub new file mode 100644 index 0000000..e69de29 diff --git a/templates/nix-configs/server/flake.nix b/templates/nix-configs/server/flake.nix new file mode 100644 index 0000000..4840249 --- /dev/null +++ b/templates/nix-configs/server/flake.nix @@ -0,0 +1,89 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-old-stable.url = "github:nixos/nixpkgs/nixos-25.05"; + + synix.url = "git+https://git.sid.ovh/sid/synix.git?ref=release-25.11"; + synix.imputs.nixpkgs.follows = "nixpkgs"; + + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + ... + }@inputs: + let + inherit (self) outputs; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + lib = nixpkgs.lib.extend (final: prev: inputs.synix.lib or { }); + + mkNixosConfiguration = + system: modules: + nixpkgs.lib.nixosSystem { + inherit system modules; + specialArgs = { + inherit inputs outputs lib; + }; + }; + in + { + packages = forAllSystems (system: import ./pkgs nixpkgs.legacyPackages.${system}); + + overlays = import ./overlays { inherit inputs; }; + + nixosModules = import ./modules/nixos; + + nixosConfigurations = { + HOSTNAME = mkNixosConfiguration "x86_64-linux" [ ./hosts/HOSTNAME ]; + }; + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + flakePkgs = self.packages.${system}; + overlaidPkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.modifications ]; + }; + in + { + pre-commit-check = inputs.git-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt.enable = true; + }; + }; + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + build-overlays = pkgs.linkFarm "flake-overlays-${system}" { + # package = overlaidPkgs.package; + }; + } + ); + }; +} diff --git a/templates/nix-configs/server/hosts/HOSTNAME/boot.nix b/templates/nix-configs/server/hosts/HOSTNAME/boot.nix new file mode 100644 index 0000000..53a9686 --- /dev/null +++ b/templates/nix-configs/server/hosts/HOSTNAME/boot.nix @@ -0,0 +1,7 @@ +{ + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 10; + }; + boot.loader.efi.canTouchEfiVariables = true; +} diff --git a/templates/nix-configs/server/hosts/HOSTNAME/default.nix b/templates/nix-configs/server/hosts/HOSTNAME/default.nix new file mode 100644 index 0000000..5fbf9d6 --- /dev/null +++ b/templates/nix-configs/server/hosts/HOSTNAME/default.nix @@ -0,0 +1,22 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + inputs.synix.nixosModules.common + + outputs.nixosModules.common + ]; + + system.stateVersion = "25.11"; +} diff --git a/templates/nix-configs/server/hosts/HOSTNAME/disks.sh b/templates/nix-configs/server/hosts/HOSTNAME/disks.sh new file mode 100644 index 0000000..5e06bc8 --- /dev/null +++ b/templates/nix-configs/server/hosts/HOSTNAME/disks.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +SSD='/dev/disk/by-id/FIXME' +MNT='/mnt' +SWAP_GB=4 + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:+"$SWAP_GB"G -t2:8200 -c2:SWAP $SSD +sgdisk -n3:0:0 -t3:8304 -c3:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device ${SSD}-part1 +wait_for_device ${SSD}-part2 +wait_for_device ${SSD}-part3 + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}-part1" +mkswap -L SWAP "${SSD}-part2" +mkfs.ext4 -L ROOT "${SSD}-part3" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}-part3" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}-part1" "$MNT/boot" + +echo "Enabling swap..." +swapon "${SSD}-part2" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL diff --git a/templates/nix-configs/server/hosts/HOSTNAME/hardware.nix b/templates/nix-configs/server/hosts/HOSTNAME/hardware.nix new file mode 100644 index 0000000..2bfd7b4 --- /dev/null +++ b/templates/nix-configs/server/hosts/HOSTNAME/hardware.nix @@ -0,0 +1,48 @@ +{ + config, + lib, + pkgs, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "ahci" + "nvme" + "sd_mod" + "sdhci_pci" + "sr_mod" + "usb_storage" + "virtio_pci" + "virtio_scsi" + "xhci_pci" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0022" + "dmask=0022" + ]; + }; + + swapDevices = [ { device = "/dev/disk/by-label/SWAP"; } ]; + + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/templates/nix-configs/server/hosts/HOSTNAME/networking.nix b/templates/nix-configs/server/hosts/HOSTNAME/networking.nix new file mode 100644 index 0000000..f96e974 --- /dev/null +++ b/templates/nix-configs/server/hosts/HOSTNAME/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "HOSTNAME"; + networking.domain = "HOSTNAME.local"; +} diff --git a/templates/nix-configs/server/hosts/HOSTNAME/packages.nix b/templates/nix-configs/server/hosts/HOSTNAME/packages.nix new file mode 100644 index 0000000..96cc691 --- /dev/null +++ b/templates/nix-configs/server/hosts/HOSTNAME/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} diff --git a/templates/nix-configs/server/hosts/HOSTNAME/services/default.nix b/templates/nix-configs/server/hosts/HOSTNAME/services/default.nix new file mode 100644 index 0000000..c8695e0 --- /dev/null +++ b/templates/nix-configs/server/hosts/HOSTNAME/services/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ]; +} diff --git a/templates/nix-configs/server/hosts/HOSTNAME/services/nginx.nix b/templates/nix-configs/server/hosts/HOSTNAME/services/nginx.nix new file mode 100644 index 0000000..04a2482 --- /dev/null +++ b/templates/nix-configs/server/hosts/HOSTNAME/services/nginx.nix @@ -0,0 +1,14 @@ +{ + inputs, + ... +}: + +{ + imports = [ inputs.synix.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} diff --git a/templates/nix-configs/server/hosts/HOSTNAME/services/openssh.nix b/templates/nix-configs/server/hosts/HOSTNAME/services/openssh.nix new file mode 100644 index 0000000..b851d18 --- /dev/null +++ b/templates/nix-configs/server/hosts/HOSTNAME/services/openssh.nix @@ -0,0 +1,12 @@ +{ + inputs, + ... +}: + +{ + imports = [ + inputs.synix.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/templates/nix-configs/server/hosts/HOSTNAME/users.nix b/templates/nix-configs/server/hosts/HOSTNAME/users.nix new file mode 100644 index 0000000..253394d --- /dev/null +++ b/templates/nix-configs/server/hosts/HOSTNAME/users.nix @@ -0,0 +1,9 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.synix.nixosModules.normalUsers + + ../../users/USERNAME + ]; +} diff --git a/templates/nix-configs/server/modules/nixos/common/default.nix b/templates/nix-configs/server/modules/nixos/common/default.nix new file mode 100644 index 0000000..aa96a5f --- /dev/null +++ b/templates/nix-configs/server/modules/nixos/common/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./overlays.nix + ]; +} diff --git a/templates/nix-configs/server/modules/nixos/common/overlays.nix b/templates/nix-configs/server/modules/nixos/common/overlays.nix new file mode 100644 index 0000000..348ae08 --- /dev/null +++ b/templates/nix-configs/server/modules/nixos/common/overlays.nix @@ -0,0 +1,11 @@ +{ outputs, ... }: + +{ + nixpkgs.overlays = [ + outputs.overlays.synix-packages + outputs.overlays.local-packages + outputs.overlays.modifications + outputs.overlays.old-stable-packages + outputs.overlays.unstable-packages + ]; +} diff --git a/templates/nix-configs/server/modules/nixos/default.nix b/templates/nix-configs/server/modules/nixos/default.nix new file mode 100644 index 0000000..28a636c --- /dev/null +++ b/templates/nix-configs/server/modules/nixos/default.nix @@ -0,0 +1,3 @@ +{ + common = import ./common; +} diff --git a/templates/nix-configs/server/overlays/default.nix b/templates/nix-configs/server/overlays/default.nix new file mode 100644 index 0000000..23332b5 --- /dev/null +++ b/templates/nix-configs/server/overlays/default.nix @@ -0,0 +1,35 @@ +{ inputs, ... }: + +{ + # synix packages accessible through 'pkgs.synix' + synix-packages = final: prev: { synix = inputs.synix.packages."${final.system}"; }; + + # packages in `pkgs/` accessible through 'pkgs.local' + local-packages = final: prev: { local = import ../pkgs { pkgs = final; }; }; + + # https://nixos.wiki/wiki/Overlays + modifications = + final: prev: + let + files = [ + ]; + imports = builtins.map (f: import f final prev) files; + in + builtins.foldl' (a: b: a // b) { } imports // inputs.synix.overlays.modifications final prev; + + # old-stable nixpkgs accessible through 'pkgs.old-stable' + old-stable-packages = final: prev: { + old-stable = import inputs.nixpkgs-old-stable { + inherit (final) system; + inherit (prev) config; + }; + }; + + # unstable nixpkgs accessible through 'pkgs.unstable' + unstable-packages = final: prev: { + unstable = import inputs.nixpkgs-unstable { + inherit (final) system; + inherit (prev) config; + }; + }; +} diff --git a/templates/nix-configs/server/pkgs/default.nix b/templates/nix-configs/server/pkgs/default.nix new file mode 100644 index 0000000..2dadf8a --- /dev/null +++ b/templates/nix-configs/server/pkgs/default.nix @@ -0,0 +1,8 @@ +{ + pkgs ? import , + ... +}: + +{ + # example = pkgs.callPackage ./example { }; +} diff --git a/templates/nix-configs/server/users/USERNAME/default.nix b/templates/nix-configs/server/users/USERNAME/default.nix new file mode 100644 index 0000000..9885271 --- /dev/null +++ b/templates/nix-configs/server/users/USERNAME/default.nix @@ -0,0 +1,8 @@ +{ + normalUsers.USERNAME = { + extraGroups = [ + "wheel" + ]; + # sshKeyFiles = [ ./pubkeys/YOUR_PUBKEY.pub ]; # FIXME + }; +} diff --git a/templates/nix-configs/server/users/USERNAME/pubkeys/YOUR_PUBKEY.pub b/templates/nix-configs/server/users/USERNAME/pubkeys/YOUR_PUBKEY.pub new file mode 100644 index 0000000..e69de29 diff --git a/templates/nix-configs/vm-uefi/flake.nix b/templates/nix-configs/vm-uefi/flake.nix new file mode 100644 index 0000000..4840249 --- /dev/null +++ b/templates/nix-configs/vm-uefi/flake.nix @@ -0,0 +1,89 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11"; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; + nixpkgs-old-stable.url = "github:nixos/nixpkgs/nixos-25.05"; + + synix.url = "git+https://git.sid.ovh/sid/synix.git?ref=release-25.11"; + synix.imputs.nixpkgs.follows = "nixpkgs"; + + git-hooks.url = "github:cachix/git-hooks.nix"; + git-hooks.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = + { + self, + nixpkgs, + ... + }@inputs: + let + inherit (self) outputs; + + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + ]; + + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + + lib = nixpkgs.lib.extend (final: prev: inputs.synix.lib or { }); + + mkNixosConfiguration = + system: modules: + nixpkgs.lib.nixosSystem { + inherit system modules; + specialArgs = { + inherit inputs outputs lib; + }; + }; + in + { + packages = forAllSystems (system: import ./pkgs nixpkgs.legacyPackages.${system}); + + overlays = import ./overlays { inherit inputs; }; + + nixosModules = import ./modules/nixos; + + nixosConfigurations = { + HOSTNAME = mkNixosConfiguration "x86_64-linux" [ ./hosts/HOSTNAME ]; + }; + + formatter = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + config = self.checks.${system}.pre-commit-check.config; + inherit (config) package configFile; + script = '' + ${pkgs.lib.getExe package} run --all-files --config ${configFile} + ''; + in + pkgs.writeShellScriptBin "pre-commit-run" script + ); + + checks = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + flakePkgs = self.packages.${system}; + overlaidPkgs = import nixpkgs { + inherit system; + overlays = [ self.overlays.modifications ]; + }; + in + { + pre-commit-check = inputs.git-hooks.lib.${system}.run { + src = ./.; + hooks = { + nixfmt.enable = true; + }; + }; + build-packages = pkgs.linkFarm "flake-packages-${system}" flakePkgs; + build-overlays = pkgs.linkFarm "flake-overlays-${system}" { + # package = overlaidPkgs.package; + }; + } + ); + }; +} diff --git a/templates/nix-configs/vm-uefi/hosts/HOSTNAME/boot.nix b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/boot.nix new file mode 100644 index 0000000..53a9686 --- /dev/null +++ b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/boot.nix @@ -0,0 +1,7 @@ +{ + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 10; + }; + boot.loader.efi.canTouchEfiVariables = true; +} diff --git a/templates/nix-configs/vm-uefi/hosts/HOSTNAME/default.nix b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/default.nix new file mode 100644 index 0000000..5fbf9d6 --- /dev/null +++ b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/default.nix @@ -0,0 +1,22 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./packages.nix + ./services + ./users.nix + + inputs.synix.nixosModules.common + + outputs.nixosModules.common + ]; + + system.stateVersion = "25.11"; +} diff --git a/templates/nix-configs/vm-uefi/hosts/HOSTNAME/disks.sh b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/disks.sh new file mode 100644 index 0000000..a1f9ba8 --- /dev/null +++ b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/disks.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash + +SSD='/dev/sda' # FIXME: Replace with your actual disk +MNT='/mnt' + +# Helper function to wait for devices +wait_for_device() { + local device=$1 + echo "Waiting for device: $device ..." + while [[ ! -e $device ]]; do + sleep 1 + done + echo "Device $device is ready." +} + +# Function to install a package if it's not already installed +install_if_missing() { + local cmd="$1" + local package="$2" + if ! command -v "$cmd" &> /dev/null; then + echo "$cmd not found, installing $package..." + nix-env -iA "nixos.$package" + fi +} + +install_if_missing "sgdisk" "gptfdisk" +install_if_missing "partprobe" "parted" + +swapoff --all +udevadm settle + +wait_for_device $SSD + +echo "Wiping filesystem on $SSD..." +wipefs -a $SSD + +echo "Clearing partition table on $SSD..." +sgdisk --zap-all $SSD + +echo "Partitioning $SSD..." +sgdisk -n1:1M:+1G -t1:EF00 -c1:BOOT $SSD +sgdisk -n2:0:0 -t2:8304 -c2:ROOT $SSD +partprobe -s $SSD +udevadm settle + +wait_for_device "${SSD}1" +wait_for_device "${SSD}2" + +echo "Formatting partitions..." +mkfs.vfat -F 32 -n BOOT "${SSD}1" +mkfs.ext4 -L ROOT "${SSD}2" + +echo "Mounting partitions..." +mount -o X-mount.mkdir "${SSD}2" "$MNT" +mkdir -p "$MNT/boot" +mount -t vfat -o fmask=0077,dmask=0077,iocharset=iso8859-1 "${SSD}1" "$MNT/boot" + +echo "Partitioning and setup complete:" +lsblk -o NAME,FSTYPE,SIZE,MOUNTPOINT,LABEL diff --git a/templates/nix-configs/vm-uefi/hosts/HOSTNAME/hardware.nix b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/hardware.nix new file mode 100644 index 0000000..3a61ae9 --- /dev/null +++ b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/hardware.nix @@ -0,0 +1,41 @@ +{ + inputs, + config, + lib, + pkgs, + modulesPath, + ... +}: + +{ + imports = [ inputs.synix.nixosModules.device.vm ]; + + boot.initrd.availableKernelModules = [ + "sd_mod" + "sr_mod" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0077" + "dmask=0077" + ]; + }; + + swapDevices = [ ]; + + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + virtualisation.hypervGuest.enable = true; +} diff --git a/templates/nix-configs/vm-uefi/hosts/HOSTNAME/networking.nix b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/networking.nix new file mode 100644 index 0000000..f96e974 --- /dev/null +++ b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "HOSTNAME"; + networking.domain = "HOSTNAME.local"; +} diff --git a/templates/nix-configs/vm-uefi/hosts/HOSTNAME/packages.nix b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/packages.nix new file mode 100644 index 0000000..96cc691 --- /dev/null +++ b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/packages.nix @@ -0,0 +1,5 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ ]; +} diff --git a/templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/default.nix b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/default.nix new file mode 100644 index 0000000..c8695e0 --- /dev/null +++ b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ]; +} diff --git a/templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/nginx.nix b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/nginx.nix new file mode 100644 index 0000000..04a2482 --- /dev/null +++ b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/nginx.nix @@ -0,0 +1,14 @@ +{ + inputs, + ... +}: + +{ + imports = [ inputs.synix.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} diff --git a/templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/openssh.nix b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/openssh.nix new file mode 100644 index 0000000..b851d18 --- /dev/null +++ b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/services/openssh.nix @@ -0,0 +1,12 @@ +{ + inputs, + ... +}: + +{ + imports = [ + inputs.synix.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/templates/nix-configs/vm-uefi/hosts/HOSTNAME/users.nix b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/users.nix new file mode 100644 index 0000000..253394d --- /dev/null +++ b/templates/nix-configs/vm-uefi/hosts/HOSTNAME/users.nix @@ -0,0 +1,9 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.synix.nixosModules.normalUsers + + ../../users/USERNAME + ]; +} diff --git a/templates/nix-configs/vm-uefi/modules/nixos/common/default.nix b/templates/nix-configs/vm-uefi/modules/nixos/common/default.nix new file mode 100644 index 0000000..aa96a5f --- /dev/null +++ b/templates/nix-configs/vm-uefi/modules/nixos/common/default.nix @@ -0,0 +1,5 @@ +{ + imports = [ + ./overlays.nix + ]; +} diff --git a/templates/nix-configs/vm-uefi/modules/nixos/common/overlays.nix b/templates/nix-configs/vm-uefi/modules/nixos/common/overlays.nix new file mode 100644 index 0000000..348ae08 --- /dev/null +++ b/templates/nix-configs/vm-uefi/modules/nixos/common/overlays.nix @@ -0,0 +1,11 @@ +{ outputs, ... }: + +{ + nixpkgs.overlays = [ + outputs.overlays.synix-packages + outputs.overlays.local-packages + outputs.overlays.modifications + outputs.overlays.old-stable-packages + outputs.overlays.unstable-packages + ]; +} diff --git a/templates/nix-configs/vm-uefi/modules/nixos/default.nix b/templates/nix-configs/vm-uefi/modules/nixos/default.nix new file mode 100644 index 0000000..28a636c --- /dev/null +++ b/templates/nix-configs/vm-uefi/modules/nixos/default.nix @@ -0,0 +1,3 @@ +{ + common = import ./common; +} diff --git a/templates/nix-configs/vm-uefi/overlays/default.nix b/templates/nix-configs/vm-uefi/overlays/default.nix new file mode 100644 index 0000000..23332b5 --- /dev/null +++ b/templates/nix-configs/vm-uefi/overlays/default.nix @@ -0,0 +1,35 @@ +{ inputs, ... }: + +{ + # synix packages accessible through 'pkgs.synix' + synix-packages = final: prev: { synix = inputs.synix.packages."${final.system}"; }; + + # packages in `pkgs/` accessible through 'pkgs.local' + local-packages = final: prev: { local = import ../pkgs { pkgs = final; }; }; + + # https://nixos.wiki/wiki/Overlays + modifications = + final: prev: + let + files = [ + ]; + imports = builtins.map (f: import f final prev) files; + in + builtins.foldl' (a: b: a // b) { } imports // inputs.synix.overlays.modifications final prev; + + # old-stable nixpkgs accessible through 'pkgs.old-stable' + old-stable-packages = final: prev: { + old-stable = import inputs.nixpkgs-old-stable { + inherit (final) system; + inherit (prev) config; + }; + }; + + # unstable nixpkgs accessible through 'pkgs.unstable' + unstable-packages = final: prev: { + unstable = import inputs.nixpkgs-unstable { + inherit (final) system; + inherit (prev) config; + }; + }; +} diff --git a/templates/nix-configs/vm-uefi/pkgs/default.nix b/templates/nix-configs/vm-uefi/pkgs/default.nix new file mode 100644 index 0000000..2dadf8a --- /dev/null +++ b/templates/nix-configs/vm-uefi/pkgs/default.nix @@ -0,0 +1,8 @@ +{ + pkgs ? import , + ... +}: + +{ + # example = pkgs.callPackage ./example { }; +} diff --git a/templates/nix-configs/vm-uefi/users/USERNAME/default.nix b/templates/nix-configs/vm-uefi/users/USERNAME/default.nix new file mode 100644 index 0000000..9885271 --- /dev/null +++ b/templates/nix-configs/vm-uefi/users/USERNAME/default.nix @@ -0,0 +1,8 @@ +{ + normalUsers.USERNAME = { + extraGroups = [ + "wheel" + ]; + # sshKeyFiles = [ ./pubkeys/YOUR_PUBKEY.pub ]; # FIXME + }; +} diff --git a/templates/nix-configs/vm-uefi/users/USERNAME/pubkeys/YOUR_PUBKEY.pub b/templates/nix-configs/vm-uefi/users/USERNAME/pubkeys/YOUR_PUBKEY.pub new file mode 100644 index 0000000..e69de29 diff --git a/tests/build/hm-hyprland/default.nix b/tests/build/hm-hyprland/default.nix new file mode 100644 index 0000000..9b034fa --- /dev/null +++ b/tests/build/hm-hyprland/default.nix @@ -0,0 +1,28 @@ +{ + inputs, + outputs, + pkgs, + ... +}: + +{ + imports = [ + inputs.synix.homeModules.common + inputs.synix.homeModules.hyprland + inputs.synix.homeModules.nixvim + inputs.synix.homeModules.stylix + ]; + + home.username = "test-user"; + + programs.nixvim.enable = true; + + wayland.windowManager.hyprland = { + enable = true; + autostart = true; + }; + + stylix.enable = true; + + home.stateVersion = "25.11"; +} diff --git a/tests/build/nixos-hyprland/boot.nix b/tests/build/nixos-hyprland/boot.nix new file mode 100644 index 0000000..53a9686 --- /dev/null +++ b/tests/build/nixos-hyprland/boot.nix @@ -0,0 +1,7 @@ +{ + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 10; + }; + boot.loader.efi.canTouchEfiVariables = true; +} diff --git a/tests/build/nixos-hyprland/default.nix b/tests/build/nixos-hyprland/default.nix new file mode 100644 index 0000000..6191e8e --- /dev/null +++ b/tests/build/nixos-hyprland/default.nix @@ -0,0 +1,20 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./users.nix + + inputs.synix.nixosModules.common + inputs.synix.nixosModules.device.laptop + inputs.synix.nixosModules.hyprland + ]; + + system.stateVersion = "25.11"; +} diff --git a/tests/build/nixos-hyprland/hardware.nix b/tests/build/nixos-hyprland/hardware.nix new file mode 100644 index 0000000..2bfd7b4 --- /dev/null +++ b/tests/build/nixos-hyprland/hardware.nix @@ -0,0 +1,48 @@ +{ + config, + lib, + pkgs, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "ahci" + "nvme" + "sd_mod" + "sdhci_pci" + "sr_mod" + "usb_storage" + "virtio_pci" + "virtio_scsi" + "xhci_pci" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0022" + "dmask=0022" + ]; + }; + + swapDevices = [ { device = "/dev/disk/by-label/SWAP"; } ]; + + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/tests/build/nixos-hyprland/networking.nix b/tests/build/nixos-hyprland/networking.nix new file mode 100644 index 0000000..149c700 --- /dev/null +++ b/tests/build/nixos-hyprland/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "test-host"; + networking.domain = "test-host.local"; +} diff --git a/tests/build/nixos-hyprland/users.nix b/tests/build/nixos-hyprland/users.nix new file mode 100644 index 0000000..cf7c655 --- /dev/null +++ b/tests/build/nixos-hyprland/users.nix @@ -0,0 +1,13 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.synix.nixosModules.normalUsers + ]; + + normalUsers.test-user = { + extraGroups = [ + "wheel" + ]; + }; +} diff --git a/tests/build/nixos-server/boot.nix b/tests/build/nixos-server/boot.nix new file mode 100644 index 0000000..53a9686 --- /dev/null +++ b/tests/build/nixos-server/boot.nix @@ -0,0 +1,7 @@ +{ + boot.loader.systemd-boot = { + enable = true; + configurationLimit = 10; + }; + boot.loader.efi.canTouchEfiVariables = true; +} diff --git a/tests/build/nixos-server/default.nix b/tests/build/nixos-server/default.nix new file mode 100644 index 0000000..d7fb543 --- /dev/null +++ b/tests/build/nixos-server/default.nix @@ -0,0 +1,20 @@ +{ + inputs, + outputs, + ... +}: + +{ + imports = [ + ./boot.nix + ./hardware.nix + ./networking.nix + ./services + ./users.nix + + inputs.synix.nixosModules.common + inputs.synix.nixosModules.device.server + ]; + + system.stateVersion = "25.11"; +} diff --git a/tests/build/nixos-server/hardware.nix b/tests/build/nixos-server/hardware.nix new file mode 100644 index 0000000..2bfd7b4 --- /dev/null +++ b/tests/build/nixos-server/hardware.nix @@ -0,0 +1,48 @@ +{ + config, + lib, + pkgs, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "ahci" + "nvme" + "sd_mod" + "sdhci_pci" + "sr_mod" + "usb_storage" + "virtio_pci" + "virtio_scsi" + "xhci_pci" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-label/ROOT"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-label/BOOT"; + fsType = "vfat"; + options = [ + "fmask=0022" + "dmask=0022" + ]; + }; + + swapDevices = [ { device = "/dev/disk/by-label/SWAP"; } ]; + + networking.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; +} diff --git a/tests/build/nixos-server/networking.nix b/tests/build/nixos-server/networking.nix new file mode 100644 index 0000000..149c700 --- /dev/null +++ b/tests/build/nixos-server/networking.nix @@ -0,0 +1,4 @@ +{ + networking.hostName = "test-host"; + networking.domain = "test-host.local"; +} diff --git a/tests/build/nixos-server/services/default.nix b/tests/build/nixos-server/services/default.nix new file mode 100644 index 0000000..c8695e0 --- /dev/null +++ b/tests/build/nixos-server/services/default.nix @@ -0,0 +1,6 @@ +{ + imports = [ + ./nginx.nix + ./openssh.nix + ]; +} diff --git a/tests/build/nixos-server/services/nginx.nix b/tests/build/nixos-server/services/nginx.nix new file mode 100644 index 0000000..04a2482 --- /dev/null +++ b/tests/build/nixos-server/services/nginx.nix @@ -0,0 +1,14 @@ +{ + inputs, + ... +}: + +{ + imports = [ inputs.synix.nixosModules.nginx ]; + + services.nginx = { + enable = true; + forceSSL = true; + openFirewall = true; + }; +} diff --git a/tests/build/nixos-server/services/openssh.nix b/tests/build/nixos-server/services/openssh.nix new file mode 100644 index 0000000..b851d18 --- /dev/null +++ b/tests/build/nixos-server/services/openssh.nix @@ -0,0 +1,12 @@ +{ + inputs, + ... +}: + +{ + imports = [ + inputs.synix.nixosModules.openssh + ]; + + services.openssh.enable = true; +} diff --git a/tests/build/nixos-server/users.nix b/tests/build/nixos-server/users.nix new file mode 100644 index 0000000..cf7c655 --- /dev/null +++ b/tests/build/nixos-server/users.nix @@ -0,0 +1,13 @@ +{ inputs, ... }: + +{ + imports = [ + inputs.synix.nixosModules.normalUsers + ]; + + normalUsers.test-user = { + extraGroups = [ + "wheel" + ]; + }; +} diff --git a/tests/run/synapse.nix b/tests/run/synapse.nix new file mode 100644 index 0000000..3a5774a --- /dev/null +++ b/tests/run/synapse.nix @@ -0,0 +1,95 @@ +{ + name = "synapse-test"; + + nodes.machine = + { + config, + lib, + pkgs, + ... + }: + let + cfg = config.services.matrix-synapse; + + keyFile = pkgs.writeText "livekit.key" "API Secret: secret"; + in + { + # fake sops module + options.sops = lib.mkOption { + type = lib.types.attrs; + default = { }; + }; + + imports = [ + ../../modules/nixos/matrix-synapse + ../../modules/nixos/coturn + ../../modules/nixos/maubot + ]; + + config = { + services.matrix-synapse = { + enable = true; + coturn.enable = true; + settings = { + registration_shared_secret = "secret"; + turn_shared_secret = "turn-secret"; + }; + }; + + services.coturn = { + enable = true; + no-tls = true; + static-auth-secret = "turn-secret"; + }; + + services.maubot = { + enable = true; + extraConfigFile = builtins.toString ( + pkgs.writeText "maubot-extra" '' + homeservers: + ${cfg.settings.server_name}: + url: http://127.0.0.1:${builtins.toString cfg.port} + secret: ${cfg.settings.registration_shared_secret} + admins: + alice: password + '' + ); + }; + + services.livekit.keyFile = keyFile; + services.lk-jwt-service.keyFile = keyFile; + + services.nginx.enable = true; + networking.domain = "example.com"; + networking.firewall.enable = false; # simplify networking for test + + # Override SSL/ACME requirements for test + services.nginx.virtualHosts."example.com".forceSSL = lib.mkForce false; + services.nginx.virtualHosts."example.com".enableACME = lib.mkForce false; + }; + }; + + testScript = '' + start_all() + + machine.wait_for_unit("default.target") + + machine.wait_for_unit("matrix-synapse.service") + machine.wait_for_open_port(8008) + + machine.wait_for_unit("livekit.service") + machine.wait_for_open_port(7880) + + machine.wait_for_unit("lk-jwt-service.service") + machine.wait_for_open_port(8080) + + machine.wait_for_unit("coturn.service") + machine.wait_for_open_port(3478) + + machine.wait_for_unit("maubot.service") + machine.wait_for_open_port(29316) + + machine.succeed("curl -f http://localhost:8008/_matrix/client/versions") + # machine.succeed("curl -f http://localhost:29316/_matrix/maubot/v1/logs") # TODO: add auth + ''; +}