SwarselSystems: NixOS + Emacs Configuration
Table of Contents
- 1. Introduction (no code)
- 2. An introduction to nix
- 3. flake.nix
- 3.1. flake.nix skeleton (inputs)
- 3.2. Auxiliary files
- 3.3. Library functions
- 3.4. Packages (pkgs)
- 3.5. Globals
- 3.6. Hosts
- 3.7. Topology (nix-topology generated network diagram)
- 3.8. Devshell (checks/pre-commit-hooks)
- 3.9. Templates
- 3.10. Formatter (treefmt-nix)
- 3.11. Modules
- 3.12. Apps
- 3.13. Overlays
- 3.14. Installer images (iso, kexec)
- 3.15. Installer flake
- 4. System
- 4.1. Manual steps when setting up a new machine
- 4.2. TODO Current issues
- 4.3. System specific configuration
- 4.3.1. TODO Template
- 4.3.2. Physical hosts
- 4.3.2.1. pyramid (Framework Laptop 16)
- 4.3.2.2. Bakery (Lenovo ThinkPad)
- 4.3.2.3. Winters (Server: ASRock J4105-ITX)
- 4.3.2.4. Summers (Server: ASUS Z10PA-D8)
- 4.3.2.4.1. Main Configuration
- 4.3.2.4.2. hardware-configuration
- 4.3.2.4.3. disko
- 4.3.2.4.4. Guests
- 4.3.2.4.4.1. Kavita
- 4.3.2.4.4.2. Jellyfin
- 4.3.2.4.4.3. Audio
- 4.3.2.4.4.4. Postgresql
- 4.3.2.4.4.5. Matrix
- 4.3.2.4.4.6. Nextcloud
- 4.3.2.4.4.7. Immich
- 4.3.2.4.4.8. Paperless
- 4.3.2.4.4.9. Transmission
- 4.3.2.4.4.10. Storage
- 4.3.2.4.4.11. Monitoring
- 4.3.2.4.4.12. FreshRSS
- 4.3.2.4.4.13. Kanidm
- 4.3.2.4.4.14. Firefly-III
- 4.3.2.4.4.15. Koillection
- 4.3.2.4.4.16. Radicale
- 4.3.2.4.4.17. Atuin
- 4.3.2.4.4.18. Forgejo
- 4.3.2.4.4.19. Anki Sync Server
- 4.3.2.4.4.20. Homebox
- 4.3.2.5. Hintbooth (Router: HUNSN RM02)
- 4.3.2.6. machpizza (MacBook Pro)
- 4.3.2.7. Magicant (Phone)
- 4.3.2.8. Treehouse (DGX Spark)
- 4.3.3. Virtual hosts
- 4.3.4. Utility hosts
- 4.4. NixOS
- 4.4.1. Common
- 4.4.1.1. Imports
- 4.4.1.2. Share configuration between nodes (distributed config, automatically active)
- 4.4.1.3. Global options (automatically active)
- 4.4.1.4. Expose home-manager sops secrets in NixOS (automatically active)
- 4.4.1.5. Topology (automatically active)
- 4.4.1.6. General NixOS settings (nix config, stateVersion)
- 4.4.1.7. Setup home-manager base
- 4.4.1.8. User setup, Make users non-mutable
- 4.4.1.9. Setup login keymap
- 4.4.1.10. Time, locale settings
- 4.4.1.11. TODO PII management
- 4.4.1.12. Lanzaboote (secure boot)
- 4.4.1.13. Boot
- 4.4.1.14. Impermanence
- 4.4.2. Client
- 4.4.2.1. Imports
- 4.4.2.2. System Packages
- 4.4.2.3. Environment setup
- 4.4.2.4. Security (polkit)
- 4.4.2.5. Reduce systemd timeouts
- 4.4.2.6. Hardware settings
- 4.4.2.7. Pulseaudio
- 4.4.2.8. Pipewire
- 4.4.2.9. Common network settings
- 4.4.2.10. sops
- 4.4.2.11. Remote building
- 4.4.2.12. Theme (stylix)
- 4.4.2.13. Programs (including zsh setup)
- 4.4.2.14. Services
- 4.4.2.15. Hardware compatibility settings (Yubikey, Ledger, Keyboards) - udev rules
- 4.4.2.16. System Login (greetd)
- 4.4.2.17. nix-ld
- 4.4.2.18. Summary of nixos-rebuild diff
- 4.4.2.19. gnome-keyring
- 4.4.2.20. Sway
- 4.4.2.21. xdg-portal (Screensharing)
- 4.4.2.22. Podman (distrobox)
- 4.4.2.23. Appimage
- 4.4.2.24. Handle lid switch correctly
- 4.4.2.25. Low battery notification
- 4.4.2.26. Auto-login
- 4.4.2.27. UWSM
- 4.4.2.28. Firezone Client
- 4.4.3. Server
- 4.4.3.1. Imports
- 4.4.3.2. General NixOS Server settings
- 4.4.3.3. System Packages (Server Programs)
- 4.4.3.4. nfs/samba (smb)
- 4.4.3.5. acme
- 4.4.3.6. NGINX
- 4.4.3.7. ssh
- 4.4.3.8. Bastion
- 4.4.3.9. ssh builder config
- 4.4.3.10. Network settings (globals.networks population)
- 4.4.3.11. Disk encryption
- 4.4.3.12. Attic setup
- 4.4.3.13. TODO Wireguard
- 4.4.3.14. BTRFS
- 4.4.3.15. Router
- 4.4.3.16. kavita
- 4.4.3.17. jellyfin
- 4.4.3.18. navidrome
- 4.4.3.19. spotifyd
- 4.4.3.20. mpd
- 4.4.3.21. pipewire
- 4.4.3.22. postgresql
- 4.4.3.23. podman
- 4.4.3.24. matrix
- 4.4.3.25. nextcloud
- 4.4.3.26. immich
- 4.4.3.27. paperless (tika, gotenberg)
- 4.4.3.28. transmission
- 4.4.3.29. syncthing
- 4.4.3.30. restic
- 4.4.3.31. monitoring (Grafana, Prometheus)
- 4.4.3.32. Jenkins (currently unused)
- 4.4.3.33. Emacs elfeed (RSS Server)
- 4.4.3.34. FreshRSS
- 4.4.3.35. forgejo (git server)
- 4.4.3.36. Anki Sync Server
- 4.4.3.37. kanidm
- 4.4.3.38. oauth2-proxy
- 4.4.3.39. Firefly-III
- 4.4.3.40. Koillection
- 4.4.3.41. Atuin
- 4.4.3.42. Radicale
- 4.4.3.43. croc
- 4.4.3.44. microbin
- 4.4.3.45. shlink
- 4.4.3.46. slink
- 4.4.3.47. Snipe-IT (currently unused)
- 4.4.3.48. Homebox
- 4.4.3.49. OPKSSH
- 4.4.3.50. Garage
- 4.4.3.51. Set host domain for dns
- 4.4.3.52. Set dns host entries for home servers
- 4.4.3.53. nsd (dns)
- 4.4.3.54. Minecraft
- 4.4.3.55. Mailserver
- 4.4.3.56. Attic (nix binary cache)
- 4.4.3.57. Hydra
- 4.4.3.58. Kea DHCP
- 4.4.3.59. nftables (firewall)
- 4.4.3.60. Firezone
- 4.4.3.61. Adguardhome
- 4.4.4. Darwin
- 4.4.5. TODO Optional
- 4.4.5.1. Niri
- 4.4.5.2. gaming
- 4.4.5.3. VirtualBox
- 4.4.5.4. VmWare
- 4.4.5.5. nswitch-rcm
- 4.4.5.6. Framework
- 4.4.5.7. AMD CPU
- 4.4.5.8. AMD GPU
- 4.4.5.9. Hibernation
- 4.4.5.10. work
- 4.4.5.11. Uni
- 4.4.5.12. microvm-host
- 4.4.5.13. microvm-guest
- 4.4.5.14. systemd-networkd (base)
- 4.4.5.15. systemd-networkd (server base)
- 4.4.5.16. TODO systemd-networkd (server home)
- 4.4.5.17. nix-topology node config
- 4.4.1. Common
- 4.5. Home-manager
- 4.5.1. Steps to setup/upgrade home-manager only
- 4.5.2. TODO Common
- 4.5.2.1. Imports
- 4.5.2.2. Mirror home-manager shared options (automatically active)
- 4.5.2.3. General home-manager-settings (nix)
- 4.5.2.4. nixGL
- 4.5.2.5. Installed packages
- 4.5.2.6. sops
- 4.5.2.7. Yubikey
- 4.5.2.8. SSH Machines
- 4.5.2.9. Theme (stylix)
- 4.5.2.10. Desktop Entries, MIME types (xdg)
- 4.5.2.11. Linking dotfiles (Symlinks home.file)
- 4.5.2.12. Sourcing environment variables
- 4.5.2.13. General Programs: bottom, imv, less, lesspipe, sioyek, bat, carapace, wlogout, swayr, yt-dlp, mpv, jq, nix-index, ripgrep, pandoc, fzf, zoxide, timidity
- 4.5.2.14. nix-index
- 4.5.2.15. nix-your-shell
- 4.5.2.16. password-store
- 4.5.2.17. direnv
- 4.5.2.18. eza
- 4.5.2.19. atuin
- 4.5.2.20. git
- 4.5.2.21. Fuzzel
- 4.5.2.22. Starship
- 4.5.2.23. Kitty
- 4.5.2.24. zsh
- 4.5.2.25. bash
- 4.5.2.26. zellij
- 4.5.2.27. tmux
- 4.5.2.28. Mail
- 4.5.2.29. Home-manager: Emacs
- 4.5.2.30. Waybar
- 4.5.2.31. Firefox
- 4.5.2.32. Services
- 4.5.2.32.1. gnome-keyring
- 4.5.2.32.2. KDE Connect
- 4.5.2.32.3. Mako
- 4.5.2.32.4. SwayOSD
- 4.5.2.32.5. yubikey-touch-detector
- 4.5.2.32.6. blueman-applet
- 4.5.2.32.7. network-manager-applet
- 4.5.2.32.8. obsidian service for tray
- 4.5.2.32.9. anki service for tray
- 4.5.2.32.10. element service for tray
- 4.5.2.32.11. vesktop service for tray
- 4.5.2.32.12. firezone service for tray
- 4.5.2.32.13. syncthing service for tray
- 4.5.2.32.14. TODO attic store push service
- 4.5.2.33. Sway
- 4.5.2.34. Kanshi
- 4.5.2.35. gpg-agent
- 4.5.2.36. gammastep
- 4.5.2.37. Spicetify
- 4.5.2.38. Obsidian
- 4.5.2.39. Anki
- 4.5.2.40. Element-desktop
- 4.5.2.41. Hexchat
- 4.5.2.42. obs-studio
- 4.5.2.43. spotify-player
- 4.5.2.44. vesktop
- 4.5.2.45. batsignal
- 4.5.2.46. autotiling
- 4.5.2.47. swayidle
- 4.5.2.48. swaylock
- 4.5.2.49. opkssh
- 4.5.3. Server
- 4.5.4. Darwin
- 4.5.5. Optional
- 4.6. Shared
- 4.6.1. TODO Configuration options
- 4.6.2. Variables (vars; holds firefox & stylix config parts)
- 4.6.3. Meta options (options only)
- 4.6.4. Config Library (confLib)
- 4.6.5. Packages
- 4.6.5.1. Packages (flake)
- 4.6.5.1.1. pass-fuzzel
- 4.6.5.1.2. quickpass
- 4.6.5.1.3. cura5
- 4.6.5.1.4. hm-specialisation
- 4.6.5.1.5. cdw
- 4.6.5.1.6. cdb
- 4.6.5.1.7. prstatus
- 4.6.5.1.8. bak
- 4.6.5.1.9. timer
- 4.6.5.1.10. e
- 4.6.5.1.11. command-not-found
- 4.6.5.1.12. swarselcheck
- 4.6.5.1.13. swarselcheck-niri
- 4.6.5.1.14. swarselzellij
- 4.6.5.1.15. waybarupdate
- 4.6.5.1.16. opacitytoggle
- 4.6.5.1.17. fs-diff
- 4.6.5.1.18. github-notifications
- 4.6.5.1.19. kanshare
- 4.6.5.1.20. swarsel-bootstrap
- 4.6.5.1.21. swarsel-rebuild
- 4.6.5.1.22. swarsel-install
- 4.6.5.1.23. swarsel-postinstall
- 4.6.5.1.24. t2ts
- 4.6.5.1.25. ts2t
- 4.6.5.1.26. vershell
- 4.6.5.1.27. eontimer
- 4.6.5.1.28. project
- 4.6.5.1.29. fhs
- 4.6.5.1.30. swarsel-displaypower
- 4.6.5.1.31. swarsel-mgba
- 4.6.5.1.32. swarsel-deploy
- 4.6.5.1.33. swarsel-build
- 4.6.5.1.34. swarsel-instantiate
- 4.6.5.1.35. sshrm
- 4.6.5.1.36. endme
- 4.6.5.1.37. git-replace
- 4.6.5.1.38. gen-sops-guest
- 4.6.5.2. Packages (config)
- 4.6.5.1. Packages (flake)
- 4.7. Profiles
- 5. Emacs
- 5.1. Initialization (early-init.el)
- 5.2. Personal settings
- 5.2.1. Custom functions
- 5.2.1.1. Emacs/Evil state toggle
- 5.2.1.2. Switching to last used buffer
- 5.2.1.3. mu4e functions
- 5.2.1.4. Create non-existant directories when finding file
- 5.2.1.5. [crux] Duplicate Lines
- 5.2.1.6. [prot] org-id-headings
- 5.2.1.7. Inhibit Messages in Echo Area
- 5.2.1.8. Move up one directory for find-file
- 5.2.1.9. Magit: List directories using vertico/consult
- 5.2.1.10. org-mode: General setup
- 5.2.1.11. org-mode: Visual-fill column
- 5.2.1.12. org-mode: Upon-save actions (Auto-tangle, export to html, formatting)
- 5.2.1.13. org-mode: Fold current heading
- 5.2.1.14. corfu: Do not interrupt navigation
- 5.2.1.15. Disable garbage collection while minibuffer is active
- 5.2.1.16. Insert link to another header in org file
- 5.2.2. Custom Keybindings
- 5.2.3. Directory setup / File structure
- 5.2.4. Unclutter .emacs.d
- 5.2.5. Move backup files to another location
- 5.2.1. Custom functions
- 5.3. General init.el setup + UI
- 5.3.1. General setup
- 5.3.2. Mark all themes as safe
- 5.3.3. Show less compilation warnings
- 5.3.4. Better garbage collection
- 5.3.5. Indentation
- 5.3.6. Scrolling
- 5.3.7. Evil
- 5.3.8. ispell
- 5.3.9. Font Configuration
- 5.3.10. Theme
- 5.3.11. Icons
- 5.3.12. Variable Pitch Mode
- 5.3.13. Modeline
- 5.3.14. mini-modeline
- 5.3.15. Helper Modes
- 5.3.16. Ligatures
- 5.3.17. Popup (popper) + Shackle Buffers
- 5.3.18. Indicate first and last line of buffer
- 5.3.19. Authentication
- 5.4. Modules
- 5.4.1. Org Mode
- 5.4.1.1. General org-mode
- 5.4.1.2. org-appear
- 5.4.1.3. Centered org-mode Buffers
- 5.4.1.4. Fix headings not folding sometimes
- 5.4.1.5. Babel
- 5.4.1.6. aucTex
- 5.4.1.7. org-download
- 5.4.1.8. org-fragtog
- 5.4.1.9. org-modern
- 5.4.1.10. Presentations
- 5.4.1.11. Render markdown blocks as body to expand noweb blocks
- 5.4.2. Nix Mode
- 5.4.3. HCL Mode
- 5.4.4. Jenkinsfile/Groovy
- 5.4.5. Ansible
- 5.4.6. Dockerfile
- 5.4.7. Terraform Mode
- 5.4.8. nix formatting
- 5.4.9. shfmt
- 5.4.10. Markdown Mode
- 5.4.11. elfeed
- 5.4.12. Ripgrep
- 5.4.13. Tree-sitter
- 5.4.14. direnv (envrc)
- 5.4.15. avy
- 5.4.16. devdocs
- 5.4.17. Projectile
- 5.4.18. Magit
- 5.4.19. Yubikey support
- 5.4.20. Forge
- 5.4.21. git-timemachine
- 5.4.22. Delimiters (brackets): rainbow-delimiters, highlight-parentheses
- 5.4.23. rainbow-mode
- 5.4.24. Corfu
- 5.4.25. cape
- 5.4.26. rust
- 5.4.27. Tramp
- 5.4.28. diff-hl
- 5.4.29. Commenting
- 5.4.30. eglot
- 5.4.31. lsp-mode & company
- 5.4.32. lsp-mode in org-src blocks
- 5.4.33. lsp-bridge
- 5.4.34. sideline-flymake
- 5.4.35. Prevent breaking of hardlinks
- 5.4.36. Dirvish
- 5.4.37. undo-tree
- 5.4.38. Hydra
- 5.4.39. Email
- 5.4.40. Calendar
- 5.4.41. Dashboard: emacs startup screen
- 5.4.42. vterm
- 5.4.43. multiple cursors
- 5.4.44. Less logging
- 5.4.45. Popup frames
- 5.4.1. Org Mode
- 6. Appendix A: Noweb-Ref blocks
- 7. Appendix B: Supplementary Files
- 7.1. Server Emacs config
- 7.2. tridactylrc
- 7.3. tridactyl theme
- 7.4. Waybar style.css
- 7.5. Doc Page style.css
- 7.6. justfile
- 7.7. aspell.conf
- 7.8. nix-plugins.patch
- 7.9. Zellij layout swarsel.kdl.nix
- 7.10. Zellij config.kdl.nix
- 7.11. Vieb config
- 7.12. swayidle
- 7.13. stylix color scheme
- 7.14. .gitmessage
- 7.15. userChrome.css
- 7.16. Default Flake Template
- 7.17. C++ Flake Template
- 7.18. Go Flake Template
- 7.19. LaTeX Flake Template
- 7.20. Python Flake Template
- 7.21. Rust Flake Template
- 7.22. GitHub Readme
- 7.23. GitHub Workflow: Build-and-deploy
- 7.24. GitHub Workflow: Flake check
- 7.25. Private topology flake
- 7.26. Public topology flake
- 8. Appendix C: Explanations to nix functions and operators
- 8.1. Concepts
- 8.2. Builtin functions
- 8.3. Builtin functions exported to nixpkgs
- 8.3.1. nixpkgs.lib.listToAttrs
- 8.3.2. nixpkgs.lib.attrNames
- 8.3.3. nixpkgs.lib.map
- 8.3.4. nixpkgs.lib.filter
- 8.3.5. nixpkgs.lib.concatLists
- 8.3.6. nixpkgs.lib.genList
- 8.3.7. nixpkgs.lib.concatStringsSep
- 8.3.8. nixpkgs.lib.split
- 8.3.9. nixpkgs.lib.tail
- 8.3.10. nixpkgs.lib.head
- 8.3.11. nixpkgs.lib.isString
- 8.3.12. nixpkgs.lib.length
- 8.3.13. nixpkgs.lib.stringLength
- 8.3.14. nixpkgs.lib.subString
- 8.4. Functions in nixpgks
This file has 145814 words spanning 37646 lines and was last revised on 2026-01-11 01:05:02 +0000.
1. Introduction (no code)
This literate configuration file holds the entirety of all configuration for both NixOS PCs and servers as well as home-manager only systems across all machines that I currently use. It also holds an extensive Emacs configuration. I use this project to manage my entire home + cloud infrastructure
This configuration is part of a NixOS system that is for the most part fully declarative (execpt for the steps outlined in Manual steps when setting up a new machine) and can be found here:
The literate configuration approach lets me explain my choices to my future self as well as you, the reader. I go to great lengths to explain the choices for all design steps that I take in order for me to pay due diligence in crafting my setup, and not simply copying big chunks of other peoples code. Also, this is very convenient to me as I only need to keep of (ideally) a single file to manage all of my configuration. I hope that this documentation will make it easier for beginners to get into NixOS (and, to some extent, Emacs) as I know it can be a struggle in the beginning.
1.1. What I achieve with this project
(click to enlarge)
This project manages my entire IT infrastructure. In particular:
- A mailserver (Eagleland (Hetzner))
- My home router (Hintbooth (Router: HUNSN RM02)) and its MicroVMs
- Two homeservers (Summers (Server: ASUS Z10PA-D8), Winters (Server: ASRock J4105-ITX) and their respective MicroVMs) and one cloud server (Moonside (OCI)) that are using other services defined in Services
- Two servers (the cloud host Twothreetunnel (OCI) and one microvm Nginx hosted on Hintbooth (Router: HUNSN RM02)) that proxy requests to those services
- A NixOS hydra buildfarm (Belchsfactory (OCI)) with binary caching that helps me build derivations faster and cache them for reuse
- An authoritative DNS server (Stoicclub (OCI)) that pushes records to both Hetzner and Hurricane Electric DNS
- An SSH bastion (Liliputsteps (OCI)) that gatekeeps access to all cloud hosts
- My work laptop (pyramid (Framework Laptop 16)) and my personal laptop (Bakery (Lenovo ThinkPad))
- My work workstation (Treehouse (DGX Spark))
My phone (Magicant (Phone))
This is a system that grew organically over 1506 days and has reached considerable complexity at this point. This documents exists to try and make it understandable to other people as well.
1.2. How to use this document
When I started out with nix, it was a painful time. For a beginner, the available resources tend to be too detailed or assume too much prior knowledge. Also, it seems that using nix requires the user to understand it pretty well before many things start to make sense.
That is the reason why I keep this configuration as a literate one: so that I am able to explain how everything works as best as I can. In the start, it was my goal to keep this project simple, so that it would be easy to understand when seen by a beginner. However, over time I have implemented more and more complicated solutions. Still, I try to keep the prosaic descriptions sufficient.
For a beginner, I recommend to read this file like a book, from start to finish. I will try to explain concepts whenever they first come up, and will regularly link to Appendix C: Explanations to nix functions and operators when more context is needed. For the first few times that I am using a new function, I will place such a link again. However, to keep the writing of this file manageable, I will generally only do this no more than three times.
This page offers some utilities to you: you can pin specific headings to the right "pinned" bar by hovering over the heading and clicking [pin]. If a section seems uninteresting to you, you can press the ↓ button to skip to the next one. And if you want to send a section to somebody else, you can click the # in order to copy its link to the clipboard. Your pinned headings will be saved locally, so you can continue reading in case you take a break.
1.3. Structure of this file
Now, I will outline how this document is structured. I have segmented this file into the following sections:
- Introduction (no code) This is the block you are currently in. It holds no code that actually builds the system, it just outlines the general approach and explains the rough design mentality. For understanding the nix (or Emacs) code in here, reading this should not be necessary if you already know some nix (feel free to skip to flake.nix). Otherwise, I will also give a brief introduction to some nix terms here.
- An introduction to nix Here I will give a coarse overview over some important concepts in the nix landscape. This is aimed at beginners in the field; others might want to skip this section as they are likely to not find much that will be worth their time.
- flake.nix
This block holds everything related to the heart of the nix side of the configuration - the
flake.nixfile. I am using flake-parts to manage this flake, so different aspects of the configuration are handled by flake-part modules in different files. - System This section holds all configuration options that apply to NixOS or home-manager. In other words, here we are doing system and user level configuration. In a way, I consider this the most important part of this file, as (nearly) all of the nix magic is going to happen here.
- Emacs
This section defines my Emacs configuration. For a while, I considered to use rycee's
emacs-initmodule (https://github.com/nix-community/nur-combined/blob/master/repos/rycee/hm-modules/emacs-init.nix) to manage my Emacs configuration; I have since come to the conclusion that this would be a bad idea: at the moment, even though it might seem as I am very bound to the configuration file that you are currently reading, if I ever decide to change how I run my system, I can simply take the generated.nixand.elfiles and put them wherever I need them. This file only simplifies that generation without putting further restrictions on my. If I were however to switch toemacs-initthen I would be indeed to some level confined to the nix ecosystem with my Emacs configuration, as I would no longer have a valid.orgfile to manage it with, instead generating aninit.eldirectly from nix code. I like to keep that level of freedom for potential future use. Also, you will notice there is no package system setup in this configuration. This is because packages are automatically handled on the NixOS side by parsing the generatedinit.elfile for package installs.
nix build My emacs is built using the emacs-overlay nix flake, which builds a bleeding edge emacs on wayland (pgtk) with utilities like treesitter support. By executing the below source block, the current build setting can be updated at any time, and you can see my most up-to-date build options (last updated: 2026-01-11 01:05:02 +0000)
system-configuration-options
--prefix=/nix/store/lymgpfqr5dp1wc0khbcbhhjnxq8ccsy9-emacs-pgtk-20240521.0 --disable-build-details --with-modules --with-pgtk --with-compress-install --with-toolkit-scroll-bars --with-native-compilation --without-imagemagick --with-mailutils --without-small-ja-dic --with-tree-sitter --without-xinput2 --with-xwidgets --with-dbus --with-selinux
This file is not loaded by Emacs directly as the configuration (even though this would be possible - actually I did that in the past!) - instead, it generates two more files:
early-init.elThis file handle startup optimization and sets up the basic frame that I will be working in.init.elThis file handles the rest of the Emacs configuration.
By using the configuration offered by this file, the file you are reading right now (SwarselSystems.org) will not be freshly tangled on every file save, as this slows down emacs over time. However, when you clone this configuration yourself and have not yet activated it, you need to tangle the file yourself. This can be done using the general keybind C-c C-v t or my personal chord C-SPC o t. Alternatively, execute the following block:
(org-babel-tangle)
This section holds code that can be templated by other parts of the configuration.
These blocks were used in several places throughout the configurations, but not on all machines necessarily. For example, the theming section used need to be in a NixOS block on NixOS machines but in a home-manager block on non-NixOS.
Originally, I used this method a lot throughout my configuration. However, as my knowledge of NixOS grew, I have been weeding these snippets out more and more as I find more efficient native solutions. Now, only the theming block remains. For example, the above problem can be solved by defining a theme attribute set and using lib.recursiveUpdate as shown in Shared Configuration Options (holds firefox & stylix config parts) and Theme (stylix). Nowadays I use this mostly for some meta information in this file, e.g. for definining the Programs, Services and Hosts blocks only once. I also use it to merge the Manual steps when setting up a new machine.
As such, this served to reduce code duplication in this file. The tangled files experienced no size reduction, since noweb-ref only substitutes these blocks in.
An example of using a noweb-ref block:
First we define the block:
enable = true;
which can then be used in a block like:
<<blockName>>
and is finally parsed as:
enable = true;
Note that noweb-reffed blocks will not always be indented correctly. You will want to account for that when checking your nix flake with the formatter of your choice. Personally, I have solved this issue using the functions defined in org-mode: Upon-save actions (Auto-tangle, export to html, formatting). Originally, I also automatically exported to html there, but it incurred a too high memory penalty which made Emacs become sluggish over time - instead I now build the website version whenever I push to GitHub.
- Appendix B: Supplementary Files This section holds files that are not written in nix but are still referenced in the configuration in some way. This is mostly used for configuration of programs that have no native nix support, like tridactyl, as well as "meta" files like the GitHub Readme or the GitHub Workflow: Build-and-deploy. Note that shell scripts are still defined under their respective entry in Packages. Over time, the goal is to reduce this section to a minimum, but things like the aforementioned tridactyl might stay for a long time, until we have a stable nix interface to configure browser plugins.
- Appendix C: Explanations to nix functions and operators When I started to learn about nix, I found that journey quite arduous; while I disagree with the general public in that the documentation is too sparse, I will say that, while it is very good, reading (and understanding!) it requires a certain level of existing nix knowledge that one will problably not have when starging out. Hence, the goal of this document is to explain common nix functions as they come up in this document (I thing I wrote this before :sweat:), in hopes that you will be able to understand most of the code. When a new function appears for the first time, I will try to link to an entry in the appendix.
1.3.1. HTML version of this document
I will also quickly talk about the webpage that (I hope) you are currently viewing works:
The .html version of this page is built by a GitHub workflow; it can also be generated manually by calling the chord C-SPC o e, or by executing the below block
(org-html-export-to-html)
The web version is useful because it allows navigation using org-mode links, which makes the configuration easier to follow in a number of ways:
- mobile-friendly documentation
- Functionality to save links to certain headers in a "Pinned" menu that can be toggled
- These links are locally saved
I add a javascript bit to the file in order to have a darkmode toggle when exporting to html (defined in HTML Export: Darkmode toggle). NOTE: What I am doing is defining an elisp multiline string which gives me heredoc capabilities for defining blocks that should be both exported to the final .html but should also be shown in the document itself (only putting a #+[begin/end]_export block would not show the block in the content of the page) which is then output as html and exported to both.
" <script src='https://cdn.jsdelivr.net/npm/darkmode-js@1.5.7/lib/darkmode-js.min.js'></script> <script> function addDarkmodeWidget() { new Darkmode({label:'[☀︎]'}).showWidget(); } window.addEventListener('load', addDarkmodeWidget); </script> "
I also add this javascript to add header pinning functionality to the site, using the same trick as above (this is defined in HTML Export: Docs QoL):
" <script> (function() { function ready(fn) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', fn); } else { fn(); } } ready(function initPinned() { const STORAGE_KEY = 'org-pinned-items-v2'; let pinnedPanel = document.getElementById('pinned-panel'); if (!pinnedPanel) { pinnedPanel = document.createElement('aside'); pinnedPanel.id = 'pinned-panel'; pinnedPanel.innerHTML = ` <div id='pinned-panel-header'> <h2>Pinned</h2> <button id='toggle-pinned-btn' type='button' title='Hide pinned panel'>✕</button> </div> <button id='clear-all-pins-btn' type='button'>Clear All</button> <ul id='pinned-list'></ul> `; document.body.appendChild(pinnedPanel); } let showBtn = document.getElementById('show-pinned-btn'); if (!showBtn) { showBtn = document.createElement('button'); showBtn.id = 'show-pinned-btn'; showBtn.type = 'button'; showBtn.textContent = 'Pinned'; document.body.appendChild(showBtn); } const content = document.getElementById('content'); const pinnedList = document.getElementById('pinned-list'); const toggleBtn = document.getElementById('toggle-pinned-btn'); const clearAllBtn = document.getElementById('clear-all-pins-btn'); const toc = document.getElementById('table-of-contents'); const body = document.body; if (!content || !pinnedList || !toggleBtn || !clearAllBtn || !toc) return; function injectSearch() { // Check if already injected if (document.getElementById('toc-search-input')) return; const searchContainer = document.createElement('div'); searchContainer.id = 'toc-search-container'; const searchInput = document.createElement('input'); searchInput.id = 'toc-search-input'; searchInput.type = 'text'; searchInput.placeholder = 'Search TOC...'; searchInput.autocomplete = 'off'; const clearBtn = document.createElement('button'); clearBtn.id = 'toc-search-clear'; clearBtn.type = 'button'; clearBtn.textContent = 'Clear'; searchContainer.appendChild(searchInput); searchContainer.appendChild(clearBtn); toc.insertBefore(searchContainer, toc.firstChild); function filterTOC(term) { const allLinks = toc.querySelectorAll('a'); allLinks.forEach(link => { const li = link.closest('li'); if (!li) return; const text = link.textContent.toLowerCase(); const matches = text.includes(term); if (matches) { li.classList.remove('hidden-by-search'); let parent = li.parentElement; while (parent && parent !== toc) { if (parent.tagName === 'UL') { parent.style.display = ''; } if (parent.tagName === 'LI') { parent.classList.remove('hidden-by-search'); } parent = parent.parentElement; } } else { li.classList.add('hidden-by-search'); } }); if (term === '') { const allLis = toc.querySelectorAll('li'); allLis.forEach(li => li.classList.remove('hidden-by-search')); } } searchInput.addEventListener('input', function(e) { const term = e.target.value.toLowerCase(); filterTOC(term); }); clearBtn.addEventListener('click', function() { searchInput.value = ''; filterTOC(''); searchInput.focus(); }); } injectSearch(); function addHeadingLinks() { const headers = content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9'); headers.forEach(header => { const id = header.getAttribute('id'); if (!id) return; if (header.querySelector('.heading-link')) return; const link = document.createElement('a'); link.className = 'heading-link'; link.href = '#' + id; link.textContent = '#'; link.title = 'Copy link to this heading'; const pinBtn = header.querySelector('.toc-pin-btn'); if (pinBtn) { header.insertBefore(link, pinBtn); } else { header.appendChild(link); } link.addEventListener('click', function(e) { e.preventDefault(); const url = window.location.origin + window.location.pathname + '#' + id; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(url) .then(() => { const originalText = link.textContent; link.textContent = '✓'; setTimeout(() => { link.textContent = originalText; }, 1000); }) .catch(err => { console.warn('Failed to copy to clipboard', err); window.location.hash = id; }); } else { window.location.hash = id; } }); }); } addHeadingLinks(); function addNextHeadingButtons() { const headers = Array.from(content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9')); headers.forEach((header, index) => { // Skip if button already exists if (header.querySelector('.heading-next')) return; // Find next heading const nextHeader = headers[index + 1]; if (!nextHeader) return; // No next heading const nextId = nextHeader.getAttribute('id'); if (!nextId) return; const nextBtn = document.createElement('button'); nextBtn.className = 'heading-next'; nextBtn.type = 'button'; nextBtn.textContent = '↓'; nextBtn.title = 'Jump to next heading'; // Insert after the heading link, before the pin button const headingLink = header.querySelector('.heading-link'); const pinBtn = header.querySelector('.toc-pin-btn'); if (pinBtn) { header.insertBefore(nextBtn, pinBtn); } else if (headingLink) { headingLink.after(nextBtn); } else { header.appendChild(nextBtn); } nextBtn.addEventListener('click', function(e) { e.preventDefault(); nextHeader.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Update URL hash history.pushState(null, null, '#' + nextId); }); }); } addNextHeadingButtons(); let mobileTocBtn = document.getElementById('mobile-toc-toggle'); if (!mobileTocBtn) { mobileTocBtn = document.createElement('button'); mobileTocBtn.id = 'mobile-toc-toggle'; mobileTocBtn.type = 'button'; mobileTocBtn.textContent = 'TOC'; document.body.appendChild(mobileTocBtn); } let mobilePinnedBtn = document.getElementById('mobile-pinned-toggle'); if (!mobilePinnedBtn) { mobilePinnedBtn = document.createElement('button'); mobilePinnedBtn.id = 'mobile-pinned-toggle'; mobilePinnedBtn.type = 'button'; mobilePinnedBtn.textContent = 'Pinned'; document.body.appendChild(mobilePinnedBtn); } function anyMobilePanelOpen() { return toc.classList.contains('mobile-visible') || pinnedPanel.classList.contains('mobile-visible'); } function updateBodyMobilePanelState() { if (anyMobilePanelOpen()) body.classList.add('mobile-panel-open'); else body.classList.remove('mobile-panel-open'); } document.addEventListener('click', function(e) { if (window.innerWidth > 1000) return; if (!anyMobilePanelOpen()) return; const clickedInsideToc = toc.contains(e.target); const clickedInsidePinned = pinnedPanel.contains(e.target); const clickedTocBtn = mobileTocBtn.contains(e.target); const clickedPinnedBtn = mobilePinnedBtn.contains(e.target); const clickedInteractive = e.target.tagName === 'A' || e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.closest('a') || e.target.closest('button'); if (!clickedInsideToc && !clickedInsidePinned && !clickedTocBtn && !clickedPinnedBtn && !clickedInteractive) { toc.classList.remove('mobile-visible'); pinnedPanel.classList.remove('mobile-visible'); updateBodyMobilePanelState(); } }); mobileTocBtn.addEventListener('click', function() { const isOpen = toc.classList.toggle('mobile-visible'); if (isOpen) { pinnedPanel.classList.remove('mobile-visible'); } updateBodyMobilePanelState(); }); mobilePinnedBtn.addEventListener('click', function() { const isOpen = pinnedPanel.classList.toggle('mobile-visible'); if (isOpen) { toc.classList.remove('mobile-visible'); } updateBodyMobilePanelState(); }); const pinnedItems = new Map(); let initiallyPinnedHrefs = new Set(); function loadFromStorage() { try { const raw = window.localStorage && localStorage.getItem(STORAGE_KEY); if (!raw) return; const arr = JSON.parse(raw); if (!Array.isArray(arr)) return; initiallyPinnedHrefs = new Set(arr); } catch (e) { console.warn('Pinned: failed to load from localStorage', e); } } function saveToStorage() { try { if (!window.localStorage) return; const arr = []; pinnedItems.forEach((entry, href) => { if (entry.li) arr.push(href); }); localStorage.setItem(STORAGE_KEY, JSON.stringify(arr)); } catch (e) { console.warn('Pinned: failed to save to localStorage', e); } } function sortPinnedList() { const items = Array.from(pinnedList.children) .map(li => { const link = li.querySelector('a'); return { li: li, text: link ? link.textContent.trim() .toLowerCase() : '' }; }); items.sort((a, b) => a.text.localeCompare(b.text)); items.forEach(item => pinnedList.appendChild(item.li)); } function hidePinnedPanel() { pinnedPanel.classList.add('hidden'); content.classList.add('pinned-hidden'); showBtn.classList.add('visible'); } function showPinnedPanel() { pinnedPanel.classList.remove('hidden'); content.classList.remove('pinned-hidden'); showBtn.classList.remove('visible'); } toggleBtn.addEventListener('click', hidePinnedPanel); showBtn.addEventListener('click', showPinnedPanel); clearAllBtn.addEventListener('click', function() { if (pinnedItems.size === 0) return; const confirmed = confirm('Are you sure you want to clear all pinned items?'); if (!confirmed) return; pinnedItems.forEach((entry, href) => { if (entry.li && entry.li.parentElement) { entry.li.parentElement.removeChild(entry.li); } entry.li = null; entry.btns.forEach(b => b.textContent = '[pin]'); }); saveToStorage(); }); function attachPinBehavior(pinBtn, href, text) { if (!href) return; if (!pinnedItems.has(href)) { pinnedItems.set(href, { li: null, btns: new Set(), text: text }); } const entry = pinnedItems.get(href); entry.btns.add(pinBtn); pinBtn.textContent = entry.li ? '[unpin]' : '[pin]'; pinBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); const current = pinnedItems.get(href); if (!current) return; if (current.li) { if (current.li.parentElement) { current.li.parentElement.removeChild(current.li); } current.li = null; current.btns.forEach(b => b.textContent = '[pin]'); saveToStorage(); } else { const li = document.createElement('li'); const a = document.createElement('a'); a.href = href; a.textContent = current.text; const removeBtn = document.createElement('button'); removeBtn.className = 'pin-remove'; removeBtn.type = 'button'; removeBtn.textContent = '✕'; removeBtn.addEventListener('click', () => { const cur = pinnedItems.get(href); if (!cur) return; if (cur.li && cur.li.parentElement) { cur.li.parentElement.removeChild(cur.li); } cur.li = null; cur.btns.forEach(b => b.textContent = '[pin]'); saveToStorage(); }); li.appendChild(a); li.appendChild(removeBtn); pinnedList.appendChild(li); current.li = li; current.btns.forEach(b => b.textContent = '[unpin]'); sortPinnedList(); saveToStorage(); } }); } loadFromStorage(); const tocLinks = document.querySelectorAll('#text-table-of-contents a'); tocLinks.forEach(link => { if (link.parentElement && link.parentElement.classList.contains('toc-entry')) { return; } const li = link.closest('li'); if (!li) return; const wrapper = document.createElement('span'); wrapper.className = 'toc-entry'; li.insertBefore(wrapper, link); wrapper.appendChild(link); const pinBtn = document.createElement('button'); pinBtn.className = 'toc-pin-btn'; pinBtn.type = 'button'; pinBtn.textContent = '[pin]'; wrapper.appendChild(pinBtn); const href = link.getAttribute('href'); const text = link.textContent.trim(); attachPinBehavior(pinBtn, href, text); }); const headers = content.querySelectorAll('h2, h3, h4, h5, h6, h7, h8, h9'); headers.forEach(header => { const id = header.getAttribute('id'); if (!id) return; if (header.querySelector('.toc-pin-btn')) return; const href = '#' + id; const text = header.textContent.trim(); const pinBtn = document.createElement('button'); pinBtn.className = 'toc-pin-btn'; pinBtn.type = 'button'; pinBtn.textContent = '[pin]'; pinBtn.style.marginLeft = '0.8rem'; pinBtn.style.fontSize = '0.75em'; header.appendChild(pinBtn); attachPinBehavior(pinBtn, href, text); }); initiallyPinnedHrefs.forEach(href => { const entry = pinnedItems.get(href); if (!entry) return; const li = document.createElement('li'); const a = document.createElement('a'); a.href = href; a.textContent = entry.text; const removeBtn = document.createElement('button'); removeBtn.className = 'pin-remove'; removeBtn.type = 'button'; removeBtn.textContent = '✕'; removeBtn.addEventListener('click', () => { const cur = pinnedItems.get(href); if (!cur) return; if (cur.li && cur.li.parentElement) { cur.li.parentElement.removeChild(cur.li); } cur.li = null; cur.btns.forEach(b => b.textContent = '[pin]'); saveToStorage(); }); li.appendChild(a); li.appendChild(removeBtn); pinnedList.appendChild(li); entry.li = li; entry.btns.forEach(b => b.textContent = '[unpin]'); }); sortPinnedList(); saveToStorage(); }); })(); </script> "
1.4. TODO Structure of this flake
The structure of this flake as seen many revisions, however lately I have settled on a system that I have grown to like:
hosts: This folder is used to house all configurations that are used across the infrastructure. At the top level, it splits into the subfoldersnixos,home,darwin, andandroid. These folders specify the mode that the configuration is running in:- nixos: Full NixOS host (may or may not also use home-manager)
- darwin: Host that uses NixOS on MacOS (may or may not use home-manager)
- home: Host that uses only home-manager (no full NixOS)
- android: Phone using nix-on-droid (may or may not use home-manager)
The corresponding configurations are automatically generated by mkFullHostConfigs and mkHalfHostConfigs. A "full" host either in the nixos or darwin folder, while a "half" host is in either of home or android. This has to do with the scheme in which these configurations are generated.
These <hosttype> folders hold on the first level a folder describing the machine archetype (x86_64-linux or aarch64-linux for linux, x86_64-darwin or aarch64-darwin for macs). Those folders then hold a number of <hostname> folders, the actual configurations. At this time, the files stored in this folder are:
- default.nix: This file holds the abstracted configuration of the host. This should mostly be enabling Profiles as well as setting some Configuration options.
- hardware-config.nix: It is not clearly defined what I hold in this file. Mostly it is just the attributes that nix originally sets when setting up the system for the first time (although at this time modified by me!), bar any filesystem configuration. This makes my deployment in swarsel-bootstrap a little bit simpler.
- disk-config.nix Holds the aforementioned filesystem configuration and is applied using disko.
- The hosts/<hosttype>/<hostname> folders may also have a
secretsfolder, under which files of the ending.nix.encmay be stored. As the name suggests, these files should be encrypted. Specifically, they need to be sops-encrypted files (sops does not seem to suggest a file ending other than .yml or others, which is not verbose enough for me, so I went with.enc). This should have the structure of a nix expression, e.g.:
{
my_value = 2;
my_attrSet = {
enable = true;
};
}
It is also possible to pass it as a function:
{ config }: {
my_value = 2;
my_attrSet = {
enable = config.myconfig.enable;
};
}
Using the mechanisms in PII management (which in turn uses extra-builtins and sops-decrypt-and-cache), these files are decrypted during evaluation time and stored under a persistent directory. As the name suggests, I am using these files to store personally identifiable information - these "secrets" are stored world-readable in the nix store. As such, this should not be used to store important secrets, but rather information that you would not like everyone on the internet to easily find in your git repo.
Other than that, the secrets folder will also be used to store conventional (decryted at activation-time) sops-encrypted secrets in the standard .yaml / .toml / .ini formats.
modulesThis folder holds the most part of the actual system configuration done in this repository. At some point I thought it was cool to have my whole configuration exposed under the flakesnixosModules, which is indeed achieved (its usefulness is however debatable). In any way, this folder splits up as:- nixos: Holds true NixOS configuration
- home: Holds configuration to be used by home-manager (either as a NixOS submodule or not)
- shared: This is for configuraion bits that are to be used by both types.
The nixos and home folders further split up:
- common: Configuration that can be used by all hosts (TODO: this currently includes configuration used by my user devices, which will mostly not be used by servers)
- server: Configuration to be used on servers
- darwin: Holds configuration for nix-darwin.
optional: Configuration that will be used rather rarely
This structure is very optionated and highly subjective. I will possibly change this in the future.
By themselves, most of the files in the modules folder will not do anything. In order for them to do something, their corresponding
config.swarselmodulesattribute needs to be enabled. This is partly done using…profiles: This folder splits up intohomeandnixossubfolders, where groupings of module enablers are stored for the respective home and nix setups. Note thathomeprofiles are also used in NixOS setups (extensively even)! This is used to quickly enable common configuration for a machine use, e.g. the Server profile.nix: This special folder holds mostly.nixfiles that are not automatically loaded, but rather setup specific things that affect most of the flake. For example, here lies the aforementioned extra-builtins as well as the setup for the Globals system. Also in here are the flake-parts files that you read about earlier. This gives the following functionality:lib: I define some utility functions that I add to the nixpkgs library under theswarselsystemsattribute set. An example would be themkIfElsefunction.checks: As part of a Devshell (checks), I declare pre-commit hooks that should run before I push changes to my repo.overlays: Here we also define the main (default) overlay I am using in my configuration. It is responsible for adding my defined packages and modifications to the final nixpkgs. Also I add some other conveniences like all past stable nixpkgs and some other package sets.apps: I also define Apps, which is an output of derivations that can be called bynix runwithout having the flake locally - this is mostly used for myswarsel-*utilities.topology: I also created a diagram of my infrastructure using nix-topology. While I do not update this too often, this (I think) can quickly give a good overview of the scope of this flake as well as its services.
pkgs: This folder holds derivations (mostly packages) that I define myself. This is mostly used to grab versions that are not (yet) in nixpkgs, or modified versions of another package. Each derivation in this folder is in turn in its own folder which holds a defautlt.nix. Using the mechanism in Packages, these are automatically built and available to all configurations (packages still need to be installed e.g. inenvironment.systemPackages). Note that the folder at the top level splits up inconfigandflakesubdirectories:- The
configdir is used for packages that need the actual config of the machine where they run in order to be built. These packages cannot simply be released as a flake output (or better, it would not make a lot of sense). Instead, these are added within the configuration as an overlay - The
flakedir is used for the conventional packages that I described above.
- The
files: This is kind of a catchall folder that holds (nearly) all non-nix files. It mostly holds blocks created in Appendix B: Supplementary Files, but also some more specific directories:scripts: This folder holds a bunch of shell scripts that I use for various tasks. Nearly all of these are made into a derivation usingpkgs.writeShellApplication. In the future (TODO?), I might convert these to native nix, but in the past I kept the as true shellfiles in case I ever wanted to move away from nix. This is becoming less and less likely, however. And even in case that this would happen, I could retrieve these files from the nix store and would simply have to remove the nix store paths.wallpaper: Holds my wallpapers and profile pictures :)topology-images: Holds pictures used by Topology :)
secrets: Unlike the similar folder underhosts, this folder holds sops-encrypted secrets and PIIs that are used by a number of hosts that is greater than one.install: This folder holds another Installer flake. That flake pulls in thenixosConfigurationsMinimalthat are defined in Hosts of the main flake, which enables me to build an extemely reduced configuration when I deploy a new host for the first time - this is used by swarsel-bootstrap in the first installation step. It also holds the configuration of the two installer images that I use to deploy this flake:- Drugstore (ISO installer config): This is the general installer ISO that I use whenever I can when I want to deploy a new host. It has a few conveniences like some of my utility programs for figuring out some dependencies or network quirks, as well as my public ssh keys so that I can immediately login to them.
- Brick Road (kexec image): This is a kexec tarball that can be used by swarsel-bootstrap in case that I need to deploy to a machine that has less than 1GB of RAM. It is basically just an even more stripped down version of the detault one used by nixos-anywhere, but notably I added cryptsetup so that it can be used when setting up an encrypted device using disko.
.github: Canonically, this holds github related files like the GitHub Readme and some workflows.
1.5. Hosts
Here I give a brief overview over the host machines that I am using. This is held in markdown so that I can render it into my GitHub Readme without further effort.
| Name | Hardware | Use | |---------------------|-----------------------------------------------------|-----------------------------------------------------------------| |💻 **pyramid** | Framework Laptop 16, AMD 7940HS, RX 7700S, 64GB RAM | Work laptop | |💻 **bakery** | Lenovo Ideapad 720S-13IKB | Personal laptop | |💻 **machpizza** | MacBook Pro 2016 | MacOS reference and build sandbox | |🏠 **treehouse** | NVIDIA DGX Spark | AI Workstation, remote builder, hm-only-reference | |🖥️ **summers** | ASUS Z10PA-D8, 2* Intel Xeon E5-2650 v4, 128GB RAM | Homeserver (microvms), remote builder, data storage | |🖥️ **winters** | ASRock J4105-ITX, 32GB RAM | Homeserver (IoT server in spe) | |🖥️ **hintbooth** | HUNSN RM02, 8GB RAM | Router, DNS Resolver, home NGINX endpoint | |☁️ **stoicclub** | Cloud Server: 1 vCPUs, 8GB RAM | Authoritative DNS server | |☁️ **liliputsteps** | Cloud Server: 1 vCPUs, 8GB RAM | SSH bastion | |☁️ **twothreetunnel**| Cloud Server: 2 vCPUs, 8GB RAM | Service proxy | |☁️ **eagleland** | Cloud Server: 2 vCPUs, 8GB RAM | Mailserver | |☁️ **moonside** | Cloud Server: 4 vCPUs, 24GB RAM | Game servers, syncthing + other lightweight services | |☁️ **belchsfactory** | Cloud Server: 4 vCPUs, 24GB RAM | Hydra builder and nix binary cache | |🪟 **chaostheater** | Asus Z97-A, i7-4790k, GTX970, 32GB RAM | Home Game Streaming Server (Windows/AtlasOS, not nix-managed) | |📱 **magicant** | Samsung Galaxy Z Flip 6 | Phone | |💿 **drugstore** | - | NixOS-installer ISO for bootstrapping new hosts | |💿 **brickroad** | - | Kexec tarball for bootstrapping low-memory machines | |❔ **hotel** | - | Demo config for checking out this configuration | |❔ **toto** | - | Helper configuration for testing purposes |
1.6. Programs
This is meant to give a brief overview over the main programs/components that I use on a daily basis on my client machines. This should be mostly useful for people wanting to rice their config, or people who believed this repos title and are looking for .dotfiles :p
| Topic | Program | |---------------|-----------------------------------------------------------------------------------------------------------------------------| |🐚 **Shell** | [zsh](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/zsh.nix) | |🚪 **DM** | [greetd](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/common/login.nix) | |🪟 **WM** | [SwayFX](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/sway.nix) | |⛩️ **Bar** | [Waybar](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/waybar.nix) | |✒️ **Editor** | [Emacs](https://github.com/Swarsel/.dotfiles/tree/main/files/emacs/init.el) | |🖥️ **Terminal**| [Kitty](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/kitty.nix) | |🚀 **Launcher**| [Fuzzel](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/fuzzel.nix) | |🚨 **Alerts** | [Mako](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/mako.nix) | |🌐 **Browser** | [Firefox](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/zsh.nix) | |🎨 **Theme** | [City-Lights (managed by stylix)](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/sharedsetup.nix) |
1.7. Services
This is a comprehensive list of the services/components ran by my server machines.
| Topic | Program | |------------------------------|----------------------------------------------------------------------------------------------------------------| |📖 **Books** | [Kavita](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/kavita.nix) | |📼 **Videos** | [Jellyfin](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/jellyfin.nix) | |🎵 **Music** | [Navidrome](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/navidrome.nix) + [Spotifyd](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/spotifyd.nix) + [MPD](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/mpd.nix) | |🗨️ **Messaging** | [Matrix](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/matrix.nix) | |📁 **Filesharing** | [Nectcloud](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/nextcloud.nix) | |🎞️ **Photos** | [Immich](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/immich.nix) | |📄 **Documents** | [Paperless](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/paperless.nix) | |🔄 **File Sync** | [Syncthing](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/syncthing.nix) | |💾 **Backups** | [Restic](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/restic.nix) | |👁️ **Monitoring** | [Grafana](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/monitoring.nix) | |🍴 **RSS** | [FreshRss](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/freshrss.nix) | |🌳 **Git** | [Forgejo](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/forgejo.nix) | |⚓ **Anki Sync** | [Anki Sync Server](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/ankisync.nix) | |🪪 **SSO** | [Kanidm](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/kanidm.nix) + [oauth2-proxy](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/oauth2-proxy.nix) | |💸 **Finance** | [Firefly-III](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/firefly-iii.nix) | |🃏 **Collections** | [Koillection](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/koillection.nix) | |🗃️ **Shell History** | [Atuin](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/atuin.nix) | |📅 **CalDav/CardDav** | [Radicale](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/radicale.nix) | |↔️ **P2P Filesharing** | [Croc](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/croc.nix) | |✂️ **Paste Tool** | [Microbin](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/microbin.nix) | |📸 **Image Sharing** | [Slink](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/slink.nix) | |🔗 **Link Shortener** | [Shlink](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/shlink.nix) | |⛏️ **Minecraft** | [Minecraft](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/minecraft.nix) | |☁️ **S3** | [Garage](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/garage.nix) | |🕸️ **Nix Binary Cache** | [Attic](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/attic.nix) | |🐙 **Nix Build farm** | [Attic](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/hydra.nix) | |🔑 **Cert-based SSH** | [OPKSSH](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/opkssh.nix) | |🔨 **Home Asset Management**| [Homebox](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/homebox.nix) | |👀 **DNS Records** | [NSD](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/nsd.nix) | |✉️ **Mail** | [simple-nixos-mailserver](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/mailserver.nix) | |🚇 **VPN Access** | [Firezone](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/firezone.nix) | |🛡️ **Local DNS Resolver** | [AdGuard Home](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/adguardhome.nix) | |🛎️ **DHCP** | [Kea](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/kea.nix) |
2. An introduction to nix
This is where it gets interesting.
In this section, I want to give an overview over some important concepts needed when working in the nix ecosystem.
2.1. Nix, NixOS, Nixpkgs, and Nix
First off, when talking about nix, we need to differentiate between several things:
- The nix language
- The nix package manager
- The linux distribution NixOS
- the
nix-communityinput nixpkgs
While these terms are all connected with each other, it is important to keep in mind that fundamentally those are separate entities. Let us briefly talk about each one:
2.1.1. nix language
The base of all the other concepts named above is the nix language, a pure functional programming language that is declarative, dynamically typed, and evaluated lazily. That means:
- Functions are just values, that can be assigned to variables, passed as arguments to other functions, or returned by functions
- All variables are immutable (their values cannot change during computation)
- Values will only be computed when their result is needed
- There is no sequential order of operations; instead, operations get ordered they need to evaluate another expression that they depend on
- We do not need to specify a values type when declaring it
Now, I will give a brief overview over some important concepts of the nix language. This aims to only build the essential understanding needed for dealing with the configuration I am using (this should be sufficient to then understand most other configurations online). For deepening your knowledge, you might want to check out the nix reference manual.
2.1.1.1. Derivations and the nix store
The main purpose of the nix language is to build packages. These packages are built by functions that can be roughly described by something like this:
f: { source, compiler, dependencies, etc. } -> package
This function along with all its inputs we call a derivation. What is important to realize is that such a derivation can itself depend on other derivations, and the resulting package does not necessarily need to be a callable program (a more correct name for the function result is outputs). When we as nix users define a package, we will usually use a function called pkgs.stdenv.mkDerivation (ore one of its derived wrappers like buildRustPackage; for now, it is not important what these do exactly), which will run things like install phases and setup hooks for us. It is a wrapper around nix function derivation(name, system, builder, args=[],outputs=["out"]), where name is the derivation name, system is the system architecture to build for, builder is the executable responsible for building the result, args are arbitrary arguments passed to builder, and outputs, which can be thought of a list of locations where we want to put different build artifacts that can be referenced separately when using the package. There are more attributes, but these five are the most important and commonly used ones.
Building a package (in nix terms: "realising") from a derivation causes the package to be created in the nix store. This is a read-only filesystem that is usually located at /nix/store. When building a package, nix will place its content of each output at /nix/store/<hash>-<package name>-<package version>-<output name if not 'out'>/, where <hash> is a unique identifier that changes whenever one of the inputs to the above function changes. That means that it is no problem to store different versions of the same file on one system, and each program can use the correct dependencies that it needs.
A key observation that follows from this is that packages are declarative and reproducible; when not changing the inputs, a derivation will always yield the same package with the same store path.
We call the set of derivations (and, recursively, the derivations that those depend on) that a derivation depends on its closure.
Usually when installing a package, we do not need all the outputs it provides: For example, the pcscliteWithPolkit package on nixpkgs that is used for configuring smart cards provides 5 outputs: out, dev, doc, man, and lib. By default, a NixOS system will install out, man, info and doc outputs (the latter three have respective documentation.<name>.enable NixOS options, where <name> would be either of man, info, or doc) as well as any outputs listed in environment.extraOutputsToInstall (by default an empty list). However, if we need to debug something, we might need the dev output of the package, which can then be installed by referencing pcscliteWithPolkit.dev.
Also, a note that may be useful when reading package source code (for example in nixpkgs); you will often come across $out and $src. The $out represents the root of that build output ($src are the build sources). You will often see that in build phases (it is there set as an environment variable). In other places, you will instead see placeholder <output>, which will be replaced by the outputs future location in the nix store.
2.1.1.2. Types in the nix language
The nix language supports the following types and how they look in the wild:
- null:
null - bool:
true - strings:
"text"- strings can also be defined as
multiline strings- Those will look for the lowest level of indent shared over all lines and strip it:
- strings can also be defined as
swarsel-instantiate " '' indent0 indent2 indent1 '' "
indent0\n indent2\n indent1\n
(you can ignore the swarsel-instantiate; it is just a wrapper around nix-instantiate that preloads nixpkgs. I will use this to show you the evaluation results of nix calls)
- note that tab characters will not be stripped:
swarsel-instantiate " '' indentTab indent1 indent2 '' "
\tindentTab\nindent1\n indent2\n
- an URI string can assume the type of string without need for quoting (e.g. you can write
http://about.orginstead of"http://about.org") - Strings can be interpolated by using
${expression}(read on for an example)expressioncan be any valid nix expression that is compatible with the enclosing type (here: string)
- As in many languages, you have
\n,\r, and\tavailable - in normal strings, you can escape stuff using
\- the following characters need to be escaped in normal strings:
- Backslash:
\(escape using\\) - Double quote:
"(using\") - Opening of nix expression:
${(using\${)
- Backslash:
- the following characters need to be escaped in normal strings:
- in multiline strings, you escape stuff using one or two apostrophes
'- the following characters need to be escaped in multline strings:
- Two apostrophes are escaped with a single apostrophe:
''(escape with''') - All other things are escaped using
'':- Dollar sign:
$(escape with''$) \n,\r, and\t(''\nand so on)''\is a catchall for all other escaping
- Dollar sign:
- Two apostrophes are escaped with a single apostrophe:
- somewhat interestingly, double dollars
$$(as used in Makefiles) never need to be escaped
- the following characters need to be escaped in multline strings:
- ints: 1
- floats: 1.1
- paths:
/home- a path can also be given as a relative path (e.g.
./.config)
- a path can also be given as a relative path (e.g.
- attribute sets:
{ }- these hold name value pairs, e.g.
{ a = 3; } - a "chain" of attributes, separated by dots, is called an
attribute path, e.g.config.environment.systemPackages- in such a chain, all attributes but the last will be
attribute sets config.environment.systemPackages = <some list>;is equivalent toconfig = { environment = { systemPackages = <some list>; }; };
- in such a chain, all attributes but the last will be
- these hold name value pairs, e.g.
- lists: [ ]
- these hold values, e.g.
[ { a = 3; b = 2; } ]. In this example, the list holds a single value, that is, the attribute set{ a = 3; b = 2; }.
- these hold values, e.g.
- functions:
arg: bodyargcan be any of these data types, including functions- when
argis an attribute set, some special things apply:- default values can be specified using
arg ? defaultValue, e.g.{ name ? "Default", age }: "${name} is ${age} years old"will yield"X is 20 years old"when called using the attribute set{ name = "X"; age = "20"; }. Otherwise it will yield "Default is 20 years old" when called using{ age = "20"; }. When not passingagein this example, it will throw an error (also not that we had to passageas a string as the value is not cast to a string automatically. Alternatively, we could have passedage = 20;and updated the function body to"${name} is ${builtins.toString age} years old". But that is just a sidenote)
- default values can be specified using
- when
Let's see this in action:
Calling with explicit values:
swarsel-instantiate 'let f = {name ? "Default", age }: "${name} is ${age} years old"; in f { name = "X"; age = "20"; }'
X is 20 years old
Calling by using a default value with ?:
swarsel-instantiate 'let f = {name ? "Default", age }: "${name} is ${age} years old"; in f { age = "20"; }'
Default is 20 years old
Not passing age errors out:
swarsel-instantiate 'let f = {name ? "Default", age }: "${name} is ${age} years old"; in f { }'
error:
… from call site
at «string»:1:104:
1| let lib = import <nixpkgs/lib>; in let f = {name ? "Default", age }: "${name} is ${age} years old"; in f { }
| ^
error: function 'f' called without required argument 'age'
at «string»:1:44:
1| let lib = import <nixpkgs/lib>; in let f = {name ? "Default", age }: "${name} is ${age} years old"; in f { }
| ^
- the evaluator will error out if it is called with an argument that is not explicitely mentioned in the function definition. Passing a superfluous
anothererrors out:
swarsel-instantiate 'let f = {name ? "Default", age }: "${name} is ${age} years old"; in f { age = "2"; another = "0"; }'
error:
… from call site
at «string»:1:104:
1| let lib = import <nixpkgs/lib>; in let f = {name ? "Default", age }: "${name} is ${age} years old"; in f { age = "2"; another = "0"; }
| ^
error: function 'f' called with unexpected argument 'another'
at «string»:1:44:
1| let lib = import <nixpkgs/lib>; in let f = {name ? "Default", age }: "${name} is ${age} years old"; in f { age = "2"; another = "0"; }
| ^
- this behaviour can be suppressed by adding an ellipsis
...to the function arguments ({ name ? "Default", age, ... }:). Adding this ellipsis...to the function definition, we can now passanother:
swarsel-instantiate 'let f = {name ? "Default", age, ... }: "${name} is ${age} years old"; in f { age = "2"; another = "0"; }'
Default is 2 years old
The latter is useful mainly in the NixOS module system, where a lot of things will be passed by default. More on that later.
Finally, we can make the values in ... available by using @ as in extra @ { name, ... }: "${name} likes ${extra.object}":
swarsel-instantiate 'let f = extra @ { name, ... }: "${name} likes ${extra.object}"; in f { name = "Nyx"; object = "nix"; }'
Nyx likes nix
Note that it is equivalent to write {} @ extra:
swarsel-instantiate 'let f = { name, ... } @ extra: "${name} likes ${extra.object}"; in f { name = "Nyx"; object = "nix"; }'
Nyx likes nix
This looks cumberesome on first sight, but is for example useful when referencing flake inputs in the flake outputs, as it allows you to forego listing them all in the arguments (which can be a very long list!). More on that later.
2.1.1.3. Language features
- The functions available to the nix language can be found in the nix reference manual. Some explanations to common functions are provided in Builtin functions.
- Scopes are static in nix; scoped variables are not inherited by the context of the evaluation
- Variables can be created in an enclosing scope using
let <variable declarations> in <expression>. You actually saw me use this several times in the above function call. Note that "usually", you will not be able to declare arbitrary variables; this is because usually we will be working within The module system - in that scope, you either need to resort to thelet ... inconstruct or you need to declare things as module options (more on that later). In general nix however, this would be no problem. - another way to add an expression to the scope is using the
reckeyword:- this adds all attributes of the enclosing attribute
- the following will error out:
- Variables can be created in an enclosing scope using
swarsel-instantiate ' { a = true; b = a; } '
error: undefined variable 'a'
at «string»:4:9:
3| a = true;
4| b = a;
| ^
5| }
- however,
recmakesaavailable to the scope (also remember that the order of expressions does not matter):
swarsel-instantiate ' rec { b = a; a = true; } '
{ a = true; b = true; }
- the last way to something to the scope is by using
with:- this adds the passed set to the lexical scope of the enclosing expression:
- the following will error out:
swarsel-instantiate ' let functions = { print = v: v; }; in print "ok" '
error: undefined variable 'print'
at «string»:7:5:
6| in
7| print "ok"
| ^
8|
- using
withwill make it work:
swarsel-instantiate ' let functions = { print = v: v; }; in with functions; print "ok" '
ok
- generally, the values of the variable in the innermmost scope is given precedence in evaluation:
swarsel-instantiate ' let scope = "outer"; in let scope = "inner"; in scope '
inner
- the same holds true for
with:
swarsel-instantiate ' with { scope = "outer"; }; with { scope = "inner"; }; scope '
inner
- you can copy variables from an outer scope by using
inherit (<outer scope>) <name>;- This is syntactic sugar to achieve the same as
<name> = <outer scope>.<name>;
- This is syntactic sugar to achieve the same as
- when not in a string, we can place parentheses around a nix expression to provide its return value to other expressions (remember the
builtins.toString (-1)example from before.-1is indeed a nix expression.) - you can write inline comments using
#and multiline comments using/* ... */ - The following words are reserved keywords and cannot be used for variable names:
assert,else,if,ininherit,let,or,rec,then,with
2.1.1.4. Operators
The following operators work as you know them from most other programming languages:
- Any operator involving numbers (
+,-,*,/) Sidenote: When negating a number in actual nix code you will need to wrap it in parentheses nearly every time due to function evaluation:
swarsel-instantiate ' builtins.toString (-1) '
-1
but:
swarsel-instantiate ' builtins.toString -1 '
error:
… while calling the 'sub' builtin
at «string»:2:21:
1| let lib = import <nixpkgs/lib>; in
2| builtins.toString -1
| ^
3|
… while evaluating the first argument of the subtraction
error: expected an integer but found the built-in function 'toString': «primop toString»
- Logical operators (
!,==,!=,<,>,>=,<=,&&,||)- additionally, logical implications are available as
->(defined as!v1 || v2a.k.a.if v1 then v2 else true)
- additionally, logical implications are available as
The following operators are nix specific:
- Concatenations can be done between strings and paths (in any order) using
+ - List Concatenations however use the
++operator - you can use the
?operator on an attribue set to check whether it has an attribute:
swarsel-instantiate ' { exists = true; } ? exists ' swarsel-instantiate ' { exists = true; } ? noExists '
| true |
| false |
- Attribute selection (on attribute sets) is done using
.:
swarsel-instantiate ' { v = 1; }.v '
1
- Updates to attribute sets are done using The '//' operator
2.1.1.5. Flakes
Next, we shall talk about some concepts regarding nix flakes.
What are flakes? The answer is surprisingly simple: Flakes can be thought of packages that contain nix expressions. They can include other flakes as inputs (acting as libraries if you want) and build on their functionality (in fact, this is exactly what happens when you are using NixOS with nixpkgs). What exactly is provided by a flake is up to the owner entirely, although there are some "norms" that we will get to later.
A directory becomes a flake as soon as it contains a file called flake.nix, which contains an attribute set that has at least an attribute outputs, which must be a function returning an attribute set. Hence, the simplest possible flake is the following:
{
outputs = _: { };
}
What is very nice about flakes is that each flake generates a corresponding flake.lock file the first time a nix command is called targetting it. This file exactly specifies at which revision every flake input should be pulled in. This makes flakes very nice for declaratively setting up development environments that can then be committed to a project; this enables collaborators to do nix develop which will setup a reproducible environment, which is the same for every developer.
That was a very dry introduction. Now, what we are usually interested in when pulling in a flake is any of the following:
- modules to extend our configuration with
- packages that are not in
nixpkgs
and, to a somewhat lesser degree:
- flake templates
devshells
Where these things come from we will learn soon.
2.1.1.5.1. Why flakes
Flakes are, as of writing, an experimental feature in nix, although their interface has been quite stable for a while now - in fact, stable enough that most bigger nix projects are exposing a flake nowadays. People like to (mistakenly) claim that it is because of flakes that we have version pinning in nix. In fact, people were doing this long before flakes were a thing using a construct that looks roughly like this:
let
let pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/0b20bf89e0035b6d62ad58f9db8fdbc99c2b01e8.tar.gz") {};
in
pkgs.mkShell {
buildInputs = [ pkgs.cowsay ];
}
This approach simply imports a chunk of nix code that is fetched using fetchTarball - and the version is pinned by the git revision. However, imagine a bigger project; even if you were to import every input like this and pass the expressions to the modules that need them, updating the inputs manually would be a chore. And even though projects like niv and npins exist(/ed) to help with these problems, flakes provide a uniform interface and are reasonably easy to work with, and provide a lot more other stuff on the side.
That being said, some people decide to work without flakes. Depending on the reasoning, I think this is totally fair (as really for the code itself they dont automatically provide a lot of extra functionality that cannot be achieved with other tools). However, some people dismiss flakes for the only reason being "they are too complex" with really is not at all true (which is what I hope to prove to you soon!). A simple project devshell for example could very reasonably be built only from the fetchTarball construct I showed above.
Finally and for completeness sake, a historical note on nix channels.
Nix channels are one of the official mechanisms to provide package sources. They are managed manually through the cli. A channel would be given a name (like nixpkgs) which could then in nix code be referenced for example like:
{ pkgs ? import <nixpkgs> {} }: ...
Really, this leaves it up to the caller to decide what nixpkgs really is, along with a few other considerations that I do not want to get into. This chapter is intentionally left short; I just want you to know how the pattern above (the <nixpkgs>) looks so that you know what you are working with if you encounter it in the wild.
Channels are not a great tool for reproducability and I would advise against using them if possible.
2.1.1.5.2. Flake arguments
Let us now talk about arguments that a flake (kind of) expects.
As we already learned, a flake must provide an attribute called outputs (whatever that might mean at this point!). However, unless we want to build everything that our flake provides from the ground up, we probably also want to pass some libraries or build tools to it. This is done using the attribute called inputs. An inputs definition might look something like this:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
hydra = {
url = "github:nixos/hydra/nix-2.30";
inputs.nix-eval-jobs.follows = "nix-eval-jobs";
};
nix-eval-jobs = {
url = "github:nix-community/nix-eval-jobs/v2.30.0";
flake = false;
};
toplevel.url = "./..";
};
}
This is a surprisingly real-world example from my flake.nix that we can use so explain most of the important inputs options:
- the most important attribute is
url. It describes where the source should be fetched from. As you can see from thetoplevelinput, you can put as an input a path on the local filesystem. Note that by default, it is expected that every input is itself a flake - meaning, aflake.nixexists there which will be loaded into our flake along with its dependencies. You can also see that we set this tofalsefor thenix-eval-jobsinput. This is usually done when the target project is simply not a flake. Nix will then load in the contents of the repository as a raw filetree.
However, if you go and check out nix-eval-jobs you will see that this project does have a flake.nix, and it is also very active; so what is going on here? In this particular case, this is done to avoid pulling in the dependencies that the nix-eval-jobs would bring along. Instead, the files provided by it must be then made to work using what is provided in the parent flake. This is not a common pattern, but something that is good to know.
Another interesting option is the hydra.inputs.nix-eval-jobs.follows. Let us first talk about the general interface <input>.inputs.<dependency>: This structure allows to customize options for the dependency of a flake input. For example, we could have customized the source of the nix-eval-jobs dependency of hydra had we written hydra.inputs.nix-eval-jobs.url = .... The follows option takes a similar approach by setting an input dependency to the same version as some other input of the parent flake (in our case, it is pinned to the nix-eval-jobs v2.30.0 version).
When we are done defining our flake inputs, we can then decide what our flake shoud actually do.
As we already learned earlier, outputs is a function returning an attribute set. In fact, it is usually a function that takes as an argument an attribute set containing our inputs, looking roughly like this:
{
inputs.nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
outputs = inputs @ { self, nixpkgs, ... }:
let
inherit (self) outputs;
in
{
nixosConfigurations = {
main = nixpkgs.lib.nixosSystem {
modules = [ "${self}/configuration.nix" ];
specialArgs = {
inherit inputs outputs;
};
};
};
};
}
With what we learned so far, you are already able to understand most of what is going on here:
- outputs is a function that expects
selfandnixpkgsbut tolerates other attributes in the set, and the attribute setinputscan be referenced. - an attribute
outputsis inherited from the outer scopeself, meaningoutputs = self.outputs;(whatselfis and why we can reference theoutputsin the same place where we are defining them I will explain shortly) - the resulting attribute set holds an attribute called
nixosConfigurationsthat holds an attribute calledmain, which is set by calling a function that we now do not want to get into too much.
You might (rightfully!) wonder where the self argument comes from. It is actually not a flake input. Instead, self is a special flake construct that refers to the root of the current flake, meaning all flake attributes can be accessed using self.<attribute>. Moreover, when using self as a path, it refers to the path os the current flake in the nix store. This can be used to reference files within the repository without needing relative paths (this is exactly what happenss in self.outputs.nixosConfigurations.main.modules: it loads in configuration.nix, which resides in the same directory as the flake.nix. We could have also written ./configuration.nix here instead).
Another question might be what happens when we load in self.outputs while defining outputs. Normally that should not work? Remember that nix is a lazy language. When outputs is first created in the let ... in block, its value will be something along the lines of outputs = <thunk>;. That means, not all values are loaded upon instanciation, but only whenever they are needed. And if a value is needed, nix will then move on to compute that value.
Of course that does mean that we need to be careful to not introduce circular chains of dependency. Doing that would result in what is called an infinite recursion error, a type of error that can be very hard to wrap one's head around. Luckily, when writing a simple config, you are not very likely to encounter it in a shape that is not rather easily solved.
Like we saw here with nixosConfigurations and some other outputs that we heard about earlier, there are some "standard" outputs that nix recognizes and is able to apply some defaults on. Let us quickly go over them:
I will now list output names and explain what they to; some outputs are what I call "system-scoped". That means they have toplevel attribute in the name of system architectures, e.g. x86_64-linux. I will mention when an output is system-scoped. This list is roughly ordered by importance to a NixOS beginner:
nixosConfigurations: A set of NixOS host configurationsdevShells: system-scoped. A set of devshells that can be used by calling nix develop. Defaults to the devshell calleddefault.nixosModules: A set of nixos Modules. By default, when consuming one of these modules but not specifying which one, the overlaydefaultwill be chosen.overlays: A set of nixpkgs Overlays. By default, when consuming one of these overlays but not specifying which one, the overlaydefaultwill be chosen.checks: system-scoped. Flake checks to perform when calling nix flake checkformatter: system-scoped. Formatting package/config to use when calling nix fmtpackages: system-scoped. Packages that can be directly built using nix buildapps: system-scoped. Packages that can be directly run using nix runtemplates: A set of templates that can be initialized usingnix flake init -t <template name>. By default, thedefaulttemplate will be chosen.legacyPackages: system-scoped. An alternative topackagesthat does not provide guarantees on the structure within. An attribute here might be a package, but it could also be a package set in need of further evaluation. This is used for example innix flake show nixpkgsbecause the number of packages inpackagestakes very long to evaluate. Whenever nix encounters alegacyPackageshowever, it simply displays "omitted" instead of evaluating everything within. In a way you could saylegacyPackagescontains an attribute that is closer to what the structure used by the Channels construct yields.hydraJobs: An arbitrarily deeply nested attribute set of derivations that defines jobs that should run on a hydra build farmbundlers: A set of bundlers (a bundler packages app for usage outside of the nix store). Will choose thedefaultbundler by default
The following are also recognized by nix, but are deprecated:
devShell: system-scoped, deprecated. A single devshell that can be used by calling nix develop.overlay: deprecated. A single overlay.nixosModule: deprecated. A single nixos module.defaultApp: system-scoped, deprecated. The app that this flake should default to. Instead, you can also defineapps.<system>.default.defaultPackage: system-scoped, deprecated. The package that this flake should default to. Instead, you can also definepackages.<system>.default.defaultTemplates: deprecated. The template that this flake should default to. Instead, you can also definetemplates.default.defaultBundler: deprecated. The bundler that this flake should default to. Instead, you can also definebundlers.default.bundler: deprecated. A single bundler.
Again, you can also define other outputs (for example, a widely used output that is not recognized by nix is homeModules, used in, you guessed it, modules for home-manager). nix flake check will simply emit a warning when it encounters such an output, but there are no other side-effects. Also, keep in mind that flake outputs are things that your flake exposes to others. That means, unless you have written NixOS modules that you want to share with the world, you probably do not need a nixosModules output in your flake.
2.1.1.5.3. References
When interacting with flakes on the CLI, you will always do it in the same style:
<reference>#<attribute path><optional: ^<outputs>>
The flake reference can be an absolute or relative path, but also a URI. It can also be an identifier in the Registries. This means that all of the following are valid (the output to stderr is needed because my flake loads some nix values per default that get printed to the command line on stderr, which org-babel does not capture. This makes the evaluator sad):
nix run nixpkgs#cowsay -- hello 2>/dev/null nix run github:nixos/nixpkgs#cowsay -- hello 2>/dev/null nix run .#pkgs.x86_64-linux.cowsay -- hello 2>/dev/null nix run /home/swarsel/.dotfiles#pkgs.x86_64-linux.cowsay -- hello 2>/dev/null
_______
< hello >
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
_______
< hello >
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
_______
< hello >
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
_______
< hello >
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
However:
nix shell n#cowsay^man --command hello
error: unable to execute 'hello': No such file or directory
This happens because the man output of cowsay does not provide the package itself, but only its manpage. nix run reads your mind a little more in that case and makes it available anyways (this is because the apps output is tailored towards running programs):
nix run nixpkgs#cowsay^man -- hello
_______
< hello >
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
2.1.1.5.4. Registries
All you really need to know about flake registries is that they provide a shorthand to access flake References easily. By default, the nixpkgs identifier is provided when installing nix with flakes. Additional registries can however be configured (I do this in General NixOS settings (nix config, stateVersion)). For example, I have configured a n identifier that simply mirriors the nixpkgs identifier, which allows me to save on some typing with most nix commands:
nix run n#cowsay -- hello
_______
< hello >
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
2.1.1.6. Essential commands
Finally, here are some of the most essential nix commands. Note that there is a "legacy CLI" as well as a "new CLI", the latter of which is enabled by setting =--experimental-features 'nix-command'= or enabling nix.settings.experimental-features = [ "nix-command" ];. I will only talk about the new CLI, which is meant to deprecate the legacy one. Also, these explanations assume that we are interacting with them in flakes (which also need to be separately enabled by appending flakes to the two snippets given above).
2.1.1.6.1. nix shell
This is a very important command. It spawns a subshell in which the commands specified are available. You can pass multiple arguments:
nix shell nixpkgs#cowsay n#bonsai 2>/dev/null
2.1.1.6.2. nix build
This is the essential command to trigger a nix build of stuff. Anything that is a derivation can be built. Typically, these are the packages and legacyPackages outputs of a flake; for these outputs, the full path does not need to be specified. For example, to build the very simple t2ts tool from this flake:
nix build .#ts2t 2>/dev/null && echo success
success
However, you can also specify the full path
nix build .#packages.x86_64-linux.ts2t 2>/dev/null && echo success
success
What is often useful is to build a nixosConfiguration using nix build prior to applying it. E.g. this can be done using the Bakery (Lenovo ThinkPad) host:
nix build .#nixosConfigurations.bakery.config.system.build.toplevel 2>/dev/null && echo success
success
2.1.1.6.3. nix run
This is the command that allows you to run a command directly. These apps are per default defined under the apps output, and that again allows to run them directly instead of specifying the full path. For example to run the swarsel-bootstrap tool that I expore under Apps:
nix run s#swarsel-bootstrap -- --help 2>/dev/null
Remotely installs SwarselSystem on a target machine including secret deployment.
USAGE: /nix/store/mwa8qhmhzfx1jrjw3kzynfhf7ynh2mgm-swarsel-bootstrap/bin/swarsel-bootstrap -n <target_hostname> -d <target_destination> [OPTIONS]
ARGS:
-n <target_hostname> specify target_hostname of the target host to deploy the nixos config on.
-d <target_destination> specify ip or url to the target host.
-a <targeit_arch> specify the architecture of the target host.
target during install process.
OPTIONS:
-u <target_user> specify target_user with sudo access. nix-config will be cloned to their home.
Default='swarsel'.
--port <ssh_port> specify the ssh port to use for remote access. Default=22.
--debug Enable debug mode.
--no-disko-deps Upload only disk script and not dependencies (for use on low ram).
-h | --help Print this help.
2.1.1.6.4. nix develop
This command spins up a development shell in the current directory, provided that the passed nix expression exposes a devshell:
If the current directory has such a flake.nix, you can simply do
nix develop 2>/dev/null
Otherwise you can pass the path (or any flake reference); again, per default the default devshell will be loaded. If you need another one, use the syntax we discussed above. For example, here is how to use a path in the nix store, loading the deploy devshell:
nix develop swarsel#deploy 2>&1
2.1.1.6.5. nix eval
This command evaluates the nix expression given to it:
nix eval .#nixosConfigurations.pyramid.config.networking.hostName 2>/dev/null
"pyramid"
2.1.1.6.6. nix repl
This starts the nix REPL ("Read Evaluate Print Loop"), an interactive environment for evaluating nix expressions for the given environment. Usually you will want to start this pointing to your flake like:
nix repl . 2>/dev/null
Nix 2.30.3 Type :? for help. Using saved setting for 'extra-substituters = https://nix-community.cachix.org' from ~/.local/share/nix/trusted-settings.json. Using saved setting for 'extra-trusted-public-keys = nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=' from ~/.local/share/nix/trusted-settings.json. Loading installable 'git+file:///home/swarsel/.dotfiles#'... Added 25 variables. apps, checks, darwinConfigurations, darwinConfigurationsMinimal, devShells, diskoConfigurations, formatter, globals, guestConfigurations, homeConfigurations, homeLib, homeModules, legacyPackages, lib, nixOnDroidConfigurations, nixosConfigurations, nixosConfigurationsMinimal, nixosModules, nodes, overlays ... and 5 more; view with :ll
In there you get tab completion and have all functions available that you can use in your flake. This is my preferred command to get insight into what is happening in my config if some error is not obvious.
2.1.1.6.7. nix fmt
This uses the formatter that is defined in the passed flakes formatter output and formats the repository accordingly.
nix fmt 2>&1
Using saved setting for 'extra-substituters = https://nix-community.cachix.org' from ~/.local/share/nix/trusted-settings.json. Using saved setting for 'extra-trusted-public-keys = nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=' from ~/.local/share/nix/trusted-settings.json. traversed 582 files emitted 387 files for processing formatted 0 files (0 changed) in 24ms
2.1.1.6.8. nix hash
This command allows us to convert between different hasn algorithms that are used by nix. The interesting command for us is nix hash convert, which is used like
nix hash convert --from base16 --hash-algo sha1 --to nix32 bb0d59cfdddd37f62fee1e214be3467acffe58df
vxcgxkvs8viln88yxqpzcdyxvp7mj3dv
The supported formats for --from are: base16, nix32, base64, and sri. Per default, nix will try to choose the correct format from the input.
The supported formats for --to are: base16, nix32, base64, and sri. The default is sri.
The supported hashing algorithms for --hash-algo are: blake3, md5, sha1, sha256, and sha512. For SRI hashes, this can be omitted.
2.1.1.6.9. nix flake
This is the main command that we use to interact with our flake. It has several subcommand, but only a few really interest us:
This updates your flake inputs. That means, it will fetch the latest revisions from the inputs.<name> source that we discussed in Flake arguments. By default, all flake inputs will be updates:
nix flake update
It is also possible to only update specific inputs:
nix flake update home-manager
To lock an input from ever updating, a nice trick is to pin it to a specific revision in the inputs' source url like I do in flake.nix skeleton (inputs) for the nixpkgs-kernel input.
This evaluates the entire flake and runs its tests, if there are any.
nix flake check
This lists all outputs of the passed flake. The default is the flake in the current directory. This can be useful to check whether outputs are configured correctly or which templates are exposed by a flake.
nix flake show . 2>/dev/null
This initializes a new flake. Per default it chooses the simple flake that you might have seen before if you ever tried this command. After you already have your own config flake, this command is mainly useful to create a project flake from a template. If you have a flake that exposes templates as outputs, you can list them as showen in nix flake show and then initialize the flake using:
nix flake init -t .#<template name>
Keep in mind that you are free in choosing the flake reference.
2.1.2. nix package manager
In contrast to the nix language, there really is not much important stuff that you need to know about the nix package manager. Just keep in mind that the nix package manager is the tool that the nix language was invented for in the first place. It aims to escape the dependancy problems that conventional package managers bring by the mechanisms that we learned about earlier.
In practice, you should never need to install a package with the nix package manager imperatively. For things you only need once you can resort to nix shell or nix run or even comma. Otherwise you should probably make it available in your config or in a devshell.
2.1.3. nixpkgs
Nixpkgs is the name of the community project that provides loads of packages for different architectures and is the main building block that NixOS builds upon.
The main way that we as users interact with nixpkgs (unless we contribute to it) is that we add it as one of the Inputs of our flake. That will provide the majority of packages and modules that we will need to build our system. However, there are a few things that deserves a special mention:
2.1.3.1. Configuring nixpkgs
The main way of configuring nixpkgs is the nixpkgs.config option. There we can configure what packages we want to use. Some flags that we might want to set are:
{ allowUnfree = true; }=to allow packages with an unfree license to be built{ permittedInsecurePackages = [ <package name> ]; }to selectively allow packages that are marked as insecure to be built{ cudaSupport = true; }=to build packages with CUDRA support by default{ rocmSupport = true; }=to build packages with ROCm support by default
2.1.3.2. Overrides
A simple way to modify packages that are already in nixpkgs is the use of overrides. Two main override methods exist:
overridefor overriding the arguments passed to a functionoverrideAttrsfor overriding the attribute set passed tostdenv.mkDervivationoverrideAttrsis used as follows:
pkgs.<package>.overrideAttrs (finalAttrs: previousAttrs: { pname = previousAttrs.pname + "-new"; })
in the wild, this can be used for example directly when installing packages:
{ pkgs, ... }: {
environment.systemPackages = [
(pkgs.hello.overrideAttrs (finalAttrs: previousAttrs: { pname = previousAttrs.pname + "-new"; }))
];
}
For an explanation to override and the overrideAttrs parameters, read on.
2.1.3.3. Overlays
Other than that, we can also extend nixpkgs with packages that it does not normally ship with; this is done using a concept called overlays. An overlay is defined as such
final: prev: { newVesktop = prev.vesktop.override { withSystemVencord = true; }; }
This overlay will add a newVesktop package with a vesktop package that has the withSystemVencord flag set to true to nixpkgs when applied (this is due to override overriding the arguments of the vesktop package; such flags can be found by checking out the source of the package. To find that source. you can use the nix package search to search for a package, and then click the "source" button, which for the vesktop example yields this GitHub page. There you will also see that withSystemVencord defaults to false.). In this case of course, this has the same effect as simply using an override, so this is better used for adding new packages.
When installing newVesktop afterwards, we will get a vekstop that will have system vencord enabled. Overlays can also be added by referencing a flake overlay output. Here is an example of how to use both of these methods in practise:
{ outputs, inputs, ... }:
{
nixpkgs.overlays = [
outputs.overlays.this
inputs.someExternalFlake.overlays.default
(final: prev: { vesktop = prev.vesktop.override { withSystemVencord = true; }; })
];
}
This will add the previous vesktop override as well as the this overlay from our flake (keep in mind that outputs refers to the outputs that our flake exposes) and the default overlay from a flake input called someExternalFlake.
Also keep the parentheses around the inline overlay in mind; we need to pass whole elements to the list, so we use parentheses to evaluate a nix expression. This is the standard equivalent to the ${} approach that we took with strings, as you remember.
2.1.4. NixOS
NixOS is the name of the linux distribution that relies on the nix package manager. NixOS also relies heavily on nixpkgs to provide packages, library functions, and modules. The one part this really important to understand here is the module system.
2.1.4.1. The module system
The module system can be thought of a nix library acts as an extension to nix in NixOS. It much enhances the ways in which we can set values within "the configuration" - for example, it becomes possible to set something like environment.systemPackages in two files, and both set values will be merged. Also, to these so-called "options" we can easily add type checking.
2.1.4.1.1. Modules
This provides a broad overview over the module system.
On a very high level, a module is just a function returning an attribute set. Hence the simplest possible module should be:
_: { }
Actually, because of syntactic sugar, the module arguments can be omitted if they are not accessed at all, so the simplest simplest module is actually:
{ }
but that is just a note for completeness sake.
In the module systems, the above attribute sets supports three main options:
imports, which allows for importing further filesoptions, for declaring Nixoptions that should be added to the module systemconfig, for setting said options.
imports is a simple list that takes paths to other nix files and adds them to be evaluated by the module system:
_:
{
imports = [
./configuration.nix
./extras
]
}
The module system will pass an attribute as an argument to this function. When a module requires no args from the outer scope. As soon as you pass any argument however, you also need to pass the ellipsis ... to the attribute set. This is because the module systems always adds the following arguments to each module contained within:
libfor library functionspkgsthe set of packages of the systems architectureoptions, the option declarations. Note that you only need to import this if you need the access the option declarations themselves. You will not need this for declaring options in a moduls - i.e. you will only need to add this very rarelyconfig, the option definitions - you need this whenever you want to access configuration parts that has been defined somewherethe set of values passed to
specialArgsin the call tonixpkgs.lib.nixosSystem. To that, everything should be added that is needed to be evaluated when resolving the model structure (e.g. imports)specialArgsincludesmodulesPath, which can be used to importnixpkgsmodules without knowing their location in the nix store- this happens by default in
hardware-configuration.nix:
- this happens by default in
{ modulesPath, ... }: { imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; };
Remember now what we said above: here we now need to pass the ellipsis ..., only { modulesPath } would error out because of the other passed arguments. _ works because that accepts any value.
- any values set under
_module.args(sometimes you might seeextraArgs; that has been deprecated in favor of_module.args)
If a directory is passed to imports, nix will look for a file called default.nix in the passed directory.
Options are declared (not defined) using lib.mkOption (or one of its derivatives, like lib.mkEnableOption, which creates a boolean option defaulting to false)
{ lib, ... }:
{
options.myBool = lib.mkOption {
type = lib.types.bool;
default = true;
description = "An example of a boolean option definition.";
};
}
config is what we use to actually set the values for which we declared options somewhere. Note that an option does not need to be declared in the same file where it is set (in fact, option declarations need to be unique across the entire module system). We call the process of setting a value under config.<name> an option definition.
We can extend our module from options:
{ lib, ... }:
{
options.myBool = lib.mkOption {
type = lib.types.bool;
default = true;
description = "An example of a boolean option definition.";
};
config.myBool = false;
}
This will define config.myBool to be false, which can now be referenced in another file under config.myBool.
If a file does not use either imports or options but only config, you can omit the config attribute:
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.cowsay ];
}
If we would however declare options in the same file, we would need to write:
{ lib, pkgs, ... }:
{
options.myDo = lib.mkEnableOption "do it"; # this expects a string that will be prepended by "whether to" and appended a dot
config.environment.systemPackages = [ pkgs.cowsay ];
}
3. flake.nix
Handling the flake.nix file used to be a bit of a chore, since it felt like writing so much boilerplate code just to define new systems. For a while, I used noweb-ref in order to alleviate this problem (see Appendix A: Noweb-Ref blocks, an example of the repository at that time would be acc0ad6: Add several NixOS hosts on Proxmox and Oracle Cloud.). However, the true answer laid in making use of builtin nix functionality.
Nowadays, I use flake-parts to manage my flake. It allows me to conveniently split the actual flake into multiple files ("parts") using the following mechanism:
importsare files pulled in to build the flake configuration (similar to the imports in the module system)systemsdefines the architectures that the flake should be provided for - I go here for the four "main" architectures, although true support is only provided for linux systems (see Packages (pkgs) for the main reason)
3.1. flake.nix skeleton (inputs)
In general, a nix flake consists of one or more inputs and several outputs. The inputs are used to define where nix should be looking for packages, modules, and more (the most common input is nixpkgs, which provides a lot of packages, library functions and modules). The outputs generate expressions that can be used in .nix files as well as system configurations using these files.
In the start, I enable some public cache repositories. This saves some time during rebuilds because it avoids building as many packages from scratch - this is mainly important for community flakes like emacs-overlay, which basically would trigger a rebuild whenever updating the flake. The repository does of course not hold everything, but it lightens the pain. It would look cleaner if this were to be used only inside a nix configuration block of an actual system, but I want these caches to be used for e.g. app calls as well.
In many flakes, you see a structure like this: outputs = inputs@ [...], the inputs@ makes it so that all inputs are automatically passed to the outputs and can be called as inputs.<name>, whereas explicit arguments may just be called by using <name> (for a more detailed explanation, s). For most flakes this is fully sufficient, as they do not need to be called often and it saves me maintainance effort with this file. In fact, I also used to make use of this mechanism. However, using flake-parts, all I really need for the outputs function is inputs, which is why my outputs = inputs: inputs.flake-parts.lib.mkFlake { inherit inputs; } { […] ). Note that flake-parts must inherit these inputs and no other arguments are expected.
In this section I am creating some attributes that define general concepts of my configuration:
nixosModulesimports self-defined options that I only want to use on NixOS systems. All modules are held as separately as possible, to allow for easier sharing with other people mostly.homeModulesimports modules that are to be used on NixOS and non-NixOS systems. These are mostly used to define outputs (monitors), keyboards and special commands for machines.packagesholds packages that I am building myself. These are mostly shell scripts, but also a few others such as AppImages and firefox addons.devShellsprovides a development shell that can be used as a bootstrap for new installs usingnix developwhile inside the flake directory. It received an overhaul in0a6cf0e feat: add checks to devShell, since when it is handled usingforAllSystemsand now including pre-commit-hook checks.formatterprovides the formatter that is to be used on.nixfiles. It can be called by usingnix fmt.checkprovides the pre-commit-hook checks that I have explained in Devshell (checks).overlaysimports a few community overlays (such as the emacs-overlay) and also three overlays of my own:additionsholds derivations that I am adding myself to nixpkgs - i.e. this is where the packages defined in/pkgsget added to nixpkgs.modificationsholds derivations that I have performed overrides on. The list of interesting attribute overrides can be found by looking at the source code of a derivation and looking at the start of the file for lines of the form<name> ? <val>. But this can also be used to, for example, fetch a different version of a package instead.nixpkgs-stableholds the newest version of stable nixpkgs. I only use this on packages that seem broken on unstable, which are not many.zjstatusholds some options forzellij, but I have stopped using it since I prefertmux.
They are defined in Overlays. The way this is handled was simplified in
647a2ae feat: simplify overlay structure; however, the old structure might be easier to understand as a reference.
Here we define inputs and outputs of the flake. First, the following list is for the outputs of the flake.
Format: <name>,
Mind the comma at the end. You need this because the ... is being passed as the last argument in the template at flake.nix template.
Here, just add the input names, urls and other options that are needed, like nixpkgs.follows. By using the latter option, you tell the package to not provide it's own package repository, but instead 'nest' itself into another, which is very useful.
A short overview over each input and what it does:
- nixpkgs This is the base repository that I am following for all packages. I follow the unstable branch. Also I pull in some older revisions of nixpkgs stable for various purposes.
- home-manager
This handles user-level configuration and mostly provides dotfiles that are generated and symlinked to
~/.config/. - swarsel-nix This pulls in the very dotfiles you are currently reading. I am adding this to the flake registry in order to
- NUR The nix user repository contains user provided modules, packages and expressions. These are not audited by the nix community, so be aware of supply chain vulnerabilities when using those. I am only really using rycee's firefox addons from there which saves me a lot of hassle, and it seems to be a safe resource.
- nixGL
This solves the problem that nix has with
OpenGL, as libraries are not linked and programs will often fail to find drivers. Nowadays, this is included in the nixGL module of home-manager, but even that requres a binary for nixGL, which is what I pull from this input. - stylix As described before, this handles all theme related options.
- sops-nix This provides declarative secrets management for NixOS and home manager using sops and age keys. It is a bit more cumbersome to use on home manager systems - which is a bother because I then have to resort to that configuration to keep everything supported - but it is super practical and really the primary reason why it makes sense for me to go for NixOS, as I do not have to do any extra secrets provisioning.
- Lanzaboote Provides secure boot for NixOS. Needed for my Surface Pro 3.
- nix-on-droid This brings nix to android in an app that is similar to tmux! Of course most of the configuration does not apply to this, but it is still neat to have!
- nixos-hardware Provides specific hardware setting for some hardware configurations. For example, this sets some better defaults for my Lenovo Thinkpad P14s Gen2.
- nixos-generators Provides me with images that I can use to create LXCs on Proxmox.
- nswitch-rcm-nix Allows auto injection of payloads upon connecting a Nintendo Switch.
- nix-index-database
This provides a database for
nix-indexthat is updated weekly. This allows for declarative management, without needing to run thenix-indexcommand for database assembly. - disko disko provides declarative disk partitioning, which I use for impermanence as well as nixos-anywhere.
- Impermanence Some of my machines are using a btrfs filesystem that wipes the root directory on each reboot. This forces me to pay more attention in keeping my system declarative as well as helping me keeping the system uncluttered. However, it is a chore to make sure that important files are not deleted. This flake helps with this problem, allowing me to select files and directories for persisting.
- zjstatus This provides utilities for customizing a statusbar in zellij. Currently unused as I prefer tmux for now and might be removed in the future.
- fw-fanctrl This provides access to the internal fans of Frameworks laptops. This is a bit more nice to use than directly using ectool.
- nix-darwin After learning that MacOS systems can also be configured using nix, I managed to get access to an old MacBook for testing. This allows to set most general settings that can otherwise be set using the Mac GUI.
- pre-commit-hooks Provides access to several checks that can be hooked to be run before several stages in the process.
- nix-topology This automatically creates a topology diagram of my configuration.
- flake-parts The aforementioned system that allows for more convenient flake crafting.
- devshell This provides devshell support for flake-parts
- spicetify This is a improved spotify client. This provides a NixOs module to manage it.
- niri-flake This is an optional input that I reserve to use in the future; it provides a module to manage Niri in a way that is way more all-encompassing than the current modules in nixpkgs/home-manager. However, I do not include this by default as this leads to a full compilation of latest niri - this is used only be the niri config evaluator, but is even built if niri is not included in the final config. Also, the binary cache provided by this flake does usually not have the latest niri cached.
- microvm.nix This flake brings support for microvms to nix. This is basically a more isolated alternative to classic NixOs containers, while keeping most of their benefits.
- treefmt-nix
This allows to specify a range of formatters for different languages and aspects which can all be run upon
nix fmt. nixos-extra-modules This is a collection of modules that add some qualitative functions to several aspects of nix, for example:
- microvm management
- wireguard support for nix-topology
- some extensions to the network library
At the moment I am not using the full range of modules, but my usage keeps increasing steadily. Using this module forced me to make some adjustments in my config, namely exposing the
nodesoutput in Hosts.- dns.nix This adds a module that helps with creating zone files (like nsd (dns) - site1). This flake was competing with NixOS-DNS for my favour - while the latter adds many nice utilities that generage records straight from a host configuration, I prefer to do this myself using the Globals + Share sconfiguration between nodes (automatically active) systems. In the end, I just tried out dns.nix without giving NixOS-DNS a chance and it has been working great, but I believe NixOS-DNS still deserves a mention here, as it would have been a great fit as well, most likely.
- nix-minecraft This adds a module that makes it easier to manage (modded) minecraft servers. At the moment, it does not really work with Forge 1.20.1 (which is what my server is running), so I am not making full use of it right now, but I keep close watch on it every day.
- nixos-mailserver This adds a module that basically sets up a full mailserver stack. Apart of DNS records and a few extra steps for e.g. a web client, this is one-stop solution that has been working greatly for me.
- hydra
The hydra module already exists in nixpkgs - however, because, I am also using nix-plugins, I need to build all tools that are using nix against a specific nix version (this is also why I pull in
nix-eval-jobsas a flake input). - nixos-nftables-firewall This flake introduces a module that allows for more structurized nftables config.
- topologyPrivate
This input per default provides a simple output
topologyPrivate = false;. This is the value that is normally used in the config. When I export my setup to a topology diagram, there are some public IPs and domains that I want to obfuscate. When doing that, I can then override this input.
{
description = "SwarseFlake - Nix Flake for all SwarselSystems";
nixConfig = {
extra-substituters = [
"https://nix-community.cachix.org"
];
extra-trusted-public-keys = [
"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
];
};
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
hydra.url = "github:nixos/hydra/nix-2.30";
# hydra.inputs.nix.follows = "nix";
hydra.inputs.nix-eval-jobs.follows = "nix-eval-jobs";
# nix = {
# url = "github:NixOS/nix/2.30-maintenance";
# # We want to control the deps precisely
# flake = false;
# };
nix-eval-jobs = {
url = "github:nix-community/nix-eval-jobs/v2.30.0";
# We want to control the deps precisely
flake = false;
};
smallpkgs.url = "github:nixos/nixpkgs/08fcb0dcb59df0344652b38ea6326a2d8271baff?narHash=sha256-HXIQzULIG/MEUW2Q/Ss47oE3QrjxvpUX7gUl4Xp6lnc%3D&shallow=1";
nixpkgs-dev.url = "github:Swarsel/nixpkgs/main";
nixpkgs-kernel.url = "github:NixOS/nixpkgs/063f43f2dbdef86376cc29ad646c45c46e93234c?narHash=sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o%3D"; #specifically pinned for kernel version
nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.11";
nixpkgs-stable24_05.url = "github:NixOS/nixpkgs/nixos-24.05";
nixpkgs-stable24_11.url = "github:NixOS/nixpkgs/nixos-24.11";
nixpkgs-stable25_05.url = "github:NixOS/nixpkgs/nixos-25.05";
nixpkgs-stable25_11.url = "github:NixOS/nixpkgs/nixos-25.11";
home-manager = {
# url = "github:nix-community/home-manager";
url = "github:Swarsel/home-manager/main";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-index-database = {
url = "github:nix-community/nix-index-database";
inputs.nixpkgs.follows = "nixpkgs";
};
# emacs-overlay.url = "github:nix-community/emacs-overlay";
emacs-overlay.url = "github:nix-community/emacs-overlay/aba8daa237dc07a3bb28a61c252a718e8eb38057?narHash=sha256-4OXXccXsY1sBXTXjYIthdjXLAotozSh4F8StGRuLyMQ%3D";
swarsel-nix.url = "github:Swarsel/swarsel-nix/main";
systems.url = "github:nix-systems/default";
nur.url = "github:nix-community/NUR";
nixgl.url = "github:guibou/nixGL";
stylix.url = "github:danth/stylix";
sops.url = "github:Mic92/sops-nix";
lanzaboote.url = "github:nix-community/lanzaboote";
nix-on-droid.url = "github:nix-community/nix-on-droid/release-24.05";
nixos-generators.url = "github:nix-community/nixos-generators";
nixos-images.url = "github:Swarsel/nixos-images/main";
nixos-hardware.url = "github:NixOS/nixos-hardware/master";
nswitch-rcm-nix.url = "github:Swarsel/nswitch-rcm-nix";
disko.url = "github:nix-community/disko";
impermanence.url = "github:nix-community/impermanence";
zjstatus.url = "github:dj95/zjstatus";
nix-darwin.url = "github:lnl7/nix-darwin";
pre-commit-hooks.url = "github:cachix/git-hooks.nix";
vbc-nix.url = "git+ssh://git@github.com/vbc-it/vbc-nix.git?ref=main";
nix-topology.url = "github:oddlama/nix-topology";
flake-parts.url = "github:hercules-ci/flake-parts";
devshell.url = "github:numtide/devshell";
spicetify-nix.url = "github:Gerg-l/spicetify-nix";
niri-flake.url = "github:sodiboo/niri-flake";
nixos-extra-modules.url = "github:oddlama/nixos-extra-modules/main";
microvm.url = "github:astro/microvm.nix";
treefmt-nix.url = "github:numtide/treefmt-nix";
dns.url = "github:kirelagin/dns.nix";
nix-minecraft.url = "github:Infinidoge/nix-minecraft";
simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/master";
nixos-nftables-firewall.url = "github:thelegy/nixos-nftables-firewall";
topologyPrivate.url = "./files/topology/public";
};
outputs =
inputs:
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
./nix/globals.nix
./nix/hosts.nix
./nix/topology.nix
./nix/devshell.nix
./nix/apps.nix
./nix/packages.nix
./nix/overlays.nix
./nix/lib.nix
./nix/templates.nix
./nix/formatter.nix
./nix/modules.nix
./nix/iso.nix
];
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
};
}
3.2. Auxiliary files
Here I define some extra files that are crucial for success in building my configurations. These are not pulled in by the flake directly, but I still feel like they should be mentioned at the flake level.
3.2.1. extra-builtins
This file is used by nix-plugins. nix-plugins generally allows for the introduction of arbitrary functions into the builtins set. However, I do not want to allow just any function to be added there. Instead, I only add a single function called sopsImportEncrypted. This function is used in order to help me store PII (personally identifiable information) in my repo without having to resort to either:
- git-crypt
a separate repo containing my secrets
As for the second approach, I actually used this up to some point (see for example
7e11641: feat: add initial oauth2-proxy and freshrss oidcas one of the lasts commits still using this system). However, it is quite bothersome to constantly have to keep two repositories up to date and in sync. Also, having a repo that every configuration relied upon that was also a private repo led to the problem that my demo configuration (Hotel (Demo Physical/VM)) would fail to build with that present, and I had to take several extra steps to make it buildable. Ever since deleting that dependency I also got rid of that problem. The whole system is inspired by this blog article and large parts of it are adapted from oddlama's nix-config.
The builtin that is added is a simple call to the exec function that calls a bash script. In order to keep some sanity, we are checking that we are actually calling it no an encryted nix file (even though there is no syntax check inside) and that the path given is a true nix path. Note that a string path will not be accepted, as that can have impurity implications.
# adapted from https://github.com/oddlama/nix-config/blob/main/nix/extra-builtins.nix
{ exec, ... }:
let
assertMsg = pred: msg: pred || builtins.throw msg;
hasSuffix =
suffix: content:
let
lenContent = builtins.stringLength content;
lenSuffix = builtins.stringLength suffix;
in
lenContent >= lenSuffix && builtins.substring (lenContent - lenSuffix) lenContent content == suffix;
in
{
# Instead of calling sops directly here, we call a wrapper script that will cache the output
# in a predictable path in /tmp, which allows us to only require the password for each encrypted
# file once.
sopsImportEncrypted =
nixFile:
assert assertMsg (builtins.isPath nixFile)
"The file to decrypt must be given as a path (not a string) to prevent impurity.";
assert assertMsg (hasSuffix ".nix.enc" nixFile)
"The content of the decrypted file must be a nix expression and should therefore end in .nix.enc";
exec [
./sops-decrypt-and-cache.sh
nixFile
];
}
3.2.2. sops-decrypt-and-cache
This is the file that manages the actual decryption of the files mentioned in extra-builtins. We simply fetch the appropriate system age key from the ssh host key and then call sops decrypt. Since it would be a bother to decrypt these files on every build, I keep the result cached and only re-decrypt if it changes. Keeping it cached outside the nix store incurrs a theoretical bit of impurity. However, this is easier to manage and also nothing really relies on these files being present.
# adapted from https://github.com/oddlama/nix-config/blob/main/nix/rage-decrypt-and-cache.sh set -euo pipefail print_out_path=false if [[ $1 == "--print-out-path" ]]; then print_out_path=true shift fi file="$1" shift basename="${file%".enc"}" # store path prefix or ./ if applicable [[ $file == "/nix/store/"* ]] && basename="${basename#*"-"}" [[ $file == "./"* ]] && basename="${basename#"./"}" # Calculate a unique content-based identifier (relocations of # the source file in the nix store should not affect caching) new_name="$(sha512sum "$file")" new_name="${new_name:0:32}-${basename//"/"/"%"}" # Derive the path where the decrypted file will be stored out="/var/tmp/nix-import-encrypted/$UID/$new_name" umask 077 mkdir -p "$(dirname "$out")" # Decrypt only if necessary if [[ ! -e $out ]]; then agekey=$(sudo ssh-to-age -private-key -i /etc/ssh/ssh_host_ed25519_key || sudo ssh-to-age -private-key -i ~/.ssh/sops) SOPS_AGE_KEY="$agekey" sops decrypt --output "$out" "$file" fi # Print out path or decrypted content if [[ $print_out_path == true ]]; then echo "$out" else cat "$out" fi
3.3. Library functions
This section defines all functions of my own that I add to lib. These are used in all places over the config, however mainly in the files responsible for handling various imports.
A breakdown for the functions that have a non-obvious purpose:
pkgsFor: This function reads all available systems from nixpkgs and generates pkgs for them. This is needed for my generation of home-manager and nix-on-droid systems in Hosts.- uses nixpkgs.lib.genAttrs
- Also, in that function I am defining the
pkgsthat should be used when I referencepkgsin the actual configuration. I want to make sure that the correct system is used (keep in mind this is for home-manager configurations, which need that info! As a remark, you would not set this for a NixOS host), that I load my Overlays (extra packages and modifications that I add topkgs), as well as a setting that allows me to install unfree software. As a base package set I choosenixpkgsfrom my inputs (and so does nearly every configuration out there. Keep in mind however that you could use any package set here!nixpkgshowever also comes with a lot of usefullibfunctions (that are notbuiltinsto the nix language!))
mkTrueOption: Defines a nixos module option that is by default enables (as opposed tomkEnableOptionwhich are per default disabled).- uses lib.mkOption to create the defaulted option.
mkStrong:- An alias for (nixpkgs.lib.mkOverride 60), which is higher than setting an option normally (i.e.
option = value; which has priority 100), but being of lower priority than using nixpkgs.lib.mkForce, which has priority 50 (lower priority takes precedence). For completeness' sake, the priority set when using nixpkgs.lib.mkDefault is 1000 (a very low value).
- An alias for (nixpkgs.lib.mkOverride 60), which is higher than setting an option normally (i.e.
forEachLinuxSystem: performs thepkgsForfunction for a set ofsystems(here:x86_64-linuxandaarch64-linux). I need to use this in the Packages (pkgs) section in order to avoid trying to build those packages for darwin systems.- uses nixpkgs.lib.genAttrs
readHosts: Reads the names of directories under thehosts/folder for a particular system type- uses builtins.readDir
- uses nixpkgs.lib.attrNames to acquire attribute names from the outputs of
builtins.readDir
readNix: reads all files in a directory that are notdefault.nix(usually used to simply load everything from a folder and is called inside that respectivedefault.nix).- uses builtins.readDir
- uses nixpkgs.lib.attrNames to acquire attribute names from the outputs of
builtins.readDir
mkImports: These are used to help with importing files mostly:- uses nixpkgs.lib.map to actually import the list of modules that are passed to
mkImportsin names.
- uses nixpkgs.lib.map to actually import the list of modules that are passed to
cidrToSubnetMask: this takes in an IP address in cidr notation (for example 192.168.1.0/24) and returns the matching subnet mask (here: 255.255.255.0)- uses nixpkgs.lib.toInt to grab the CIDR mask as an int
- uses nixpkgs.lib.genList to both get the bitwise representation of said CIDR mask as well as converting it to the octet notation of the subnet mask.
- uses nixpkgs.lib.concatStringsSep to build the final subnet most of the octets calculated.
mkIfElseListgenerates either one or another list based on a conditional- uses nixpkgs.lib.mkMerge to merge the results of the de-facto
ifandelse, yielding a single list in the end.
- uses nixpkgs.lib.mkMerge to merge the results of the de-facto
getBaseDomaintakes in a domain (likesub.about.com) and extracts only the base domain (here:about.com)- uses nixpkgs.lib.split to get the parts of the input domain
- uses nixpkgs.lib.filter to keep only the non-empty strings (checking using nixpkgs.lib.isString) from the list generatded by split
- uses nixpkgs.lib.tail to strip the subdomain which keeps the base domain and then conjoins the remaining two elements using nixpkgs.lib.concatStringsSep. Indeed this means that this function would break for deeper subdomain nestings, but so far I am not using these.
getSubDomainworks in a similar way on a domain subdomain (for the above example:sub)- works similar to
getBaseDomainbut I also calls nixpkgs.lib.length to make sure we have an element left which we then grab by nixpkgs.lib.head.
- works similar to
toCapitalized: returns the capitalized version of a string ("about"->"About")- uses nixpkgs.lib.stringLength to prevent working on an empty string
- nixpkgs.lib.subString as well as
nixpkgs.lib.toUpperandnixpkgs.lib.toLowerto build the respective capitalized string
Concerning the flake = _: part:
- this is a mechanism introduced by flake-parts. A
flakeoutput is akin to a 'normal' output of a standard nix flake (meaning, it will not be built specifically for each system defined bymkFlake) libis then defined as the merged set of thenixpkgsandhome-managerlib's, as well as some extra functions that I defined in the let-block (the.extend()method adds attributes to a set)
{ self, inputs, ... }:
let
swarselsystems =
let
inherit (inputs) systems;
inherit (inputs.nixpkgs) lib;
in
rec {
cidrToSubnetMask = cidr:
let
prefixLength = lib.toInt (lib.last (lib.splitString "/" cidr));
bits = lib.genList (i: if i < prefixLength then 1 else 0) 32;
octets = lib.genList
(i:
let
octetBits = lib.sublist (i * 8) 8 bits;
octetValue = lib.foldl (acc: bit: acc * 2 + bit) 0 octetBits;
in
octetValue
) 4;
subnetMask = lib.concatStringsSep "." (map toString octets);
in
subnetMask;
mkIfElseList = p: yes: no: lib.mkMerge [
(lib.mkIf p yes)
(lib.mkIf (!p) no)
];
mkIfElse = p: yes: no: if p then yes else no;
getSubDomain = domain:
let
parts = builtins.split "\\." domain;
domainParts = builtins.filter (x: builtins.isString x && x != "") parts;
in
if builtins.length domainParts > 0
then builtins.head domainParts
else "";
getBaseDomain = domain:
let
parts = builtins.split "\\." domain;
domainParts = builtins.filter (x: builtins.isString x && x != "") parts;
baseParts = builtins.tail domainParts;
in
builtins.concatStringsSep "." baseParts;
pkgsFor = lib.genAttrs (import systems) (system:
import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
config.allowUnfree = true;
}
);
toCapitalized = str:
if builtins.stringLength str == 0 then
""
else
let
first = builtins.substring 0 1 str;
rest = builtins.substring 1 (builtins.stringLength str - 1) str;
upper = lib.toUpper first;
lower = lib.toLower rest;
in
upper + lower;
mkTrueOption = lib.mkOption {
type = lib.types.bool;
default = true;
};
mkStrong = lib.mkOverride 60;
# forEachSystem = f: lib.genAttrs (import systems) (system: f pkgsFor.${system});
forEachLinuxSystem = f: lib.genAttrs [ "x86_64-linux" "aarch64-linux" ] (system: f pkgsFor.${system});
readHosts = type: lib.attrNames (builtins.readDir "${self}/hosts/${type}");
readNix = type: lib.filter (name: name != "default.nix" && name != "optional" && name != "darwin") (lib.attrNames (builtins.readDir "${self}/${type}"));
mkImports = names: baseDir: lib.map (name: "${self}/${baseDir}/${name}") names;
};
in
{
flake = _:
{
lib = inputs.nixpkgs.lib.extend (_: _: {
inherit (inputs.home-manager.lib) hm;
inherit swarselsystems;
});
swarselsystemsLib = swarselsystems;
homeLib = self.outputs.lib;
};
}
3.4. Packages (pkgs)
This does not use perSystem from flake-parts for package outputs, since some of my custom packages are not able to be built on darwin systems, and I was not yet interested in writing logic for handling that. Instead I use forEachLinuxSystem as described in Library functions in roder to only build this for linux hosts.
Other nix users can make use of these packages either by installing them directly in their config (using my flake as an input and then installing <packages.<systems>.<name>) or by making use of the overlay that I provide in Overlays. In the latter case all packages will be made available to the consuming flake.
You might now be wondering why I then have a perSystem in this file. This has to do with the nixos-extra-modules flake output that I pulled in in flake.nix skeleton (inputs): it provides a few library functions that I want to use in the Globals system. Since however these globals are evaluated to a flake output I need to make sure that these library functions are not only available from within a configuration, but also as a callable expression in the flake - and of course they also should be available in configurations. Generally, lib exists under pkgs, which means that it is built for an architecture. So, if I want to expand lib, I need to make sure this is done for all architectures. For that, I extend the flake-parts perSystem options by a pkgs option using mkTransposedPerSystemModule. This points to a file that will specify the correct pkgs - in this case, this is the same file. We then add a module arg named pkgs which can no make use of the system parameter thanks to what we did above, and set the correct overlay - the self.overlay.defaults includes the overlay from nixos-extra-modules that we need (see Overlays). Finally, we make this pkgs available as an output.
The _module.args part is needed because we need to set/override the flake-parts pkgs as per https://flake.parts/module-arguments.html?highlight=modulewith#pkgs.
More information on the actual packages build can be found in Packages.
{ self, inputs, ... }:
{
imports = [
(
{ lib, flake-parts-lib, ... }:
flake-parts-lib.mkTransposedPerSystemModule {
name = "pkgs";
file = ./packages.nix;
option = lib.mkOption {
type = lib.types.unspecified;
};
}
)
];
flake = _:
let
inherit (self.outputs) lib;
in
{
packages = lib.swarselsystems.forEachLinuxSystem (pkgs: import "${self}/pkgs/flake" { inherit self lib pkgs; });
};
perSystem = { pkgs, system, ... }:
{
# see https://flake.parts/module-arguments.html?highlight=modulewith#persystem-module-parameters
_module.args.pkgs = import inputs.nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [
self.overlays.default
];
};
inherit pkgs;
};
}
3.5. Globals
This file is used to parse each nixosConfiguration present in this flake and scan them for options set under the globals attribute set. I use lib.evalModules to evaluate a mini module system that only consists of these globals options and then load them into the actual configuration by providing a globals output to the flake. This treads a dangerous ground of infinite recursions, which is why both the module system as well as the inherited attributes are kept to the minimal size. Each module has a globals option loaded from a module file which will be separately loaded by this mini-evaluation.
- uses nixpkgs.lib.mapAttrsToList on
config.nodes - uses nixpkgs.lib.flip to reverse the function argument order of the
mapAttrsToListcall, so that we can give the attribute set (config.nodes) first. Alternatively, we could have writtenlib.mapAttrsToList (name: cfg: [...]) config.nodesbut it would be harder to read since there would be a big block between the arguments. - uses nixpkgs.lib.concatLists.
options.config._globalDefsholds theoptions.globals.definitionsfor each node (which in turn basically holds the information that has been set for each node under theglobalsoption), so the concatenated list will look something like[ { services.kanidm.domain = "foo"; }; } { services.freshrss.domain = "bar"; } ]. - uses nixpkgs.lib.mkMerge to merge these seperate attribute sets in the list into one big attribute set (the above attribute set example would become then
{ services = { kanidm.domain = "foo"; freshrss.domain = "bar"; }; }. You can see how this can now be referenced as a "global" set.
I also have a file for global values that cannot be attributed to one nixosConfiguration alsone; the structure of this globals.nix.enc requires a toplevel globals - that means, globals.nix.enc has the structure { globals = [...] }.
Lastly, in order make this actually available to my configurations, i use the inherit (globalsSystem.config.globals) [...] which produces the globals output which I will pass to the specialArgs of my nixosConfigurations, which is when I will be finally able to use these definitions in my config.
Similar to Packages (pkgs), we again create a perSystem module for globals. We want this because we need to ingest the right lib with the extensions from nixos-extra-modules as discussed in Packages (pkgs). One side effect is that instead of a single globals output, we instead create outputs of the form globals.<arch>. This is not a problem as long as we pass one of these in Hosts, but it needs to be kept in mind. In effect, because we overrode pkgs, we can now use the perSystem module argument pkgs which will fetch the right pkgs. Anther method would be using inputs' together with inputs'.pkgs.lib as per https://flake.parts/module-arguments.html?highlight=modulewith#inputs.
# adapted from https://github.com/oddlama/nix-config/blob/main/nix/globals.nix
{ self, inputs, ... }:
{
imports = [
(
{ lib, flake-parts-lib, ... }:
flake-parts-lib.mkTransposedPerSystemModule {
name = "globals";
file = ./globals.nix;
option = lib.mkOption {
type = lib.types.unspecified;
};
}
)
];
perSystem = { lib, pkgs, ... }:
{
globals =
let
globalsSystem = lib.evalModules {
prefix = [ "globals" ];
specialArgs = {
inherit (pkgs) lib;
inherit (self.outputs) nodes;
inherit inputs;
inherit (inputs.topologyPrivate) topologyPrivate;
};
modules = [
../modules/nixos/common/globals.nix
(
{ lib, ... }:
let
sopsImportEncrypted =
assert lib.assertMsg (builtins ? extraBuiltins.sopsImportEncrypted)
"The extra builtin 'sopsImportEncrypted' is not available, so repo.secrets cannot be decrypted. Did you forget to add nix-plugins and point it to `./nix/extra-builtins.nix` ?";
builtins.extraBuiltins.sopsImportEncrypted;
in
{
imports = [
(sopsImportEncrypted ../secrets/repo/globals.nix.enc)
];
}
)
(
{ lib, ... }:
{
globals = lib.mkMerge (
lib.concatLists (
lib.flip lib.mapAttrsToList self.outputs.nodes (
name: cfg:
builtins.addErrorContext "while aggregating globals from nixosConfigurations.${name} into flake-level globals:" cfg.config._globalsDefs
)
)
);
}
)
];
};
in
{
inherit (globalsSystem.config.globals)
domains
services
networks
hosts
user
root
general
;
};
};
}
3.6. Hosts
Here I define my hosts. Earlier (in flake.nix skeleton), I told you how I used to use noweb-ref blocks to achieve this task. You see, a single nixosConfiguration uses nixpkgs.lib.nixosSystem, passing modules and arguments to define itself. I have automated this process by reading all directories in the hosts/ directory and then applying nixpkgs.lib.nixosSystem as a function on these returns. I also provide a nixosConfigurationsMinimal output which is ingested by the flake in install/flake.nix to be used during the initial deployment of a new system (it basically just disables most modules).
Note that the config top-level module attribute includes the entire flake config (this is not the same behaviour as for perSystem).
There are a few interesting specialArgs to be noted:
- we pass
withHomeManagerfor all normal hosts (all hosts that are discovered) to keep compatibility with configurations that do not use home-manager. mkNixosHost: Very much akin to a simple call ofnixpkgs.lib.nixosSystem, I simply definespecialArgsandmodulesthat I want to use for every configuration. Here, I load all the extra modules from my other input flakes. Also, I add theglobalsoutput from Globals and thenodesoutput that I define right here (it simply mirrors all "full" configurations - nixOS and darwin. I like to refer to home-manager only and nix-on-droid as a "half" configurations). It is also here that I set the node name for the configuration (I prefer this explicit call over referencingnetworking.hostNameor such) and the directory that should be used for secrets of a configuration.mkDarwinHostworks in the same way but for darwin machines.mkHalfHostis a function that either creates a pure home-manager configuration or a nix-on-droid one. The type must be explicitly passed when calling the function. Here, again, we make use ofpkgsForthat we defined in Library functions. Also, we make sure to passextraSpecialArgs(the pendant tospecialArgs, just for home-manager configurations).diskoConfigurations: specifies a default disko configuration that is to be used if someone pulls in my flake as a disko configuration. This is not used by me, but I think it is kind of neat.nodes: As stated above, a shorthand for my configurations. Is built using the The '//' operatorguestConfigurations: This holds all microvm hosts.
The rest of the functions are used to build full NixOS systems as well as halfConfigurations regardless of system architecture:
readHostDirssimply gets the config directoriesmkHalfHostsForArchgenerates attribute sets for every halfHost found throughreadHostDirsmkHostsForArchdoes the same for full NixOS configurationsmkConfigurationsPerArchis the wrapper that callsmkHalfHostsForArchormkHostsForArchdepending on the config it is called for, holding all configurations in principalhalfConfigurationsPerArchreturns all halfConfigurations of a certain type (android or home-manager only)ConfigurationsPerArchdoes the same for full NixOS systems (NixOS or darwin). These can further be specialized by passing in the correspondingminimalarg that is used during bootstrapping.
{ self, inputs, ... }:
{
flake = { config, ... }:
let
inherit (self) outputs;
inherit (outputs) lib homeLib;
# lib = (inputs.nixpkgs.lib // inputs.home-manager.lib).extend (_: _: { swarselsystems = import "${self}/lib" { inherit self lib inputs outputs; inherit (inputs) systems; }; });
mkNixosHost = { minimal }: configName: arch:
inputs.nixpkgs.lib.nixosSystem {
specialArgs = {
inherit inputs outputs self minimal homeLib configName arch;
inherit (config.pkgs.${arch}) lib;
inherit (config) nodes topologyPrivate;
globals = config.globals.${arch};
type = "nixos";
withHomeManager = true;
extraModules = [ "${self}/modules/nixos/common/globals.nix" ];
};
modules = [
inputs.disko.nixosModules.disko
inputs.home-manager.nixosModules.home-manager
inputs.impermanence.nixosModules.impermanence
inputs.lanzaboote.nixosModules.lanzaboote
inputs.microvm.nixosModules.host
inputs.microvm.nixosModules.microvm
inputs.nix-index-database.nixosModules.nix-index
inputs.nix-minecraft.nixosModules.minecraft-servers
inputs.nix-topology.nixosModules.default
inputs.nswitch-rcm-nix.nixosModules.nswitch-rcm
inputs.simple-nixos-mailserver.nixosModules.default
inputs.sops.nixosModules.sops
inputs.stylix.nixosModules.stylix
inputs.swarsel-nix.nixosModules.default
inputs.nixos-nftables-firewall.nixosModules.default
(inputs.nixos-extra-modules + "/modules/guests")
(inputs.nixos-extra-modules + "/modules/interface-naming.nix")
"${self}/hosts/nixos/${arch}/${configName}"
"${self}/profiles/nixos"
"${self}/modules/nixos"
{
_module.args.dns = inputs.dns;
microvm.guest.enable = lib.mkDefault false;
networking.hostName = lib.swarselsystems.mkStrong configName;
node = {
name = lib.mkForce configName;
arch = lib.mkForce arch;
type = lib.mkForce "nixos";
secretsDir = ../hosts/nixos/${arch}/${configName}/secrets;
configDir = ../hosts/nixos/${arch}/${configName};
lockFromBootstrapping = lib.mkIf (!minimal) (lib.swarselsystems.mkStrong true);
};
swarselprofiles = {
minimal = lib.mkIf minimal (lib.swarselsystems.mkStrong true);
};
swarselmodules.server = {
ssh = lib.mkIf (!minimal) (lib.swarselsystems.mkStrong true);
};
swarselsystems = {
mainUser = lib.swarselsystems.mkStrong "swarsel";
};
}
];
};
mkDarwinHost = { minimal }: configName: arch:
inputs.nix-darwin.lib.darwinSystem {
specialArgs = {
inherit inputs lib outputs self minimal configName;
inherit (config) nodes topologyPrivate;
withHomeManager = true;
globals = config.globals.${arch};
};
modules = [
# inputs.disko.nixosModules.disko
# inputs.sops.nixosModules.sops
# inputs.impermanence.nixosModules.impermanence
# inputs.lanzaboote.nixosModules.lanzaboote
# inputs.fw-fanctrl.nixosModules.default
# inputs.nix-topology.nixosModules.default
inputs.home-manager.darwinModules.home-manager
"${self}/hosts/darwin/${arch}/${configName}"
"${self}/modules/nixos/darwin"
# needed for infrastructure
"${self}/modules/shared/meta.nix"
"${self}/modules/nixos/common/globals.nix"
{
node = {
name = lib.mkForce configName;
arch = lib.mkForce arch;
type = lib.mkForce "darwin";
secretsDir = ../hosts/darwin/${arch}/${configName}/secrets;
};
}
];
};
mkHalfHost = configName: type: arch:
let
systemFunc = if (type == "home") then inputs.home-manager.lib.homeManagerConfiguration else inputs.nix-on-droid.lib.nixOnDroidConfiguration;
pkgs = lib.swarselsystems.pkgsFor.${arch};
in
systemFunc {
inherit pkgs;
extraSpecialArgs = {
inherit inputs lib outputs self configName arch type;
inherit (config) nodes topologyPrivate;
globals = config.globals.${arch};
minimal = false;
};
modules = [
inputs.stylix.homeModules.stylix
inputs.nix-index-database.homeModules.nix-index
inputs.sops.homeManagerModules.sops
inputs.spicetify-nix.homeManagerModules.default
inputs.swarsel-nix.homeModules.default
"${self}/hosts/${type}/${arch}/${configName}"
"${self}/profiles/home"
"${self}/modules/nixos/common/pii.nix"
{
node = {
name = lib.mkForce configName;
arch = lib.mkForce arch;
type = lib.mkForce type;
secretsDir = ../hosts/${type}/${arch}/${configName}/secrets;
};
}
];
};
linuxArches = [ "x86_64-linux" "aarch64-linux" ];
darwinArches = [ "x86_64-darwin" "aarch64-darwin" ];
mkArches = type: if (type == "nixos") then linuxArches else if (type == "darwin") then darwinArches else linuxArches ++ darwinArches;
readHostDirs = hostDir:
if builtins.pathExists hostDir then
builtins.attrNames
(
lib.filterAttrs (_: type: type == "directory")
(builtins.readDir hostDir)
) else [ ];
mkHalfHostsForArch = type: arch:
let
hostDir = "${self}/hosts/${type}/${arch}";
hosts = readHostDirs hostDir;
in
lib.genAttrs hosts (host: mkHalfHost host type arch);
mkHostsForArch = type: arch: minimal:
let
hostDir = "${self}/hosts/${type}/${arch}";
hosts = readHostDirs hostDir;
in
if (type == "nixos") then
lib.genAttrs hosts (host: mkNixosHost { inherit minimal; } host arch)
else if (type == "darwin") then
lib.genAttrs hosts (host: mkDarwinHost { inherit minimal; } host arch)
else { };
mkConfigurationsPerArch = type: minimal:
let
arches = mkArches type;
toMake = if (minimal == null) then (arch: _: mkHalfHostsForArch type arch) else (arch: _: mkHostsForArch type arch minimal);
in
lib.concatMapAttrs toMake
(lib.listToAttrs (map (a: { name = a; value = { }; }) arches));
halfConfigurationsPerArch = type: mkConfigurationsPerArch type null;
configurationsPerArch = type: minimal: mkConfigurationsPerArch type minimal;
in
{
nixosConfigurations = configurationsPerArch "nixos" false;
nixosConfigurationsMinimal = configurationsPerArch "nixos" true;
darwinConfigurations = configurationsPerArch "darwin" false;
darwinConfigurationsMinimal = configurationsPerArch "darwin" true;
homeConfigurations = halfConfigurationsPerArch "home";
nixOnDroidConfigurations = halfConfigurationsPerArch "android";
guestConfigurations = lib.flip lib.concatMapAttrs config.nixosConfigurations (
_: node:
lib.flip lib.mapAttrs' (node.config.guests or { }) (
guestName: guestDef:
lib.nameValuePair guestDef.nodeName node.config.microvm.vms.${guestName}.config
)
);
diskoConfigurations.default = import "${self}/files/templates/hosts/nixos/disk-config.nix";
nodes = config.nixosConfigurations
// config.darwinConfigurations
// config.guestConfigurations;
"@" = lib.mapAttrs (_: v: v.config.system.build.toplevel) config.nodes;
};
}
3.7. Topology (nix-topology generated network diagram)
This defines some topology for the nix-topology modole that can not otherwise be parsed from the config (or is global). For example, this is used to define a number of switches, printers and routers. The topology graph is built from left to right, meaning that nodes.internet = mkInternet { connections = [ (mkConnection "moonside" "wan") ]; }; means that the node internet 'initiates' the connection to the node moonside (internet will be on the left).
Another note concerning flake-parts:
perSystemis a mechanism that tells flake-parts to build the following attribute set for all systems. This replaces the need to handle myself anysystemorpkgsvariables, this is done by flake-parts. In this case this is needed so that the topology diagram can be built locally.
{ self, inputs, ... }:
{
imports = [
inputs.nix-topology.flakeModule
];
perSystem = { system, ... }:
let
inherit (self.outputs) lib;
in
{
topology.modules = [
({ config, ... }:
let
globals = self.outputs.globals.${system};
inherit (config.lib.topology)
mkInternet
mkDevice
mkSwitch
mkRouter
mkConnection
;
in
{
renderer = "elk";
networks = {
fritz-lan = {
name = "Fritz!Box LAN";
inherit (globals.networks.home-lan) cidrv4 cidrv6;
};
services = {
name = "VLAN: Services";
inherit (globals.networks.home-lan.vlans.services) cidrv4 cidrv6;
};
home = {
name = "VLAN: Home";
inherit (globals.networks.home-lan.vlans.home) cidrv4 cidrv6;
};
devices = {
name = "VLAN: Devices";
inherit (globals.networks.home-lan.vlans.devices) cidrv4 cidrv6;
};
guests = {
name = "VLAN: Guests";
inherit (globals.networks.home-lan.vlans.guests) cidrv4 cidrv6;
};
fritz-wg = {
name = "WireGuard: Fritz!Box tunnel";
inherit (globals.networks.fritz-wg) cidrv4 cidrv6;
};
wgProxy = {
name = "WireGuard: Web proxy tunnel";
inherit (globals.networks.twothreetunnel-wgProxy) cidrv4 cidrv6;
};
wgHome = {
name = "WireGuard: Home proxy tunnel";
inherit (globals.networks.home-wgHome) cidrv4 cidrv6;
};
};
nodes = {
internet = mkInternet {
connections = [
(mkConnection "fritzbox" "dsl")
(mkConnection "magicant" "wifi")
(mkConnection "liliputsteps" "lan")
(mkConnection "treehouse" "eth1")
(mkConnection "toto" "bootstrapper")
(mkConnection "hotel" "demo host")
];
};
fritzbox = mkRouter "FRITZ!Box" {
info = "FRITZ!Box 7682";
image = "${self}/files/topology-images/Fritz!Box_7682.png";
interfaceGroups = [
[
"eth1"
"eth2"
"eth3"
"eth-wan"
"wifi"
]
[ "dsl" ]
];
connections = {
eth1 = mkConnection "winters" "eth1";
eth-wan = mkConnection "hintbooth" "lan";
};
interfaces = {
eth1 = {
addresses = [ globals.networks.home-lan.hosts.fritzbox.ipv4 ];
network = "fritz-lan";
};
eth2 = { };
eth3 = { };
eth-wan = {
addresses = [ globals.networks.home-lan.hosts.fritzbox.ipv4 ];
network = "fritz-lan";
};
wifi = {
addresses = [ globals.networks.home-lan.hosts.fritzbox.ipv4 ];
virtual = true;
renderer.hidePhysicalConnections = true;
network = "fritz-lan";
physicalConnections = [
(mkConnection "pyramid" "wifi")
(mkConnection "bakery" "wifi")
(mkConnection "machpizza" "wifi")
];
};
fritz-wg = {
addresses = [ globals.networks.fritz-wg.hosts.fritzbox.ipv4 ];
network = "fritz-wg";
virtual = true;
renderer.hidePhysicalConnections = true;
type = "wireguard";
physicalConnections = [
(mkConnection "pyramid" "fritz-wg")
(mkConnection "magicant" "fritz-wg")
];
};
};
};
switch-livingroom = mkSwitch "Switch Livingroom" {
info = "TL-SG108E";
image = "${self}/files/topology-images/TL-SG108E.png";
interfaceGroups = [
# trunk
[ "eth1" ]
# devices
[ "eth2" "eth5" "eth6" ]
# home
[ "eth3" "eth8" ]
# guests
[ "eth4" "eth7" ]
];
interfaces = {
eth2 = { network = lib.mkForce "devices"; };
eth3 = { network = lib.mkForce "home"; };
eth5 = { network = lib.mkForce "devices"; };
eth6 = { network = lib.mkForce "devices"; };
eth7 = { network = lib.mkForce "guests"; };
eth8 = { network = lib.mkForce "home"; };
};
connections = {
eth2 = mkConnection "nswitch" "eth1";
eth3 = mkConnection "bakery" "eth1";
eth5 = mkConnection "ps4" "eth1";
eth6 = mkConnection "ender3" "eth1";
eth7 = mkConnection "pc" "eth1";
eth8 = mkConnection "pyramid" "eth1";
};
};
switch-bedroom = mkDevice "Switch Bedroom" {
info = "Cisco SG 200-08";
image = "${self}/files/topology-images/Cisco_SG_200-08.png";
interfaceGroups = [
# trunk
[ "eth1" ]
# devices
[ "eth2" ]
# guests
[ "eth3" "eth4" "eth5" "eth6" "eth7" "eth8" ]
];
interfaces = {
eth2 = { network = lib.mkForce "devices"; };
eth3 = { network = lib.mkForce "guests"; };
};
connections = {
eth2 = mkConnection "printer" "eth1";
eth3 = mkConnection "machpizza" "eth1";
};
};
nswitch = mkDevice "Nintendo Switch" {
info = "Atmosphère 1.3.2 @ FW 19.0.1";
image = "${self}/files/topology-images/nintendo-switch.png";
interfaces.eth1 = { };
};
ps4 = mkDevice "PlayStation 4" {
info = "GoldHEN @ FW 5.05";
image = "${self}/files/topology-images/ps4.png";
interfaces.eth1 = { };
};
ender3 = mkDevice "Ender 3" {
info = "SKR V1.3, TMC2209 (Dual), TFT35";
image = "${self}/files/topology-images/ender3.png";
interfaces.eth1 = { };
services = {
octoprint = {
name = "OctoPrint";
icon = "${self}/files/topology-images/octoprint.png";
};
};
};
magicant = mkDevice "magicant" {
icon = "${self}/files/topology-images/phone.png";
info = "Samsung Z Flip 6";
image = "${self}/files/topology-images/zflip6.png";
interfaces = {
wifi = { };
fritz-wg.network = "fritz-wg";
};
};
machpizza = mkDevice "machpizza" {
info = "MacBook Pro 2016";
icon = "devices.laptop";
deviceIcon = "${self}/files/topology-images/mac.png";
interfaces = {
eth1.network = "guests";
wifi = { };
};
};
treehouse = mkDevice "treehouse" {
info = "NVIDIA DGX Spark";
icon = "${self}/files/topology-images/home-manager.png";
deviceIcon = "${self}/files/topology-images/dgxos.png";
interfaces = {
eth1 = { };
wifi = { };
};
services = {
ollama = {
name = "Ollama";
icon = "${self}/files/topology-images/ollama.png";
};
openwebui = {
name = "Open WebUI";
icon = "${self}/files/topology-images/openwebui.png";
};
comfyui = {
name = "Comfy UI";
icon = "${self}/files/topology-images/comfyui.png";
};
};
};
pc = mkDevice "Chaostheater" {
info = "ASUS Z97-A, i7-4790k, GTX970, 32GB RAM";
icon = "${self}/files/topology-images/windows.png";
deviceIcon = "${self}/files/topology-images/atlasos.png";
services = {
sunshine = {
name = "Sunshine";
icon = "${self}/files/topology-images/sunshine.png";
};
};
interfaces.eth1.network = "guests";
};
printer = mkDevice "Printer" {
info = "DELL C2665dnf";
image = "${self}/files/topology-images/DELL-C2665dnf.png";
interfaces.eth1 = { };
};
};
})
];
};
}
3.8. Devshell (checks/pre-commit-hooks)
This file defines a number of checks that can either be run by calling nix flake check or while in a nix-shell or nix develop. This helps me make sure that my flake confirms to my self-imposed standards. The GitHub actions perform less checks than are being done here (they are only checking the formatting, as well as statix and deadnix).
The devshell also provides a number of useful shorthand commands, as well as a 'safe' version of nixpkgs that I can use to rebuild from in case a version bump in nixpkgs suddenly breaks nix-plugins.
Aside from the default devShell which is the one that should usually be called interactively, I also define a deploy devshell: this one compiles nix-plugins against an earlier version of nix, which is needed so that the version nixos-anywhere that I am using works. However, that version is a little annoying since it had a bug in nix-plugins that is here fixed using a patch file. I guess it also serves as another fallback should problems with the current nix-plugins version arise.
{ self, inputs, ... }:
{
imports = [
inputs.devshell.flakeModule
inputs.pre-commit-hooks.flakeModule
];
perSystem = { pkgs, config, system, ... }:
{
pre-commit = {
check.enable = true;
settings = {
addGcRoot = true;
hooks = {
check-added-large-files.enable = true;
check-case-conflicts.enable = true;
check-executables-have-shebangs.enable = true;
check-shebang-scripts-are-executable.enable = false;
check-merge-conflicts.enable = true;
deadnix.enable = true;
detect-private-keys.enable = true;
end-of-file-fixer.enable = true;
fix-byte-order-marker.enable = true;
flake-checker.enable = true;
forbid-new-submodules.enable = true;
mixed-line-endings.enable = true;
nixpkgs-fmt.enable = true;
statix.enable = true;
trim-trailing-whitespace.enable = true;
treefmt.enable = true;
destroyed-symlinks = {
enable = true;
entry = "${inputs.pre-commit-hooks.checks.${system}.pre-commit-hooks}/bin/destroyed-symlinks";
};
shellcheck = {
enable = true;
entry = "${pkgs.shellcheck}/bin/shellcheck --shell=bash";
};
shfmt = {
enable = true;
entry = "${pkgs.shfmt}/bin/shfmt -i 4 -sr -d -s -l";
};
};
};
};
devshells = {
deploy =
let
nix-version = "2_28";
in {
packages = [
(builtins.trace "alarm: pinned nix_${nix-version}" pkgs.stable25_05.nixVersions."nix_${nix-version}")
pkgs.git
pkgs.just
pkgs.age
pkgs.ssh-to-age
pkgs.sops
];
env =
[
{
name = "NIX_CONFIG";
value = ''
plugin-files = ${pkgs.stable25_05.nix-plugins.overrideAttrs (o: {
buildInputs = [pkgs.stable25_05.nixVersions."nix_${nix-version}" pkgs.stable25_05.boost];
patches = (o.patches or []) ++ [./nix-plugins.patch];
})}/lib/nix/plugins
extra-builtins-file = ${self + /nix/extra-builtins.nix}
'';
}
];
};
default =
let
nix-version = "2_30";
in
{
packages = [
(builtins.trace "alarm: pinned nix_${nix-version}" pkgs.nixVersions."nix_${nix-version}")
pkgs.git
pkgs.just
pkgs.age
pkgs.ssh-to-age
pkgs.sops
pkgs.nixpkgs-fmt
self.packages.${system}.swarsel-build
self.packages.${system}.swarsel-deploy
(pkgs.symlinkJoin {
name = "home-manager";
buildInputs = [ pkgs.makeWrapper ];
paths = [ pkgs.home-manager ];
postBuild = ''
wrapProgram $out/bin/home-manager \
--append-flags '--flake .#$(hostname)'
'';
})
];
commands = [
{
package = pkgs.statix;
help = "Lint flake";
}
{
package = pkgs.deadnix;
help = "Check flake for dead code";
}
{
package = pkgs.nix-tree;
help = "Interactively browse dependency graphs of Nix derivations";
}
{
package = pkgs.nvd;
help = "Diff two nix toplevels and show which packages were upgraded";
}
{
package = pkgs.nix-diff;
help = "Explain why two Nix derivations differ";
}
{
package = pkgs.nix-output-monitor;
help = "Nix Output Monitor (a drop-in alternative for `nix` which shows a build graph)";
name = "nom \"$@\"";
}
{
name = "hm";
help = "Manage home-manager config";
command = "home-manager \"$@\"";
}
{
name = "fmt";
help = "Format flake";
command = "nixpkgs-fmt --check \"$FLAKE\"";
}
{
name = "sd";
help = "Build and deploy this nix config to nodes";
command = "swarsel-deploy \"$@\"";
}
{
name = "sl";
help = "Build and deploy a config to nodes";
command = "swarsel-deploy \${1} switch";
}
{
name = "sw";
help = "Build and switch to the host's config locally";
command = "swarsel-deploy $(hostname) switch";
}
{
name = "bld";
help = "Build a number of configurations";
command = "swarsel-build \"$@\"";
}
{
name = "c";
help = "Work with the flake git repository";
command = "git --git-dir=$FLAKE/.git --work-tree=$FLAKE/ \"$@\"";
}
];
# devshell.startup.pre-commit-install.text = "pre-commit install";
devshell.startup.pre-commit.text = config.pre-commit.installationScript;
env =
let
nix-plugins = pkgs.nix-plugins.override {
nixComponents = pkgs.nixVersions."nixComponents_${nix-version}";
};
in
[
{
name = "NIX_CONFIG";
value = ''
plugin-files = ${nix-plugins}/lib/nix/plugins
extra-builtins-file = ${self + /nix/extra-builtins.nix}
'';
}
];
};
};
};
}
3.9. Templates
This file defines the templates that are being exposed by the flake. These can be used by running nix flake init -t github:Swarsel/.dotfiles#<TEMPLATE_NAME> by others. Personally, I mostly use these as part of the project utility.
Otherwise, I define the function mkTemplates here which builds a named attribute set for each type of template that I have.
- uses builtins.listToAttrs
{ self, ... }:
{
flake = _: {
templates =
let
mkTemplates = names: builtins.listToAttrs (map
(name: {
inherit name;
value = {
path = "${self}/files/templates/${name}";
description = "${name} project ";
};
})
names);
templateNames = [
"python"
"rust"
"go"
"cpp"
"latex"
"default"
];
in
mkTemplates templateNames;
};
}
3.10. Formatter (treefmt-nix)
Defines a formatter that can be called using nix fmt.
Usually all formatting in this repo is done automatically while editing in emacs. However, it is nice to have a backup formatter to rely on and treefmt is extermely nice to work with, as it allows setting formatters for all kinds of aspects of the flake.
{ inputs, ... }:
{
imports = [
inputs.treefmt-nix.flakeModule
];
perSystem = { pkgs, ... }: {
# formatter is set by treefmt to:
# formatter = lib.mkIf config.treefmt.flakeFormatter (lib.mkDefault config.treefmt.build.wrapper);
treefmt = {
projectRootFile = "flake.nix";
programs = {
nixfmt = {
enable = true;
package = pkgs.nixpkgs-fmt;
};
deadnix.enable = true;
statix.enable = true;
shfmt = {
enable = true;
indent_size = 4;
simplify = true;
# needed to replicate what my Emacs shfmt does
# there is no builtin option for space-redirects
package = pkgs.symlinkJoin {
name = "shfmt";
buildInputs = [ pkgs.makeWrapper ];
paths = [ pkgs.shfmt ];
postBuild = ''
wrapProgram $out/bin/shfmt \
--add-flags '-sr'
'';
};
};
shellcheck.enable = true;
};
settings.formatter.shellcheck.options = [
"--shell"
"bash"
];
};
};
}
3.11. Modules
This exposes all of my modular configuration as modules. Other people can use them in their flake using imports = [ inputs.<name>.nixosModules ];. Per default, this enables some mechanisms like config sharing between nodes and the globals system.
nixosModules is a `defined` flake output, where external tools might expect some sort of structure; hence, I call the default output default, which will, in many cases, allow the user to just reference to the flake itself (which will then use nixosModules.default automatically.
homeModules on the other hand is not standardized in this way (for example, many flakes refer to homeManagerModules instead); in order not to unnecessarily break things, I leave it as is.
{ self, ... }:
{
flake = _:
let
inherit (self.outputs) lib;
in
{
nixosModules.default = import "${self}/modules/nixos" { inherit lib; };
homeModules = import "${self}/modules/home" { inherit lib; };
};
}
3.12. Apps
This defines some apps; they differ from normal packages in that they can be called using nix run <repo><appName>. So, for example, I can call my deployment script using nix run --experimental-features 'nix-command flakes' github:Swarsel/.dotfiles -- -n <CONFIGURATION_NAME> -d <TARGET_IP> (here I did not specify #swarsel-bootstrap since it is set as the default. In general, whenever the #... part is ommitted, the object under the default attribute will be used. This is also true for nixosConfigurations: in that case, the default will be the current hostname of the machine).
- uses builtins.listToAttrs
- uses The '//' operator to add the default output to thte set of built apps.
{ self, ... }:
{
perSystem = { system, ... }:
let
mkApps = system: names: self: builtins.listToAttrs (map
(name: {
inherit name;
value = {
type = "app";
program = "${self.packages.${system}.${name}}/bin/${name}";
meta = {
description = "Custom app ${name}.";
};
};
})
names);
appNames = [
"swarsel-bootstrap"
"swarsel-install"
"swarsel-rebuild"
"swarsel-postinstall"
];
appSet = mkApps system appNames self;
in
{
apps = appSet // {
default = appSet.swarsel-bootstrap;
};
};
}
3.13. Overlays
In this section I define packages that I manually add to nixpkgs, or that I want to use in a modified way. This can be useful for packages that are currently awaiting a PR or public packages that I do not want to maintain.
As such, I also define three additional local overlays:
additionsThese are for the aforementioned added packages. NOTE: The packages themselves are built in Packages (pkgs); here, we just add them to the overlay that we then use in the configuration.modificationThese are for packages that are on nixpkgs, but do not fit my usecase, meaning I need to perform modifications on them.nixpkgs-stable-versionsThese are simply mirrors of other branches of nixpkgs (mostly past stable branches). Useful for packages that are broken on nixpkgs, but do not need to be on bleeding edge anyways. Automatically fetches all inputs namesnixpkgs-<suffix>and adds them under the name in<suffix>.
As part of the modifications, I add some of my own library functions to be used alongside the functions provided by nixpkgs and home-manager.
On the structure of overlays: as you notice, all of the attributes within overlays are functions which take final and prev as arguments. This is a convention (sometimes you also see super instead of final) that aims to tell you that final represents the pkgs set after it has gone over all modifications, while prev is the pkgs set before the current modification.
- So, in
additions, thefinalset is the same as inmodifications, but theirprevsets might differ (in this case, I believe they will be the same since all modifications are done at the same step). - This starts to make a difference when you use multiple overlays and have one overlay depend on the modifications in another overlay.
- The
_argument is used like in a number of other programing languages and signals that the argument is never actually used in the function.
{ self, inputs, ... }:
let
inherit (self) outputs;
inherit (outputs) lib;
in
{
flake = _:
{
overlays = {
default = final: prev:
let
additions = final: _: import "${self}/pkgs/flake" { pkgs = final; inherit self lib; }
// {
swarsel-nix = import inputs.swarsel-nix {
pkgs = prev;
};
zjstatus = inputs.zjstatus.packages.${prev.system}.default;
};
modifications = final: prev: {
# vesktop = prev.vesktop.override {
# withSystemVencord = true;
# };
lib = prev.lib // {
swarselsystems = self.outputs.swarselsystemsLib;
hm = self.outputs.homeLib;
};
firefox = prev.firefox.override {
nativeMessagingHosts = [
prev.tridactyl-native
prev.browserpass
# prev.plasma5Packages.plasma-browser-integration
];
};
isync = prev.isync.override {
withCyrusSaslXoauth2 = true;
};
mgba = final.swarsel-mgba;
retroarch = prev.retroarch.withCores (cores: with cores; [
snes9x # snes
nestopia # nes
dosbox # dos
scummvm # scumm
vba-m # gb/a
mgba # gb/a
melonds # ds
dolphin # gc/wii
]);
};
nixpkgs-stable-versions = final: _:
let
nixpkgsInputs =
lib.filterAttrs
(name: _v: builtins.match "^nixpkgs-.*" name != null)
inputs;
rename = name: builtins.replaceStrings [ "nixpkgs-" ] [ "" ] name;
mkPkgs = src:
import src {
inherit (final) system;
config.allowUnfree = true;
};
in
builtins.listToAttrs (map
(name: {
name = rename name;
value = mkPkgs nixpkgsInputs.${name};
})
(builtins.attrNames nixpkgsInputs));
in
lib.recursiveUpdate
(
(additions final prev)
// (nixpkgs-stable-versions final prev)
// (inputs.niri-flake.overlays.niri final prev)
// (inputs.vbc-nix.overlays.default final prev)
// (inputs.nur.overlays.default final prev)
// (inputs.emacs-overlay.overlay final prev)
// (inputs.nix-topology.overlays.default final prev)
// (inputs.nix-index-database.overlays.nix-index final prev)
// (inputs.nixgl.overlay final prev)
// (inputs.nix-minecraft.overlay final prev)
// (inputs.nixos-extra-modules.overlays.default final prev)
)
(modifications final prev);
};
};
}
3.14. Installer images (iso, kexec)
This sections makes use of nixos-generators in order to easily allow me to build a live ISO of my installer system. It can be built using nix build --print-out-paths --no-link <flake path>#live-iso, and can then be copied to a USB drive using, for example, dd.
In a similar way, nix build --print-out-paths --no-link <flake-path>.#pnap-kexec --system <system> will build the kexec tarball that I need when using disko to deploy to a low-RAM systems.
This is an improvement to what I did earlier, where I did not use nixos-generators but instead manually imported the needed modules to make this configration into a bootable USB image. Now, I can just write this in the same way that I would to write any other configuration.
{ self, inputs, ... }:
{
perSystem = { pkgs, system, ... }:
{
packages = {
# nix build --print-out-paths --no-link .#live-iso
live-iso = inputs.nixos-generators.nixosGenerate {
inherit pkgs;
specialArgs = { inherit self; };
modules = [
inputs.home-manager.nixosModules.home-manager
"${self}/install/installer-config.nix"
];
format =
{
x86_64-linux = "install-iso";
aarch64-linux = "sd-aarch64-installer";
}.${system};
};
# nix build --print-out-paths --no-link .#pnap-kexec --system <system>
swarsel-kexec = (inputs.smallpkgs.legacyPackages.${system}.nixos [
{
imports = [ "${self}/install/kexec.nix" ];
_file = __curPos.file;
system.kexec-installer.name = "swarsel-kexec";
}
inputs.nixos-images.nixosModules.kexec-installer
]).config.system.build.kexecInstallerTarball;
};
};
}
3.15. Installer flake
When using tools like (the builtin) nixos-rebuild or nixos-anywhere, these tools expect the flake to have a certain structure; namely, they expect to find an output named nixosConfigurations, which is implicitely used when passing --flake .#<config name> (it is used in front of <config name>).
When I define my configurations, I am actually defining two versions for each actual system:
- One 'regular' config that should be used by all rebuild tools such as
nixos-rebuild - One 'minimal' config that should be used by
nixos-anywhereduring initial deployment of a system
Now, I could of course define a <name> and <name>-minimal attribute for each configuration and just put these under nixosConfigurations, but that would have several drawbacks:
- evaluation time would increase
- my
nodesoutput (that holds information for all actual systems) would bloat - it is actually not clear that
<name>and<name>-minimalrepresent the same config
Hence, what I instead do is to define another output nixosConfigurationsMinimal as an output to this flakes' config, and then use it to set the nixosConfigurations of another, minimal, flake that I keep in install/. When using nixos-anywhere during initial deployment, I will then point it to that minimal flake, where the minimal configs can be found.
{
description = "Minimal installer flake - automatically generated by SwarselSystems.org";
inputs.swarsel.url = "./..";
outputs = { swarsel, ... }: { nixosConfigurations = swarsel.nixosConfigurationsMinimal; };
}
4. System
This holds most of the NixOS side of configuration.
4.1. Manual steps when setting up a new machine
In the Introduction (no code), I mentioned that this is a nearly fully declarative config. In fact, most client configs are in one way or another not fully declarative. I use oneshotting systemd services + sentinel files for most such tasks (which makes them declarative!), but some of them I would rather perform manually once. This mainly concerns work related things.
Whenever I encounter a configuration bit that needs manual steps, I use a Appendix A: Noweb-Ref blocks to tangle that bit of information into a central place (here). I discern between the following scenarios:
setup: Used in a standard NixOs + home-manager deploymentworksetup: Stuff to be done only on work machineshomemanageronlysetup: Steps that are needed only on machines that are not running NixOs.
4.2. TODO Current issues
Besides the manual steps outlined above, sometimes things break when I update this flake. The fix, for me, is most of the times one of these two:
- instead of the broken package, use the package from the latest stable nixpkgs release where the package is still functoning (this is why I pull all of these in as inputs)
- if the broken component is critical, I perform manual patches/overrides.
In order to keep track of these changes, I gather them here in a similar style to what you saw in Manual steps when setting up a new machine. I simply prefix them with the date and check them after a while to see if things got better. TODO: this list is not comprehensive probably
Currently, these adaptions are made to the configuration to account for bugs in upstream repos: - 202501102: - flake: - emacs-overlay: - : version pinned because emacsclient is currently broken on latest - niri-flake: - currently not using the sugared version of screenshot-[,window], as it is currently broken - home-manager: - emacs-tramp: - using stable version in extraPackages (broken in unstable) - :ensure nil in emacs tramp settings to use package in extraPackages - emacs-calfwL - pinned to version not in nixpkgs (is in latest emacs-overlay, but that is broken) - vesktop: - running stable version (broken in unstable) - batgrep: - running stable version (broken in unstable) - swayosd: - pinned to version not in nixpkgs (fixes https://github.com/ErikReider/SwayOSD/issues/175)4.3. System specific configuration
This section mainly exists to house different default.nix files to define some modules that should be loaded on respective systems.
Every host is housed in the hosts/ directory, which is then subdivided by each respective system (nixos/, home-manager/, nix-on-droid/, darwin/). As described earlier, some of these configurations (nixos and darwin) can be defined automatically in this flake. For home-manager and nix-on-droid, the system architecture must be defined manually.
4.3.1. TODO Template
This is the template that I use for new deployments of personal machines. Servers are usually highly tailored to their specific task and I do not consider it worth a time to craft a template for that. Also, at least at the current time, I only provide a template for NixOS hosts, as I rarely ever use anything else.
TODO: I dont think this template would currently work out of the box
4.3.1.1. Main Configuration
{ self, config, inputs, pkgs, lib, ... }:
let
primaryUser = config.swarselsystems.mainUser;
modulesPath = "${self}/modules";
sharedOptions = {
isBtrfs = true;
};
in
{
imports = [
# ---- nixos-hardware here ----
./hardware-configuration.nix
./disk-config.nix
"${modulesPath}/nixos/optional/virtualbox.nix"
# "${modulesPath}/nixos/optional/vmware.nix"
"${modulesPath}/nixos/optional/autologin.nix"
"${modulesPath}/nixos/optional/nswitch-rcm.nix"
"${modulesPath}/nixos/optional/gaming.nix"
inputs.home-manager.nixosModules.home-manager
{
home-manager.users."${primaryUser}".imports = [
"${modulesPath}/home/optional/gaming.nix"
];
}
];
boot = {
kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
};
networking = {
hostName = "TEMPLATE";
firewall.enable = true;
};
swarselsystems = lib.recursiveUpdate
{
wallpaper = self + /files/wallpaper/lenovowp.png;
hasBluetooth = true;
hasFingerprint = true;
isImpermanence = true;
isSecureBoot = true;
isCrypted = true;
isSwap = true;
swapSize = "32G";
rootDisk = "TEMPLATE";
}
sharedOptions;
home-manager.users."${primaryUser}".swarselsystems = lib.recursiveUpdate
{
isLaptop = true;
isNixos = true;
cpuCount = 16;
}
sharedOptions;
}
4.3.1.2. disko
Acceptance of arbitraty argumments is here needed because disko passes diskoFile to this file.
{ lib, pkgs, config, ... }:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko.devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
environment.systemPackages = [
pkgs.yubikey-manager
];
}
4.3.2. Physical hosts
This is a list of all physical machines that I maintain.
4.3.2.1. pyramid (Framework Laptop 16)
My work machine. Built for more security, this is the gold standard of my configurations at the moment.
4.3.2.1.1. Main Configuration
{ self, config, inputs, lib, minimal, ... }:
let
primaryUser = config.swarselsystems.mainUser;
in
{
imports = [
inputs.nixos-hardware.nixosModules.framework-16-7040-amd
./disk-config.nix
./hardware-configuration.nix
"${self}/modules/nixos/optional/amdcpu.nix"
"${self}/modules/nixos/optional/amdgpu.nix"
"${self}/modules/nixos/optional/framework.nix"
"${self}/modules/nixos/optional/gaming.nix"
"${self}/modules/nixos/optional/hibernation.nix"
"${self}/modules/nixos/optional/nswitch-rcm.nix"
"${self}/modules/nixos/optional/virtualbox.nix"
"${self}/modules/nixos/optional/work.nix"
];
topology.self = {
interfaces = {
eth1.network = lib.mkForce "home";
wifi = { };
fritz-wg.network = "fritz-wg";
};
};
swarselsystems = {
lowResolution = "1280x800";
highResolution = "2560x1600";
isLaptop = true;
isNixos = true;
isBtrfs = true;
isLinux = true;
sharescreen = "eDP-2";
info = "Framework Laptop 16, 7940HS, RX7700S, 64GB RAM";
firewall = lib.mkForce true;
wallpaper = self + /files/wallpaper/lenovowp.png;
hasBluetooth = true;
hasFingerprint = true;
isImpermanence = false;
isSecureBoot = true;
isCrypted = true;
inherit (config.repo.secrets.local) hostName;
inherit (config.repo.secrets.local) fqdn;
hibernation.offset = 533760;
};
home-manager.users."${primaryUser}" = {
swarselsystems = {
isSecondaryGpu = true;
SecondaryGpuCard = "pci-0000_03_00_0";
cpuCount = 16;
temperatureHwmon = {
isAbsolutePath = true;
path = "/sys/devices/virtual/thermal/thermal_zone0/";
input-filename = "temp4_input";
};
monitors = {
main = {
# name = "BOE 0x0BC9 Unknown";
name = "BOE 0x0BC9";
mode = "2560x1600";
scale = "1";
position = "2560,0";
workspace = "15:L";
output = "eDP-2";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
personal = true;
};
# networking.nftables = {
# enable = lib.mkForce false;
# firewall.enable = lib.mkForce false;
# };
}
4.3.2.1.2. hardware-configuration
dcdebugmask enums: https://docs.kernel.org/gpu/amdgpu/driver-core.html#c.DC_DEBUG_MASK
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[
(modulesPath + "/installer/scan/not-detected.nix")
];
# Fix Wlan after suspend or Hibernate
# environment.etc."systemd/system-sleep/fix-wifi.sh".source =
# pkgs.writeShellScript "fix-wifi.sh" ''
# case $1/$2 in
# pre/*)
# ${pkgs.kmod}/bin/modprobe -r mt7921e mt792x_lib mt76
# echo 1 > /sys/bus/pci/devices/0000:04:00.0/remove
# ;;
# post/*)
# ${pkgs.kmod}/bin/modprobe mt7921e
# echo 1 > /sys/bus/pci/rescan
# ;;
# esac
# '';
boot = {
kernelPackages = lib.mkDefault pkgs.kernel.linuxPackages;
# kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
binfmt.emulatedSystems = [ "aarch64-linux" ];
initrd = {
availableKernelModules = [
"nvme"
"xhci_pci"
"thunderbolt"
"usb_storage"
"cryptd"
"usbhid"
"sd_mod"
"r8152"
"drm"
"drm_kms_helper"
"ttm"
"gpu_sched"
];
# allow to remote build on arm (needed for moonside)
kernelModules = [ "sg" ];
luks.devices."cryptroot" = {
# improve performance on ssds
bypassWorkqueues = true;
preLVM = true;
# crypttabExtraOpts = ["fido2-device=auto"];
};
};
kernelModules = [ "amdgpu" "kvm-amd" ];
kernelParams = [
# deep sleep is discontinued by amd
# "mem_sleep_default=deep"
# supposedly, this helps save power on laptops
# in reality (at least on this model), this just generate excessive heat on the CPUs
# "amd_pstate=passive"
# Fix screen flickering issue at the cost of battery life (disable PSR and PSR-SU, keep PR enabled)
# TODO: figure out if this is worth it
# test PSR/PR state with 'sudo grep '' /sys/kernel/debug/dri/0000*/eDP-2/*_capability'
# ref:
# https://old.reddit.com/r/framework/comments/1goh7hc/anyone_else_get_this_screen_flickering_issue/
# https://www.reddit.com/r/NixOS/comments/1hjruq1/graphics_corruption_on_kernel_6125_and_up/
# https://gitlab.freedesktop.org/drm/amd/-/issues/3797
"amdgpu.dcdebugmask=0x410"
];
extraModulePackages = [ ];
};
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp196s0f3u1c2.useDHCP = lib.mkDefault true;
# networking.interfaces.wlp4s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
4.3.2.1.3. disko
{
disko.devices = {
disk = {
nvme0n1 = {
type = "disk";
device = "/dev/nvme0n1";
content = {
type = "gpt";
partitions = {
ESP = {
label = "boot";
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [
"defaults"
];
};
};
luks = {
size = "100%";
label = "luks";
content = {
type = "luks";
name = "cryptroot";
extraOpenArgs = [
"--allow-discards"
"--perf-no_read_workqueue"
"--perf-no_write_workqueue"
];
# https://0pointer.net/blog/unlocking-luks2-volumes-with-tpm2-fido2-pkcs11-security-hardware-on-systemd-248.html
settings = { crypttabExtraOpts = [ "fido2-device=auto" "token-timeout=10" ]; };
content = {
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ];
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [ "subvol=root" "compress=zstd" "noatime" ];
};
"/home" = {
mountpoint = "/home";
mountOptions = [ "subvol=home" "compress=zstd" "noatime" ];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [ "subvol=nix" "compress=zstd" "noatime" ];
};
"/persist" = {
mountpoint = "/persist";
mountOptions = [ "subvol=persist" "compress=zstd" "noatime" ];
};
"/log" = {
mountpoint = "/var/log";
mountOptions = [ "subvol=log" "compress=zstd" "noatime" ];
};
"/swap" = {
mountpoint = "/swap";
swap.swapfile.size = "64G";
};
};
};
};
};
};
};
};
};
};
fileSystems = {
"/persist".neededForBoot = true;
"/home".neededForBoot = true;
"/".neededForBoot = true; # this is ok because this is not a impermanence host
"/var/log".neededForBoot = true;
};
}
4.3.2.2. Bakery (Lenovo ThinkPad)
My personal laptop. Closely follows the pyramid config, but leaves out some security features that I consider a bother on my work machine.
4.3.2.2.1. Main Configuration
{ self, config, inputs, lib, minimal, ... }:
let
primaryUser = config.swarselsystems.mainUser;
in
{
imports = [
inputs.nixos-hardware.nixosModules.common-cpu-intel
./disk-config.nix
./hardware-configuration.nix
"${self}/modules/nixos/optional/gaming.nix"
"${self}/modules/nixos/optional/nswitch-rcm.nix"
"${self}/modules/nixos/optional/virtualbox.nix"
];
topology.self.interfaces = {
eth1.network = lib.mkForce "home";
wifi = { };
};
swarselsystems = {
isLaptop = true;
isNixos = true;
isBtrfs = true;
isLinux = true;
lowResolution = "1280x800";
highResolution = "1920x1080";
sharescreen = "eDP-1";
info = "Lenovo Ideapad 720S-13IKB";
firewall = lib.mkForce true;
wallpaper = self + /files/wallpaper/lenovowp.png;
hasBluetooth = true;
hasFingerprint = true;
isImpermanence = true;
isSecureBoot = false;
isCrypted = true;
isSwap = true;
rootDisk = "/dev/nvme0n1";
swapSize = "4G";
};
home-manager.users."${primaryUser}" = {
# home.stateVersion = lib.mkForce "23.05";
swarselsystems = {
monitors = {
main = {
name = "LG Display 0x04EF Unknown";
mode = "1920x1080"; # TEMPLATE
scale = "1";
position = "1920,0";
workspace = "15:L";
output = "eDP-1";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
personal = true;
};
}
4.3.2.2.2. hardware-configuration
# Do not modify this file! It was generated by ‘nixos-generate-config’
# and may be overwritten by future invocations. Please make changes
# to /etc/nixos/configuration.nix instead.
{ config, lib, modulesPath, ... }:
{
imports =
[
(modulesPath + "/installer/scan/not-detected.nix")
];
boot = {
initrd = {
availableKernelModules = [ "xhci_pci" "nvme" "usb_storage" "sd_mod" ];
kernelModules = [ ];
};
kernelModules = [ ];
extraModulePackages = [ ];
};
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
4.3.2.2.3. disko
{ lib, pkgs, config, ... }:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko.devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
environment.systemPackages = [
pkgs.yubikey-manager
];
}
4.3.2.3. Winters (Server: ASRock J4105-ITX)
This is my main server that I run at home. It handles most tasks that require bigger amounts of storage than I can receive for free at OCI. Also it houses some data that I find too sensitive to hand over to Oracle.
4.3.2.3.1. Main Configuration
{ self, lib, minimal, globals, ... }:
{
imports = [
./hardware-configuration.nix
"${self}/modules/nixos/optional/systemd-networkd-server.nix"
"${self}/modules/nixos/optional/nix-topology-self.nix"
];
topology.self.interfaces."eth1" = { };
boot = {
loader.systemd-boot.enable = true;
loader.efi.canTouchEfiVariables = true;
};
networking.hosts = {
${globals.networks.home-lan.hosts.hintbooth.ipv4} = [ "server.hintbooth.${globals.domains.main}" ];
${globals.networks.home-lan.hosts.hintbooth.ipv6} = [ "server.hintbooth.${globals.domains.main}" ];
};
swarselsystems = {
info = "ASRock J4105-ITX, 32GB RAM";
flakePath = "/root/.dotfiles";
isImpermanence = false;
isSecureBoot = false;
isCrypted = false;
isBtrfs = false;
isLinux = true;
isNixos = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
wgHome = {
isClient = true;
serverName = "hintbooth";
};
};
restic = {
bucketName = "SwarselWinters";
paths = [
"/Vault/data/paperless"
"/Vault/data/koillection"
"/Vault/data/postgresql"
"/Vault/data/firefly-iii"
"/Vault/data/radicale"
"/Vault/data/matrix-synapse"
"/Vault/Eternor/Paperless"
"/Vault/Eternor/Bilder"
"/Vault/Eternor/Immich"
];
};
garage = {
data_dir = {
capacity = "200G";
path = "/Vault/data/garage/data";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
server = true;
};
swarselmodules.server = {
diskEncryption = lib.mkForce false;
# nginx = true; # for php stuff
# acme = false; # cert handled by proxy
# wireguard = true;
# nfs = true;
# kavita = true;
# restic = true;
# jellyfin = true;
# navidrome = true;
# spotifyd = true;
# mpd = true;
# postgresql = true;
# matrix = true;
# nextcloud = true;
# immich = true;
# paperless = true;
# transmission = true;
# syncthing = true;
# grafana = true;
# freshrss = true;
# kanidm = true;
# firefly-iii = true;
# koillection = true;
# radicale = true;
# atuin = true;
# forgejo = true;
# ankisync = true;
# homebox = true;
# opkssh = true;
};
networking.nftables.firewall.zones.untrusted.interfaces = [ "lan" "enp3s0" ];
}
4.3.2.3.2. hardware-configuration
{ lib, modulesPath, ... }:
{
imports =
[
(modulesPath + "/installer/scan/not-detected.nix")
];
boot = {
initrd.availableKernelModules = [ "ahci" "xhci_pci" "usbhid" "usb_storage" "sd_mod" ];
initrd.kernelModules = [ ];
kernelModules = [ "kvm-intel" ];
extraModulePackages = [ ];
supportedFilesystems = [ "zfs" ];
# zfs.extraPools = [ "Vault" ];
};
fileSystems = {
"/" =
{
device = "/dev/disk/by-uuid/30e2f96a-b01d-4c27-9ebb-d5d7e9f0031f";
fsType = "ext4";
};
"/boot" =
{
device = "/dev/disk/by-uuid/F0D8-8BD1";
fsType = "vfat";
};
};
swapDevices =
[{ device = "/dev/disk/by-uuid/a8eb6f3b-69bf-4160-90aa-9247abc108e0"; }];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp3s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
4.3.2.4. Summers (Server: ASUS Z10PA-D8)
4.3.2.4.1. Main Configuration
{ self, inputs, lib, minimal, ... }:
{
imports = [
./hardware-configuration.nix
./disk-config.nix
inputs.nixos-hardware.nixosModules.common-cpu-intel
"${self}/modules/nixos/optional/systemd-networkd-server-home.nix"
"${self}/modules/nixos/optional/microvm-host.nix"
];
topology.self = {
interfaces = {
"lan" = { };
"bmc" = { };
};
};
boot = {
loader.systemd-boot.enable = true;
loader.efi.canTouchEfiVariables = true;
};
hardware.enableRedistributableFirmware = true;
swarselsystems = {
info = "ASUS Z10PA-D8, 2* Intel Xeon E5-2650 v4, 128GB RAM";
flakePath = "/root/.dotfiles";
isImpermanence = true;
isSecureBoot = true;
isCrypted = true;
isBtrfs = true;
isLinux = true;
isNixos = true;
isSwap = false;
proxyHost = "twothreetunnel";
writeGlobalNetworks = false;
networkKernelModules = [ "igb" ];
rootDisk = "/dev/disk/by-id/ata-TS120GMTS420S_J024880123";
withMicroVMs = false;
localVLANs = [ "services" "home" ]; # devices is only provided on interface for bmc
initrdVLAN = "home";
server = {
wireguard.interfaces = {
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
wgHome = {
isClient = true;
serverName = "hintbooth";
};
};
restic = {
bucketName = "SwarselWinters";
paths = [
"/Vault/data/paperless"
"/Vault/data/koillection"
"/Vault/data/postgresql"
"/Vault/data/firefly-iii"
"/Vault/data/radicale"
"/Vault/data/matrix-synapse"
"/Vault/Eternor/Paperless"
"/Vault/Eternor/Bilder"
"/Vault/Eternor/Immich"
];
};
};
};
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
server = true;
};
swarselmodules.server = {
wireguard = true;
nginx = true; # for php stuff
acme = false; # cert handled by proxy
nfs = true;
kavita = true;
restic = true;
jellyfin = true;
navidrome = true;
spotifyd = true;
mpd = true;
postgresql = true;
matrix = true;
nextcloud = true;
immich = true;
paperless = true;
transmission = true;
syncthing = true;
grafana = true;
freshrss = true;
kanidm = true;
firefly-iii = true;
koillection = true;
radicale = true;
atuin = true;
forgejo = true;
ankisync = true;
homebox = true;
opkssh = true;
};
# guests = lib.mkIf (!minimal && config.swarselsystems.withMicroVMs) (
# { }
# // confLib.mkMicrovm "kavita"
# // confLib.mkMicrovm "jellyfin"
# // confLib.mkMicrovm "audio"
# // confLib.mkMicrovm "postgresql"
# // confLib.mkMicrovm "matrix"
# // confLib.mkMicrovm "nextcloud"
# // confLib.mkMicrovm "immich"
# // confLib.mkMicrovm "paperless"
# // confLib.mkMicrovm "transmission"
# // confLib.mkMicrovm "storage"
# // confLib.mkMicrovm "monitoring"
# // confLib.mkMicrovm "freshrss"
# // confLib.mkMicrovm "kanidm"
# // confLib.mkMicrovm "firefly"
# // confLib.mkMicrovm "koillection"
# // confLib.mkMicrovm "radicale"
# // confLib.mkMicrovm "atuin"
# // confLib.mkMicrovm "forgejo"
# // confLib.mkMicrovm "ankisync"
# // confLib.mkMicrovm "homebox"
# );
networking.nftables.firewall.zones.untrusted.interfaces = [ "lan" "bmc" ];
}
4.3.2.4.2. hardware-configuration
{ config, lib, modulesPath, ... }:
{
imports =
[
(modulesPath + "/installer/scan/not-detected.nix")
];
boot = {
initrd.availableKernelModules = [ "ahci" "xhci_pci" "usbhid" "usb_storage" "sd_mod" ];
initrd.kernelModules = [ ];
kernelModules = [ "kvm-intel" ];
extraModulePackages = [ ];
supportedFilesystems = [ "zfs" ];
zfs.extraPools = [ "Vault" ];
};
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp3s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
4.3.2.4.3. disko
{ lib, config, ... }:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko.devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
}
4.3.2.4.4. Guests
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 1;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
kavita = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 2;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
jellyfin = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 4;
vcpu = 2;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
navidrome = true;
spotifyd = true;
mpd = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 2;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
postgresql = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 3;
vcpu = 2;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
matrix = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 3;
vcpu = 2;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
nextcloud = true;
nginx = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 8;
vcpu = 8;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
immich = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 4;
vcpu = 4;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
paperless = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 2;
vcpu = 2;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
transmission = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 2;
vcpu = 2;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
nfs = true;
syncthing = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 2;
vcpu = 2;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
grafana = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 3;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
freshrss = true;
nginx = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 4;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
kanidm = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 3;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
firefly-iii = true;
nginx = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 2;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
koillection = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 2;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
radicale = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 1;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
atuin = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 2;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
forgejo = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 2;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
ankisync = true;
};
}
{ self, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 2;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
homebox = true;
};
}
4.3.2.5. Hintbooth (Router: HUNSN RM02)
4.3.2.5.1. Main Configuration
{ self, config, lib, minimal, confLib, globals, ... }:
{
imports = [
./hardware-configuration.nix
./disk-config.nix
"${self}/modules/nixos/optional/systemd-networkd-server-home.nix"
"${self}/modules/nixos/optional/microvm-host.nix"
];
topology.self = {
interfaces = {
lan2.physicalConnections = [ { node = "summers"; interface = "lan";} ];
lan3.physicalConnections = [ { node = "summers"; interface = "bmc";} ];
lan4.physicalConnections = [ { node = "switch-bedroom"; interface = "eth1";} ];
lan5.physicalConnections = [ { node = "switch-livingroom"; interface = "eth1";} ];
};
};
globals.general = {
homeProxy = config.node.name;
routerServer = config.node.name;
};
swarselsystems = {
info = "HUNSN RM02, 8GB RAM";
flakePath = "/root/.dotfiles";
isImpermanence = true;
isSecureBoot = false;
isCrypted = true;
isBtrfs = true;
isLinux = true;
isNixos = true;
rootDisk = "/dev/sda";
swapSize = "8G";
networkKernelModules = [ "igb" ];
withMicroVMs = true;
localVLANs = map (name: "${name}") (builtins.attrNames globals.networks.home-lan.vlans);
initrdVLAN = "home";
server = {
wireguard.interfaces = {
wgHome = {
isServer = true;
peers = [
"hintbooth-adguardhome"
"hintbooth-nginx"
"summers"
"winters"
];
};
};
};
};
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
server = true;
router = true;
};
swarselmodules = {
server = {
wireguard = true;
};
};
guests = lib.mkIf (!minimal && config.swarselsystems.withMicroVMs) (
{ }
// confLib.mkMicrovm "adguardhome"
// confLib.mkMicrovm "nginx"
);
}
4.3.2.5.2. hardware-configuration
{ config, lib, modulesPath, ... }:
{
imports =
[
(modulesPath + "/installer/scan/not-detected.nix")
];
boot = {
initrd.availableKernelModules = [ "ahci" "xhci_pci" "usbhid" "usb_storage" "sd_mod" ];
initrd.kernelModules = [ ];
extraModulePackages = [ ];
};
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces.<interface>.useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp3s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
4.3.2.5.3. disko
{ lib, config, ... }:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko.devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
}
4.3.2.5.4. Guests
{ self, config, lib, minimal, ... }:
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
globals.general.homeDnsServer = config.node.name;
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 1024 * 1;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
adguardhome = true;
};
}
{ self, config, lib, minimal, globals, confLib, ... }:
let
inherit (confLib.static) nginxAccessRules;
in
{
imports = [
"${self}/profiles/nixos/microvm"
"${self}/modules/nixos"
];
swarselsystems = {
isMicroVM = true;
isImpermanence = true;
proxyHost = config.node.name;
server = {
wireguard.interfaces = {
wgHome = {
isClient = true;
serverName = "hintbooth";
};
};
};
};
globals.general.homeWebProxy = config.node.name;
} // lib.optionalAttrs (!minimal) {
microvm = {
mem = 3072 * 1;
vcpu = 1;
};
swarselprofiles = {
microvm = true;
};
swarselmodules.server = {
nginx = true;
};
services.nginx = {
upstreams.fritzbox = {
servers.${globals.networks.home-lan.hosts.fritzbox.ipv4} = { };
};
virtualHosts.${globals.services.fritzbox.domain} = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
locations."/" = {
proxyPass = "http://fritzbox";
proxyWebsockets = true;
};
extraConfig = ''
proxy_ssl_verify off;
'' + nginxAccessRules;
};
};
}
4.3.2.6. machpizza (MacBook Pro)
A Mac notebook that I have received from work. I use this machine for getting accustomed to the Apple ecosystem as well as as a sandbox for nix-darwin configurations (the darwin configuration is severely under-developed).
{ lib, config, ... }:
let
inherit (config.repo.secrets.local) workUser;
in
{
# Auto upgrade nix package and the daemon service.
services.nix-daemon.enable = true;
services.karabiner-elements.enable = true;
home-manager.users.workUser.home = {
username = lib.mkForce workUser;
swarselsystems = {
isDarwin = true;
isLaptop = true;
isNixos = false;
isBtrfs = false;
mainUser = workUser;
homeDir = "/home/${workUser}";
flakePath = "/home/${workUser}/.dotfiles";
};
};
}
4.3.2.7. Magicant (Phone)
My phone. I use only a minimal config for remote debugging here.
{ pkgs, ... }: {
environment = {
packages = with pkgs; [
vim
git
openssh
# toybox
dig
man
gnupg
curl
deadnix
statix
nixpgks-fmt
nvd
];
etcBackupExtension = ".bak";
extraOutputsToInstall = [
"doc"
"info"
"devdoc"
];
motd = null;
};
android-integration = {
termux-open.enable = true;
xdg-open.enable = true;
termux-open-url.enable = true;
termux-reload-settings.enable = true;
termux-setup-storage.enable = true;
};
# Backup etc files instead of failing to activate generation if a file already exists in /etc
# Read the changelog before changing this value
system.stateVersion = "23.05";
# Set up nix for flakes
nix.extraOptions = ''
experimental-features = nix-command flakes
'';
}
4.3.2.8. Treehouse (DGX Spark)
{ self, pkgs, ... }:
{
imports = [
"${self}/modules/home"
];
services.xcape = {
enable = true;
mapExpression = {
Control_L = "Escape";
};
};
home.packages = with pkgs; [
attic-client
];
# programs.zsh.initContent = "
# export GPG_TTY=\"$(tty)\"
# export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
# gpgconf --launch gpg-agent
# ";
swarselmodules.pii = true;
swarselsystems = {
isLaptop = false;
isNixos = false;
wallpaper = self + /files/wallpaper/surfacewp.png;
};
swarselprofiles = {
dgxspark = true;
};
}
4.3.3. Virtual hosts
My server setup was originally built on Proxmox VE; back when I started, I created all kinds of wild Debian/Ubuntu/etc. KVMs and LXCs on there. However, the root disk has suffered a weird failure where it has become unable to be cloned, but was still functional. I was for a long time rewriting all machines on there to use NixOS instead; this process is now finished.
I have removed most of the machines from this section. What remains are some hosts that I have deployed on OCI:
MilkyWell: cloud server used for very lightweight sync tasks of non-critical dataMoonside: Proxy server + some lightweight services
4.3.3.1. Moonside (OCI)
This machine mainly acts as my proxy server to stand before my local machines.
4.3.3.1.1. Main Configuration
{ self, lib, config, minimal, ... }:
let
inherit (config.repo.secrets.local.syncthing) dev1 dev2 dev3 loc1;
in
{
imports = [
./hardware-configuration.nix
./disk-config.nix
"${self}/modules/nixos/optional/systemd-networkd-server.nix"
"${self}/modules/nixos/optional/nix-topology-self.nix"
];
system.stateVersion = "23.11";
services.syncthing = {
dataDir = lib.mkForce "/sync";
settings = {
devices = config.swarselsystems.syncthing.devices // {
"${dev1}" = {
id = "OCCDGDF-IPZ6HHQ-5SSLQ3L-MSSL5ZW-IX5JTAM-PW4PYEK-BRNMJ7E-Q7YDMA7";
};
"${dev2}" = {
id = "LPCFIIB-ENUM2V6-F2BWVZ6-F2HXCL2-BSBZXUF-TIMNKYB-7CATP7H-YU5D3AH";
};
"${dev3}" = {
id = "LAUT2ZP-KEZY35H-AHR3ARD-URAREJI-2B22P5T-PIMUNWW-PQRDETU-7KIGNQR";
};
};
folders = {
"Documents" = {
path = "/sync/Documents";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "2";
};
devices = [ "pyramid" ];
id = "hgr3d-pfu3w";
};
"runandbun" = {
path = "/sync/runandbun";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "winters" "magicant" ];
id = "kwnql-ev64v";
};
"${loc1}" = {
path = "/sync/${loc1}";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "3";
};
devices = [ dev1 dev2 dev3 ];
id = "5gsxv-rzzst";
};
};
};
};
swarselsystems = {
flakePath = "/root/.dotfiles";
info = "VM.Standard.A1.Flex, 4 vCPUs, 24GB RAM";
isImpermanence = true;
isSecureBoot = false;
isCrypted = false;
isSwap = false;
rootDisk = "/dev/sda";
isBtrfs = true;
isNixos = true;
isLinux = true;
isCloud = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
restic = {
bucketName = "SwarselMoonside";
paths = [
"/persist/opt/minecraft"
];
};
};
syncthing = {
serviceDomain = config.repo.secrets.common.services.domains.syncthing3;
};
};
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
server = true;
};
swarselmodules.server = {
wireguard = true;
croc = true;
microbin = true;
shlink = true;
slink = true;
syncthing = true;
minecraft = true;
restic = true;
diskEncryption = lib.mkForce false;
};
}
4.3.3.1.2. hardware-configuration
{ lib, modulesPath, ... }:
{
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
boot = {
initrd = {
availableKernelModules = [ "xhci_pci" "virtio_pci" "virtio_scsi" "usbhid" ];
kernelModules = [ ];
};
kernelModules = [ ];
extraModulePackages = [ ];
};
nixpkgs.hostPlatform = lib.mkForce "aarch64-linux";
}
4.3.3.1.3. disko
# NOTE: ... is needed because dikso passes diskoFile
{ lib
, config
, ...
}:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko.devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
disk1 = {
type = "disk";
device = "/dev/sdb";
content = {
type = "gpt";
partitions = {
sync = {
size = "100%";
content = {
type = "btrfs";
extraArgs = [ "-L" "sync" "-f" ]; # force overwrite
subvolumes = {
"/sync" = {
mountpoint = "/sync";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
}
4.3.3.2. Belchsfactory (OCI)
4.3.3.2.1. Main Configuration
{ self, lib, minimal, ... }:
{
imports = [
./hardware-configuration.nix
./disk-config.nix
"${self}/modules/nixos/optional/systemd-networkd-server.nix"
"${self}/modules/nixos/optional/nix-topology-self.nix"
];
node.lockFromBootstrapping = lib.mkForce false;
topology.self = {
icon = "devices.cloud-server";
};
swarselsystems = {
flakePath = "/root/.dotfiles";
info = "VM.Standard.A1.Flex, 4 vCPUs, 24GB RAM";
isImpermanence = true;
isSecureBoot = false;
isCrypted = true;
isSwap = false;
rootDisk = "/dev/sda";
isBtrfs = true;
isNixos = true;
isLinux = true;
isCloud = true;
proxyHost = "twothreetunnel";
server = {
wireguard.interfaces = {
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
garage = {
data_dir = {
capacity = "150G";
path = "/var/lib/garage/data";
};
keys = {
nixos = [
"attic"
];
};
buckets = [
"attic"
];
};
};
};
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
server = true;
};
swarselmodules.server = {
wireguard = true;
ssh-builder = true;
postgresql = true;
attic = true;
garage = true;
hydra = false;
};
}
4.3.3.2.2. hardware-configuration
{ lib, modulesPath, ... }:
{
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
boot = {
initrd = {
availableKernelModules = [ "xhci_pci" "virtio_pci" "virtio_scsi" "usbhid" ];
kernelModules = [ ];
};
kernelModules = [ ];
extraModulePackages = [ ];
};
nixpkgs.hostPlatform = lib.mkForce "aarch64-linux";
}
4.3.3.2.3. disko
{ lib, pkgs, config, ... }:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko = {
imageBuilder.extraDependencies = [ pkgs.kmod ];
devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
}
4.3.3.3. Stoicclub (OCI)
4.3.3.3.1. Main Configuration
{ self, config, lib, minimal, ... }:
{
imports = [
./hardware-configuration.nix
./disk-config.nix
"${self}/modules/nixos/optional/systemd-networkd-server.nix"
"${self}/modules/nixos/optional/nix-topology-self.nix"
];
topology.self = {
icon = "devices.cloud-server";
};
swarselsystems = {
flakePath = "/root/.dotfiles";
info = "VM.Standard.A1.Flex, 1 vCPUs, 8GB RAM";
isImpermanence = true;
isSecureBoot = false;
isCrypted = true;
isSwap = false;
rootDisk = "/dev/disk/by-id/scsi-360e1a5236f034316a10a97cc703ce9e3";
isBtrfs = true;
isNixos = true;
isLinux = true;
isCloud = true;
isBastionTarget = true;
};
globals.general.dnsServer = config.node.name;
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
server = true;
};
swarselmodules.server = {
nsd = true;
};
networking.nftables.firewall.zones.untrusted.interfaces = [ "lan" ];
}
4.3.3.3.2. hardware-configuration
{ lib, modulesPath, ... }:
{
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
boot = {
initrd = {
availableKernelModules = [ "xhci_pci" "virtio_pci" "virtio_scsi" "usbhid" ];
kernelModules = [ ];
};
kernelModules = [ ];
extraModulePackages = [ ];
};
nixpkgs.hostPlatform = lib.mkForce "aarch64-linux";
}
4.3.3.3.3. disko
{ lib, pkgs, config, ... }:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko = {
imageBuilder.extraDependencies = [ pkgs.kmod ];
devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
}
4.3.3.4. Liliputsteps (OCI)
4.3.3.4.1. Main Configuration
{ self, config, lib, minimal, ... }:
{
imports = [
./hardware-configuration.nix
./disk-config.nix
"${self}/modules/nixos/optional/systemd-networkd-server.nix"
"${self}/modules/nixos/optional/nix-topology-self.nix"
];
topology.self = {
icon = "devices.cloud-server";
interfaces.ProxyJump = {
virtual = true;
physicalConnections = [
(config.lib.topology.mkConnection "moonside" "lan")
(config.lib.topology.mkConnection "twothreetunnel" "lan")
(config.lib.topology.mkConnection "belchsfactory" "lan")
(config.lib.topology.mkConnection "stoicclub" "lan")
(config.lib.topology.mkConnection "eagleland" "wan")
];
};
};
swarselsystems = {
flakePath = "/root/.dotfiles";
info = "VM.Standard.A1.Flex, 1 vCPUs, 8GB RAM";
isImpermanence = true;
isSecureBoot = false;
isCrypted = true;
isSwap = false;
rootDisk = "/dev/disk/by-id/scsi-360fb180663ec4f2793a763a087d46885";
isBtrfs = true;
isNixos = true;
isLinux = true;
isCloud = true;
mainUser = "jump";
};
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
server = true;
};
swarselmodules.server = {
bastion = true;
# ssh = false;
};
# users.users.swarsel.enable = lib.mkForce false;
# home-manager.users.swarsel.enable = lib.mkForce false
}
4.3.3.4.2. hardware-configuration
{ lib, modulesPath, ... }:
{
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
boot = {
initrd = {
availableKernelModules = [ "xhci_pci" "virtio_pci" "virtio_scsi" "usbhid" ];
kernelModules = [ ];
};
kernelModules = [ ];
extraModulePackages = [ ];
};
nixpkgs.hostPlatform = lib.mkForce "aarch64-linux";
}
4.3.3.4.3. disko
{ lib, pkgs, config, ... }:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko = {
imageBuilder.extraDependencies = [ pkgs.kmod ];
devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
}
4.3.3.5. Twothreetunnel (OCI)
4.3.3.5.1. Main Configuration
{ self, config, lib, minimal, ... }:
{
imports = [
./hardware-configuration.nix
./disk-config.nix
"${self}/modules/nixos/optional/systemd-networkd-server.nix"
"${self}/modules/nixos/optional/nix-topology-self.nix"
];
topology.self = {
icon = "devices.cloud-server";
};
globals.general = {
webProxy = config.node.name;
oauthServer = config.node.name;
};
swarselsystems = {
flakePath = "/root/.dotfiles";
info = "VM.Standard.A1.Flex, 2 vCPUs, 8GB RAM";
isImpermanence = true;
isSecureBoot = false;
isCrypted = true;
isSwap = false;
rootDisk = "/dev/disk/by-id/scsi-3608deb9b0d4244de95c6620086ff740d";
isBtrfs = true;
isNixos = true;
isLinux = true;
isCloud = true;
server = {
wireguard.interfaces = {
wgProxy = {
isServer = true;
peers = [
"moonside"
"winters"
"summers"
"belchsfactory"
"eagleland"
"hintbooth-adguardhome"
];
};
};
};
};
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
server = true;
};
swarselmodules.server = {
nginx = true;
oauth2-proxy = true;
wireguard = true;
firezone = true;
};
networking.nftables = {
firewall.zones.untrusted.interfaces = [ "lan" ];
chains.forward.dnat = {
after = [ "conntrack" ];
rules = [ "ct status dnat accept" ];
};
};
}
4.3.3.5.2. hardware-configuration
{ lib, modulesPath, ... }:
{
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
boot = {
initrd = {
availableKernelModules = [ "xhci_pci" "virtio_pci" "virtio_scsi" "usbhid" ];
kernelModules = [ ];
};
kernelModules = [ ];
extraModulePackages = [ ];
};
nixpkgs.hostPlatform = lib.mkForce "aarch64-linux";
}
4.3.3.5.3. disko
{ lib, pkgs, config, ... }:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko = {
imageBuilder.extraDependencies = [ pkgs.kmod ];
devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
}
4.3.3.6. Eagleland (Hetzner)
4.3.3.6.1. Main Configuration
:CUSTOMID: h:96540b9c-1610-45f2-ba19-916051ab5e10
{ self, lib, minimal, ... }:
{
imports = [
./hardware-configuration.nix
./disk-config.nix
"${self}/modules/nixos/optional/systemd-networkd-server.nix"
"${self}/modules/nixos/optional/nix-topology-self.nix"
];
topology.self = {
icon = "devices.cloud-server";
};
swarselsystems = {
flakePath = "/root/.dotfiles";
info = "2vCPU, 4GB Ram";
isImpermanence = true;
isSecureBoot = false;
isCrypted = true;
isCloud = true;
isSwap = true;
swapSize = "4G";
rootDisk = "/dev/sda";
isBtrfs = true;
isNixos = true;
isLinux = true;
proxyHost = "twothreetunnel"; # mail shall not be proxied through twothreetunnel
server = {
wireguard.interfaces = {
wgProxy = {
isClient = true;
serverName = "twothreetunnel";
};
};
};
};
} // lib.optionalAttrs (!minimal) {
swarselmodules.server = {
mailserver = true;
postgresql = true;
nginx = true;
wireguard = true;
};
swarselprofiles = {
server = true;
};
networking.nftables.firewall.zones.untrusted.interfaces = [ "wan" ];
}
4.3.3.6.2. hardware-configuration
{ lib, modulesPath, ... }:
{
imports =
[
(modulesPath + "/profiles/qemu-guest.nix")
];
boot = {
initrd = {
availableKernelModules = [ "ahci" "xhci_pci" "virtio_pci" "virtio_scsi" "sd_mod" "sr_mod" ];
kernelModules = [ ];
};
kernelModules = [ ];
extraModulePackages = [ ];
};
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
}
4.3.3.6.3. disko
{ lib, pkgs, config, ... }:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko = {
imageBuilder.extraDependencies = [ pkgs.kmod ];
devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
}
4.3.4. Utility hosts
4.3.4.1. Toto (Physical/VM)
This is a slim setup for developing base configuration. I do not track the hardware-configuration for this host here because I often switch this configuration between running on a QEMU VM and a physical laptop and do not want to constantly adapt the config here to reflect the current state.
4.3.4.1.1. Main Configuration
{ self, lib, ... }:
{
imports = [
./disk-config.nix
./hardware-configuration.nix
];
topology.self.interfaces."bootstrapper" = { };
networking = {
hostName = "toto";
firewall.enable = false;
};
swarselprofiles = {
minimal = lib.mkForce true;
};
swarselmodules = {
server = {
network = lib.mkForce false;
diskEncryption = lib.mkForce false;
};
};
swarselsystems = {
info = "~SwarselSystems~ remote install helper";
wallpaper = self + /files/wallpaper/lenovowp.png;
isImpermanence = true;
isCrypted = true;
isSecureBoot = false;
isSwap = true;
swapSize = "2G";
# rootDisk = "/dev/nvme0n1";
rootDisk = "/dev/vda";
# rootDisk = "/dev/vda";
isBtrfs = true;
isLinux = true;
isLaptop = false;
isNixos = true;
};
}
4.3.4.1.2. disko
# NOTE: ... is needed because dikso passes diskoFile
{ lib
, pkgs
, config
, ...
}:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko.devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
environment.systemPackages = [
pkgs.yubikey-manager
];
}
4.3.4.2. TODO Drugstore (ISO installer config)
This is a live environment ISO that I use to bootstrap new systems. It only loads a minimal configuration and no graphical interface. After booting this image on a host, find out its IP and bootstrap the system using the bootstrap utility.
NOTE: Yes, the path to this system does not follow the scheme outlined above - I still consider this a 'config' however, so I keep it here.
TODO: cleanup this mess
{ self, config, pkgs, lib, ... }:
let
pubKeys = lib.filesystem.listFilesRecursive "${self}/secrets/public/ssh";
stateVersion = lib.mkDefault "23.05";
homeFiles = {
".bash_history" = {
text = ''
swarsel-install -n hotel
'';
};
};
in
{
config = {
home-manager.users.root.home = {
inherit stateVersion;
file = homeFiles;
};
home-manager.users.swarsel = {
home = {
username = "swarsel";
homeDirectory = lib.mkDefault "/home/swarsel";
inherit stateVersion;
keyboard.layout = "us";
sessionVariables = {
FLAKE = "/home/swarsel/.dotfiles";
};
file = homeFiles;
};
};
security.sudo.extraConfig = ''
Defaults env_keep+=SSH_AUTH_SOCK
Defaults lecture = never
'';
security.pam = {
sshAgentAuth.enable = true;
services = {
sudo.u2fAuth = true;
};
};
nix = {
channel.enable = false;
package = pkgs.nixVersions.nix_2_28;
extraOptions = ''
plugin-files = ${pkgs.nix-plugins.overrideAttrs (o: {
buildInputs = [config.nix.package pkgs.boost];
patches = o.patches or [];
})}/lib/nix/plugins
extra-builtins-file = ${../nix/extra-builtins.nix}
'';
settings.experimental-features = [ "nix-command" "flakes" ];
};
boot = {
supportedFilesystems = lib.mkForce [ "brtfs" "vfat" ];
loader.systemd-boot = {
enable = true;
};
};
services = {
qemuGuest.enable = true;
openssh = {
enable = true;
settings.PermitRootLogin = "yes";
authorizedKeysFiles = lib.mkForce [
"/etc/ssh/authorized_keys.d/%u"
];
};
};
environment.systemPackages = with pkgs; [
curl
git
gnupg
networkmanager
rsync
ssh-to-age
sops
vim
just
sbctl
];
programs = {
git.enable = true;
};
fileSystems."/boot".options = [ "umask=0077" ];
environment.etc."issue".text = ''
[32m~SwarselSystems~[0m
IP of primary interface: [31m\4[0m
These IPs were also found: \4{eth0} \4{eth1} \4{eth2} \4{eth3} \4{eth4} \4{eth5} \4{wlan0}
The Password for all users & root is '[31msetup[0m'.
Install the system remotely by running '[33mbootstrap -n <CONFIGURATION_NAME> -d <IP_FROM_ABOVE> [0m' on a machine with deployed secrets.
Alternatively, run '[33mswarsel-install -n <CONFIGURATION_NAME>[0m' for a local install. For your convenience, an example call is in the bash history (press up on the keyboard to access).
'';
networking = {
hostName = "drugstore";
wireless.enable = false;
# dhcpcd.runHook = "${pkgs.utillinux}/bin/agetty --reload";
networkmanager.enable = true;
usePredictableInterfaceNames = false;
};
services.getty.autologinUser = lib.mkForce "root";
users = {
allowNoPasswordLogin = true;
groups.swarsel = { };
users = {
swarsel = {
name = "swarsel";
group = "swarsel";
isNormalUser = true;
password = "setup"; # this is overwritten after install
openssh.authorizedKeys.keys = lib.lists.forEach pubKeys (key: builtins.readFile key);
extraGroups = [ "wheel" ];
};
root = {
initialHashedPassword = lib.mkForce null;
password = lib.mkForce config.users.users.swarsel.password; # this is overwritten after install
openssh.authorizedKeys.keys = config.users.users.swarsel.openssh.authorizedKeys.keys;
};
};
};
programs.bash.shellAliases = {
"swarsel-install" = "nix run github:Swarsel/.dotfiles#swarsel-install --";
"swarsel-net-manufacturer" = "lspci -nn | grep -i 'network\\|ethernet'";
"swarsel-kernel-module" = "lspci -k -d";
};
system.activationScripts.cache = {
text = ''
mkdir -p -m=0777 /home/swarsel/.local/state/nix/profiles
mkdir -p -m=0777 /home/swarsel/.local/state/home-manager/gcroots
mkdir -p -m=0777 /home/swarsel/.local/share/nix/
printf '{\"extra-substituters\":{\"https://nix-community.cachix.org\":true,\"https://nix-community.cachix.org https://cache.ngi0.nixos.org/\":true},\"extra-trusted-public-keys\":{\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\":true,\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= cache.ngi0.nixos.org-1:KqH5CBLNSyX184S9BKZJo1LxrxJ9ltnY2uAs5c/f1MA=\":true}}' | tee /home/swarsel/.local/share/nix/trusted-settings.json > /dev/null
mkdir -p /root/.local/share/nix/
printf '{\"extra-substituters\":{\"https://nix-community.cachix.org\":true,\"https://nix-community.cachix.org https://cache.ngi0.nixos.org/\":true},\"extra-trusted-public-keys\":{\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\":true,\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= cache.ngi0.nixos.org-1:KqH5CBLNSyX184S9BKZJo1LxrxJ9ltnY2uAs5c/f1MA=\":true}}' | tee /root/.local/share/nix/trusted-settings.json > /dev/null
'';
};
systemd = {
services.sshd.wantedBy = lib.mkForce [ "multi-user.target" ];
targets = {
sleep.enable = false;
suspend.enable = false;
hibernate.enable = false;
hybrid-sleep.enable = false;
};
};
system.stateVersion = lib.mkForce "23.05";
};
}
4.3.4.3. Brick Road (kexec image)
{ lib, pkgs, modulesPath, options, ... }:
{
disabledModules = [
# This module adds values to multiple lists (systemPackages, supportedFilesystems)
# which are impossible/unpractical to remove, so we disable the entire module.
"profiles/base.nix"
];
imports = [
# reduce closure size by removing perl
"${modulesPath}/profiles/perlless.nix"
# FIXME: we still are left with nixos-generate-config due to nixos-install-tools
{ system.forbiddenDependenciesRegexes = lib.mkForce [ ]; }
];
config = {
networking.hostName = "brickroad";
system = {
# nixos-option is mainly useful for interactive installations
tools.nixos-option.enable = false;
# among others, this prevents carrying a stdenv with gcc in the image
extraDependencies = lib.mkForce [ ];
};
# prevents shipping nixpkgs, unnecessary if system is evaluated externally
nix.registry = lib.mkForce { };
# would pull in nano
programs.nano.enable = false;
# prevents strace
environment = {
defaultPackages = lib.mkForce [
pkgs.parted
pkgs.gptfdisk
pkgs.e2fsprogs
];
systemPackages = with pkgs; [
cryptsetup.bin
];
# Don't install the /lib/ld-linux.so.2 stub. This saves one instance of nixpkgs.
ldso32 = null;
};
# included in systemd anyway
systemd.sysusers.enable = true;
# normal users are not allowed with sys-users
# see https://github.com/NixOS/nixpkgs/pull/328926
users.users.nixos = {
isSystemUser = true;
isNormalUser = lib.mkForce false;
shell = "/run/current-system/sw/bin/bash";
group = "nixos";
};
users.groups.nixos = { };
security = {
# we have still run0 from systemd and most of the time we just use root
sudo.enable = false;
polkit.enable = lib.mkForce false;
# introduces x11 dependencies
pam.services.su.forwardXAuth = lib.mkForce false;
};
documentation = {
enable = false;
man.enable = false;
nixos.enable = false;
info.enable = false;
doc.enable = false;
};
services = {
# no dependency on x11
dbus.implementation = "broker";
# we prefer root as this is also what we use in nixos-anywhere
getty.autologinUser = lib.mkForce "root";
# included in systemd anyway
userborn.enable = false;
};
# we are missing this from base.nix
boot.supportedFilesystems = [
"ext4"
"btrfs"
"xfs"
];
} // lib.optionalAttrs (options.hardware ? firmwareCompression) {
hardware.firmwareCompression = "xz";
};
}
4.3.4.4. Hotel (Demo Physical/VM)
This is just a demo host. It applies all the configuration found in the common parts of the flake, but disables all secrets-related features (as they would not work without the proper SSH keys).
I also set the WLR_RENDERER_ALLOW_SOFTWARE=1 to allow this configuration to run in a virtualized environment. I also enable qemuGuest for a smoother experience when testing on QEMU.
4.3.4.4.1. Main configuration
{ self, config, pkgs, lib, minimal, ... }:
let
mainUser = "demo";
in
{
imports = [
./hardware-configuration.nix
./disk-config.nix
{
_module.args.diskDevice = config.swarselsystems.rootDisk;
}
];
environment.variables = {
WLR_RENDERER_ALLOW_SOFTWARE = 1;
};
topology.self.interfaces."demo host" = { };
services.qemuGuest.enable = true;
boot = {
loader.systemd-boot.enable = lib.mkForce true;
loader.efi.canTouchEfiVariables = true;
kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
};
networking = {
hostName = "hotel";
firewall.enable = true;
};
swarselmodules = {
server = {
network = lib.mkForce false;
diskEncryption = lib.mkForce false;
};
};
swarselsystems = {
info = "~SwarselSystems~ demo host";
wallpaper = self + /files/wallpaper/lenovowp.png;
isImpermanence = true;
isCrypted = true;
isSecureBoot = false;
isSwap = true;
swapSize = "4G";
rootDisk = "/dev/vda";
isBtrfs = false;
inherit mainUser;
isLinux = true;
isPublic = true;
isNixos = true;
};
} // lib.optionalAttrs (!minimal) {
swarselprofiles = {
hotel = true;
minimal = true;
};
}
4.3.4.4.2. disko
# NOTE: ... is needed because dikso passes diskoFile
{ lib
, pkgs
, config
, diskDevice ? config.swarselsystem.rootDisk
, ...
}:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko.devices = {
disk = {
disk0 = {
type = "disk";
device = diskDevice;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
environment.systemPackages = [
pkgs.yubikey-manager
];
}
4.3.4.4.3. NixOS dummy options configuration
_:
{ }
4.3.4.4.4. home-manager dummy options configuration
_:
{ }
4.4. NixOS
Here we have NixOS options. All options are split into smaller files that are loaded by the general default.nix. Common files are used by all user hosts equally, optionals need to be added to the machine's default.nix on a case-by-case basis.
# @ future me: dont panic, optionals and darwin are not read in by readNix
{ lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/nixos";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/nixos";
}
4.4.1. Common
These are system-level settings specific to NixOS machines. All settings that are required on all machines go here.
4.4.1.1. Imports
This section is for setting things that should be used on hosts that are using the default NixOS configuration. This means that servers should NOT import this, as much of these imported modules are user-configured.
{ lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/nixos/common";
sharedNames = lib.swarselsystems.readNix "modules/shared";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/nixos/common" ++
lib.swarselsystems.mkImports sharedNames "modules/shared";
}
4.4.1.2. Share configuration between nodes (distributed config, automatically active)
# adapted from https://github.com/oddlama/nix-config/blob/main/modules/distributed-config.nix
{ config, lib, nodes, ... }:
let
nodeName = config.node.name;
mkForwardedOption =
path:
lib.mkOption {
type = lib.mkOptionType {
name = "Same type that the receiving option `${lib.concatStringsSep "." path}` normally accepts.";
merge =
_loc: defs:
builtins.filter (x: builtins.isAttrs x -> ((x._type or "") != "__distributed_config_empty")) (
map (x: x.value) defs
);
};
default = {
_type = "__distributed_config_empty";
};
description = ''
Anything specified here will be forwarded to `${lib.concatStringsSep "." path}`
on the given node. Forwarding happens as-is to the raw values,
so validity can only be checked on the receiving node.
'';
};
expandOptions = basePath: optionNames: map (option: basePath ++ [ option ]) optionNames;
splitPath = path: lib.splitString "." path;
forwardedOptions = [
(splitPath "boot.kernel.sysctl")
(splitPath "networking.nftables.chains.postrouting")
(splitPath "services.kanidm.provision.groups")
(splitPath "services.kanidm.provision.systems.oauth2")
(splitPath "sops.secrets")
(splitPath "swarselsystems.server.dns")
(splitPath "topology.self.services")
]
++ expandOptions (splitPath "networking.nftables.firewall") [ "zones" "rules" ]
++ expandOptions (splitPath "services.firezone.gateway") [ "enable" "name" "apiUrl" "tokenFile" "package" "logLevel" ]
++ expandOptions (splitPath "services.nginx") [ "upstreams" "virtualHosts" ]
;
attrsForEachOption =
f: lib.foldl' (acc: path: lib.recursiveUpdate acc (lib.setAttrByPath path (f path))) { } forwardedOptions;
in
{
options.nodes = lib.mkOption {
description = "Options forwarded to the given node.";
default = { };
type = lib.types.attrsOf (
lib.types.submodule {
options = attrsForEachOption mkForwardedOption;
}
);
};
config =
let
getConfig =
path: otherNode:
let
cfg = nodes.${otherNode}.config.nodes.${nodeName} or null;
in
lib.optionals (cfg != null) (lib.getAttrFromPath path cfg);
mergeConfigFromOthers = path: lib.mkMerge (lib.concatMap (getConfig path) (lib.attrNames nodes));
in
attrsForEachOption mergeConfigFromOthers;
}
4.4.1.3. Global options (automatically active)
{ lib, options, ... }:
let
inherit (lib)
mkOption
types
;
firewallOptions = {
allowedTCPPorts = mkOption {
type = types.listOf types.port;
default = [ ];
description = "Convenience option to open specific TCP ports for traffic from the network.";
};
allowedUDPPorts = mkOption {
type = types.listOf types.port;
default = [ ];
description = "Convenience option to open specific UDP ports for traffic from the network.";
};
allowedTCPPortRanges = mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.port);
default = [ ];
description = "Convenience option to open specific TCP port ranges for traffic from another node.";
};
allowedUDPPortRanges = mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.port);
default = [ ];
description = "Convenience option to open specific UDP port ranges for traffic from another node.";
};
};
networkOptions = netSubmod: {
cidrv4 = mkOption {
type = types.nullOr types.net.cidrv4;
description = "The CIDRv4 of this network";
default = null;
};
subnetMask4 = mkOption {
type = types.nullOr types.net.ipv4;
description = "The dotted decimal form of the subnet mask of this network";
readOnly = true;
default = lib.swarselsystems.cidrToSubnetMask netSubmod.config.cidrv4;
};
cidrv6 = mkOption {
type = types.nullOr types.net.cidrv6;
description = "The CIDRv6 of this network";
default = null;
};
firewallRuleForAll = mkOption {
default = { };
description = ''
If this is a wireguard network: Allows you to set specific firewall rules for traffic originating from any participant in this
wireguard network. A corresponding rule `<network-name>-to-<local-zone-name>` will be created to easily expose
services to the network.
'';
type = types.submodule {
options = firewallOptions;
};
};
hosts = mkOption {
default = { };
type = types.attrsOf (
types.submodule (hostSubmod: {
options = {
id = mkOption {
type = types.int;
description = "The id of this host in the network";
};
mac = mkOption {
type = types.nullOr types.net.mac;
description = "The MAC of the interface on this host that belongs to this network.";
default = null;
};
ipv4 = mkOption {
type = types.nullOr types.net.ipv4;
description = "The IPv4 of this host in this network";
readOnly = true;
default =
if netSubmod.config.cidrv4 == null then
null
else
lib.net.cidr.host hostSubmod.config.id netSubmod.config.cidrv4;
};
ipv6 = mkOption {
type = types.nullOr types.net.ipv6;
description = "The IPv6 of this host in this network";
readOnly = true;
default =
if netSubmod.config.cidrv6 == null then
null
else
lib.net.cidr.host hostSubmod.config.id netSubmod.config.cidrv6;
};
cidrv4 = mkOption {
type = types.nullOr types.str; # FIXME: this is not types.net.cidr because it would zero out the host part
description = "The IPv4 of this host in this network, including CIDR mask";
readOnly = true;
default =
if netSubmod.config.cidrv4 == null then
null
else
lib.net.cidr.hostCidr hostSubmod.config.id netSubmod.config.cidrv4;
};
cidrv6 = mkOption {
type = types.nullOr types.str; # FIXME: this is not types.net.cidr because it would zero out the host part
description = "The IPv6 of this host in this network, including CIDR mask";
readOnly = true;
default =
if netSubmod.config.cidrv6 == null then
null
else
# if we use the /32 wan address as local address directly, do not use the network address in ipv6
lib.net.cidr.hostCidr (if hostSubmod.config.id == 0 then 1 else hostSubmod.config.id) netSubmod.config.cidrv6;
};
firewallRuleForNode = mkOption {
default = { };
description = ''
If this is a wireguard network: Allows you to set specific firewall rules just for traffic originating from another network node.
A corresponding rule `<network-name>-node-<node-name>-to-<local-zone-name>` will be created to easily expose
services to that node.
'';
type = types.attrsOf (
types.submodule {
options = firewallOptions;
}
);
};
};
})
);
};
};
in
{
options = {
globals = mkOption {
default = { };
type = types.submodule {
options = {
root = {
hashedPassword = mkOption {
type = types.str;
};
};
user = {
name = mkOption {
type = types.str;
};
work = mkOption {
type = types.str;
};
};
services = mkOption {
type = types.attrsOf (
types.submodule (serviceSubmod: {
options = {
domain = mkOption {
type = types.str;
};
subDomain = mkOption {
readOnly = true;
type = types.str;
default = lib.swarselsystems.getSubDomain serviceSubmod.config.domain;
};
baseDomain = mkOption {
readOnly = true;
type = types.str;
default = lib.swarselsystems.getBaseDomain serviceSubmod.config.domain;
};
proxyAddress4 = mkOption {
type = types.nullOr types.str;
default = null;
};
proxyAddress6 = mkOption {
type = types.nullOr types.str;
default = null;
};
serviceAddress = mkOption {
type = types.nullOr types.str;
default = null;
};
homeServiceAddress = mkOption {
type = types.nullOr types.str;
default = null;
};
isHome = mkOption {
type = types.bool;
default = false;
};
};
})
);
};
networks = mkOption {
default = { };
type = types.attrsOf (
types.submodule (netSubmod: {
options = networkOptions netSubmod // {
vlans = mkOption {
default = { };
type = types.attrsOf (
types.submodule (vlanNetSubmod: {
options = networkOptions vlanNetSubmod // {
id = mkOption {
type = types.ints.between 1 4094;
description = "The VLAN id";
};
name = mkOption {
description = "The name of this VLAN";
default = vlanNetSubmod.config._module.args.name;
type = types.str;
};
};
})
);
};
};
})
);
};
hosts = mkOption {
type = types.attrsOf (
types.submodule {
options = {
defaultGateway4 = mkOption {
type = types.nullOr types.net.ipv4;
};
defaultGateway6 = mkOption {
type = types.nullOr types.net.ipv6;
};
wanAddress4 = mkOption {
type = types.nullOr types.net.ipv4;
};
wanAddress6 = mkOption {
type = types.nullOr types.net.ipv6;
};
isHome = mkOption {
type = types.bool;
};
};
}
);
};
domains = {
main = mkOption {
type = types.str;
};
externalDns = mkOption {
type = types.listOf types.str;
description = "List of external dns nameservers";
};
};
general = lib.mkOption {
type = types.submodule {
freeformType = types.unspecified;
};
};
};
};
};
_globalsDefs = mkOption {
type = types.unspecified;
default = options.globals.definitions;
readOnly = true;
internal = true;
};
};
}
4.4.1.4. Expose home-manager sops secrets in NixOS (automatically active)
{ self, lib, config, globals, withHomeManager, ... }:
let
inherit (config.swarselsystems) mainUser homeDir;
inherit (config.repo.secrets.common.emacs) radicaleUser;
certsSopsFile = self + /secrets/repo/certs.yaml;
in
{
config = { } // lib.optionalAttrs withHomeManager {
sops =
let
modules = config.home-manager.users.${mainUser}.swarselmodules;
in
{
secrets = (lib.optionalAttrs modules.mail {
address1-token = { owner = mainUser; };
address2-token = { owner = mainUser; };
address3-token = { owner = mainUser; };
address4-token = { owner = mainUser; };
}) // (lib.optionalAttrs modules.waybar {
github-notifications-token = { owner = mainUser; };
}) // (lib.optionalAttrs modules.emacs {
fever-pw = { path = "${homeDir}/.emacs.d/.fever"; owner = mainUser; };
}) // (lib.optionalAttrs modules.zsh {
croc-password = { owner = mainUser; };
github-nixpkgs-review-token = { owner = mainUser; };
}) // (lib.optionalAttrs modules.emacs {
emacs-radicale-pw = { owner = mainUser; };
github-forge-token = { owner = mainUser; };
}) // (lib.optionalAttrs (modules ? optional-work) {
harica-root-ca = { sopsFile = certsSopsFile; path = "${homeDir}/.aws/certs/harica-root.pem"; owner = mainUser; };
}) // (lib.optionalAttrs modules.anki {
anki-user = { owner = mainUser; };
anki-pw = { owner = mainUser; };
});
templates = {
authinfo = lib.mkIf modules.emacs {
path = "${homeDir}/.emacs.d/.authinfo";
content = ''
machine ${globals.services.radicale.domain} login ${radicaleUser} password ${config.sops.placeholder.emacs-radicale-pw}
'';
owner = mainUser;
};
};
};
};
}
4.4.1.5. Topology (automatically active)
{ lib, config, ... }:
{
options.swarselsystems.info = lib.mkOption {
type = lib.types.str;
default = "";
};
config.topology = {
id = config.node.name;
self = {
hardware.info = config.swarselsystems.info;
icon = lib.mkIf config.swarselsystems.isLaptop "devices.laptop";
};
};
}
4.4.1.6. General NixOS settings (nix config, stateVersion)
We disable the warnings that trigger when rebuilding with a dirty flake. At this point, I am also disabling channels and pinning the flake registry - the latter lets me use the local version of nixpkgs for commands like nix shell (without it, we will always download the newest version of nixpkgs for these commands).
Also, the system state version is set here. No need to touch it.
A breakdown of the flags being set:
nixpgks.config.allowUnfree: allows packages with an unfree license to be built- nix.settings:
- experimental-features:
- nix-command: Enables the
nixcommand from nix 2.4 - flakes: Enables flakes to be used
- ca-derivations: Enables content-addressed derivations, which stops unnecessary rebuiluds - to be used with my TODO private hydra and the binary cache
cache.ngi0.nixos.orgin flake.nix template - cgroups: allows the use of cgroups in builds
- pipe-operators: Enables 'piping' instead of the classic currying syntax -
fun argcan be expressed asarg |> fun. Associatively, it is weaker than functions:a |> b |> d c |> e = e ((d c) (b a))
- nix-command: Enables the
- trusted-users: these users have elevated privileges in nix (mostly used to acknowledge binary caches) - root is added per default here
- connect-timeout: normally, nix tries to reach the cache for 300 seconds for each derivation per cache. This setting lets me change that
- bash-prompt-prefix: adds a prefix to shells spawned by
nix develop - [min,max]-free: amounts of space where intermittent GC will be run during builds
- flake registry: URI of the global flake registry (I disable it)
- auto-optimise-store: create hardlinks in the nix store to save space
- warn-dirty: I do not need to see the warning when I have uncommited changes
- max-jobs: How many build jobs should be run in parallel.
autosets this to the number of CPUs (which is all) - on systems with many cores this can lead to OOM situations. The default is now1, but used to beauto, I set this manually just to be safe in the future. - use-cgroups: Actually run builds within cgroups
- experimental-features:
- nix.channel.enable: whether to use channels
- nix.registry: Sets the registry for this flake, which I set to its inputs. This allows me to use e.g.
nixpkgsdirectly innix repl - nix.nixPath: Basically the same as
nix.registry, but for the legacy nix commands
{ self, lib, pkgs, config, outputs, inputs, minimal, globals, withHomeManager, ... }:
let
inherit (config.swarselsystems) mainUser;
inherit (config.repo.secrets.common) atticPublicKey;
settings = if minimal then { } else {
environment.etc."nixos/configuration.nix".source = pkgs.writeText "configuration.nix" ''
assert builtins.trace "This location is not used. The config is found in ${config.swarselsystems.flakePath}!" false;
{ }
'';
nix =
let
flakeInputs = lib.filterAttrs (_: lib.isType "flake") inputs;
in
{
settings = {
connect-timeout = 5;
bash-prompt-prefix = "[33m$SHLVL:\\w [0m";
bash-prompt = "$(if [[ $? -gt 0 ]]; then printf \"[31m\"; else printf \"[32m\"; fi)λ [0m";
fallback = true;
min-free = 128000000;
max-free = 1000000000;
flake-registry = "";
auto-optimise-store = true;
warn-dirty = false;
max-jobs = 1;
use-cgroups = lib.mkIf config.swarselsystems.isLinux true;
};
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 10d";
};
optimise = {
automatic = true;
dates = "weekly";
};
channel.enable = false;
registry = rec {
nixpkgs.flake = inputs.nixpkgs;
# swarsel.flake = inputs.swarsel;
swarsel.flake = self;
n = nixpkgs;
s = swarsel;
};
nixPath = lib.mapAttrsToList (n: _: "${n}=flake:${n}") flakeInputs;
};
services.dbus.implementation = "broker";
systemd.services.nix-daemon = {
environment.TMPDIR = "/var/tmp";
};
};
in
{
options.swarselmodules.general = lib.mkEnableOption "general nix settings";
config = lib.mkIf config.swarselmodules.general
(lib.recursiveUpdate
{
sops.secrets = lib.mkIf (!minimal) {
github-api-token = { owner = mainUser; };
};
nix =
let
nix-version = "2_30";
in
{
package = pkgs.nixVersions."nix_${nix-version}";
settings = {
experimental-features = [
"nix-command"
"flakes"
"ca-derivations"
"cgroups"
"pipe-operators"
];
substituters = [
"https://${globals.services.attic.domain}/${mainUser}"
];
trusted-public-keys = [
atticPublicKey
];
trusted-users = [
"@wheel"
"${config.swarselsystems.mainUser}"
(lib.mkIf config.swarselmodules.server.ssh-builder "builder")
];
};
# extraOptions = ''
# plugin-files = ${pkgs.dev.nix-plugins}/lib/nix/plugins
# extra-builtins-file = ${self + /nix/extra-builtins.nix}
# '' + lib.optionalString (!minimal) ''
# !include ${config.sops.secrets.github-api-token.path}
# '';
# extraOptions = ''
# plugin-files = ${pkgs.nix-plugins.overrideAttrs (o: {
# buildInputs = [config.nix.package pkgs.boost];
# patches = o.patches or [];
# })}/lib/nix/plugins
# extra-builtins-file = ${self + /nix/extra-builtins.nix}
# '';
extraOptions =
let
nix-plugins = pkgs.nix-plugins.override {
nixComponents = pkgs.nixVersions."nixComponents_${nix-version}";
};
in
''
plugin-files = ${nix-plugins}/lib/nix/plugins
extra-builtins-file = ${self + /nix/extra-builtins.nix}
'' + lib.optionalString (!minimal) ''
!include ${config.sops.secrets.github-api-token.path}
'';
};
system.stateVersion = lib.mkDefault "23.05";
nixpkgs = {
overlays = [
outputs.overlays.default
] ++ lib.optionals withHomeManager [
(final: prev:
let
additions = final: _: import "${self}/pkgs/config" {
inherit self config lib;
pkgs = final;
homeConfig = config.home-manager.users.${config.swarselsystems.mainUser} or { };
};
in
additions final prev
)
];
config = lib.mkIf (!config.swarselsystems.isMicroVM) {
allowUnfree = true;
};
};
}
settings);
}
4.4.1.7. Setup home-manager base
We enable the use of home-manager as a NixoS module. A nice trick here is the extraSpecialArgs = inputs line, which enables the use of seflf in most parts of the configuration. This is useful to refer to the root of the flake (which is otherwise quite hard while maintaining flake purity).
{ self, inputs, config, lib, homeLib, outputs, globals, nodes, minimal, configName, arch, type, withHomeManager, ... }:
{
options.swarselmodules.home-manager = lib.mkEnableOption "home-manager";
config = lib.mkIf config.swarselmodules.home-manager {
home-manager = lib.mkIf withHomeManager {
useGlobalPkgs = true;
useUserPackages = true;
verbose = true;
backupFileExtension = "hm-bak";
overwriteBackup = true;
users.${config.swarselsystems.mainUser}.imports = [
inputs.nix-index-database.homeModules.nix-index
# inputs.sops.homeManagerModules.sops # this is not needed!! we add these secrets in nixos scope
inputs.spicetify-nix.homeManagerModules.default
inputs.swarsel-nix.homeModules.default
{
imports = [
"${self}/profiles/home"
"${self}/modules/home"
{
swarselprofiles = {
minimal = lib.mkIf minimal true;
};
}
];
# node = {
# secretsDir = if (!config.swarselsystems.isNixos) then ../../../hosts/home/${configName}/secrets else ../../../hosts/nixos/${configName}/secrets;
# };
home.stateVersion = lib.mkDefault config.system.stateVersion;
}
];
extraSpecialArgs = {
inherit (inputs) self nixgl;
inherit inputs outputs globals nodes minimal configName arch type;
lib = homeLib;
};
};
};
}
4.4.1.8. User setup, Make users non-mutable
This ensures that all user-configuration happens here in the config file. In case of using a fully setup system, this makes also sure that no further user level modifications can be made using CLI utilities (e.g. usermod etc.). Everything must be defined in the flake.
For that reason, make sure that sops-nix is properly working before finishing the minimal setup, otherwise we might lose user access. The bootstrapping script takes care of this.
{ pkgs, config, lib, globals, minimal, ... }:
{
options.swarselmodules.users = lib.mkEnableOption "user config";
config = lib.mkIf config.swarselmodules.users {
sops.secrets.main-user-hashed-pw = lib.mkIf (!config.swarselsystems.isPublic) { neededForUsers = true; };
users = {
mutableUsers = lib.mkIf (!minimal) false;
users = {
root = {
inherit (globals.root) hashedPassword;
# shell = pkgs.zsh;
};
"${config.swarselsystems.mainUser}" = {
isNormalUser = true;
description = "Leon S";
password = lib.mkIf (minimal || config.swarselsystems.isPublic) "setup";
hashedPasswordFile = lib.mkIf (!minimal && !config.swarselsystems.isPublic) config.sops.secrets.main-user-hashed-pw.path;
extraGroups = [ "wheel" ] ++ lib.optionals (!minimal && !config.swarselsystems.isMicroVM) [ "networkmanager" "syncthing" "docker" "lp" "audio" "video" "vboxusers" "libvirtd" "scanner" ];
packages = with pkgs; [ ];
};
};
};
};
}
4.4.1.9. Setup login keymap
Next, we setup the keymap in case we are not in a graphical session. At this point, I always resort to us/altgr-intl, as it is comfortable to use and I do not write too much German anyways.
{ lib, config, ... }:
{
options.swarselmodules.xserver = lib.mkEnableOption "xserver keymap";
config = lib.mkIf config.swarselmodules.packages {
services.xserver = {
xkb = {
layout = "us";
variant = "altgr-intl";
};
};
};
}
4.4.1.10. Time, locale settings
Setup timezone and locale. I want to use the US layout, but have the rest adapted to my country and timezone. Also, there is an issue with running Windows/Linux dualboot on the same machine where the hardware clock desyncs between the two OS'es. We fix that bug here as well.
{ lib, config, ... }:
{
options.swarselmodules.time = lib.mkEnableOption "time config";
config = lib.mkIf config.swarselmodules.time {
time = {
timeZone = "Europe/Vienna";
# hardwareClockInLocalTime = true;
};
i18n = {
defaultLocale = "en_US.UTF-8";
extraLocaleSettings = {
LC_ADDRESS = "de_AT.UTF-8";
LC_IDENTIFICATION = "de_AT.UTF-8";
LC_MEASUREMENT = "de_AT.UTF-8";
LC_MONETARY = "de_AT.UTF-8";
LC_NAME = "de_AT.UTF-8";
LC_NUMERIC = "de_AT.UTF-8";
LC_PAPER = "de_AT.UTF-8";
LC_TELEPHONE = "de_AT.UTF-8";
LC_TIME = "de_AT.UTF-8";
};
};
};
}
4.4.1.11. TODO PII management
This is also exposed to home-manager configurations, in case this ever breaks, I can also go back to importing nixosConfig as an attribute in the input attribute set and call the secrets using nixosConfig.repo.secrets.
Two modes of operation are supported:
- loading in a secret as a plain attribute set
{a = 3;} - loading in a function
{ nodes, ...}: {mac = nodes.xxx.interface.mac;}
Both cases should return the proper values - in the second case the parent set must be passed to the inherit under options.repo.secrets.
In general, there are three types of pii file:
- Per-node secrets: found under the nodes
secretDiraspii.nix.enc(exposed asconfig.repo.secrets.local) - Common secrets: found in
secrets/repo/pii.nix.enc(exposed asconfig.repo.secrets.common) - Global definitions of networks and domains: found in
secrets/repo/globals.nix.enc(not exposed to nodes, but only loaded in in Globals)
This system, while highly pleasant to work with during everyday use, sometimes has quirks:
nixos-rebuildcannot be used- this is because we need to call
nix buildin a separate step where sops-decrypt-and-cache will be cached. Once we have a finished build we can switch to that (all of this is handled by swarsel-deploy) - this is a bit cumbersome for hosts that are not supported by swarsel-deploy (currently this is mostly home-manager only configurations). In principal, building their config locally should work without issue, however, sometimes the decrypt step hiccups. In that case I usually resort to scp'ing the decrypted secrets to the host in question using justfile's
just secretscommand. After that, the secrets in/var/tmp/nix-import-encryptedneed to be moved to the correct dir depending on the build user uid (0/root or 1000/swarsel). After that I delete the cached secrets again. TODO: fix this behaviour.
- this is because we need to call
- the used nix version needs to be kept in sync with the version of nix that nix-plugins is compiled against
- currently, this mostly poses an issue when provisioning new hosts - the version of nixos-anywhere that I am using uses nix
2.28.x, so I wrote a dedicated Devshell (checks) (calleddeploy) that is used to set this environment up. This devshell is automatically used by the justfile commandjust bootstrap.
- currently, this mostly poses an issue when provisioning new hosts - the version of nixos-anywhere that I am using uses nix
# largely based on https://github.com/oddlama/nix-config/blob/main/modules/secrets.nix
{ config, inputs, lib, nodes, ... }:
let
# If the given expression is a bare set, it will be wrapped in a function,
# so that the imported file can always be applied to the inputs, similar to
# how modules can be functions or sets.
constSet = x: if builtins.isAttrs x then (_: x) else x;
# Try to access the extra builtin we loaded via nix-plugins.
# Throw an error if that doesn't exist.
sopsImportEncrypted =
assert lib.assertMsg (builtins ? extraBuiltins.sopsImportEncrypted)
"The extra builtin 'sopsImportEncrypted' is not available, so repo.secrets cannot be decrypted. Did you forget to add nix-plugins and point it to `<flakeRoot>/nix/extra-builtins.nix` ?";
builtins.extraBuiltins.sopsImportEncrypted;
# This "imports" an encrypted .nix.age file by evaluating the decrypted content.
importEncrypted =
path:
constSet (
if builtins.pathExists path then
sopsImportEncrypted path
else
{ }
);
in
{
options = {
repo = {
secretFiles = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.path;
example = lib.literalExpression "{ local = ./pii.nix.enc; }";
description = ''
This file manages the origin for this machine's repository-secrets. Anything that is
technically not a secret in the classical sense (i.e. that it has to be protected
after it has been deployed), but something you want to keep secret from the public;
Anything that you wouldn't want people to see on GitHub, but that can live unencrypted
on your own devices. Consider it a more ergonomic nix alternative to using git-crypt.
All of these secrets may (and probably will be) put into the world-readable nix-store
on the build and target hosts. You'll most likely want to store personally identifiable
information here, such as:
- MAC Addreses
- Static IP addresses
- Your full name (when configuring your users)
- Your postal address (when configuring e.g. home-assistant)
- ...
Each path given here must be an sops-encrypted .nix file. For each attribute `<name>`,
the corresponding file will be decrypted, imported and exposed as {option}`repo.secrets.<name>`.
'';
};
secrets = lib.mkOption {
readOnly = true;
default = lib.mapAttrs (_: x: importEncrypted x { inherit lib nodes inputs; inherit (inputs.topologyPrivate) topologyPrivate; }) config.repo.secretFiles;
type = lib.types.unspecified;
description = "Exposes the loaded repo secrets. This option is read-only.";
};
};
swarselmodules.pii = lib.mkEnableOption "enable pii management";
};
config = lib.mkIf config.swarselmodules.pii {
repo.secretFiles =
let
local = config.node.secretsDir + "/pii.nix.enc";
in
(lib.optionalAttrs (lib.pathExists local) { inherit local; }) // lib.optionalAttrs true {
common = ../../../secrets/repo/pii.nix.enc;
};
};
}
4.4.1.12. Lanzaboote (secure boot)
This dynamically uses systemd boot or Lanzaboote depending on the minimal system state and `config.swarselsystems.isSecureBoot`.
{ lib, pkgs, config, minimal, ... }:
let
inherit (config.swarselsystems) isSecureBoot isImpermanence;
in
{
options.swarselmodules.lanzaboote = lib.mkEnableOption "lanzaboote config";
config = lib.mkIf config.swarselmodules.lanzaboote {
environment.systemPackages = lib.mkIf isSecureBoot [
pkgs.sbctl
];
environment.persistence."/persist" = lib.mkIf (isImpermanence && isSecureBoot) {
directories = [{ directory = "/var/lib/sbctl"; }];
};
boot = {
loader = {
efi.canTouchEfiVariables = true;
systemd-boot.enable = lib.swarselsystems.mkIfElse (minimal || !isSecureBoot) (lib.mkForce true) (lib.mkForce false);
};
lanzaboote = lib.mkIf (!minimal && isSecureBoot) {
enable = true;
pkiBundle = "/var/lib/sbctl";
configurationLimit = 6;
};
};
};
}
4.4.1.13. Boot
{ lib, pkgs, config, globals, ... }:
{
options.swarselmodules.boot = lib.mkEnableOption "boot config";
config = lib.mkIf config.swarselmodules.boot {
boot = {
initrd.systemd = {
enable = true;
emergencyAccess = globals.root.hashedPassword;
users.root.shell = "${pkgs.bashInteractive}/bin/bash";
storePaths = [ "${pkgs.bashInteractive}/bin/bash" ];
extraBin = {
ip = "${pkgs.iproute2}/bin/ip";
ping = "${pkgs.iputils}/bin/ping";
cryptsetup = "${pkgs.cryptsetup}/bin/cryptsetup";
};
};
kernelParams = [ "log_buf_len=16M" ];
tmp.useTmpfs = true;
loader.timeout = lib.mkDefault 2;
};
console.earlySetup = true;
};
}
4.4.1.14. Impermanence
This is where the impermanence magic happens. When this is enabled, the root directory is rolled back to a blanket state on each reboot.
Normally, doing that also resets the lecture that happens on the first use of sudo, so we disable that at this point. Also, here we can set files to be persisted. Do note that you should still pay attention to files that need sudo access, as these need to be copied manually.
{ config, lib, ... }:
let
mapperTarget = lib.swarselsystems.mkIfElse config.swarselsystems.isCrypted "/dev/mapper/cryptroot" "/dev/disk/by-label/nixos";
inherit (config.swarselsystems) isImpermanence isCrypted isBtrfs;
in
{
options.swarselmodules.impermanence = lib.mkEnableOption "impermanence config";
config = lib.mkIf config.swarselmodules.impermanence {
security.sudo.extraConfig = lib.mkIf isImpermanence ''
# rollback results in sudo lectures after each reboot
Defaults lecture = never
'';
# This script does the actual wipe of the system
# So if it doesn't run, the btrfs system effectively acts like a normal system
# Taken from https://github.com/NotAShelf/nyx/blob/2a8273ed3f11a4b4ca027a68405d9eb35eba567b/modules/core/common/system/impermanence/default.nix
boot.tmp.useTmpfs = lib.mkIf (!isImpermanence) true;
boot.initrd.systemd = lib.mkIf (isImpermanence && isBtrfs) {
enable = true;
services.rollback = {
description = "Rollback BTRFS root subvolume to a pristine state";
wantedBy = [ "initrd.target" ];
# make sure it's done after encryption
# i.e. LUKS/TPM process
after = lib.swarselsystems.mkIfElseList isCrypted [ "systemd-cryptsetup@cryptroot.service" ] [ "dev-disk-by\\x2dlabel-nixos.device" ];
requires = lib.mkIf (!isCrypted) [ "dev-disk-by\\x2dlabel-nixos.device" ];
# mount the root fs before clearing
before = [ "sysroot.mount" ];
unitConfig.DefaultDependencies = "no";
serviceConfig.Type = "oneshot";
script = ''
mkdir -p /mnt
# We first mount the btrfs root to /mnt
# so we can manipulate btrfs subvolumes.
mount -o subvolid=5 -t btrfs ${mapperTarget} /mnt
btrfs subvolume list -o /mnt/root
# While we're tempted to just delete /root and create
# a new snapshot from /root-blank, /root is already
# populated at this point with a number of subvolumes,
# which makes `btrfs subvolume delete` fail.
# So, we remove them first.
#
# /root contains subvolumes:
# - /root/var/lib/portables
# - /root/var/lib/machines
btrfs subvolume list -o /mnt/root |
cut -f9 -d' ' |
while read subvolume; do
echo "deleting /$subvolume subvolume..."
btrfs subvolume delete "/mnt/$subvolume"
done &&
echo "deleting /root subvolume..." &&
btrfs subvolume delete /mnt/root
echo "restoring blank /root subvolume..."
btrfs subvolume snapshot /mnt/root-blank /mnt/root
# Once we're done rolling back to a blank snapshot,
# we can unmount /mnt and continue on the boot process.
umount /mnt
'';
};
};
environment.persistence."/persist" = lib.mkIf isImpermanence {
hideMounts = true;
directories =
[
"/root/.dotfiles"
"/etc/nix"
"/etc/NetworkManager/system-connections"
"/var/lib/nixos"
"/var/tmp"
{
directory = "/var/tmp/nix-import-encrypted"; # Decrypted repo-secrets can be kept
mode = "1777";
}
# "/etc/secureboot"
];
files = [
"/etc/ssh/ssh_host_ed25519_key"
"/etc/ssh/ssh_host_ed25519_key.pub"
"/etc/machine-id"
];
};
};
}
4.4.2. Client
This section is to be used for modules that are most likely only used on client PCs (like my laptops) but no on servers.
4.4.2.1. Imports
This section is for setting things that should be used on hosts that are using the default NixOS configuration. This means that servers should NOT import this, as much of these imported modules are user-configured.
{ lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/nixos/client";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/nixos/client";
}
4.4.2.2. System Packages
Mostly used to install some compilers and lsp's that I want to have available when not using a devShell flake. Most other packages should go in Installed packages.
{ lib, config, pkgs, minimal, ... }:
{
options.swarselmodules.packages = lib.mkEnableOption "install packages";
config = lib.mkIf config.swarselmodules.packages {
environment.systemPackages = with pkgs; lib.optionals (!minimal) [
# yubikey packages
gnupg
yubikey-personalization
yubico-pam
yubioath-flutter
yubikey-manager
yubikey-touch-detector
yubico-piv-tool
cfssl
pcsc-tools
pcscliteWithPolkit.out
# ledger packages
ledger-live-desktop
# pinentry
dbus
# swaylock-effects
syncthingtray-minimal
swayosd
# secure boot
sbctl
libsForQt5.qt5.qtwayland
# do not do this! clashes with the flake
# nix-index
nixos-generators
# commit hooks
pre-commit
# proc info
acpi
# pci info
pciutils
usbutils
# better make for general tasks
just
# sops
ssh-to-age
sops
# keyboards
qmk
vial
via
# theme related
adwaita-icon-theme
# kde-connect
xdg-desktop-portal
xdg-desktop-portal-gtk
xdg-desktop-portal-wlr
# bluetooth
bluez
ghostscript_headless
wireguard-tools
nixd
zig
zls
elk-to-svg
] ++ lib.optionals minimal [
networkmanager
curl
git
gnupg
rsync
ssh-to-age
sops
vim
just
sbctl
];
nixpkgs.config.permittedInsecurePackages = lib.mkIf (!minimal) [
"jitsi-meet-1.0.8043"
"electron-29.4.6"
"SDL_ttf-2.0.11"
# audacity?
"mbedtls-2.28.10"
# "qtwebengine-5.15.19"
];
};
}
4.4.2.3. Environment setup
Next, we will setup some environment variables that need to be set on the system-side. We apply some compatibility options for chromium apps on wayland, enable the wordlist and make metadata reading possible for my file explorer (nautilus).
{ lib, config, pkgs, ... }:
{
options.swarselmodules.env = lib.mkEnableOption "environment config";
config = lib.mkIf config.swarselmodules.env {
environment = {
wordlist.enable = true;
sessionVariables = {
NIXOS_OZONE_WL = "1";
SWARSEL_LO_RES = config.swarselsystems.lowResolution;
SWARSEL_HI_RES = config.swarselsystems.highResolution;
GST_PLUGIN_SYSTEM_PATH_1_0 = lib.makeSearchPathOutput "lib" "lib/gstreamer-1.0" (with pkgs.gst_all_1; [
gst-plugins-good
gst-plugins-bad
gst-plugins-ugly
gst-libav
]);
} // (lib.optionalAttrs (!config.swarselsystems.isPublic) { });
};
};
}
4.4.2.4. Security (polkit)
Needed for control over system-wide privileges etc. Also I make sure that the root user has access to SSH_AUTH_SOCK (without this, root will not be able to read my nix-secrets repository).
{ lib, config, minimal, ... }:
{
options.swarselmodules.security = lib.mkEnableOption "security config";
config = lib.mkIf config.swarselmodules.security {
security = {
# pki.certificateFiles = [
# config.sops.secrets.harica-root-ca.path
# ];
pam.services = lib.mkIf (!minimal) {
login.u2fAuth = true;
sudo.u2fAuth = true;
sshd.u2fAuth = false;
swaylock = {
u2fAuth = true;
fprintAuth = false;
};
};
polkit.enable = lib.mkIf (!minimal) true;
sudo.extraConfig = ''
Defaults env_keep+=SSH_AUTH_SOCK
'' + lib.optionalString (!minimal) ''
Defaults env_keep+=XDG_RUNTIME_DIR
Defaults env_keep+=WAYLAND_DISPLAY
'';
};
};
}
4.4.2.5. Reduce systemd timeouts
There is a persistent bug over Linux kernels that makes the user wait 1m30s on system shutdown due to the reason a stop job is running for session 1 of user .... I do not want to wait that long and am confident no important data is lost by doing this.
{ lib, config, ... }:
{
options.swarselmodules.systemdTimeout = lib.mkEnableOption "systemd timeout config";
config = lib.mkIf config.swarselmodules.systemdTimeout {
# systemd
systemd.settings.Manager = {
DefaultTimeoutStartSec = "60s";
DefaultTimeoutStopSec = "15s";
};
};
}
4.4.2.6. Hardware settings
Enable OpenGL, Sound, Bluetooth and various drivers.
{ pkgs, config, lib, ... }:
{
options.swarselmodules.hardware = lib.mkEnableOption "hardware config";
options.swarselsystems = {
hasBluetooth = lib.mkEnableOption "bluetooth availability";
hasFingerprint = lib.mkEnableOption "fingerprint sensor availability";
trackpoint = {
isAvailable = lib.mkEnableOption "trackpoint availability";
trackpoint.device = lib.mkOption {
type = lib.types.str;
default = "";
};
};
};
config = lib.mkIf config.swarselmodules.hardware {
hardware = {
# opengl.driSupport32Bit = true is replaced with graphics.enable32Bit and hence redundant
graphics = {
enable = true;
enable32Bit = true;
};
trackpoint = lib.mkIf config.swarselsystems.trackpoint.isAvailable {
enable = true;
inherit (config.swarselsystems.trackpoint) device;
};
keyboard.qmk.enable = true;
enableAllFirmware = lib.mkDefault true;
bluetooth = lib.mkIf config.swarselsystems.hasBluetooth {
enable = true;
package = pkgs.stable.bluez;
powerOnBoot = true;
settings = {
General = {
Enable = "Source,Sink,Media,Socket";
};
};
};
};
services.fprintd.enable = lib.mkIf config.swarselsystems.hasFingerprint true;
};
}
4.4.2.7. Pulseaudio
This is only used on systems not running Pipewire.
{ config, pkgs, lib, ... }: {
options.swarselmodules.pulseaudio = lib.mkEnableOption "pulseaudio config";
config = lib.mkIf config.swarselmodules.pulseaudio {
services.pulseaudio = {
enable = lib.mkIf (!config.services.pipewire.enable) true;
package = pkgs.pulseaudioFull;
};
};
}
4.4.2.8. Pipewire
Pipewire handles communication on Wayland. This enables several sound tools as well as screen sharing in combinaton with xdg-desktop-portal-wlr.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.pipewire = lib.mkEnableOption "pipewire config";
config = lib.mkIf config.swarselmodules.pipewire {
security.rtkit.enable = true; # this is required for pipewire real-time access
services.pipewire = {
enable = true;
package = pkgs.stable.pipewire;
pulse.enable = true;
jack.enable = true;
audio.enable = true;
wireplumber.enable = true;
alsa = {
enable = true;
support32Bit = true;
};
};
};
}
4.4.2.9. Common network settings
Here I only enable networkmanager and a few default networks. The rest of the network config is done separately in System specific configuration.
{ self, lib, pkgs, config, globals, ... }:
let
certsSopsFile = self + /secrets/repo/certs.yaml;
clientSopsFile = config.node.secretsDir + "/secrets.yaml";
inherit (config.repo.secrets.common.network) wlan1 mobile1 vpn1-location vpn1-cipher vpn1-address eduroam-anon;
iwd = config.networking.networkmanager.wifi.backend == "iwd";
in
{
options.swarselsystems = {
firewall = lib.swarselsystems.mkTrueOption;
};
options.swarselmodules.network = lib.mkEnableOption "network config";
config = lib.mkIf config.swarselmodules.network {
sops = {
secrets = lib.mkIf (!config.swarselsystems.isPublic) {
wlan1-pw = { };
wlan2-pw = { };
laptop-hotspot-pw = { };
mobile-hotspot-pw = { };
eduroam-user = { };
eduroam-pw = { };
pia-vpn-user = { };
pia-vpn-pw = { };
home-wireguard-client-private-key = { sopsFile = clientSopsFile; };
home-wireguard-server-public-key = { };
home-wireguard-endpoint = { };
pia-vpn1-crl-pem = { sopsFile = certsSopsFile; };
pia-vpn1-ca-pem = { sopsFile = certsSopsFile; };
};
templates = lib.mkIf (!config.swarselsystems.isPublic) {
"network-manager.env".content = ''
WLAN1_PW=${config.sops.placeholder.wlan1-pw}
WLAN2_PW=${config.sops.placeholder.wlan2-pw}
LAPTOP_HOTSPOT_PW=${config.sops.placeholder.laptop-hotspot-pw}
MOBILE_HOTSPOT_PW=${config.sops.placeholder.mobile-hotspot-pw}
EDUROAM_USER=${config.sops.placeholder.eduroam-user}
EDUROAM_PW=${config.sops.placeholder.eduroam-pw}
PIA_VPN_USER=${config.sops.placeholder.pia-vpn-user}
PIA_VPN_PW=${config.sops.placeholder.pia-vpn-pw}
HOME_WIREGUARD_CLIENT_PRIVATE_KEY=${config.sops.placeholder.home-wireguard-client-private-key}
HOME_WIREGUARD_SERVER_PUBLIC_KEY=${config.sops.placeholder.home-wireguard-server-public-key}
HOME_WIREGUARD_ENDPOINT=${config.sops.placeholder.home-wireguard-endpoint}
'';
};
};
services.resolved.enable = true;
networking = {
hostName = config.node.name;
hosts = {
"${globals.networks.home-lan.hosts.winters.ipv4}" = [ globals.services.transmission.domain ];
};
wireless.iwd = {
enable = true;
settings = {
IPv6 = {
Enabled = true;
};
Settings = {
AutoConnect = true;
};
# DriverQuirks = {
# UseDefaultInterface = true;
# };
};
};
nftables.enable = lib.mkDefault true;
enableIPv6 = lib.mkDefault true;
firewall = {
enable = lib.swarselsystems.mkStrong config.swarselsystems.firewall;
checkReversePath = lib.mkDefault false;
allowedUDPPorts = [ 51820 ]; # 51820: wireguard
allowedTCPPortRanges = [
{ from = 1714; to = 1764; } # kde-connect
];
allowedUDPPortRanges = [
{ from = 1714; to = 1764; } # kde-connect
];
};
networkmanager = {
enable = true;
wifi.backend = "iwd";
dns = "systemd-resolved";
plugins = [
# list of plugins: https://search.nixos.org/packages?query=networkmanager-
# docs https://networkmanager.dev/docs/vpn/
pkgs.networkmanager-openconnect
pkgs.networkmanager-openvpn
];
ensureProfiles = lib.mkIf (!config.swarselsystems.isPublic) {
environmentFiles = [
"${config.sops.templates."network-manager.env".path}"
];
profiles =
let
inherit (config.repo.secrets.local.network) home-wireguard-address home-wireguard-allowed-ips;
in
{
${wlan1} = {
connection = {
id = wlan1;
# permissions = "";
type = "wifi";
autoconnect-priority = "999";
};
ipv4 = {
# dns-search = "";
method = "auto";
};
ipv6 = {
addr-gen-mode = "stable-privacy";
# dns-search = "";
method = "auto";
};
wifi = {
# mac-address-blacklist = "";
mode = "infrastructure";
# band = "a";
ssid = wlan1;
};
wifi-security = {
# auth-alg = "open";
key-mgmt = "wpa-psk";
psk = "$WLAN1_PW";
};
};
LAN-Party = {
connection = {
autoconnect = "false";
id = "LAN-Party";
type = "ethernet";
};
ethernet = {
auto-negotiate = "true";
cloned-mac-address = "preserve";
};
ipv4 = { method = "shared"; };
ipv6 = {
addr-gen-mode = "stable-privacy";
method = "auto";
};
proxy = { };
};
eduroam = {
"802-1x" = {
eap = if (!iwd) then "ttls;" else "peap;";
identity = "$EDUROAM_USER";
password = "$EDUROAM_PW";
phase2-auth = "mschapv2";
anonymous-identity = lib.mkIf iwd eduroam-anon;
};
connection = {
id = "eduroam";
type = "wifi";
};
ipv4 = { method = "auto"; };
ipv6 = {
addr-gen-mode = "default";
method = "auto";
};
proxy = { };
wifi = {
mode = "infrastructure";
ssid = "eduroam";
};
wifi-security = {
auth-alg = "open";
key-mgmt = "wpa-eap";
};
};
local = {
connection = {
autoconnect = "false";
id = "local";
type = "ethernet";
};
ethernet = { };
ipv4 = {
address1 = "10.42.1.1/24";
method = "shared";
};
ipv6 = {
addr-gen-mode = "stable-privacy";
method = "auto";
};
proxy = { };
};
${mobile1} = {
connection = {
id = mobile1;
type = "wifi";
autoconnect-priority = "500";
};
ipv4 = { method = "auto"; };
ipv6 = {
addr-gen-mode = "default";
method = "auto";
};
proxy = { };
wifi = {
mode = "infrastructure";
ssid = mobile1;
};
wifi-security = {
auth-alg = "open";
key-mgmt = "wpa-psk";
psk = "$MOBILE_HOTSPOT_PW";
};
};
home-wireguard = {
connection = {
id = "HomeVPN";
type = "wireguard";
autoconnect = "false";
interface-name = "wg1";
};
wireguard = { private-key = "$HOME_WIREGUARD_CLIENT_PRIVATE_KEY"; };
"wireguard-peer.$HOME_WIREGURARD_SERVER_PUBLIC_KEY" = {
endpoint = "$HOME_WIREGUARD_ENDPOINT";
allowed-ips = home-wireguard-allowed-ips;
};
ipv4 = {
method = "ignore";
address1 = home-wireguard-address;
};
ipv6 = {
addr-gen-mode = "stable-privacy";
method = "ignore";
};
proxy = { };
};
pia-vpn1 = {
connection = {
autoconnect = "false";
id = "PIA ${vpn1-location}";
type = "vpn";
};
ipv4 = { method = "auto"; };
ipv6 = {
addr-gen-mode = "stable-privacy";
method = "auto";
};
proxy = { };
vpn = {
auth = "sha1";
ca = config.sops.secrets."pia-vpn1-ca-pem".path;
challenge-response-flags = "2";
cipher = vpn1-cipher;
compress = "yes";
connection-type = "password";
crl-verify-file = config.sops.secrets."pia-vpn1-crl-pem".path;
dev = "tun";
password-flags = "0";
remote = vpn1-address;
remote-cert-tls = "server";
reneg-seconds = "0";
service-type = "org.freedesktop.NetworkManager.openvpn";
username = "$PIA_VPN_USER";
};
vpn-secrets = { password = "$PIA_VPN_PW"; };
};
Hotspot = {
connection = {
autoconnect = "false";
id = "Hotspot";
type = "wifi";
};
ipv4 = { method = "shared"; };
ipv6 = {
addr-gen-mode = "default";
method = "ignore";
};
proxy = { };
wifi = {
mode = "ap";
ssid = "Hotspot-${config.swarselsystems.mainUser}";
};
wifi-security = {
group = "ccmp;";
key-mgmt = "wpa-psk";
pairwise = "ccmp;";
proto = "rsn;";
psk = "$MOBILE_HOTSPOT_PW";
};
};
};
};
};
};
systemd.services.NetworkManager-ensure-profiles.after = [ "NetworkManager.service" ];
};
}
4.4.2.10. sops
I use sops-nix to handle secrets that I want to have available on my machines at all times. Procedure to add a new machine:
- `ssh-keygen -t ed25519 -C "NAME sops"` in .ssh directory (or wherever) - name e.g. "sops"
- cat ~/.ssh/sops.pub | ssh-to-age | wl-copy
- add the output to .sops.yaml
- cp ~/.ssh/sops.pub ~/.dotfiles/secrets/public/NAME.pub
- update entry for sops.age.sshKeyPaths
{ self, config, lib, ... }:
{
options.swarselmodules.sops = lib.mkEnableOption "sops config";
config = lib.mkIf config.swarselmodules.sops {
sops = {
# age.sshKeyPaths = lib.swarselsystems.mkIfElseList config.swarselsystems.isBtrfs [ "/persist/.ssh/sops" "/persist/.ssh/ssh_host_ed25519_key" ] [ "${config.swarselsystems.homeDir}/.ssh/sops" "/etc/ssh/sops" "/etc/ssh/ssh_host_ed25519_key" ];
age.sshKeyPaths = [ "${if config.swarselsystems.isImpermanence then "/persist" else ""}/etc/ssh/ssh_host_ed25519_key" ];
# defaultSopsFile = "${if config.swarselsystems.isImpermanence then "/persist" else ""}${config.swarselsystems.flakePath}/secrets/repo/common.yaml";
defaultSopsFile = self + "/secrets/repo/common.yaml";
validateSopsFiles = false;
};
};
}
4.4.2.11. Remote building
{ lib, config, globals, ... }:
let
inherit (config.swarselsystems) homeDir mainUser isClient;
in
{
options.swarselmodules.remotebuild = lib.mkEnableOption "enable remote builds on this machine";
config = lib.mkIf config.swarselmodules.remotebuild {
sops.secrets = {
builder-key = lib.mkIf isClient { owner = mainUser; path = "${homeDir}/.ssh/builder"; mode = "0600"; };
nixbuild-net-key = { owner = mainUser; path = "${homeDir}/.ssh/nixbuild-net"; mode = "0600"; };
};
nix = {
settings.builders-use-substitutes = true;
distributedBuilds = true;
buildMachines = [
(lib.mkIf isClient {
hostName = config.repo.secrets.common.builder1-ip;
system = "aarch64-linux";
maxJobs = 20;
speedFactor = 10;
})
(lib.mkIf isClient {
hostName = globals.hosts.belchsfactory.wanAddress4;
system = "aarch64-linux";
maxJobs = 4;
speedFactor = 2;
protocol = "ssh-ng";
})
{
hostName = "eu.nixbuild.net";
system = "x86_64-linux";
maxJobs = 100;
speedFactor = 2;
supportedFeatures = [ "big-parallel" ];
}
];
};
programs.ssh = {
knownHosts = {
nixbuild = {
hostNames = [ "eu.nixbuild.net" ];
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPIQCZc54poJ8vqawd8TraNryQeJnvH1eLpIDgbiqymM";
};
builder1 = lib.mkIf isClient {
hostNames = [ config.repo.secrets.common.builder1-ip ];
publicKey = config.repo.secrets.common.builder1-pubHostKey;
};
jump = lib.mkIf isClient {
hostNames = [ globals.hosts.liliputsteps.wanAddress4 ];
publicKey = config.repo.secrets.common.jump-pubHostKey;
};
builder2 = lib.mkIf isClient {
hostNames = [ globals.hosts.belchsfactory.wanAddress4 ];
publicKey = config.repo.secrets.common.builder2-pubHostKey;
};
};
extraConfig = ''
Host eu.nixbuild.net
ConnectTimeout 1
PubkeyAcceptedKeyTypes ssh-ed25519
ServerAliveInterval 60
IPQoS throughput
IdentityFile ${config.sops.secrets.nixbuild-net-key.path}
'' + lib.optionalString isClient ''
Host ${config.repo.secrets.common.builder1-ip}
ConnectTimeout 1
User ${mainUser}
IdentityFile ${config.sops.secrets.builder-key.path}
Host ${globals.hosts.belchsfactory.wanAddress4}
ConnectTimeout 5
ProxyJump ${globals.hosts.liliputsteps.wanAddress4}
User builder
IdentityFile ${config.sops.secrets.builder-key.path}
Host ${globals.hosts.liliputsteps.wanAddress4}
ConnectTimeout 1
User jump
IdentityFile ${config.sops.secrets.builder-key.path}
'';
};
};
}
4.4.2.12. Theme (stylix)
By default, stylix wants to style GRUB as well. However, I think that looks horrible.
theme is defined in stylix color scheme.
{ self, lib, config, vars, withHomeManager, ... }:
{
options.swarselmodules.stylix = lib.mkEnableOption "stylix config";
config = {
stylix = {
enable = true;
base16Scheme = "${self}/files/stylix/swarsel.yaml";
} // lib.optionalAttrs config.swarselmodules.stylix
(lib.recursiveUpdate
{
targets.grub.enable = false; # the styling makes grub more ugly
image = config.swarselsystems.wallpaper;
}
vars.stylix);
} // lib.optionalAttrs withHomeManager {
home-manager.users."${config.swarselsystems.mainUser}" = {
stylix = {
targets = vars.stylixHomeTargets;
};
};
};
}
4.4.2.13. Programs (including zsh setup)
Some programs profit from being installed through dedicated NixOS settings on system-level; these go here. Notably the zsh setup goes here and cannot be deleted under any circumstances.
{ lib, config, ... }:
{
options.swarselmodules.programs = lib.mkEnableOption "small program modules config";
config = lib.mkIf config.swarselmodules.programs {
programs = {
dconf.enable = true;
evince.enable = true;
kdeconnect.enable = true;
};
};
}
4.4.2.13.1. zsh
Here I disable global completion to prevent redundant compinit calls and cache invalidation that slow down shell startup (enabled on the home-manager side).
{ lib, config, pkgs, ... }:
{
options.swarselmodules.zsh = lib.mkEnableOption "zsh base config";
config = lib.mkIf config.swarselmodules.zsh {
programs.zsh = {
enable = true;
enableCompletion = false;
};
users.defaultUserShell = pkgs.zsh;
environment.shells = with pkgs; [ zsh ];
environment.pathsToLink = [ "/share/zsh" ];
};
}
4.4.2.13.2. syncthing
{ lib, config, pkgs, ... }:
let
inherit (config.swarselsystems) mainUser homeDir;
devices = config.swarselsystems.syncthing.syncDevices;
servicePort = 8384;
in
{
options.swarselmodules.syncthing = lib.mkEnableOption "syncthing config";
config = lib.mkIf config.swarselmodules.syncthing {
services.syncthing = {
enable = true;
systemService = true;
guiAddress = "127.0.0.1:${builtins.toString servicePort}";
package = pkgs.syncthing;
user = mainUser;
dataDir = homeDir;
configDir = "${homeDir}/.config/syncthing";
openDefaultPorts = true;
overrideDevices = true;
overrideFolders = true;
settings = {
options = {
urAccepted = -1;
};
inherit (config.swarselsystems.syncthing) devices;
folders = {
"Default Folder" = lib.mkDefault {
path = "${homeDir}/Sync";
inherit devices;
id = "default";
};
"Obsidian" = {
path = "${homeDir}/Obsidian";
inherit devices;
id = "yjvni-9eaa7";
};
"Org" = {
path = "${homeDir}/Org";
inherit devices;
id = "a7xnl-zjj3d";
};
"Vpn" = {
path = "${homeDir}/Vpn";
inherit devices;
id = "hgp9s-fyq3p";
};
};
};
};
};
}
4.4.2.14. Services
Setting up some hardware services as well as keyboard related settings. Here we make sure that we can use the CAPS key as a ESC/CTRL double key, which is a lifesaver.
4.4.2.14.1. blueman
Enables the blueman service including the nice system tray icon.
{ lib, config, ... }:
{
options.swarselmodules.blueman = lib.mkEnableOption "blueman config";
config = lib.mkIf config.swarselmodules.blueman {
services.blueman.enable = true;
services.hardware.bolt.enable = true;
};
}
4.4.2.14.2. Network devices
In this section we enable compatibility with several network devices I have at home, mainly printers and scanners.
This allows me to use my big scanner/printer's scanning function over the network. This also allows me to use my big scanner/printer's printing function over the network. Most of the settings are driver related. Avahi is the service used for the network discovery.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.networkDevices = lib.mkEnableOption "network device config";
config = lib.mkIf config.swarselmodules.networkDevices {
# enable scanners over network
hardware.sane = {
enable = true;
extraBackends = [ pkgs.sane-airscan ];
};
# enable discovery and usage of network devices (esp. printers)
services.printing = {
enable = true;
drivers = [
pkgs.gutenprint
pkgs.gutenprintBin
];
browsedConf = ''
BrowseDNSSDSubTypes _cups,_print
BrowseLocalProtocols all
BrowseRemoteProtocols all
CreateIPPPrinterQueues All
BrowseProtocols all
'';
};
services.avahi = {
enable = true;
nssmdns4 = true;
openFirewall = true;
};
};
}
4.4.2.14.3. enable GVfs
This is being set to allow myself to use all functions of nautilus in NixOS
{ lib, config, ... }:
{
options.swarselmodules.gvfs = lib.mkEnableOption "gvfs config for nautilus";
config = lib.mkIf config.swarselmodules.gvfs {
services.gvfs.enable = true;
};
}
4.4.2.14.4. interception-tools: Make CAPS work as ESC/CTRL
This is a super-convenient package that lets my remap my CAPS key to ESC if pressed shortly, and CTRL if being held.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.interceptionTools = lib.mkEnableOption "interception tools config";
config = lib.mkIf config.swarselmodules.interceptionTools {
# Make CAPS work as a dual function ESC/CTRL key
services.interception-tools = {
enable = true;
udevmonConfig =
let
dualFunctionKeysConfig = builtins.toFile "dual-function-keys.yaml" ''
TIMING:
TAP_MILLISEC: 200
DOUBLE_TAP_MILLISEC: 0
MAPPINGS:
- KEY: KEY_CAPSLOCK
TAP: KEY_ESC
HOLD: KEY_LEFTCTRL
'';
in
''
- JOB: |
${pkgs.interception-tools}/bin/intercept -g $DEVNODE \
| ${pkgs.interception-tools-plugins.dual-function-keys}/bin/dual-function-keys -c ${dualFunctionKeysConfig} \
| ${pkgs.interception-tools}/bin/uinput -d $DEVNODE
DEVICE:
EVENTS:
EV_KEY: [KEY_CAPSLOCK]
'';
};
};
}
4.4.2.14.5. keyd: remap SUPER
{ lib, config, ... }:
let
moduleName = "keyd";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "${moduleName} tools config";
config = lib.mkIf config.swarselmodules.${moduleName} {
services.keyd = {
enable = true;
keyboards = {
default = {
ids = [ "*" ];
settings = {
main = {
leftmeta = "overload(meta, macro(rightmeta+z))";
rightmeta = "overload(meta, macro(rightmeta+z))";
};
};
};
};
};
};
}
4.4.2.14.6. power-profiles-daemon
This enables power profile management. The available modes are:
- power-saver
- balanced
- performance
Most of the time I am using power-saver, however, it is good to be able to choose.
{ lib, config, ... }:
{
options.swarselmodules.ppd = lib.mkEnableOption "power profiles daemon config";
config = lib.mkIf config.swarselmodules.ppd {
services.power-profiles-daemon.enable = true;
};
}
4.4.2.14.7. SwayOSD
{ lib, pkgs, config, ... }:
{
options.swarselmodules.swayosd = lib.mkEnableOption "swayosd settings";
config = lib.mkIf config.swarselmodules.swayosd {
environment.systemPackages = [ pkgs.dev.swayosd ];
services.udev.packages = [ pkgs.dev.swayosd ];
systemd.services.swayosd-libinput-backend = {
description = "SwayOSD LibInput backend for listening to certain keys like CapsLock, ScrollLock, VolumeUp, etc.";
documentation = [ "https://github.com/ErikReider/SwayOSD" ];
wantedBy = [ "graphical.target" ];
partOf = [ "graphical.target" ];
after = [ "graphical.target" ];
serviceConfig = {
Type = "dbus";
BusName = "org.erikreider.swayosd";
ExecStart = "${pkgs.dev.swayosd}/bin/swayosd-libinput-backend";
Restart = "on-failure";
};
};
};
}
4.4.2.15. Hardware compatibility settings (Yubikey, Ledger, Keyboards) - udev rules
4.4.2.15.1. Yubikey
This takes care of the main Yubikey related configuration on the NixOS side - note that the starting of the gpg-agent is done in the sway settings, to also perform this step of the setup for non NixOS-machines at the same time.
I want to use the ssh-agent from gpg-agent's ssh compatibility, which is why we disable ssh-agent. Also, we load some extra udev rules using hardware.gpgSmartcards.enable.
Many guides state that it is needed to enable pcscd to use the smartcard mode (CCID) of the Yubikey. However, enabling it causes some problems when locking the screen and unplugging the Yubikey, after which the Yubikey only becomes available again as a smart card after about one minute. I found that is is sufficient to enable services.gpg-agent.enableScDaemon in home-manager instead.
Also, since I use a GPG key in sops, it seems that scdaemon creates an instance at boot which sometimes hogs the Yubikey, which leads to significant delays after e.g. locking the screen and unplugging the Yubikey. Since I do not need the GPG key for the actual sops secrets (I use machine age keys instead), I kill that process.
{ lib, config, pkgs, ... }:
let
inherit (config.swarselsystems) mainUser;
inherit (config.repo.secrets.common.yubikeys) cfg1 cfg2;
in
{
options.swarselmodules.yubikey = lib.mkEnableOption "yubikey config";
config = lib.mkIf config.swarselmodules.yubikey {
programs.ssh.startAgent = false;
services.pcscd.enable = false;
hardware.gpgSmartcards.enable = true;
security.pam.u2f = {
enable = true;
control = "sufficient";
settings = {
interactive = false; # displays a prompt BEFORE asking for presence
cue = true; # prints a message that a touch is requrired
origin = "pam://${mainUser}"; # make the keys work on all machines
authfile = pkgs.writeText "u2f-mappings" (lib.concatStrings [
mainUser
cfg1
cfg2
]);
};
};
services.udev.packages = with pkgs; [
yubikey-personalization
];
};
}
4.4.2.15.2. Ledger
This performs the necessary configuration to support this hardware.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.ledger = lib.mkEnableOption "ledger config";
config = lib.mkIf config.swarselmodules.ledger {
hardware.ledger.enable = true;
services.udev.packages = with pkgs; [
ledger-udev-rules
];
};
}
4.4.2.15.3. Keyboards
This loads some udev rules that I need for my split keyboards.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.keyboards = lib.mkEnableOption "keyboards config";
config = lib.mkIf config.swarselmodules.keyboards {
services.udev.packages = with pkgs; [
qmk-udev-rules
vial
via
];
};
}
4.4.2.16. System Login (greetd)
This section houses the greetd related settings. I do not really want to use a display manager, but it is useful to have setup in some ways - in my case for starting sway on system startup. Notably the default user login setting that is commented out here goes into the system specific settings, make sure to update it there
{ lib, config, pkgs, ... }:
{
options.swarselmodules.login = lib.mkEnableOption "login config";
config = lib.mkIf config.swarselmodules.login {
services.greetd = {
enable = true;
settings = {
# initial_session.command = "sway";
initial_session.command = "uwsm start -- sway-uwsm.desktop";
# --cmd sway
default_session.command = ''
${pkgs.tuigreet}/bin/tuigreet \
--time \
--asterisks \
--user-menu \
--cmd "uwsm start -- sway-uwsm.desktop"
'';
};
};
# environment.etc."greetd/environments".text = ''
# sway
# '';
};
}
4.4.2.17. nix-ld
This provides libraries for binaries that are not patched for use on NixOS. This really makes the biggest gripe with NixOS go away, that being having to run a binary that is only found in a single spot. It is most of the times possible to patch such a file, but this makes such a situation take much less time to resolve.
Only some binaries that touch system settings might still not work, apart from that, the list of libraries I have curated here should be quite exhaustive.
When a program does not work, start with nix-ldd <program>. This will tell you which library is missing. Afterwards, continue with nix-locate <program> to find which packages provide that library. Add it to libraries below and rebuild. After a reboot, it will be visible using nix-ldd. It can also be useful to take a look at ldd to see which libraries are needed in general.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.nix-ld = lib.mkEnableOption "nix-ld config";
config = lib.mkIf config.swarselmodules.nix-ld {
programs.nix-ld = {
enable = true;
libraries = with pkgs; [
SDL
SDL2
SDL2_image
SDL2_mixer
SDL2_ttf
SDL_image
SDL_mixer
SDL_ttf
alsa-lib
at-spi2-atk
at-spi2-core
atk
bzip2
cairo
cups
curl
dbus
dbus-glib
expat
ffmpeg
flac
fontconfig
freeglut
freetype
fuse3
gdk-pixbuf
glew110
glib
gnome2.GConf
pango
gtk2
gtk3
icu
libGL
libappindicator-gtk2
libappindicator-gtk3
libcaca
libcanberra
libcap
libdbusmenu-gtk2
libdrm
libelf
libgbm
libgcrypt
libglvnd
libidn
libindicator-gtk2
libjpeg
libmikmod
libnotify
libogg
libpng
libpng12
libpulseaudio
librsvg
libsamplerate
libtheora
libtiff
libudev0-shim
libunwind
libusb1
libuuid
libva
libvdpau
libvorbis
libvpx
libxkbcommon
libxml2
libz
mesa
nspr
nss
openssl
pango
pipewire
pixman
speex
# stable.cc.cc
stable25_05.steam-fhsenv-without-steam
systemd
tbb
vulkan-loader
xorg.libICE
xorg.libSM
xorg.libX11
xorg.libXScrnSaver
xorg.libXcomposite
xorg.libXcursor
xorg.libXdamage
xorg.libXext
xorg.libXfixes
xorg.libXft
xorg.libXi
xorg.libXinerama
xorg.libXmu
xorg.libXrandr
xorg.libXrender
xorg.libXt
xorg.libXtst
xorg.libXxf86vm
xorg.libxcb
xorg.libxshmfence
zlib
];
};
};
}
4.4.2.18. Summary of nixos-rebuild diff
This snipped is added to the activation script that is run after every rebuild and shows what packages have been added and removed. This is actually not the optimal place to add that snipped, but the correct spot is in some perl file that I have not had the leisure to take a look at yet.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.nvd = lib.mkEnableOption "nvd config";
config = lib.mkIf config.swarselmodules.nvd {
environment.systemPackages = [
pkgs.nvd
];
# system.activationScripts.diff = {
# supportsDryActivation = true;
# text = ''
# ${pkgs.nvd}/bin/nvd --color=always --nix-bin-dir=${pkgs.nix}/bin diff \
# /run/current-system "$systemConfig"
# '';
# };
};
}
4.4.2.19. gnome-keyring
Used for storing sessions in e.g. Nextcloud. Using this on a system level keeps the login information when logging out of the session as well.
{ lib, config, ... }:
{
options.swarselmodules.gnome-keyring = lib.mkEnableOption "gnome-keyring config";
config = lib.mkIf config.swarselmodules.gnome-keyring {
services.gnome.gnome-keyring = {
enable = true;
};
programs.seahorse.enable = true;
};
}
4.4.2.20. Sway
This is used to better integrate Sway into the system on NixOS hosts. On the home-manager side, the package attribute will be null for such an host, using the systems derivation instead.
{ lib, config, pkgs, withHomeManager, ... }:
let
inherit (config.swarselsystems) mainUser;
in
{
options.swarselmodules.sway = lib.mkEnableOption "sway config";
config = lib.mkIf config.swarselmodules.sway
{
programs.sway = {
enable = true;
package = pkgs.swayfx;
wrapperFeatures = {
base = true;
gtk = true;
};
};
} // lib.optionalAttrs withHomeManager {
inherit (config.home-manager.users.${mainUser}.wayland.windowManager.sway) extraSessionCommands;
};
}
4.4.2.21. xdg-portal (Screensharing)
This allows me to use screen sharing on Wayland. The implementation is a bit crude and only the whole screen can be shared. However, most of the time that is all I need to do anyways.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.xdg-portal = lib.mkEnableOption "xdg portal config";
config = lib.mkIf config.swarselmodules.xdg-portal {
xdg.portal = {
enable = true;
config = {
common = {
default = "wlr";
};
};
wlr.enable = true;
wlr.settings.screencast = {
output_name = "eDP-1";
chooser_type = "simple";
chooser_cmd = "${pkgs.slurp}/bin/slurp -f %o -or";
};
};
};
}
4.4.2.22. Podman (distrobox)
I am using distrobox to quickly circumvent isses that I cannot immediately solve on NixOS. It is always the goal to quickly get things working on NixOS, but this prevents me from getting completely stuck.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.distrobox = lib.mkEnableOption "distrobox config";
config = lib.mkIf config.swarselmodules.distrobox {
environment.systemPackages = with pkgs; [
distrobox
boxbuddy
];
virtualisation.podman = {
enable = true;
dockerCompat = true;
package = pkgs.stable.podman;
};
};
}
4.4.2.23. Appimage
Adds the necessary tools to allow .appimage programs easily.
{ lib, config, ... }:
{
options.swarselmodules.appimage = lib.mkEnableOption "appimage config";
config = lib.mkIf config.swarselmodules.appimage {
programs.appimage = {
enable = true;
binfmt = true;
};
};
}
4.4.2.24. Handle lid switch correctly
This turns off the display when the lid is closed.
{ lib, config, ... }:
{
options.swarselmodules.lid = lib.mkEnableOption "lid config";
config = lib.mkIf config.swarselmodules.lid {
services.logind.settings.Login = {
HandleLidSwitch = "suspend";
HandleLidSwitchDocked = "ignore";
};
services.acpid = {
enable = true;
handlers.lidClosed = {
event = "button/lid \\w+ close";
action = ''
cat /sys/class/backlight/amdgpu_bl1/device/enabled
if grep -Fxq disabled /sys/class/backlight/amdgpu_bl1/device/enabled
then
echo "Lid closed. Disabling fprintd."
systemctl stop fprintd
ln -s /dev/null /run/systemd/transient/fprintd.service
systemctl daemon-reload
fi
'';
};
handlers.lidOpen = {
event = "button/lid \\w+ open";
action = ''
if ! $(systemctl is-active --quiet fprintd); then
echo "Lid open. Enabling fprintd."
rm -f /run/systemd/transient/fprintd.service
systemctl daemon-reload
systemctl start fprintd
fi
'';
};
};
};
}
4.4.2.25. Low battery notification
Since I hide the waybar completely during normal operation, I run the risk of not noticing when my battery is about to run out. This module sends a notification when the battery level falls below 10%. Written by cafkafk.
{ pkgs, lib, config, ... }:
{
options.swarselmodules.lowBattery = lib.mkEnableOption "low battery notification config";
config = lib.mkIf config.swarselmodules.lowBattery {
systemd.user.services."battery-low" = {
enable = true;
description = "Timer for battery check that alerts at 10% or less";
partOf = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
Type = "simple";
ExecStart = pkgs.writeShellScript "battery-low-notification"
''
if (( 10 >= $(${lib.getExe pkgs.acpi} -b | head -n 1 | ${lib.getExe pkgs.ripgrep} -o "\d+%" | ${lib.getExe pkgs.ripgrep} -o "\d+") && $(${lib.getExe pkgs.acpi} -b | head -n 1 | ${lib.getExe pkgs.ripgrep} -o "\d+%" | ${lib.getExe pkgs.ripgrep} -o "\d+") > 0 ));
then ${lib.getExe pkgs.libnotify} --urgency=critical "low battery" "$(${lib.getExe pkgs.acpi} -b | head -n 1 | ${lib.getExe pkgs.ripgrep} -o "\d+%")";
fi;
'';
};
};
systemd.user.timers."battery-low" = {
wantedBy = [ "timers.target" ];
timerConfig = {
# Every Minute
OnCalendar = "*-*-* *:*:00";
Unit = "battery-low.service";
};
};
};
}
4.4.2.26. Auto-login
Auto login for the initial session.
{ lib, config, ... }:
let
inherit (config.swarselsystems) mainUser;
in
{
options.swarselmodules.autologin = lib.mkEnableOption "optional autologin settings";
config = lib.mkIf config.swarselmodules.autologin {
services = {
getty.autologinUser = mainUser;
greetd.settings.initial_session.user = mainUser;
};
};
}
4.4.2.27. UWSM
Auto login for the initial session.
{ lib, config, ... }:
let
moduleName = "uwsm";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "${moduleName} settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
programs.uwsm = {
enable = true;
waylandCompositors = {
sway = {
prettyName = "Sway";
comment = "Sway compositor managed by UWSM";
binPath = "/run/current-system/sw/bin/sway";
};
niri = lib.mkIf (config.swarselmodules ? niri) {
prettyName = "Niri";
comment = "Niri compositor managed by UWSM";
binPath = "/run/current-system/sw/bin/niri-session";
};
};
};
};
}
4.4.2.28. Firezone Client
{ lib, config, ... }:
let
moduleName = "firezone-client";
inherit (config.swarselsystems) mainUser;
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "${moduleName} settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
services.firezone.gui-client = {
enable = true;
inherit (config.node) name;
allowedUsers = [ mainUser ];
};
};
}
4.4.3. Server
In a similar way as the Client section, these modules are to be used mostly on servers.
4.4.3.1. Imports
First, we enable the use of home-manager as a NixoS module.
Also, we disable the warnings that trigger when rebuilding with a dirty flake. At this point, I am also disabling channels and pinning the flake registry - the latter lets me use the local version of nixpkgs for commands like nix shell (without it, we will always download the newest version of nixpkgs for these commands).
Also, the system state version is set here. No need to touch it.
{ lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/nixos/server";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/nixos/server";
}
4.4.3.2. General NixOS Server settings
Here we just define some aliases for rebuilding the system, and we allow some insecure packages that are needed by some server derivations. It would be more elegant to define these in the respective module, but nixpkgs needs to be defined before we can evaluate modules within it, so this must be a top-level configuration.
{ lib, config, ... }:
let
inherit (config.swarselsystems) flakePath;
in
{
options.swarselmodules.server.general = lib.mkEnableOption "general setting on server";
options.swarselsystems = {
shellAliases = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
};
};
config = lib.mkIf config.swarselmodules.server.general {
environment.shellAliases = lib.recursiveUpdate
{
nswitch = "cd ${flakePath}; swarsel-deploy $(hostname) switch; cd -;";
ntest = "cd ${flakePath}; swarsel-deploy $(hostname) test; cd -;";
nboot = "cd ${flakePath}; swarsel-deploy $(hostname) boot; cd -;";
ndry = "cd ${flakePath}; swarsel-deploy $(hostname) dry-activate; cd -;";
}
config.swarselsystems.shellAliases;
nixpkgs.config = lib.mkIf (!config.swarselsystems.isMicroVM) {
permittedInsecurePackages = [
# matrix
"olm-3.2.16"
# sonarr
"aspnetcore-runtime-wrapped-6.0.36"
"aspnetcore-runtime-6.0.36"
"dotnet-sdk-wrapped-6.0.428"
"dotnet-sdk-6.0.428"
#
"SDL_ttf-2.0.11"
];
};
};
}
4.4.3.3. System Packages (Server Programs)
This is a collection of packages that are useful for server-type hosts that do not really fit into other modules; the optional part of the list is for packages that are built as part of Packages (config); systems that are not built with home-manager will not be able to pass the required parameters to build these packages, hence we cannot build them here. This mostly applies to microvms.
{ lib, config, pkgs, withHomeManager, ... }:
{
options.swarselmodules.server.packages = lib.mkEnableOption "enable packages on server";
config = lib.mkIf config.swarselmodules.server.packages {
environment.systemPackages = with pkgs; [
gnupg
nvd
nix-output-monitor
ssh-to-age
git
emacs
vim
sops
tmux
busybox
ndisc6
tcpdump
swarsel-deploy
] ++ lib.optionals withHomeManager [
swarsel-gens
swarsel-switch
];
};
}
4.4.3.4. nfs/samba (smb)
{ lib, config, pkgs, globals, ... }:
let
nfsUser = globals.user.name;
in
{
options.swarselmodules.server.nfs = lib.mkEnableOption "enable nfs on server";
config = lib.mkIf config.swarselmodules.server.nfs {
services = {
# add a user with sudo smbpasswd -a <user>
samba = {
# package = pkgs.samba4Full;
package = pkgs.samba4;
# extraConfig = ''
# workgroup = WORKGROUP
# server role = standalone server
# dns proxy = no
# pam password change = yes
# map to guest = bad user
# create mask = 0664
# force create mode = 0664
# directory mask = 0775
# force directory mode = 0775
# follow symlinks = yes
# '';
enable = true;
openFirewall = true;
settings.Eternor = {
browseable = "yes";
"read only" = "no";
"guest ok" = "no";
path = "/Vault/Eternor";
writable = "true";
comment = "Eternor";
"valid users" = nfsUser;
};
};
avahi = {
publish.enable = true;
publish.userServices = true; # Needed to allow samba to automatically register mDNS records without the need for an `extraServiceFile`
nssmdns4 = true;
enable = true;
openFirewall = true;
};
# This enables autodiscovery on windows since SMB1 (and thus netbios) support was discontinued
samba-wsdd = {
enable = true;
openFirewall = true;
};
};
};
}
4.4.3.5. acme
{ self, pkgs, lib, config, globals, ... }:
let
inherit (config.repo.secrets.common) dnsProvider dnsBase dnsMail;
sopsFile = self + "/secrets/nginx/acme.json";
in
{
options.swarselmodules.server.acme = lib.mkEnableOption "enable acme on server";
config = lib.mkIf config.swarselmodules.server.acme {
environment.systemPackages = with pkgs; [
lego
];
sops = {
secrets = {
acme-creds = { format = "json"; key = ""; group = "acme"; inherit sopsFile; mode = "0660"; };
};
templates."certs.secret".content = ''
ACME_DNS_API_BASE = ${dnsBase}
ACME_DNS_STORAGE_PATH=${config.sops.secrets.acme-creds.path}
'';
};
users.groups.acme.members = lib.mkIf config.swarselmodules.server.nginx [ "nginx" ];
security.acme = {
acceptTerms = true;
defaults = {
inherit dnsProvider;
email = dnsMail;
environmentFile = "${config.sops.templates."certs.secret".path}";
reloadServices = [ "nginx" ];
dnsPropagationCheck = true;
};
certs."${globals.domains.main}" = {
domain = "*.${globals.domains.main}";
};
};
environment.persistence."/persist" = lib.mkIf config.swarselsystems.isImpermanence {
directories = [{ directory = "/var/lib/acme"; }];
};
};
}
4.4.3.6. NGINX
{ pkgs, lib, config, ... }:
let
serviceUser = "nginx";
serviceGroup = serviceUser;
sslBasePath = "/etc/ssl";
dhParamsPathBase = "${sslBasePath}/dhparams.pem";
dhParamsPath =
if config.swarselsystems.isImpermanence then
"/persist/${dhParamsPathBase}"
else
"${dhParamsPathBase}";
in
{
options.swarselmodules.server.nginx = lib.mkEnableOption "enable nginx on server";
options.services.nginx = {
recommendedSecurityHeaders = lib.mkEnableOption "additional security headers by default in each location block.";
defaultStapling = lib.mkEnableOption "add ssl stapling in each location block..";
virtualHosts = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (topmod: {
options = {
defaultStapling = lib.mkOption {
type = lib.types.bool;
default = config.services.nginx.defaultStapling;
description = "Whether to add ssl stapling to this location.";
};
locations = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (submod: {
options = {
recommendedSecurityHeaders = lib.mkOption {
type = lib.types.bool;
default = config.services.nginx.recommendedSecurityHeaders;
description = "Whether to add additional security headers to this location.";
};
X-Frame-Options = lib.mkOption {
type = lib.types.str;
default = "DENY";
description = "The value to use for X-Frame-Options";
};
};
config = {
extraConfig = lib.mkIf submod.config.recommendedSecurityHeaders (lib.mkBefore ''
# Hide upstream's versions
proxy_hide_header Strict-Transport-Security;
proxy_hide_header Referrer-Policy;
proxy_hide_header X-Content-Type-Options;
proxy_hide_header X-Frame-Options;
# Enable HTTP Strict Transport Security (HSTS)
add_header Strict-Transport-Security "max-age=63072000; includeSubdomains; preload";
# Minimize information leaked to other domains
add_header Referrer-Policy "origin-when-cross-origin";
add_header X-XSS-Protection "1; mode=block";
add_header X-Frame-Options "${submod.config.X-Frame-Options}";
add_header X-Content-Type-Options "nosniff";
''
);
};
})
);
};
};
config = {
extraConfig = lib.mkIf topmod.config.defaultStapling (lib.mkBefore ''
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
'');
};
})
);
};
};
config = lib.mkIf config.swarselmodules.server.nginx {
swarselmodules.server.acme = lib.mkDefault true;
networking.firewall.allowedTCPPorts = [ 80 443 ];
environment.persistence."/persist" = lib.mkIf config.swarselsystems.isImpermanence {
files = [ dhParamsPathBase ];
};
services.nginx = {
enable = true;
user = serviceUser;
group = serviceGroup;
statusPage = true;
recommendedProxySettings = true;
recommendedTlsSettings = true;
recommendedOptimisation = true;
recommendedGzipSettings = true;
recommendedBrotliSettings = true;
recommendedSecurityHeaders = true;
defaultStapling = true;
sslCiphers = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305:!aNULL";
sslDhparam = dhParamsPathBase;
virtualHosts.fallback = {
default = true;
rejectSSL = true;
locations."/".extraConfig = ''
deny all;
'';
};
};
systemd.services.generateDHParams = {
before = [ "nginx.service" ];
requiredBy = [ "nginx.service" ];
after = [ "local-fs.target" ];
requires = [ "local-fs.target" ];
serviceConfig = {
Type = "oneshot";
};
script = ''
set -eu
install -d -m 0755 ${sslBasePath}
${if config.swarselsystems.isImpermanence then "${pkgs.coreutils}/bin/install -d -m 0755 /persist${sslBasePath}" else ""}
if [ ! -f "${dhParamsPath}" ]; then
${pkgs.openssl}/bin/openssl dhparam -out "${dhParamsPath}" 4096
chmod 0644 "${dhParamsPath}"
chown ${serviceUser}:${serviceGroup} "${dhParamsPath}"
else
echo 'Already generated DHParams'
fi
'';
};
# system.activationScripts."createPersistentStorageDirs" = lib.mkIf config.swarselsystems.isImpermanence {
# deps = [ "generateDHParams" "users" "groups" ];
# };
# system.activationScripts."generateDHParams" =
# {
# text = ''
# set -eu
# ${if config.swarselsystems.isImpermanence then "${pkgs.coreutils}/bin/install -d -m 0755 /persist${sslBasePath}" else "${pkgs.coreutils}/bin/install -d -m 0755 ${sslBasePath}"}
# if [ ! -f "${dhParamsPath}" ]; then
# ${pkgs.openssl}/bin/openssl dhparam -out ${dhParamsPath} 4096
# chmod 0644 ${dhParamsPath}
# chown ${serviceUser}:${serviceGroup} ${dhParamsPath}
# fi
# '';
# deps = [
# (lib.mkIf config.swarselsystems.isImpermanence "specialfs")
# (lib.mkIf (!config.swarselsystems.isImpermanence) "etc")
# ];
# };
};
}
4.4.3.7. ssh
Here I am forcing startWhenNeeded to false so that the value will not be set to true in containers = this would be a problem because it would delay ssh host key generation.
{ self, lib, config, withHomeManager, ... }:
{
options.swarselmodules.server.ssh = lib.mkEnableOption "enable ssh on server";
config = lib.mkIf config.swarselmodules.server.ssh {
services.openssh = {
enable = true;
startWhenNeeded = lib.mkForce false;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "yes";
AllowUsers = [
"root"
config.swarselsystems.mainUser
];
};
hostKeys = [
{
path = "/etc/ssh/ssh_host_ed25519_key";
type = "ed25519";
}
];
};
users.users = {
"${config.swarselsystems.mainUser}".openssh.authorizedKeys.keyFiles = lib.mkIf withHomeManager [
(self + /secrets/public/ssh/yubikey.pub)
(self + /secrets/public/ssh/magicant.pub)
# (lib.mkIf config.swarselsystems.isBastionTarget (self + /secrets/public/ssh/jump.pub))
];
root.openssh.authorizedKeys.keyFiles = [
(self + /secrets/public/ssh/yubikey.pub)
(self + /secrets/public/ssh/magicant.pub)
# (lib.mkIf config.swarselsystems.isBastionTarget (self + /secrets/public/ssh/jump.pub))
];
};
security.sudo.extraConfig = ''
Defaults env_keep+=SSH_AUTH_SOCK
'';
};
}
4.4.3.8. Bastion
{ self, lib, config, withHomeManager, ... }:
{
options.swarselmodules.server.bastion = lib.mkEnableOption "enable bastion on server";
config = lib.mkIf config.swarselmodules.server.bastion ({
users = {
groups = {
jump = { };
};
users = {
"jump" = {
isNormalUser = true;
useDefaultShell = true;
group = lib.mkForce "jump";
createHome = lib.mkForce true;
openssh.authorizedKeys.keyFiles = [
(self + /secrets/public/ssh/yubikey.pub)
(self + /secrets/public/ssh/magicant.pub)
(self + /secrets/public/ssh/builder.pub)
];
};
};
};
services.openssh = {
enable = true;
startWhenNeeded = lib.mkForce false;
authorizedKeysInHomedir = false;
extraConfig = ''
Match User jump
PermitTTY no
X11Forwarding no
PermitTunnel no
GatewayPorts no
AllowAgentForwarding no
'';
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = lib.mkDefault "no";
AllowUsers = [
"jump"
];
};
hostKeys = lib.mkIf (!config.swarselmodules.server.ssh) [
{
path = "/etc/ssh/ssh_host_ed25519_key";
type = "ed25519";
}
];
};
} // lib.optionalAttrs withHomeManager {
home-manager.users.jump.config = {
home.stateVersion = lib.mkDefault "23.05";
programs.ssh = {
enable = true;
enableDefaultConfig = false;
matchBlocks = {
"*" = {
forwardAgent = false;
};
} // config.repo.secrets.local.ssh.hosts;
};
};
});
}
4.4.3.9. ssh builder config
Restricts access to the system by the nix build user as per https://discourse.nixos.org/t/wrapper-to-restrict-builder-access-through-ssh-worth-upstreaming/25834.
{ self, pkgs, lib, config, ... }:
let
ssh-restrict = "restrict,pty,command=\"${wrapper-dispatch-ssh-nix}/bin/wrapper-dispatch-ssh-nix\" ";
wrapper-dispatch-ssh-nix = pkgs.writeShellScriptBin "wrapper-dispatch-ssh-nix" ''
case $SSH_ORIGINAL_COMMAND in
"nix-daemon --stdio")
exec env NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt ${config.nix.package}/bin/nix-daemon --stdio
;;
"nix-store --serve --write")
exec env NIX_SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt ${config.nix.package}/bin/nix-store --serve --write
;;
*)
echo "Access only allowed for using the nix remote builder" 1>&2
exit
esac
'';
in
{
options.swarselmodules.server.ssh-builder = lib.mkEnableOption "enable ssh-builder config on server";
config = lib.mkIf config.swarselmodules.server.ssh-builder {
users = {
groups.builder = { };
users.builder = {
useDefaultShell = true;
isSystemUser = true;
group = "builder";
openssh.authorizedKeys.keys = [
''${ssh-restrict} ${builtins.readFile "${self}/secrets/public/ssh/builder.pub"}''
];
};
};
services.openssh = {
settings = {
AllowUsers = [
"builder"
];
};
};
};
}
4.4.3.10. Network settings (globals.networks population)
Generate hostId using head -c4 /dev/urandom | od -A none -t x4
This section is mainly used to populate entries in globals.networks with the interfaces defined in the local secrets of the respective host. Also, we expose some convenient values under globals.hosts and setup basic networking.
{ lib, config, ... }:
let
netConfig = config.repo.secrets.local.networking;
netPrefix = "${if config.swarselsystems.isCloud then config.node.name else "home"}";
in
{
options = {
swarselmodules.server.network = lib.mkEnableOption "enable server network config";
swarselsystems.server = {
localNetwork = lib.mkOption {
type = lib.types.str;
default = "";
};
netConfigName = lib.mkOption {
type = lib.types.str;
default = "${netPrefix}-${config.swarselsystems.server.localNetwork}";
readOnly = true;
};
netConfigPrefix = lib.mkOption {
type = lib.types.str;
default = netPrefix;
readOnly = true;
};
};
};
config = lib.mkIf config.swarselmodules.server.network {
swarselsystems.server.localNetwork = netConfig.localNetwork or "";
globals.networks = lib.mkIf config.swarselsystems.writeGlobalNetworks (lib.mapAttrs'
(netName: _:
lib.nameValuePair "${netPrefix}-${netName}" {
hosts.${config.node.name} = {
inherit (netConfig.networks.${netName}) id;
mac = netConfig.networks.${netName}.mac or null;
};
}
)
netConfig.networks);
globals.hosts.${config.node.name} = {
defaultGateway4 = netConfig.defaultGateway4 or null;
defaultGateway6 = netConfig.defaultGateway6 or null;
wanAddress4 = netConfig.wanAddress4 or null;
wanAddress6 = netConfig.wanAddress6 or null;
isHome = if (netPrefix == "home") then true else false;
};
networking = {
inherit (netConfig) hostId;
hostName = config.node.name;
nftables.enable = lib.mkDefault false;
enableIPv6 = lib.mkDefault true;
firewall = {
enable = lib.mkDefault true;
};
};
};
}
4.4.3.11. Disk encryption
The hostkey can be generated with ssh-keygen -t ed25519 -N "" -f /etc/secrets/initrd/ssh_host_ed25519_key.
Use lspci -v | grep -iA8 'network\|ethernet' to supposedly find out which kernel module is needed for networking in initrd. However I prefer a different approach:
Use lspci -nn | grep -i network to find out manufacturer info:
lspci -nn | grep -i 'network\|ethernet'
04:00.0 Network controller [0280]: MEDIATEK Corp. MT7922 802.11ax PCI Express Wireless Network Adapter [14c3:0616]
From the last bracket you then find out the correct kernel module:
lspci -k -d 14c3:0616
| 04:00.0 | Network | controller: | MEDIATEK | Corp. | MT7922 | 802.11ax | PCI | Express | Wireless | Network | Adapter |
| Subsystem: | MEDIATEK | Corp. | Device | e616 | |||||||
| Kernel | driver | in | use: | mt7921e | |||||||
| Kernel | modules: | mt7921e |
A little note about the secrets part:
systemd-initrd provides a lightweight SSH server in form of dropbear - in order to not crash we need to have a hostkey ready in the initrd. I achieve this by generating a hostkey in the build process in casy I am doing an initial install (another way - and safer - would be to push that in swarsel-bootstrap I guess) - this results in the hostkey landing in the nix store. However, I only ever spend like 5 minutes in this state before I rebuild to the full system, where this hostkey is no longer used. This is because upon first activation, we will then run another ssh-keygen that will then write to persisted storage. All "unlock" SSH hosts are to be reached over port 2222, and systemctl default will be run immediately upon login, which will guide towards attaining a working system (in normal operation, it will simply ask for the password).
I also take some precautions in how I get networking information during stage 1. For the most part, I just use systemd-networkd (server), however, for hosts in my local network, I take another step to define the network in the kernelParams, to make extra sure I can reach it.
{ self, pkgs, lib, config, globals, minimal, ... }:
let
localIp = globals.networks.${config.swarselsystems.server.netConfigName}.hosts.${config.node.name}.ipv4;
subnetMask = globals.networks.${config.swarselsystems.server.netConfigName}.subnetMask4;
gatewayIp = globals.hosts.${config.node.name}.defaultGateway4;
inherit (globals.general) routerServer;
isRouter = config.node.name == routerServer;
hostKeyPathBase = "/etc/secrets/initrd/ssh_host_ed25519_key";
hostKeyPath =
if config.swarselsystems.isImpermanence then
"/persist/${hostKeyPathBase}"
else
"${hostKeyPathBase}";
# this key is only used only for ssh to stage 1 in initial provisioning (in nix store)
generatedHostKey = pkgs.runCommand "initrd-hostkey" { } ''
${pkgs.openssh}/bin/ssh-keygen -t ed25519 -N "" -f $out
'';
in
{
options.swarselmodules.server.diskEncryption = lib.mkEnableOption "enable disk encryption config";
options.swarselsystems.networkKernelModules = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
config = lib.mkIf (config.swarselmodules.server.diskEncryption && config.swarselsystems.isCrypted) {
# as soon as we hit a stable system, we will use a persisted key
# @future me: dont mkIf this to minimal, we need to create this as soon as possible
system.activationScripts.ensureInitrdHostkey = {
text = ''
[[ -e ${hostKeyPath} ]] || ${pkgs.openssh}/bin/ssh-keygen -t ed25519 -N "" -f ${hostKeyPath}
'';
deps = [
"users"
"createPersistentStorageDirs"
];
};
environment.persistence."/persist" = lib.mkIf (config.swarselsystems.isImpermanence && (config.swarselprofiles.server || minimal)) {
files = [ hostKeyPathBase ];
};
boot = lib.mkIf (!config.swarselsystems.isClient) {
kernelParams = lib.mkIf (!config.swarselsystems.isCloud && ((config.swarselsystems.localVLANs == []) || isRouter)) [
"ip=${localIp}::${gatewayIp}:${subnetMask}:${config.networking.hostName}::none"
];
initrd = {
secrets."/tmp${hostKeyPathBase}" = if minimal then (lib.mkForce generatedHostKey) else (lib.mkForce hostKeyPath); # need to mkForce this or it behaves stateful
availableKernelModules = config.swarselsystems.networkKernelModules;
kernelModules = config.swarselsystems.networkKernelModules; # at least summers needs this to actually find the interfaces
network = {
enable = true;
flushBeforeStage2 = true;
ssh = {
enable = true;
port = 2222; # avoid hostkey changed nag
authorizedKeys = [
''command="/bin/systemctl default" ${builtins.readFile "${self}/secrets/public/ssh/yubikey.pub"}''
''command="/bin/systemctl default" ${builtins.readFile "${self}/secrets/public/ssh/magicant.pub"}''
];
hostKeys = [ "/tmp${hostKeyPathBase}" ]; # use a tmp file otherwise persist mount will be unhappy
};
};
systemd = {
initrdBin = with pkgs; [
cryptsetup
];
};
};
};
};
}
4.4.3.12. Attic setup
By default, attic only provides a cli client to authenticate to caches. I want all my servers to use my main binary cache, which is what I set up here.
{ lib, config, pkgs, globals, ... }:
{
options.swarselmodules.server.attic-setup = lib.mkEnableOption "enable attic setup";
config = lib.mkIf config.swarselmodules.server.attic-setup {
environment.systemPackages = with pkgs; [
attic-client
];
sops = {
secrets = {
attic-cache-key = { };
};
templates = {
"attic-env".content = ''
DOMAIN=https://${globals.services.attic.domain}
TOKEN=${config.sops.placeholder.attic-cache-key}
'';
};
};
systemd.services.attic-cache-setup = {
description = "Ensure attic is authenticated to cache";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
EnvironmentFile = [
config.sops.templates.attic-env.path
];
};
script = let
attic = lib.getExe pkgs.attic-client;
in ''
set -eu
if ${attic} cache info ${config.swarselsystems.mainUser} >/dev/null 2>&1; then
echo "cache already authenticated"
exit 0
fi
echo "cache not authenticated, attempting login..."
${attic} login ${config.swarselsystems.mainUser} "$DOMAIN" "$TOKEN" --set-default
${attic} use ${config.swarselsystems.mainUser}
'';
};
};
}
4.4.3.13. TODO Wireguard
I use wireguard for two things:
- proxying of my services (both internal and external) to my proxy node
- proxying of my internal services to an internal NGINX in order to save on bandwidth
At current, this means that I am running two wireguard interfaces - the following configuration allows me to define an arbitrary number of wireguard interfaces that each host can be part of (either as "client" or "server"). All of these connections are really point-to-point. On the client side, I set persistentKeepalive unconditionally, which is lazy (and a bit of a security issue). Also, I noticed that I lose 12 bits of MTU somewhere - I would have expected to be able to set MTU 1420, but that does not seem to be the case. TODO:fix both
In order to define a new wireguard interface, I have to:
- add another <nodeName>-<wgInterfaceName> network to globals
- add its members under hosts
- add the
wgInterfaceNametogether with id to the server nodes local pii - make sure all members have their private keys in their secrets file and their public key under
secrets/public/wg - make sure that all preshared keys exist in
secrets/repo/wg.yamlTODO: maybe split wg.yaml into per-interface files with finer-grained acl
{ self, lib, pkgs, config, confLib, nodes, globals, ... }:
let
inherit (confLib.gen {
name = "wireguard";
port = 52829;
user = "systemd-network";
group = "systemd-network";
}) servicePort serviceName serviceUser serviceGroup;
inherit (config.swarselsystems) sopsFile;
wgSopsFile = self + "/secrets/repo/wg.yaml";
cfg = config.swarselsystems.server.wireguard;
inherit (cfg) interfaces;
ifaceList = builtins.attrValues interfaces;
in
{
options = {
swarselmodules.server.${serviceName} =
lib.mkEnableOption "enable ${serviceName} settings";
swarselsystems.server.wireguard = {
interfaces = let
topConfig = config;
in lib.mkOption {
type = lib.types.attrsOf (lib.types.submodule ({ config, name, ... }: {
options = {
isServer = lib.mkEnableOption "set this interface as a wireguard server";
isClient = lib.mkEnableOption "set this interface as a wireguard client";
serverName = lib.mkOption {
type = lib.types.str;
default = if config.isServer then topConfig.node.name else "";
description = "Hostname of the WireGuard server this interface connects to (when isClient = true).";
};
serverNetConfigPrefix = lib.mkOption {
type = lib.types.str;
default =
let
serverCfg = nodes.${config.serverName}.config;
in
if serverCfg.swarselsystems.isCloud
then serverCfg.node.name
else "home";
readOnly = true;
description = "Prefix used to look up the server network in globals.networks.\"<prefix>-wg\".";
};
ifName = lib.mkOption {
type = lib.types.str;
default = name;
description = "Name of the WireGuard interface.";
};
port = lib.mkOption {
type = lib.types.int;
default = servicePort;
description = "Port of the WireGuard interface.";
};
peers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = lib.attrNames (lib.filterAttrs (name: _: name != topConfig.node.name) globals.networks."${config.serverNetConfigPrefix}-${config.ifName}".hosts);
description = "WireGuard peer config names of this wireguardinterface.";
};
};
}));
default = { };
description = "WireGuard interfaces defined on this host.";
};
};
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
assertions = lib.concatLists (
lib.flip lib.mapAttrsToList interfaces (
ifName: ifCfg:
let
assertionPrefix = "While evaluating the wireguard network ${ifName}:";
in
[
{
assertion = ifCfg.isServer || (ifCfg.isClient && ifCfg.serverName != "");
message = "${assertionPrefix}: This node must either be a server for the wireguard network or a client with serverName set.";
}
{
assertion = lib.stringLength ifName < 16;
message = "${assertionPrefix}: The specified linkName '${ifName}' is too long (must be max 15 characters).";
}
]
)
);
topology.self.interfaces = lib.mapAttrs'
(wgName: _:
lib.nameValuePair "${wgName}" {
network = wgName;
}
)
config.swarselsystems.server.wireguard.interfaces;
environment.systemPackages = with pkgs; [
wireguard-tools
];
sops.secrets =
lib.mkMerge (
[
{
wireguard-private-key = {
inherit sopsFile;
owner = serviceUser;
group = serviceGroup;
mode = "0600";
};
}
] ++ (map
(i:
let
clientSecrets =
lib.optionalAttrs i.isClient {
"wireguard-${i.serverName}-${config.node.name}-${i.ifName}-presharedKey" = {
sopsFile = wgSopsFile;
owner = serviceUser;
group = serviceGroup;
mode = "0600";
};
};
serverSecrets =
lib.optionalAttrs i.isServer (builtins.listToAttrs (map
(clientName: {
name = "wireguard-${config.node.name}-${clientName}-${i.ifName}-presharedKey";
value = {
sopsFile = wgSopsFile;
owner = serviceUser;
group = serviceGroup;
mode = "0600";
};
})
i.peers));
in
clientSecrets // serverSecrets
)
ifaceList)
);
networking.firewall = {
checkReversePath = lib.mkIf (lib.any (i: i.isClient) ifaceList) "loose";
allowedUDPPorts = lib.mkMerge (
lib.flip lib.mapAttrsToList interfaces (
_: ifCfg:
lib.optional ifCfg.isServer ifCfg.port
)
);
};
networking.nftables.firewall = {
zones = lib.mkMerge
(
lib.flip lib.mapAttrsToList interfaces (
ifName: ifCfg:
{
${ifName}.interfaces = [ ifName ];
}
// lib.listToAttrs (map
(peer:
let
peerNet = globals.networks."${ifCfg.serverNetConfigPrefix}-${ifName}".hosts.${peer};
in
lib.nameValuePair "${ifName}-node-${peer}" {
parent = ifName;
ipv4Addresses = lib.optional (peerNet.ipv4 != null) peerNet.ipv4;
ipv6Addresses = lib.optional (peerNet.ipv6 != null) peerNet.ipv6;
}
)
ifCfg.peers)
)
);
rules = lib.mkMerge (
lib.flip lib.mapAttrsToList interfaces (
ifName: ifCfg:
let
inherit (config.networking.nftables.firewall) localZoneName;
netCfg = globals.networks."${ifCfg.serverNetConfigPrefix}-${ifName}";
in
{
"${ifName}-to-${localZoneName}" = {
inherit (netCfg.firewallRuleForAll) allowedTCPPorts allowedUDPPorts allowedTCPPortRanges allowedUDPPortRanges;
from = [ ifName ];
to = [ localZoneName ];
ignoreEmptyRule = true;
};
}
// lib.listToAttrs (map
(peer:
lib.nameValuePair "${ifName}-node-${peer}-to-${localZoneName}" (
lib.mkIf (netCfg.hosts.${config.node.name}.firewallRuleForNode ? ${peer}) {
inherit (netCfg.hosts.${config.node.name}.firewallRuleForNode.${peer}) allowedTCPPorts allowedTCPPortRanges allowedUDPPorts allowedUDPPortRanges;
from = [ "${ifName}-node-${peer}" ];
to = [ localZoneName ];
ignoreEmptyRule = true;
}
)
)
ifCfg.peers)
)
);
};
systemd.network = {
enable = true;
networks = lib.mkMerge (map
(i:
let
inherit (i) ifName;
in
{
"50-${ifName}" = {
matchConfig.Name = ifName;
linkConfig = {
MTUBytes = 1408; # TODO: figure out where we lose those 12 bits (8 from pppoe maybe + ???)
};
address = [
globals.networks."${i.serverNetConfigPrefix}-${ifName}".hosts.${config.node.name}.cidrv4
globals.networks."${i.serverNetConfigPrefix}-${ifName}".hosts.${config.node.name}.cidrv6
];
};
})
ifaceList);
netdevs = lib.mkMerge (map
(i:
let
inherit (i) ifName;
in
{
"50-${ifName}" = {
netdevConfig = {
Kind = "wireguard";
Name = ifName;
};
wireguardConfig = {
ListenPort = lib.mkIf i.isServer servicePort;
PrivateKeyFile = config.sops.secrets.wireguard-private-key.path;
RouteTable = lib.mkIf i.isClient "main";
};
wireguardPeers =
lib.optionals i.isClient [
{
PublicKey =
builtins.readFile "${self}/secrets/public/wg/${i.serverName}.pub";
PresharedKeyFile =
config.sops.secrets."wireguard-${i.serverName}-${config.node.name}-${i.ifName}-presharedKey".path;
Endpoint =
"server.${i.serverName}.${globals.domains.main}:${toString servicePort}";
PersistentKeepalive = 25;
AllowedIPs =
let
wgNetwork = globals.networks."${i.serverNetConfigPrefix}-${i.ifName}";
in
(lib.optional (wgNetwork.cidrv4 != null) wgNetwork.cidrv4)
++ (lib.optional (wgNetwork.cidrv6 != null) wgNetwork.cidrv6);
}
]
++ lib.optionals i.isServer (map
(clientName: {
PublicKey =
builtins.readFile "${self}/secrets/public/wg/${clientName}.pub";
PresharedKeyFile =
config.sops.secrets."wireguard-${i.serverName}-${clientName}-${i.ifName}-presharedKey".path;
AllowedIPs =
let
clientInWgNetwork =
globals.networks."${i.serverNetConfigPrefix}-${i.ifName}".hosts.${clientName};
in
(lib.optional (clientInWgNetwork.ipv4 != null)
(lib.net.cidr.make 32 clientInWgNetwork.ipv4))
++ (lib.optional (clientInWgNetwork.ipv6 != null)
(lib.net.cidr.make 128 clientInWgNetwork.ipv6));
})
i.peers);
};
})
ifaceList);
};
};
}
4.4.3.14. BTRFS
{ lib, config, ... }:
{
options.swarselmodules.btrfs = lib.mkEnableOption "optional btrfs settings";
config = lib.mkIf config.swarselmodules.btrfs {
boot = {
supportedFilesystems = lib.mkIf config.swarselsystems.isBtrfs [ "btrfs" ];
};
};
}
4.4.3.15. Router
This is the configuration to make Hintbooth (Router: HUNSN RM02) act as the router for my internal network. This is not a reusable module and highly adapted to its hardware. Below is a rough sketch of the functionality:
- six LAN ports, five of which are bridged
- lan1–lan5 are enslaved into said bridge and behave as a single VLAN‑aware switch
- all VLANs are defined under
globals.networks.home-lan.vlans - for each VLAN, a routed interface me-${vlanName} is created on the router (NOTE: this interface also serves as the communication link to the local microvms - the respective extra interfaces are defined in systemd-networkd (server home))
- RA and forwarding are enabled on these me-* interfaces so the router advertises vlanCfg.cidrv6 and routes between VLANs / WAN / WireGuard
- all VLANs are defined under
- the sixth LAN port is used as the WAN / untrusted uplink to the
Fritz!Box - the mapping from MAC addresses to interfaces is defined in
config.repo.secrets.local.networking.networks.<ifName>.mac(and performed in systemd-networkd (base) usingrenameInterfacesByMac)
- lan1–lan5 are enslaved into said bridge and behave as a single VLAN‑aware switch
connectivity to microvms should not be lost in case there is no cable connected to the router
- this is achieved by connecting the veth interface pair veth-br / veth-int
- veth-br is part of the bridge and carries all VLANs tagged, as if it were another physical switch port
- veth-int stays on the host side and is used as the internal attachment point for microvms / guests
- ConfigureWithoutCarrier and ActivationPolicy = "always-up" are used so that the bridge and veth side stay UP; this however does not guarantee connectivity by itself as the kernel will not route packets if the underlying interface is not up (see also systemd-networkd (server home))
- nftables firewall is derived from the same VLAN definitions:
- a zone
vlan-*is created for each VLAN and bound tome-*, as well as zones for WAN, WG, and DNS - all internal
vlan-*zones are allowed to go to untrusted; NAT is implemented via a custom postrouting chain that masquerades both IPv4 and IPv6 traffic - any VLAN with internet access is allowed to reach AdGuardHome for DNS (access-adguardhome-dns)
- this is important so that we can make use of the internal nginx instance to prevent bottlenecks over the web proxy
- policy between internal networks:
- the home VLAN is allowed to access the services and devices VLANs
- the services VLAN is allowed to reach selected ports on local (currently wireguard)
- WireGuard peers in wgHome are allowed to talk to each other (wgHome → wgHome)
- a zone
- global IPv4/IPv6 forwarding is enabled via boot.kernel.sysctl so this host acts as the main router between all VLANs, the WireGuard network, and the WAN (untrusted)
{ lib, config, globals, ... }:
let
serviceName = "router";
bridgeVLANs = lib.mapAttrsToList
(_: vlan: {
VLAN = vlan.id;
})
globals.networks.home-lan.vlans;
selectVLANs = vlans: map (vlan: { VLAN = globals.networks.home-lan.vlans.${vlan}.id; }) vlans;
lan3VLANs = selectVLANs [ "home" "devices" "services" ];
lan4VLANs = lan3VLANs;
lan5VLANs = selectVLANs [ "home" "devices" "guests" ];
inherit (globals.general) homeDnsServer;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName}
{
services.avahi.reflector = true;
topology.self.interfaces = (lib.mapAttrs'
(vlanName: _:
lib.nameValuePair "vlan-${vlanName}" {
network = lib.mkForce vlanName;
}
)
globals.networks.home-lan.vlans) // (lib.mapAttrs'
(vlanName: _:
lib.nameValuePair "me-${vlanName}" {
network = lib.mkForce vlanName;
}
)
globals.networks.home-lan.vlans);
networking.nftables = {
firewall = {
zones = {
untrusted.interfaces = [ "lan" ];
wgHome.interfaces = [ "wgHome" ];
adguardhome.ipv4Addresses = [ globals.networks.home-lan.vlans.services.hosts.${homeDnsServer}.ipv4 ];
adguardhome.ipv6Addresses = [ globals.networks.home-lan.vlans.services.hosts.${homeDnsServer}.ipv6 ];
}
// lib.flip lib.concatMapAttrs globals.networks.home-lan.vlans (
vlanName: _: {
"vlan-${vlanName}".interfaces = [ "me-${vlanName}" ];
}
);
rules = {
masquerade-internet = {
from = map (name: "vlan-${name}") globals.general.internetVLANs;
to = [ "untrusted" ];
# masquerade = true; NOTE: custom rule below for ip4 + ip6
late = true; # Only accept after any rejects have been processed
verdict = "accept";
};
# Allow access to the AdGuardHome DNS server from any VLAN that has internet access
access-adguardhome-dns = {
from = map (name: "vlan-${name}") globals.general.internetVLANs;
to = [ "adguardhome" ];
verdict = "accept";
};
# Allow devices in the home VLAN to talk to any of the services or home devices.
access-services = {
from = [ "vlan-home" ];
to = [
"vlan-services"
"vlan-devices"
];
late = true;
verdict = "accept";
};
# Allow the services VLAN to talk to our wireguard server
services-to-local = {
from = [ "vlan-services" ];
to = [ "local" ];
allowedUDPPorts = [ 52829 547 ];
};
# Forward traffic between wireguard participants
forward-proxy-home-vpn-traffic = {
from = [ "wgHome" ];
to = [ "wgHome" ];
verdict = "accept";
};
};
};
chains.postrouting = {
masquerade-internet = {
after = [ "hook" ];
late = true;
rules =
lib.forEach
(map (name: "vlan-${name}") globals.general.internetVLANs)
(
zone:
lib.concatStringsSep " " [
"meta protocol { ip, ip6 }"
(lib.head config.networking.nftables.firewall.zones.${zone}.ingressExpression)
(lib.head config.networking.nftables.firewall.zones.untrusted.egressExpression)
"masquerade random"
]
);
};
};
};
boot.kernel.sysctl = {
"net.ipv4.ip_forward" = 1;
"net.ipv4.conf.all.forwarding" = true;
"net.ipv6.conf.all.forwarding" = true;
};
systemd.network = {
wait-online.anyInterface = true;
netdevs = {
"10-veth" = {
netdevConfig = {
Kind = "veth";
Name = "veth-br";
};
peerConfig = {
Name = "veth-int";
};
};
"20-br" = {
netdevConfig = {
Kind = "bridge";
Name = "br";
};
bridgeConfig = {
VLANFiltering = true;
};
};
};
networks = {
"40-br" = {
matchConfig.Name = "br";
bridgeConfig = { };
linkConfig = {
ActivationPolicy = "always-up";
RequiredForOnline = "no";
};
networkConfig = {
ConfigureWithoutCarrier = true;
LinkLocalAddressing = "no";
};
};
"15-veth-br" = {
matchConfig.Name = "veth-br";
linkConfig = {
RequiredForOnline = "no";
};
networkConfig = {
Bridge = "br";
};
inherit bridgeVLANs;
};
"15-veth-int" = {
matchConfig.Name = "veth-int";
linkConfig = {
ActivationPolicy = "always-up";
RequiredForOnline = "no";
};
networkConfig = {
ConfigureWithoutCarrier = true;
LinkLocalAddressing = "no";
};
vlan = map (name: "vlan-${name}") (builtins.attrNames globals.networks.home-lan.vlans);
};
# br
"30-lan1" = {
matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan1.mac;
linkConfig.RequiredForOnline = "enslaved";
networkConfig = {
Bridge = "br";
ConfigureWithoutCarrier = true;
};
inherit bridgeVLANs;
};
# wifi
"30-lan2" = {
matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan2.mac;
linkConfig.RequiredForOnline = "enslaved";
networkConfig = {
Bridge = "br";
ConfigureWithoutCarrier = true;
};
inherit bridgeVLANs;
};
# summers
"30-lan3" = {
matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan3.mac;
linkConfig.RequiredForOnline = "enslaved";
networkConfig = {
Bridge = "br";
ConfigureWithoutCarrier = true;
};
bridgeVLANs = lan3VLANs;
};
# summers
"30-lan4" = {
matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan4.mac;
linkConfig.RequiredForOnline = "enslaved";
networkConfig = {
Bridge = "br";
ConfigureWithoutCarrier = true;
};
bridgeVLANs = lan4VLANs;
};
# lr
"30-lan5" = {
matchConfig.MACAddress = config.repo.secrets.local.networking.networks.lan5.mac;
linkConfig.RequiredForOnline = "enslaved";
networkConfig = {
Bridge = "br";
ConfigureWithoutCarrier = true;
};
bridgeVLANs = lan5VLANs;
};
} // lib.flip lib.concatMapAttrs globals.networks.home-lan.vlans (
vlanName: vlanCfg: {
"40-me-${vlanName}" = lib.mkForce {
address = [
vlanCfg.hosts.${config.node.name}.cidrv4
vlanCfg.hosts.${config.node.name}.cidrv6
];
matchConfig.Name = "me-${vlanName}";
networkConfig = {
IPv4Forwarding = "yes";
IPv6PrivacyExtensions = "yes";
IPv6SendRA = true;
IPv6AcceptRA = false;
};
ipv6Prefixes = [
{
Prefix = vlanCfg.cidrv6;
}
];
ipv6SendRAConfig = {
Managed = true; # set RA M flag -> DHCPv6 for addresses
OtherInformation = true; # optional, for “other info” via DHCPv6
};
linkConfig.RequiredForOnline = "routable";
};
}
);
};
};
}
4.4.3.16. kavita
{ self, lib, config, pkgs, globals, dns, confLib, ... }:
let
inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "kavita"; port = 8080; }) servicePort serviceName serviceUser serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf nginxAccessRules homeServiceAddress;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
environment.systemPackages = with pkgs; [
calibre
];
users.users.${serviceUser} = {
extraGroups = [ "users" ];
};
sops.secrets.kavita-token = { inherit sopsFile; owner = serviceUser; };
# networking.firewall.allowedTCPPorts = [ servicePort ];
topology.self.services.${serviceName} = {
name = "Kavita";
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.${serviceName} = {
enable = true;
user = serviceUser;
settings.Port = servicePort;
tokenKeyFile = config.sops.secrets.kavita-token.path;
dataDir = "/Vault/data/${serviceName}";
};
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.17. jellyfin
{ pkgs, lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "jellyfin"; port = 8096; }) servicePort serviceName serviceUser serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf nginxAccessRules homeServiceAddress;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users.users.${serviceUser} = {
extraGroups = [ "video" "render" "users" ];
};
# nixpkgs.config.packageOverrides = pkgs: {
# intel-vaapi-driver = pkgs.intel-vaapi-driver.override { enableHybridCodec = true; };
# };
hardware.graphics = {
enable = true;
extraPackages = with pkgs; [
intel-media-driver # LIBVA_DRIVER_NAME=iHD
# intel-vaapi-driver # LIBVA_DRIVER_NAME=i965 (older but works better for Firefox/Chromium)
libva-vdpau-driver
libvdpau-va-gl
];
};
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.${serviceName} = {
enable = true;
user = serviceUser;
# openFirewall = true; # this works only for the default ports
};
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.18. navidrome
{ pkgs, config, lib, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "navidrome"; port = 4040; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf nginxAccessRules homeServiceAddress;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
environment.systemPackages = with pkgs; [
pciutils
alsa-utils
mpv
];
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
users = {
groups = {
${serviceGroup} = {
gid = 61593;
};
};
users = {
${serviceUser} = {
isSystemUser = true;
uid = 61593;
group = serviceGroup;
extraGroups = [ "audio" "utmp" "users" "pipewire" ];
};
};
};
hardware = {
enableAllFirmware = lib.mkForce true;
};
# networking.firewall.allowedTCPPorts = [ servicePort ];
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.snapserver = {
enable = true;
settings = {
stream = {
port = 1704;
source = "pipe:///tmp/snapfifo?name=default";
bind_to_address = "0.0.0.0";
};
};
};
systemd.services = {
${serviceName}.serviceConfig = {
PrivateDevices = lib.mkForce false;
PrivateUsers = lib.mkForce false;
RestrictRealtime = lib.mkForce false;
SystemCallFilter = lib.mkForce null;
RootDirectory = lib.mkForce null;
};
};
services.${serviceName} = {
enable = true;
# openFirewall = true;
settings = {
LogLevel = "debug";
Address = "0.0.0.0";
Port = servicePort;
MusicFolder = "/Vault/Eternor/Music";
PlaylistsPath = "./Playlists";
AutoImportPlaylists = false;
EnableSharing = true;
EnableTranscodingConfig = true;
Scanner.GroupAlbumReleases = true;
ScanSchedule = "@every 24h";
# MPVPath = "";
# MPVCommandTemplate = "${pkgs.mpv}/bin/mpv --audio-device=%d --input-ipc-server=%s --no-audio-display --log-file=/tmp/mpv.log --pause %f";
# MPVCmdTemplate = "${pkgs.mpv}/bin/mpv --no-audio-display --pause %f --input-ipc-server=%s --audio-channels=stereo --audio-samplerate=48000 --audio-format=s16 --ao=pcm --ao-pcm-file=/tmp/snapfifo --log-file=/tmp/mpv.log";
ReverseProxyWhitelist = "0.0.0.0/0";
ReverseProxyUserHeader = "X-User";
Jukebox = {
Enabled = true;
Default = "default";
Devices = [
# use mpv --audio-device=help to get these
[ "default" "alsa/sysdefault:CARD=PCH" ]
];
};
# Switch using --impure as these credential files are not stored within the flake
# sops-nix is not supported for these which is why we need to resort to these
LastFM = {
inherit (config.repo.secrets.local.LastFM) ApiKey Secret;
};
Spotify = {
inherit (config.repo.secrets.local.Spotify) ID Secret;
};
UILoginBackgroundUrl = "https://i.imgur.com/OMLxi7l.png";
UIWelcomeMessage = "~SwarselSound~";
EnableInsightsCollector = false;
};
};
nodes =
let
genNginx = toAddress: extraConfigPre: {
upstreams = {
${serviceName} = {
servers = {
"${toAddress}:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
oauth2 = {
enable = true;
allowedGroups = [ "navidrome_access" ];
};
extraConfig = extraConfigPre;
locations =
let
extraConfig = ''
proxy_redirect http:// https://;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_buffering off;
proxy_request_buffering off;
client_max_body_size 0;
'';
in
{
"/" = {
proxyPass = "http://${serviceName}";
proxyWebsockets = true;
inherit extraConfig;
};
"/share" = {
proxyPass = "http://${serviceName}";
proxyWebsockets = true;
setOauth2Headers = false;
bypassAuth = true;
inherit extraConfig;
};
"/rest" = {
proxyPass = "http://${serviceName}";
proxyWebsockets = true;
setOauth2Headers = false;
bypassAuth = true;
inherit extraConfig;
};
};
};
};
};
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = genNginx serviceAddress "";
${homeWebProxy}.services.nginx = lib.mkIf isHome (genNginx homeServiceAddress nginxAccessRules);
};
};
}
4.4.3.19. spotifyd
{ lib, config, confLib, ... }:
let
inherit (confLib.gen { name = "spotifyd"; port = 1025; }) servicePort serviceName serviceUser serviceGroup;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users.groups.${serviceGroup} = {
gid = 65136;
};
users.users.${serviceUser} = {
isSystemUser = true;
uid = 65136;
group = serviceGroup;
extraGroups = [ "audio" "utmp" "pipewire" ];
};
networking.firewall.allowedTCPPorts = [ servicePort ];
services.pipewire.systemWide = true;
# https://github.com/Spotifyd/spotifyd/issues/1366
networking.hosts."0.0.0.0" = [ "apresolve.spotify.com" ];
# hacky way to enable multi-session
# when another user connects, the service will crash and the new user will login
systemd.services.spotifyd.serviceConfig.RestartSec = lib.mkForce 1;
services.spotifyd = {
enable = true;
settings = {
global = {
dbus_type = "session";
use_mpris = false;
device = "sysdefault:CARD=PCH";
# device = "default";
device_name = "SwarselSpot";
# backend = "pulseaudio";
backend = "alsa";
# mixer = "alsa";
zeroconf_port = servicePort;
};
};
};
};
}
4.4.3.20. mpd
{ self, lib, config, pkgs, confLib, ... }:
let
inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "mpd"; port = 3254; }) servicePort serviceName serviceUser serviceGroup;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users = {
groups = {
mpd = { };
};
users = {
${serviceUser} = {
isSystemUser = true;
group = serviceGroup;
extraGroups = [ "audio" "utmp" ];
};
};
};
sops = {
secrets.mpd-pw = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
environment.systemPackages = with pkgs; [
pciutils
alsa-utils
mpv
];
topology.self.services.${serviceName} = {
name = lib.toUpper serviceName;
info = "http://localhost:${builtins.toString servicePort}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
services.${serviceName} = {
enable = true;
musicDirectory = "/media";
user = serviceUser;
group = serviceGroup;
network = {
port = servicePort;
listenAddress = "any";
};
credentials = [
{
passwordFile = config.sops.secrets.mpd-pw.path;
permissions = [
"read"
"add"
"control"
"admin"
];
}
];
};
};
}
4.4.3.21. pipewire
{ lib, config, ... }:
{
config = lib.mkIf (config?swarselmodules.server.mpd || config?swarselmodules.server.navidrome) {
security.rtkit.enable = true; # this is required for pipewire real-time access
services.pipewire = {
enable = true;
pulse.enable = true;
jack.enable = true;
audio.enable = true;
wireplumber.enable = true;
alsa = {
enable = true;
support32Bit = true;
};
};
};
}
4.4.3.22. postgresql
{ self, config, lib, pkgs, confLib, ... }:
let
inherit (confLib.gen { name = "postgresql"; port = 3254; }) serviceName;
postgresVersion = 14;
postgresDirPrefix = if config.swarselsystems.isCloud then "/var/lib" else "/Vault/data" ;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
topology.self.services = {
${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
icon = "${self}/files/topology-images/${serviceName}.png";
};
};
services = {
${serviceName} = {
enable = true;
package = pkgs."postgresql_${builtins.toString postgresVersion}";
dataDir = "${postgresDirPrefix}/${serviceName}/${builtins.toString postgresVersion}";
};
};
environment.persistence."/persist".directories = lib.mkIf (config.swarselsystems.isImpermanence && config.swarselsystems.isCloud) [
{ directory = "/var/lib/postgresql"; user = "postgres"; group = "postgres"; mode = "0750"; }
];
};
}
4.4.3.23. podman
{ config, lib, ... }:
let
serviceName = "podman";
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
virtualisation = {
podman.enable = true;
oci-containers.backend = "podman";
};
networking.nftables.firewall = lib.mkIf config.networking.nftables.enable {
zones.podman = {
interfaces = [ "podman0" ];
};
rules = {
podman-to-postgres = lib.mkIf config.services.postgresql.enable {
from = [ "podman" ];
to = [ "local" ];
before = [ "drop" ];
allowedTCPPorts = [ config.services.postgresql.settings.port ];
};
local-to-podman = {
from = [ "local" "wgProxy" "wgHome"];
to = [ "podman" ];
before = [ "drop" ];
verdict = "accept";
};
};
};
};
}
4.4.3.24. matrix
{ self, lib, config, pkgs, globals, dns, confLib, ... }:
let
inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "matrix"; user = "matrix-synapse"; port = 8008; }) servicePort serviceName serviceUser serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
federationPort = 8448;
whatsappPort = 29318;
telegramPort = 29317;
signalPort = 29328;
baseUrl = "https://${serviceDomain}";
clientConfig."m.homeserver".base_url = baseUrl;
serverConfig."m.server" = "${serviceDomain}:443";
mkWellKnown = data: ''
default_type application/json;
add_header Access-Control-Allow-Origin *;
return 200 '${builtins.toJSON data}';
'';
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
environment.systemPackages = with pkgs; [
matrix-synapse
lottieconverter
ffmpeg
];
sops = {
secrets = {
matrix-shared-secret = { inherit sopsFile; owner = serviceUser; };
mautrix-telegram-as-token = { inherit sopsFile; owner = serviceUser; };
mautrix-telegram-hs-token = { inherit sopsFile; owner = serviceUser; };
mautrix-telegram-api-id = { inherit sopsFile; owner = serviceUser; };
mautrix-telegram-api-hash = { inherit sopsFile; owner = serviceUser; };
};
templates = {
"matrix_user_register.sh".content = ''
register_new_matrix_user -k ${config.sops.placeholder.matrix-shared-secret} http://localhost:${builtins.toString servicePort}
'';
matrixshared = {
owner = serviceUser;
content = ''
registration_shared_secret: ${config.sops.placeholder.matrix-shared-secret}
'';
};
mautrixtelegram = {
owner = serviceUser;
content = ''
MAUTRIX_TELEGRAM_APPSERVICE_AS_TOKEN=${config.sops.placeholder.mautrix-telegram-as-token}
MAUTRIX_TELEGRAM_APPSERVICE_HS_TOKEN=${config.sops.placeholder.mautrix-telegram-hs-token}
MAUTRIX_TELEGRAM_TELEGRAM_API_ID=${config.sops.placeholder.mautrix-telegram-api-id}
MAUTRIX_TELEGRAM_TELEGRAM_API_HASH=${config.sops.placeholder.mautrix-telegram-api-hash}
'';
};
};
};
# networking.firewall.allowedTCPPorts = [ servicePort federationPort ];
topology.self.services = {
${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
} // (lib.listToAttrs (map
(service:
lib.nameValuePair "mautrix-${service}" {
name = "mautrix-${service}";
icon = "${self}/files/topology-images/mautrix.png";
})
[ "whatsapp" "signal" "telegram" ]));
systemd = {
timers."restart-bridges" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "1d";
OnUnitActiveSec = "1d";
Unit = "restart-bridges.service";
};
};
services = {
"restart-bridges" = {
script = ''
systemctl restart mautrix-whatsapp.service
systemctl restart mautrix-signal.service
systemctl restart mautrix-telegram.service
'';
serviceConfig = {
Type = "oneshot";
User = "root";
};
};
mautrix-telegram.path = with pkgs; [
lottieconverter # for animated stickers conversion, unfree package
ffmpeg # if converting animated stickers to webm (very slow!)
];
};
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort federationPort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
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";
CREATE ROLE "mautrix-telegram" WITH LOGIN PASSWORD 'telegram';
CREATE DATABASE "mautrix-telegram" WITH OWNER "mautrix-telegram"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
CREATE ROLE "mautrix-whatsapp" WITH LOGIN PASSWORD 'whatsapp';
CREATE DATABASE "mautrix-whatsapp" WITH OWNER "mautrix-whatsapp"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
CREATE ROLE "mautrix-signal" WITH LOGIN PASSWORD 'signal';
CREATE DATABASE "mautrix-signal" WITH OWNER "mautrix-signal"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
'';
};
matrix-synapse = {
enable = true;
dataDir = "/Vault/data/matrix-synapse";
settings = {
app_service_config_files =
let
inherit (config.services.matrix-synapse) dataDir;
in
[
"${dataDir}/telegram-registration.yaml"
"${dataDir}/whatsapp-registration.yaml"
"${dataDir}/signal-registration.yaml"
"${dataDir}/doublepuppet.yaml"
];
server_name = serviceDomain;
public_baseurl = "https://${serviceDomain}";
listeners = [
{
port = servicePort;
bind_addresses = [
"0.0.0.0"
# "::1"
];
type = "http";
tls = false;
x_forwarded = true;
resources = [
{
names = [ "client" "federation" ];
compress = true;
}
];
}
];
};
extraConfigFiles = [
config.sops.templates.matrixshared.path
];
};
mautrix-telegram = {
enable = true;
environmentFile = config.sops.templates.mautrixtelegram.path;
registerToSynapse = false;
settings = {
homeserver = {
address = "http://localhost:${builtins.toString servicePort}";
domain = serviceDomain;
};
appservice = {
address = "http://localhost:${builtins.toString telegramPort}";
hostname = "0.0.0.0";
port = telegramPort;
provisioning.enabled = true;
id = "telegram";
# ephemeral_events = true; # not needed due to double puppeting
public = {
enabled = false;
};
database = "postgresql:///mautrix-telegram?host=/run/postgresql";
};
bridge = {
relaybot.authless_portals = true;
allow_avatar_remove = true;
allow_contact_info = true;
sync_channel_members = true;
startup_sync = true;
sync_create_limit = 0;
sync_direct_chats = true;
telegram_link_preview = true;
permissions = {
"*" = "relaybot";
"@swarsel:${serviceDomain}" = "admin";
};
animated_sticker = {
target = "gif";
args = {
width = 256;
height = 256;
fps = 30; # only for webm
background = "020202"; # only for gif, transparency not supported
};
};
};
};
};
mautrix-whatsapp = {
enable = true;
registerToSynapse = false;
settings = {
homeserver = {
address = "http://localhost:${builtins.toString servicePort}";
domain = serviceDomain;
};
database = {
type = "postgres";
uri = "postgresql:///mautrix-whatsapp?host=/run/postgresql";
};
appservice = {
address = "http://localhost:${builtins.toString whatsappPort}";
hostname = "0.0.0.0";
port = whatsappPort;
};
bridge = {
displayname_template = "{{or .FullName .PushName .JID}} (WA)";
history_sync = {
backfill = true;
max_initial_conversations = -1;
message_count = -1;
request_full_sync = true;
full_sync_config = {
days_limit = 900;
size_mb_limit = 5000;
storage_quota_mb = 5000;
};
};
login_shared_secret_map = {
${serviceDomain} = "as_token:doublepuppet";
};
sync_manual_marked_unread = true;
send_presence_on_typing = true;
parallel_member_sync = true;
url_previews = true;
caption_in_message = true;
extev_polls = true;
permissions = {
"*" = "relay";
"@swarsel:${serviceDomain}" = "admin";
};
};
};
};
mautrix-signal = {
enable = true;
registerToSynapse = false;
settings = {
homeserver = {
address = "http://localhost:${builtins.toString servicePort}";
domain = serviceDomain;
};
database = {
type = "postgres";
uri = "postgresql:///mautrix-signal?host=/run/postgresql";
};
appservice = {
address = "http://localhost:${builtins.toString signalPort}";
hostname = "0.0.0.0";
port = signalPort;
};
bridge = {
displayname_template = "{{or .ContactName .ProfileName .PhoneNumber}} (Signal)";
login_shared_secret_map = {
${serviceDomain} = "as_token:doublepuppet";
};
caption_in_message = true;
permissions = {
"*" = "relay";
"@swarsel:${serviceDomain}" = "admin";
};
};
};
};
};
# restart the bridges daily. this is done for the signal bridge mainly which stops carrying
# messages out after a while.
nodes =
let
genNginx = toAddress: extraConfig: {
upstreams = {
${serviceName} = {
servers = {
"${toAddress}:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
listen = [
{
addr = "0.0.0.0";
port = 8448;
ssl = true;
extraParameters = [
"default_server"
];
}
{
addr = "[::0]";
port = 8448;
ssl = true;
extraParameters = [
"default_server"
];
}
{
addr = "0.0.0.0";
port = 443;
ssl = true;
}
{
addr = "[::0]";
port = 443;
ssl = true;
}
];
inherit extraConfig;
locations = {
"~ ^(/_matrix|/_synapse/client)" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
"= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig;
"= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig;
};
};
};
};
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = genNginx serviceAddress "";
${homeWebProxy}.services.nginx = genNginx homeServiceAddress nginxAccessRules;
};
};
}
4.4.3.25. nextcloud
{ pkgs, lib, config, globals, dns, confLib, ... }:
let
inherit (config.repo.secrets.local.nextcloud) adminuser;
inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "nextcloud"; port = 80; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome dnsServer webProxy homeWebProxy homeServiceAddress nginxAccessRules;
nextcloudVersion = "32";
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
sops.secrets = {
nextcloud-admin-pw = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
kanidm-nextcloud-client = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
globals.services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
services = {
${serviceName} = {
enable = true;
settings = {
trusted_proxies = [ "0.0.0.0" ];
overwriteprotocol = "https";
};
package = pkgs."nextcloud${nextcloudVersion}";
hostName = serviceDomain;
home = "/Vault/data/${serviceName}";
datadir = "/Vault/data/${serviceName}";
https = true;
configureRedis = true;
maxUploadSize = "4G";
extraApps = {
inherit (pkgs."nextcloud${nextcloudVersion}Packages".apps) mail calendar contacts cospend phonetrack polls tasks sociallogin;
};
extraAppsEnable = true;
config = {
inherit adminuser;
adminpassFile = config.sops.secrets.nextcloud-admin-pw.path;
dbtype = "sqlite";
};
};
};
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.26. immich
{ lib, pkgs, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "immich"; port = 3001; }) servicePort serviceName serviceUser serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users.users.${serviceUser} = {
extraGroups = [ "video" "render" "users" ];
};
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
# networking.firewall.allowedTCPPorts = [ servicePort ];
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.${serviceName} = {
enable = true;
package = pkgs.immich;
host = "0.0.0.0";
port = servicePort;
# openFirewall = true;
mediaLocation = "/Vault/Eternor/Immich"; # dataDir
environment = {
IMMICH_MACHINE_LEARNING_URL = lib.mkForce "http://localhost:3003";
};
};
nodes =
let
extraConfigLoc = ''
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
send_timeout 600s;
'';
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName extraConfigLoc; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName extraConfigLoc; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.27. paperless (tika, gotenberg)
This is my personal document management system. It automatically pulls documents from several sources, the only manual step for physical documents is to put them in my scanner and use email delivery.
Also I install Tika and Gotenberg, which are needed to create PDFs out of .eml's. This is needed for e.g. online services that only send their invoices through email body text.
{ lib, pkgs, config, dns, globals, confLib, ... }:
let
inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "paperless"; port = 28981; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
tikaPort = 9998;
gotenbergPort = 3002;
kanidmDomain = globals.services.kanidm.domain;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users.users.${serviceUser} = {
extraGroups = [ "users" ];
};
sops.secrets = {
paperless-admin-pw = { inherit sopsFile; owner = serviceUser; };
kanidm-paperless-client = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
# networking.firewall.allowedTCPPorts = [ servicePort ];
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services = {
${serviceName} = {
enable = true;
mediaDir = "/Vault/Eternor/Paperless";
dataDir = "/Vault/data/${serviceName}";
user = serviceUser;
port = servicePort;
passwordFile = config.sops.secrets.paperless-admin-pw.path;
address = "0.0.0.0";
settings = {
PAPERLESS_OCR_LANGUAGE = "deu+eng";
PAPERLESS_URL = "https://${serviceDomain}";
PAPERLESS_OCR_USER_ARGS = builtins.toJSON {
optimize = 1;
invalidate_digital_signatures = true;
pdfa_image_compression = "lossless";
};
PAPERLESS_TIKA_ENABLED = "true";
PAPERLESS_TIKA_ENDPOINT = "http://localhost:${builtins.toString tikaPort}";
PAPERLESS_TIKA_GOTENBERG_ENDPOINT = "http://localhost:${builtins.toString gotenbergPort}";
PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect";
PAPERLESS_SOCIALACCOUNT_PROVIDERS = builtins.toJSON {
openid_connect = {
OAUTH_PKCE_ENABLED = "True";
APPS = [
rec {
provider_id = "kanidm";
name = "Kanidm";
client_id = "paperless";
# secret will be added by paperless-web.service (see below)
#secret = "";
settings.server_url = "https://${kanidmDomain}/oauth2/openid/${client_id}/.well-known/openid-configuration";
}
];
};
};
};
};
tika = {
enable = true;
port = tikaPort;
openFirewall = false;
listenAddress = "127.0.0.1";
enableOcr = true;
};
gotenberg = {
enable = true;
package = pkgs.stable.gotenberg;
port = gotenbergPort;
bindIP = "127.0.0.1";
timeout = "600s";
chromium.package = pkgs.stable.chromium;
};
};
# Add secret to PAPERLESS_SOCIALACCOUNT_PROVIDERS
systemd.services.paperless-web.script = lib.mkBefore ''
oidcSecret=$(< ${config.sops.secrets.kanidm-paperless-client.path})
export PAPERLESS_SOCIALACCOUNT_PROVIDERS=$(
${pkgs.jq}/bin/jq <<< "$PAPERLESS_SOCIALACCOUNT_PROVIDERS" \
--compact-output \
--arg oidcSecret "$oidcSecret" '.openid_connect.APPS.[0].secret = $oidcSecret'
)
'';
nodes =
let
extraConfigLoc = ''
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
send_timeout 300;
'';
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName extraConfigLoc; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName extraConfigLoc; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.28. transmission
{ self, pkgs, lib, config, confLib, ... }:
let
inherit (confLib.gen { name = "transmission"; }) serviceName serviceDomain;
inherit (confLib.static) isHome;
lidarrUser = "lidarr";
lidarrGroup = lidarrUser;
lidarrPort = 8686;
radarrUser = "radarr";
radarrGroup = radarrUser;
radarrPort = 7878;
sonarrUser = "sonarr";
sonarrGroup = sonarrUser;
sonarrPort = 8989;
readarrUser = "readarr";
readarrGroup = readarrUser;
readarrPort = 8787;
prowlarrUser = "prowlarr";
prowlarrGroup = prowlarrUser;
prowlarrPort = 9696;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} and friends on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
# this user/group section is probably unneeded
users = {
groups = {
dockeruser = {
gid = 1155;
};
"${radarrGroup}" = { };
"${readarrGroup}" = { };
"${sonarrGroup}" = { };
"${lidarrGroup}" = { };
"${prowlarrGroup}" = { };
};
users = {
dockeruser = {
isSystemUser = true;
uid = 1155;
group = "docker";
extraGroups = [ "users" ];
};
"${radarrUser}" = {
isSystemUser = true;
group = radarrGroup;
extraGroups = [ "users" ];
};
"${readarrGroup}" = {
isSystemUser = true;
group = readarrGroup;
extraGroups = [ "users" ];
};
"${sonarrGroup}" = {
isSystemUser = true;
group = sonarrGroup;
extraGroups = [ "users" ];
};
"${lidarrUser}" = {
isSystemUser = true;
group = lidarrGroup;
extraGroups = [ "users" ];
};
"${prowlarrGroup}" = {
isSystemUser = true;
group = prowlarrGroup;
extraGroups = [ "users" ];
};
};
};
virtualisation.docker.enable = true;
environment.systemPackages = with pkgs; [
docker
];
topology.self.services = {
radarr.info = "https://${serviceDomain}/radarr";
readarr = {
name = "Readarr";
info = "https://${serviceDomain}/readarr";
icon = "${self}/files/topology-images/readarr.png";
};
sonarr.info = "https://${serviceDomain}/sonarr";
lidarr.info = "https://${serviceDomain}/lidarr";
prowlarr.info = "https://${serviceDomain}/prowlarr";
};
globals.services.transmission = {
domain = serviceDomain;
inherit isHome;
};
services = {
radarr = {
enable = true;
user = radarrUser;
group = radarrGroup;
settings.server.port = radarrPort;
openFirewall = true;
dataDir = "/Vault/data/radarr";
};
readarr = {
enable = true;
user = readarrUser;
group = readarrGroup;
settings.server.port = readarrPort;
openFirewall = true;
dataDir = "/Vault/data/readarr";
};
sonarr = {
enable = true;
user = sonarrUser;
group = sonarrGroup;
settings.server.port = sonarrPort;
openFirewall = true;
dataDir = "/Vault/data/sonarr";
};
lidarr = {
enable = true;
user = lidarrUser;
group = lidarrGroup;
settings.server.port = lidarrPort;
openFirewall = true;
dataDir = "/Vault/data/lidarr";
};
prowlarr = {
enable = true;
settings.server.port = prowlarrPort;
openFirewall = true;
};
nginx = {
virtualHosts = {
"${serviceDomain}" = {
enableACME = false;
forceSSL = false;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://localhost:9091";
extraConfig = ''
client_max_body_size 0;
'';
};
"/radarr" = {
proxyPass = "http://localhost:${builtins.toString radarrPort}";
extraConfig = ''
client_max_body_size 0;
'';
};
"/readarr" = {
proxyPass = "http://localhost:${builtins.toString readarrPort}";
extraConfig = ''
client_max_body_size 0;
'';
};
"/sonarr" = {
proxyPass = "http://localhost:${builtins.toString sonarrPort}";
extraConfig = ''
client_max_body_size 0;
'';
};
"/lidarr" = {
proxyPass = "http://localhost:${builtins.toString lidarrPort}";
extraConfig = ''
client_max_body_size 0;
'';
};
"/prowlarr" = {
proxyPass = "http://localhost:${builtins.toString prowlarrPort}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
};
};
}
4.4.3.29. syncthing
{ lib, config, globals, dns, confLib, ... }:
let
inherit (config.swarselsystems.syncthing) serviceDomain;
inherit (confLib.gen { name = "syncthing"; port = 8384; }) servicePort serviceName serviceUser serviceGroup serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
specificServiceName = "${serviceName}-${config.node.name}";
cfg = config.services.${serviceName};
devices = config.swarselsystems.syncthing.syncDevices;
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
swarselsystems.syncthing = {
serviceDomain = lib.mkOption {
type = lib.types.str;
default = config.repo.secrets.common.services.domains.syncthing1;
};
syncDevices = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "magicant" "winters" "pyramid" "moonside@oracle" ];
};
devices = lib.mkOption {
type = lib.types.attrs;
default = {
"magicant" = {
id = "VMWGEE2-4HDS2QO-KNQOVGN-LXLX6LA-666E4EK-ZBRYRRO-XFEX6FB-6E3XLQO";
};
"winters" = {
id = "O7RWDMD-AEAHPP7-7TAVLKZ-BSWNBTU-2VA44MS-EYGUNBB-SLHKB3C-ZSLMOAA";
};
"moonside@oracle" = {
id = "VPCDZB6-MGVGQZD-Q6DIZW3-IZJRJTO-TCC3QUQ-2BNTL7P-AKE7FBO-N55UNQE";
};
"pyramid" = {
id = "YAPV4BV-I26WPTN-SIP32MV-SQP5TBZ-3CHMTCI-Z3D6EP2-MNDQGLP-53FT3AB";
};
};
};
};
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users.users.${serviceUser} = {
extraGroups = [ "users" ];
group = serviceGroup;
isSystemUser = true;
};
users.groups.${serviceGroup} = { };
# networking.firewall.allowedTCPPorts = [ servicePort ];
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy} = {
allowedTCPPorts = [ servicePort 22000 ];
allowedUDPPorts = [ 20000 21027 ];
};
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy} = {
allowedTCPPorts = [ servicePort 20000 ];
allowedUDPPorts = [ 20000 21027 ];
};
};
};
services.${specificServiceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.${serviceName} = rec {
enable = true;
user = serviceUser;
group = serviceGroup;
dataDir = lib.mkDefault "/Vault/data/${serviceName}";
configDir = "${cfg.dataDir}/.config/${serviceName}";
guiAddress = "0.0.0.0:${builtins.toString servicePort}";
openDefaultPorts = lib.mkIf (!isProxied) true; # opens ports TCP/UDP 22000 and UDP 21027 for discovery
relay.enable = false;
settings = {
urAccepted = -1;
inherit (config.swarselsystems.syncthing) devices;
folders = {
"Default Folder" = lib.mkForce {
path = "${cfg.dataDir}/Sync";
type = "receiveonly";
versioning = null;
inherit devices;
id = "default";
};
"Obsidian" = {
path = "${cfg.dataDir}/Obsidian";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
inherit devices;
id = "yjvni-9eaa7";
};
"Org" = {
path = "${cfg.dataDir}/Org";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
inherit devices;
id = "a7xnl-zjj3d";
};
"Vpn" = {
path = "${cfg.dataDir}/Vpn";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
inherit devices;
id = "hgp9s-fyq3p";
};
};
};
};
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${specificServiceName}.baseDomain}.subdomainRecords = {
"${globals.services.${specificServiceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain; serviceName = specificServiceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain; serviceName = specificServiceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.30. restic
This manages backups for my pictures and obsidian files.
Note: you still need to run restic-<name> init once on the host to get the bucket running.
{ lib, pkgs, config, ... }:
let
inherit (config.swarselsystems) sopsFile;
in
{
options.swarselmodules.server.restic = lib.mkEnableOption "enable restic backups on server";
options.swarselsystems.server.restic = {
bucketName = lib.mkOption {
type = lib.types.str;
};
paths = lib.mkOption {
type = lib.types.listOf lib.types.str;
};
};
config = lib.mkIf config.swarselmodules.server.restic {
sops = {
secrets = {
resticpw = { inherit sopsFile; };
resticaccesskey = { inherit sopsFile; };
resticsecretaccesskey = { inherit sopsFile; };
};
templates = {
"restic-env".content = ''
AWS_ACCESS_KEY_ID=${config.sops.placeholder.resticaccesskey}
AWS_SECRET_ACCESS_KEY=${config.sops.placeholder.resticsecretaccesskey}
'';
};
};
services.restic =
let
inherit (config.repo.secrets.local) resticRepo;
in
{
backups = {
"${config.swarselsystems.server.restic.bucketName}" = {
environmentFile = config.sops.templates."restic-env".path;
passwordFile = config.sops.secrets.resticpw.path;
inherit (config.swarselsystems.server.restic) paths;
pruneOpts = [
"--keep-daily 3"
"--keep-weekly 2"
"--keep-monthly 3"
"--keep-yearly 100"
];
backupPrepareCommand = ''
${pkgs.restic}/bin/restic prune
'';
repository = "${resticRepo}";
initialize = true;
timerConfig = {
OnCalendar = "03:00";
};
};
};
};
};
}
4.4.3.31. monitoring (Grafana, Prometheus)
This section exposes several metrics that I use to check the health of my server. I need to expand on the exporters section at some point, but for now I have everything I need.
{ lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "grafana"; port = 3000; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
prometheusPort = 9090;
prometheusUser = "prometheus";
prometheusGroup = prometheusUser;
grafanaUpstream = "grafana";
prometheusUpstream = "prometheus";
prometheusWebRoot = "prometheus";
kanidmDomain = globals.services.kanidm.domain;
inherit (config.swarselsystems) sopsFile;
# sopsFile2 = config.node.secretsDir + "/secrets2.yaml";
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
sops = {
secrets = {
grafana-admin-pw = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
prometheus-admin-pw = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
kanidm-grafana-client = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
# prometheus-admin-hash = { sopsFile = sopsFile2; owner = prometheusUser; group = prometheusGroup; mode = "0440"; };
prometheus-admin-hash = { inherit sopsFile; owner = prometheusUser; group = prometheusGroup; mode = "0440"; };
};
templates = {
"web-config" = {
content = ''
basic_auth_users:
admin: ${config.sops.placeholder.prometheus-admin-hash}
'';
owner = prometheusUser;
group = prometheusGroup;
mode = "0440";
};
};
};
users = {
users = {
nextcloud-exporter = {
extraGroups = [ "nextcloud" ];
};
${serviceUser} = {
extraGroups = [ "users" ];
};
};
};
# networking.firewall.allowedTCPPorts = [ servicePort prometheusPort ];
topology.self.services.prometheus.info = "https://${serviceDomain}/${prometheusWebRoot}";
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort prometheusPort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort prometheusPort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services = {
${serviceName} = {
enable = true;
dataDir = "/Vault/data/${serviceName}";
provision = {
enable = true;
datasources.settings = {
datasources = [
{
name = "prometheus";
type = "prometheus";
url = "https://${serviceDomain}/prometheus";
editable = false;
access = "proxy";
basicAuth = true;
basicAuthUser = "admin";
jsonData = {
httpMethod = "POST";
manageAlerts = true;
prometheusType = "Prometheus";
prometheusVersion = "> 2.50.x";
cacheLevel = "High";
disableRecordingRules = false;
incrementalQueryOverlapWindow = "10m";
};
secureJsonData = {
basicAuthPassword = "$__file{/run/secrets/prometheus-admin-pw}";
};
}
];
};
};
settings = {
analytics.reporting_enabled = false;
users.allow_sign_up = false;
security = {
admin_password = "$__file{/run/secrets/grafana-admin-pw}";
cookie_secure = true;
disable_gravatar = true;
};
server = {
domain = serviceDomain;
root_url = "https://${serviceDomain}";
http_port = servicePort;
http_addr = "0.0.0.0";
protocol = "http";
enforce_domain = true;
enable_gzip = true;
};
"auth.generic_oauth" = {
enabled = true;
name = "Kanidm";
icon = "signin";
allow_sign_up = true;
#auto_login = true;
client_id = "grafana";
client_secret = "$__file{${config.sops.secrets.kanidm-grafana-client.path}}";
scopes = "openid email profile";
login_attribute_path = "preferred_username";
auth_url = "https://${kanidmDomain}/ui/oauth2";
token_url = "https://${kanidmDomain}/oauth2/token";
api_url = "https://${kanidmDomain}/oauth2/openid/grafana/userinfo";
use_pkce = true;
use_refresh_token = true;
# Allow mapping oauth2 roles to server admin
allow_assign_grafana_admin = true;
role_attribute_path = "contains(groups[*], 'server_admin') && 'GrafanaAdmin' || contains(groups[*], 'admin') && 'Admin' || contains(groups[*], 'editor') && 'Editor' || 'Viewer'";
};
};
};
prometheus =
let
nextcloudUser = config.repo.secrets.local.nextcloud.adminuser;
in
{
enable = true;
webExternalUrl = "https://${serviceDomain}/${prometheusWebRoot}";
port = prometheusPort;
listenAddress = "0.0.0.0";
globalConfig = {
scrape_interval = "10s";
};
webConfigFile = config.sops.templates.web-config.path;
scrapeConfigs = [
{
job_name = "node";
static_configs = [{
targets = [ "localhost:${toString config.services.prometheus.exporters.node.port}" ];
}];
}
{
job_name = "zfs";
static_configs = [{
targets = [ "localhost:${toString config.services.prometheus.exporters.zfs.port}" ];
}];
}
{
job_name = "nginx";
static_configs = [{
targets = [ "localhost:${toString config.services.prometheus.exporters.nginx.port}" ];
}];
}
{
job_name = "nextcloud";
static_configs = [{
targets = [ "localhost:${toString config.services.prometheus.exporters.nextcloud.port}" ];
}];
}
];
exporters = {
node = {
enable = true;
port = 9000;
enabledCollectors = [ "systemd" ];
extraFlags = [ "--collector.ethtool" "--collector.softirqs" "--collector.tcpstat" "--collector.wifi" ];
};
zfs = {
enable = true;
port = 9134;
pools = [
"Vault"
];
};
restic = {
enable = false;
port = 9753;
};
nginx = {
enable = true;
port = 9113;
sslVerify = false;
scrapeUri = "http://localhost/nginx_status";
};
nextcloud = lib.mkIf config.swarselmodules.server.nextcloud {
enable = true;
port = 9205;
url = "https://${serviceDomain}/ocs/v2.php/apps/serverinfo/api/v1/info";
username = nextcloudUser;
passwordFile = config.sops.secrets.nextcloud-admin-pw.path;
};
};
};
};
nodes =
let
extraConfig = ''
allow ${globals.networks.home-lan.vlans.services.cidrv4};
allow ${globals.networks.home-lan.vlans.services.cidrv6};
'';
genNginx = toAddress: extraConfigPre: {
upstreams = {
"${grafanaUpstream}" = {
servers = {
"${toAddress}:${builtins.toString servicePort}" = { };
};
};
"${prometheusUpstream}" = {
servers = {
"${toAddress}:${builtins.toString prometheusPort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
extraConfig = extraConfigPre;
locations =
let
extraConfig = ''
client_max_body_size 0;
'';
in
{
"/" = {
proxyPass = "http://${grafanaUpstream}";
proxyWebsockets = true;
inherit extraConfig;
};
"/${prometheusWebRoot}" = {
proxyPass = "http://${prometheusUpstream}";
inherit extraConfig;
};
};
};
};
};
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = genNginx serviceAddress "";
${homeWebProxy}.services.nginx = genNginx homeServiceAddress (extraConfig + nginxAccessRules);
};
};
}
4.4.3.32. Jenkins (currently unused)
This is a WIP Jenkins instance. It is used to automatically build a new system when pushes to the main repository are detected. I have turned this service off for now however, as I actually prefer to start my builds manually.
{ pkgs, lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "jenkins"; port = 8088; }) servicePort serviceName serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.jenkins = {
enable = true;
withCLI = true;
port = servicePort;
packages = [ pkgs.stdenv pkgs.git pkgs.jdk17 config.programs.ssh.package pkgs.nix ];
listenAddress = "0.0.0.0";
home = "/Vault/apps/${serviceName}";
};
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.33. Emacs elfeed (RSS Server)
This was an approach of hosting an RSS server from within emacs. That would have been useful as it would have allowed me to allow my feeds from any device. However, it proved impossible to do bidirectional syncing, so I abandoned this configuration in favor of FreshRSS.
{ lib, config, confLib, ... }:
let
inherit (confLib.gen { name = "emacs"; port = 9812; }) servicePort serviceName;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} server on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
networking.firewall.allowedTCPPorts = [ servicePort ];
services.${serviceName} = {
enable = true;
install = true;
startWithGraphical = false;
};
};
}
4.4.3.34. FreshRSS
FreshRSS is a more 'classical' RSS aggregator that I can just host as a distinct service. This also has its upsides because I jave more control over the state this way.
It serves both a Greader API at https://${servicename}/api/greader.php, as well as a Fever API at https://${servicename}/api/fever.php.
I am using this with CapyReader on my phone, set it up as a FreshRSS account with Server URL =https://${servicename}/api/greader.php
FreshRSS claims to support HTTP header auth, but at least it does not work with my oauth2-proxy setup. Until this is fixed, I resorted to the "form" login, since I mostly do not use the web version anyways.
{ self, lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "freshrss"; port = 80; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome webProxy homeWebProxy dnsServer homeServiceAddress nginxAccessRules;
inherit (config.swarselsystems) sopsFile;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users.users.${serviceUser} = {
extraGroups = [ "users" ];
group = serviceGroup;
isSystemUser = true;
};
users.groups.${serviceGroup} = { };
sops = {
secrets = {
freshrss-pw = { inherit sopsFile; owner = serviceUser; };
kanidm-freshrss-client = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
# freshrss-oidc-crypto-key = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
# templates = {
# "freshrss-env" = {
# content = ''
# DATA_PATH=${config.services.freshrss.dataDir}
# OIDC_ENABLED=1
# OIDC_PROVIDER_METADATA_URL=https://${kanidmDomain}/.well-known/openid-configuration
# OIDC_CLIENT_ID=freshrss
# OIDC_CLIENT_SECRET=${config.sops.placeholder.kanidm-freshrss-client}
# OIDC_CLIENT_CRYPTO_KEY=${config.sops.placeholder.oidc-crypto-key}
# OIDC_REMOTE_USER_CLAIM=preferred_username
# OIDC_SCOPES=openid groups email profile
# OIDC_X_FORWARDED_HEADERS=X-Forwarded-Host X-Forwarded-Port X-Forwarded-Proto
# '';
# owner = "freshrss";
# group = "freshrss";
# mode = "0440";
# };
# };
};
topology.self.services.${serviceName} = {
name = "FreshRSS";
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
globals.services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
services.${serviceName} =
let
inherit (config.repo.secrets.local.freshrss) defaultUser;
in
{
inherit defaultUser;
enable = true;
virtualHost = serviceDomain;
baseUrl = "https://${serviceDomain}";
authType = "form";
dataDir = "/Vault/data/tt-rss";
passwordFile = config.sops.secrets.freshrss-pw.path;
};
# systemd.services.freshrss-config.serviceConfig.EnvironmentFile = [
# config.sops.templates.freshrss-env.path
# ];
nodes =
let
genNginx = toAddress: extraConfig: {
upstreams = {
${serviceName} = {
servers = {
"${toAddress}:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
oauth2.enable = true;
oauth2.allowedGroups = [ "ttrss_access" ];
inherit extraConfig;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
};
"/api" = {
proxyPass = "http://${serviceName}";
setOauth2Headers = false;
bypassAuth = true;
};
};
};
};
};
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = genNginx serviceAddress "";
${homeWebProxy}.services.nginx = genNginx homeServiceAddress nginxAccessRules;
};
};
}
4.4.3.35. forgejo (git server)
{ lib, config, pkgs, globals, dns, confLib, ... }:
let
inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "forgejo"; port = 3004; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
kanidmDomain = globals.services.kanidm.domain;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
# networking.firewall.allowedTCPPorts = [ servicePort ];
users.users.${serviceUser} = {
group = serviceGroup;
isSystemUser = true;
};
users.groups.${serviceGroup} = { };
sops.secrets = {
kanidm-forgejo-client = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.${serviceName} = {
enable = true;
stateDir = "/Vault/data/${serviceName}";
user = serviceUser;
group = serviceGroup;
lfs.enable = lib.mkDefault true;
settings = {
DEFAULT = {
APP_NAME = "~SwaGit~";
};
server = {
PROTOCOL = "http";
HTTP_PORT = servicePort;
HTTP_ADDR = "0.0.0.0";
DOMAIN = serviceDomain;
ROOT_URL = "https://${serviceDomain}";
};
# federation.ENABLED = true;
service = {
DISABLE_REGISTRATION = false;
ALLOW_ONLY_INTERNAL_REGISTRATION = false;
ALLOW_ONLY_EXTERNAL_REGISTRATION = true;
SHOW_REGISTRATION_BUTTON = false;
};
session.COOKIE_SECURE = true;
oauth2_client = {
# Never use auto account linking with this, otherwise users cannot change
# their new user name and they could potentially overtake other users accounts
# by setting their email address to an existing account.
# With "login" linking the user must choose a non-existing username first or login
# with the existing account to link.
ACCOUNT_LINKING = "login";
USERNAME = "nickname";
# This does not mean that you cannot register via oauth, but just that there should
# be a confirmation dialog shown to the user before the account is actually created.
# This dialog allows changing user name and email address before creating the account.
ENABLE_AUTO_REGISTRATION = false;
REGISTER_EMAIL_CONFIRM = false;
UPDATE_AVATAR = true;
};
};
};
systemd.services.${serviceName} = {
serviceConfig.RestartSec = "60"; # Retry every minute
preStart =
let
exe = lib.getExe config.services.forgejo.package;
providerName = "kanidm";
clientId = serviceName;
args = lib.escapeShellArgs (
lib.concatLists [
[
"--name"
providerName
]
[
"--provider"
"openidConnect"
]
[
"--key"
clientId
]
[
"--auto-discover-url"
"https://${kanidmDomain}/oauth2/openid/${clientId}/.well-known/openid-configuration"
]
[
"--scopes"
"email"
]
[
"--scopes"
"profile"
]
[
"--group-claim-name"
"groups"
]
[
"--admin-group"
"admin"
]
[ "--skip-local-2fa" ]
]
);
in
lib.mkAfter ''
provider_id=$(${exe} admin auth list | ${pkgs.gnugrep}/bin/grep -w '${providerName}' | cut -f1)
SECRET="$(< ${config.sops.secrets.kanidm-forgejo-client.path})"
if [[ -z "$provider_id" ]]; then
${exe} admin auth add-oauth ${args} --secret "$SECRET"
else
${exe} admin auth update-oauth --id "$provider_id" ${args} --secret "$SECRET"
fi
'';
};
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.36. Anki Sync Server
{ self, lib, config, globals, dns, confLib, ... }:
let
inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "ankisync"; port = 27701; }) servicePort serviceName serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
ankiUser = globals.user.name;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
# networking.firewall.allowedTCPPorts = [ servicePort ];
sops.secrets.anki-pw = { inherit sopsFile; owner = "root"; };
topology.self.services.anki = {
name = lib.mkForce "Anki Sync Server";
icon = lib.mkForce "${self}/files/topology-images/${serviceName}.png";
info = "https://${serviceDomain}";
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.anki-sync-server = {
enable = true;
port = servicePort;
address = "0.0.0.0";
# openFirewall = true;
users = [
{
username = ankiUser;
passwordFile = config.sops.secrets.anki-pw.path;
}
];
};
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.37. kanidm
The forgejo configuration is a little broken and will show a 500 error when signing in through kanidm. However, when pressing back and refreshing the page, I am logged in. Currently I cannot be bothered to fix this.
A stupid (but simple) way to get the originUrl is to simply set any URL there and try to auth using kanidm. Then check the logs (journalctl -eu kanidm) and check for the line that says something along the lines of
`🚧 [warn]: Invalid OAuth2 redirecturi (must be an exact match to a redirect-url) - got <your =originURL=>`
To get other URLs (token, etc.), use https://<kanidmDomain>/oauth2/openid/<clientID>/.well-known/oauth-authorization-server, e.g. https://<kanidmDomain>/oauth2/openid/nextcloud/.well-known/oauth-authorization-server, with clienID being the client name as specified in kanidm.
Create user:
kanidm login -D idmadmin
kanidm person credential create-reset-token <user>
{ self, lib, pkgs, config, globals, dns, confLib, ... }:
let
certsSopsFile = self + /secrets/repo/certs.yaml;
inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "kanidm"; port = 8300; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy homeProxyIf webProxyIf dnsServer homeServiceAddress nginxAccessRules;
oauth2ProxyDomain = globals.services.oauth2-proxy.domain;
immichDomain = globals.services.immich.domain;
paperlessDomain = globals.services.paperless.domain;
forgejoDomain = globals.services.forgejo.domain;
grafanaDomain = globals.services.grafana.domain;
nextcloudDomain = globals.services.nextcloud.domain;
certBase = "/etc/ssl";
certsDir = "${certBase}/certs";
privateDir = "${certBase}/private";
certPathBase = "${certsDir}/${serviceName}.crt";
certPath =
if config.swarselsystems.isImpermanence then
"/persist${certPathBase}"
else
"${certPathBase}";
keyPathBase = "${privateDir}/${serviceName}.key";
keyPath =
if config.swarselsystems.isImpermanence then
"/persist${keyPathBase}"
else
"${keyPathBase}";
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users = {
users.${serviceUser} = {
group = serviceGroup;
isSystemUser = true;
};
groups.${serviceGroup} = { };
};
sops = {
secrets = {
"kanidm-self-signed-crt" = { sopsFile = certsSopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-self-signed-key" = { sopsFile = certsSopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-admin-pw" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-idm-admin-pw" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-immich" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-paperless" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-forgejo" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-grafana" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-nextcloud" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-freshrss" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-oauth2-proxy" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
};
# networking.firewall.allowedTCPPorts = [ servicePort ];
globals = {
general.idmServer = config.node.name;
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
environment.persistence."/persist" = lib.mkIf config.swarselsystems.isImpermanence {
files = [
certPathBase
keyPathBase
];
};
systemd.services."generateSSLCert-${serviceName}" =
let
daysValid = 3650;
renewBeforeDays = 365;
in
{
before = [ "${serviceName}.service" ];
requiredBy = [ "${serviceName}.service" ];
after = [ "local-fs.target" ];
requires = [ "local-fs.target" ];
serviceConfig = {
Type = "oneshot";
};
script = ''
set -eu
${pkgs.coreutils}/bin/install -d -m 0755 ${certsDir}
${if config.swarselsystems.isImpermanence then "${pkgs.coreutils}/bin/install -d -m 0755 /persist${certsDir}" else ""}
${pkgs.coreutils}/bin/install -d -m 0750 ${privateDir}
${if config.swarselsystems.isImpermanence then "${pkgs.coreutils}/bin/install -d -m 0750 /persist${privateDir}" else ""}
need_gen=0
if [ ! -f "${certPath}" ] || [ ! -f "${keyPath}" ]; then
need_gen=1
else
enddate="$(${pkgs.openssl}/bin/openssl x509 -noout -enddate -in "${certPath}" | cut -d= -f2)"
end_epoch="$(${pkgs.coreutils}/bin/date -d "$enddate" +%s)"
now_epoch="$(${pkgs.coreutils}/bin/date +%s)"
seconds_left=$(( end_epoch - now_epoch ))
days_left=$(( seconds_left / 86400 ))
if [ "$days_left" -lt ${toString renewBeforeDays} ]; then
need_gen=1
else
echo 'Certificate exists and is still valid'
fi
fi
if [ "$need_gen" -eq 1 ]; then
${pkgs.openssl}/bin/openssl req -x509 -nodes -days ${toString daysValid} -newkey rsa:4096 -sha256 \
-keyout "${keyPath}" \
-out "${certPath}" \
-subj "/CN=${serviceDomain}" \
-addext "subjectAltName=DNS:${serviceDomain}"
chmod 0644 "${certPath}"
chmod 0600 "${keyPath}"
chown ${serviceUser}:${serviceGroup} "${certPath}" "${keyPath}"
fi
'';
};
# system.activationScripts."createPersistentStorageDirs" = lib.mkIf config.swarselsystems.isImpermanence {
# deps = [ "generateSSLCert-${serviceName}" "users" "groups" ];
# };
# system.activationScripts."generateSSLCert-${serviceName}" =
# let
# daysValid = 3650;
# renewBeforeDays = 365;
# in
# {
# text = ''
# set -eu
# ${pkgs.coreutils}/bin/install -d -m 0755 ${certsDir}
# ${if config.swarselsystems.isImpermanence then "${pkgs.coreutils}/bin/install -d -m 0755 /persist${certsDir}" else ""}
# ${pkgs.coreutils}/bin/install -d -m 0750 ${privateDir}
# ${if config.swarselsystems.isImpermanence then "${pkgs.coreutils}/bin/install -d -m 0750 /persist${privateDir}" else ""}
# need_gen=0
# if [ ! -f "${certPathBase}" ] || [ ! -f "${keyPathBase}" ]; then
# need_gen=1
# else
# enddate="$(${pkgs.openssl}/bin/openssl x509 -noout -enddate -in "${certPathBase}" | cut -d= -f2)"
# end_epoch="$(${pkgs.coreutils}/bin/date -d "$enddate" +%s)"
# now_epoch="$(${pkgs.coreutils}/bin/date +%s)"
# seconds_left=$(( end_epoch - now_epoch ))
# days_left=$(( seconds_left / 86400 ))
# if [ "$days_left" -lt ${toString renewBeforeDays} ]; then
# need_gen=1
# fi
# fi
# if [ "$need_gen" -eq 1 ]; then
# ${pkgs.openssl}/bin/openssl req -x509 -nodes -days ${toString daysValid} -newkey rsa:4096 -sha256 \
# -keyout "${keyPath}" \
# -out "${certPath}" \
# -subj "/CN=${serviceDomain}" \
# -addext "subjectAltName=DNS:${serviceDomain}"
# chmod 0644 "${certPath}"
# chmod 0600 "${keyPath}"
# chown ${serviceUser}:${serviceGroup} "${certPath}" "${keyPath}"
# fi
# '';
# deps = [
# "etc"
# (lib.mkIf config.swarselsystems.isImpermanence "specialfs")
# ];
# };
services = {
${serviceName} = {
package = pkgs.kanidmWithSecretProvisioning_1_8;
enableServer = true;
serverSettings = {
domain = serviceDomain;
origin = "https://${serviceDomain}";
# tls_chain = config.sops.secrets.kanidm-self-signed-crt.path;
tls_chain = certPathBase;
# tls_key = config.sops.secrets.kanidm-self-signed-key.path;
tls_key = keyPathBase;
bindaddress = "0.0.0.0:${toString servicePort}";
trust_x_forward_for = true;
};
enableClient = true;
clientSettings = {
uri = config.services.kanidm.serverSettings.origin;
verify_ca = true;
verify_hostnames = true;
};
provision = {
enable = true;
adminPasswordFile = config.sops.secrets.kanidm-admin-pw.path;
idmAdminPasswordFile = config.sops.secrets.kanidm-idm-admin-pw.path;
groups = {
"immich.access" = { };
"paperless.access" = { };
"forgejo.access" = { };
"forgejo.admins" = { };
"grafana.access" = { };
"grafana.editors" = { };
"grafana.admins" = { };
"grafana.server-admins" = { };
"nextcloud.access" = { };
"nextcloud.admins" = { };
"navidrome.access" = { };
"freshrss.access" = { };
"firefly.access" = { };
"radicale.access" = { };
"slink.access" = { };
"opkssh.access" = { };
"adguardhome.access" = { };
};
inherit (config.repo.secrets.local) persons;
systems = {
oauth2 = {
immich = {
displayName = "Immich";
originUrl = [
"https://${immichDomain}/auth/login"
"https://${immichDomain}/user-settings"
"app.immich:///oauth-callback"
"https://${immichDomain}/api/oauth/mobile-redirect"
];
originLanding = "https://${immichDomain}/";
basicSecretFile = config.sops.secrets.kanidm-immich.path;
preferShortUsername = true;
enableLegacyCrypto = true; # can use RS256 / HS256, not ES256
scopeMaps."immich.access" = [
"openid"
"email"
"profile"
];
};
paperless = {
displayName = "Paperless";
originUrl = "https://${paperlessDomain}/accounts/oidc/kanidm/login/callback/";
originLanding = "https://${paperlessDomain}/";
basicSecretFile = config.sops.secrets.kanidm-paperless.path;
preferShortUsername = true;
scopeMaps."paperless.access" = [
"openid"
"email"
"profile"
];
};
forgejo = {
displayName = "Forgejo";
originUrl = "https://${forgejoDomain}/user/oauth2/kanidm/callback";
originLanding = "https://${forgejoDomain}/";
basicSecretFile = config.sops.secrets.kanidm-forgejo.path;
scopeMaps."forgejo.access" = [
"openid"
"email"
"profile"
];
# XXX: PKCE is currently not supported by gitea/forgejo,
# see https://github.com/go-gitea/gitea/issues/21376.
allowInsecureClientDisablePkce = true;
preferShortUsername = true;
claimMaps.groups = {
joinType = "array";
valuesByGroup."forgejo.admins" = [ "admin" ];
};
};
grafana = {
displayName = "Grafana";
originUrl = "https://${grafanaDomain}/login/generic_oauth";
originLanding = "https://${grafanaDomain}/";
basicSecretFile = config.sops.secrets.kanidm-grafana.path;
preferShortUsername = true;
scopeMaps."grafana.access" = [
"openid"
"email"
"profile"
];
claimMaps.groups = {
joinType = "array";
valuesByGroup = {
"grafana.editors" = [ "editor" ];
"grafana.admins" = [ "admin" ];
"grafana.server-admins" = [ "server_admin" ];
};
};
};
nextcloud = {
displayName = "Nextcloud";
originUrl = " https://${nextcloudDomain}/apps/sociallogin/custom_oidc/kanidm";
originLanding = "https://${nextcloudDomain}/";
basicSecretFile = config.sops.secrets.kanidm-nextcloud.path;
allowInsecureClientDisablePkce = true;
scopeMaps."nextcloud.access" = [
"openid"
"email"
"profile"
];
preferShortUsername = true;
claimMaps.groups = {
joinType = "array";
valuesByGroup = {
"nextcloud.admins" = [ "admin" ];
};
};
};
opkssh = {
displayName = "OPKSSH";
originUrl = [
"http://localhost:3000"
"http://localhost:3000/login-callback"
"http://localhost:10001/login-callback"
"http://localhost:11110/login-callback"
];
originLanding = "http://localhost:3000";
public = true;
enableLocalhostRedirects = true;
scopeMaps."opkssh.access" = [
"openid"
"email"
"profile"
];
};
oauth2-proxy = {
displayName = "Oauth2-Proxy";
originUrl = "https://${oauth2ProxyDomain}/oauth2/callback";
originLanding = "https://${oauth2ProxyDomain}/";
basicSecretFile = config.sops.secrets.kanidm-oauth2-proxy.path;
scopeMaps = {
"freshrss.access" = [
"openid"
"email"
"profile"
];
"navidrome.access" = [
"openid"
"email"
"profile"
];
"firefly.access" = [
"openid"
"email"
"profile"
];
"radicale.access" = [
"openid"
"email"
"profile"
];
"slink.access" = [
"openid"
"email"
"profile"
];
"adguardhome.access" = [
"openid"
"email"
"profile"
];
};
preferShortUsername = true;
claimMaps.groups = {
joinType = "array";
valuesByGroup = {
"freshrss.access" = [ "ttrss_access" ];
"navidrome.access" = [ "navidrome_access" ];
"firefly.access" = [ "firefly_access" ];
"radicale.access" = [ "radicale_access" ];
"slink.access" = [ "slink_access" ];
"adguardhome.access" = [ "adguardhome_access" ];
};
};
};
};
};
};
};
};
systemd.services.${serviceName}.serviceConfig.RestartSec = "30";
nodes = let
extraConfig = ''
allow ${globals.networks.home-lan.vlans.services.cidrv4};
allow ${globals.networks.home-lan.vlans.services.cidrv6};
'';
in {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; protocol = "https"; noSslVerify = true; };
${homeWebProxy}.services.nginx = confLib.genNginx { inherit servicePort serviceDomain serviceName; protocol = "https"; noSslVerify = true; extraConfig = extraConfig + nginxAccessRules; serviceAddress = homeServiceAddress; };
};
};
}
4.4.3.38. oauth2-proxy
{ lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "oauth2-proxy"; port = 3004; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf oauthServer nginxAccessRules homeServiceAddress;
kanidmDomain = globals.services.kanidm.domain;
mainDomain = globals.domains.main;
inherit (config.swarselsystems) sopsFile;
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
# largely based on https://github.com/oddlama/nix-config/blob/main/modules/oauth2-proxy.nix
services.nginx.virtualHosts = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
options.oauth2 = {
enable = lib.mkEnableOption "access protection of this virtualHost using oauth2-proxy.";
allowedGroups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
A list of kanidm groups that are allowed to access this resource, or the
empty list to allow any authenticated client.
'';
};
X-User = lib.mkOption {
type = lib.types.str;
default = "$upstream_http_x_auth_request_user";
description = "The variable to set as X-User";
};
X-Email = lib.mkOption {
type = lib.types.str;
default = "$upstream_http_x_auth_request_email";
description = "The variable to set as X-Email";
};
X-Access-Token = lib.mkOption {
type = lib.types.str;
default = "$upstream_http_x_auth_request_access_token";
description = "The variable to set as X-Access-Token";
};
};
options.locations = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (locationSubmodule: {
options = {
setOauth2Headers = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to add oauth2 headers to this location. Only takes effect is oauth2 is actually enabled on the parent vhost.";
};
bypassAuth = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to set auth_request off for this location. Only takes effect is oauth2 is actually enabled on the parent vhost.";
};
};
config = lib.mkIf config.oauth2.enable {
extraConfig = lib.optionalString locationSubmodule.config.setOauth2Headers ''
proxy_set_header X-User $user;
proxy_set_header Remote-User $user;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Email $email;
# proxy_set_header X-Access-Token $token;
add_header Set-Cookie $auth_cookie;
'' + lib.optionalString locationSubmodule.config.bypassAuth ''
auth_request off;
'';
};
})
);
};
config = lib.mkIf config.oauth2.enable {
extraConfig = ''
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
# set variables that can be used in locations.<name>.extraConfig
# pass information via X-User and X-Email headers to backend,
# requires running with --set-xauthrequest flag
auth_request_set $user ${config.oauth2.X-User};
auth_request_set $email ${config.oauth2.X-Email};
# if you enabled --pass-access-token, this will pass the token to the backend
# auth_request_set $token ${config.oauth2.X-Access-Token};
# if you enabled --cookie-refresh, this is needed for it to work with auth_request
auth_request_set $auth_cookie $upstream_http_set_cookie;
'';
locations = {
"/oauth2/" = {
proxyPass = "http://oauth2-proxy";
setOauth2Headers = false;
bypassAuth = true;
extraConfig = ''
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
'';
};
"= /oauth2/auth" = {
proxyPass = "http://oauth2-proxy/oauth2/auth" + lib.optionalString (config.oauth2.allowedGroups != [ ]) "?allowed_groups=${lib.concatStringsSep "," config.oauth2.allowedGroups}";
setOauth2Headers = false;
bypassAuth = true;
extraConfig = ''
internal;
proxy_set_header X-Scheme $scheme;
# nginx auth_request includes headers but not body
proxy_set_header Content-Length "";
proxy_pass_request_body off;
'';
};
};
};
}
)
);
};
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
sops = {
secrets = {
"oauth2-cookie-secret" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-oauth2-proxy-client" = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
templates = {
"kanidm-oauth2-proxy-client-env" = {
content = ''
OAUTH2_PROXY_CLIENT_SECRET="${config.sops.placeholder.kanidm-oauth2-proxy-client}"
OAUTH2_PROXY_COOKIE_SECRET=${config.sops.placeholder.oauth2-cookie-secret}
'';
owner = serviceUser;
group = serviceGroup;
mode = "0440";
};
};
};
# needed for homeWebProxy
networking.firewall.allowedTCPPorts = [ servicePort ];
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services = {
${serviceName} = {
enable = true;
cookie = {
domain = ".${mainDomain}";
secure = true;
expire = "900m";
secret = null; # set by service EnvironmentFile
};
clientSecret = null; # set by service EnvironmentFile
reverseProxy = true;
httpAddress = "0.0.0.0:${builtins.toString servicePort}";
redirectURL = "https://${serviceDomain}/oauth2/callback";
setXauthrequest = true;
extraConfig = {
code-challenge-method = "S256";
whitelist-domain = ".${mainDomain}";
set-authorization-header = true;
pass-access-token = true;
skip-jwt-bearer-tokens = true;
upstream = "static://202";
oidc-issuer-url = "https://${kanidmDomain}/oauth2/openid/oauth2-proxy";
provider-display-name = "Kanidm";
};
provider = "oidc";
scope = "openid email";
loginURL = "https://${kanidmDomain}/ui/oauth2";
redeemURL = "https://${kanidmDomain}/oauth2/token";
validateURL = "https://${kanidmDomain}/oauth2/openid/oauth2-proxy/userinfo";
clientID = serviceName;
email.domains = [ "*" ];
};
};
systemd.services = {
${serviceName} = {
# after = [ "kanidm.service" ];
serviceConfig = {
RuntimeDirectory = serviceName;
RuntimeDirectoryMode = "0750";
UMask = "007"; # TODO remove once https://github.com/oauth2-proxy/oauth2-proxy/issues/2141 is fixed
RestartSec = "60"; # Retry every minute
EnvironmentFile = [
config.sops.templates.kanidm-oauth2-proxy-client-env.path
];
};
};
};
nodes =
let
extraConfig = ''
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
allow ${globals.networks.home-lan.vlans.services.cidrv4};
allow ${globals.networks.home-lan.vlans.services.cidrv6};
'';
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit servicePort serviceAddress serviceDomain serviceName extraConfig; };
${homeWebProxy}.services.nginx = confLib.genNginx { inherit servicePort serviceDomain serviceName; extraConfig = extraConfig + nginxAccessRules; serviceAddress = globals.hosts.${oauthServer}.wanAddress4; };
};
};
}
4.4.3.39. Firefly-III
{ self, lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "firefly-iii"; port = 80; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome dnsServer webProxy homeWebProxy homeServiceAddress nginxAccessRules;
nginxGroup = "nginx";
inherit (config.swarselsystems) sopsFile;
cfg = config.services.firefly-iii;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users = {
groups.${serviceGroup} = { };
users.${serviceUser} = {
group = lib.mkForce serviceGroup;
extraGroups = lib.mkIf cfg.enableNginx [ nginxGroup ];
isSystemUser = true;
};
};
sops = {
secrets = {
"firefly-iii-app-key" = { inherit sopsFile; owner = serviceUser; group = if cfg.enableNginx then nginxGroup else serviceGroup; mode = "0440"; };
};
};
topology.self.services.${serviceName} = {
name = "Firefly-III";
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
globals.services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
services = {
${serviceName} = {
enable = true;
user = serviceUser;
group = if cfg.enableNginx then nginxGroup else serviceGroup;
dataDir = "/Vault/data/${serviceName}";
settings = {
TZ = config.repo.secrets.common.location.timezone;
APP_URL = "https://${serviceDomain}";
APP_KEY_FILE = config.sops.secrets.firefly-iii-app-key.path;
APP_ENV = "local";
DB_CONNECTION = "sqlite";
TRUSTED_PROXIES = "**";
# turning these on breaks api access using the waterfly app
# AUTHENTICATION_GUARD = "remote_user_guard";
# AUTHENTICATION_GUARD_HEADER = "X-User";
# AUTHENTICATION_GUARD_EMAIL = "X-Email";
};
enableNginx = true;
virtualHost = serviceDomain;
};
nginx = {
virtualHosts = {
"${serviceDomain}" = {
locations = {
"/api" = {
setOauth2Headers = false;
extraConfig = ''
index index.php;
try_files $uri $uri/ /index.php?$query_string;
add_header Access-Control-Allow-Methods 'GET, POST, HEAD, OPTIONS';
'';
};
};
};
};
};
};
nodes =
let
genNginx = toAddress: extraConfig: {
upstreams = {
${serviceName} = {
servers = {
"${toAddress}:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
oauth2 = {
enable = true;
allowedGroups = [ "firefly_access" ];
};
inherit extraConfig;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
};
"/api" = {
proxyPass = "http://${serviceName}";
setOauth2Headers = false;
bypassAuth = true;
};
};
};
};
};
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = genNginx serviceAddress "";
${homeWebProxy}.services.nginx = genNginx homeServiceAddress nginxAccessRules;
};
};
}
4.4.3.40. Koillection
{ self, lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "koillection"; port = 2282; dir = "/Vault/data/koillection"; }) servicePort serviceName serviceUser serviceDir serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
serviceDB = "koillection";
postgresUser = config.systemd.services.postgresql.serviceConfig.User; # postgres
postgresPort = config.services.postgresql.settings.port; # 5432
containerRev = "sha256:96693e41a6eb2aae44f96033a090378270f024ddf4e6095edf8d57674f21095d";
inherit (config.swarselsystems) sopsFile;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
swarselmodules.server = {
podman = true;
postgresql = true;
};
sops.secrets = {
koillection-db-password = { inherit sopsFile; owner = postgresUser; group = postgresUser; mode = "0440"; };
koillection-env-file = { inherit sopsFile; };
};
topology.self.services.${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort postgresPort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort postgresPort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
virtualisation.oci-containers.containers = {
koillection = {
image = "koillection/koillection@${containerRev}";
ports = [
"${toString servicePort}:80"
];
volumes = [
"${serviceDir}/uploads:/uploads"
];
environment = {
APP_DEBUG = "0";
APP_ENV = "prod";
HTTPS_ENABLED = "1";
UPLOAD_MAX_FILESIZE = "512M";
PHP_MEMORY_LIMIT = "512M";
PHP_TZ = config.repo.secrets.common.location.timezone;
CORS_ALLOW_ORIGIN = "https?://(localhost|swag\\.swarsel\\.win)(:[0-9]+)?$";
DB_DRIVER = "pdo_pgsql";
DB_NAME = serviceDB;
DB_HOST = "host.docker.internal";
DB_USER = serviceUser;
# DB_PASSWORD set in koillection-env-file
DB_PORT = "${toString postgresPort}";
DB_VERSION = "16";
};
environmentFiles = [
config.sops.secrets.koillection-env-file.path
];
extraOptions = [
"--add-host=host.docker.internal:host-gateway" # podman
];
};
};
# networking.firewall.allowedTCPPorts = [ servicePort postgresPort ];
systemd.services.postgresql.postStart =
let
passwordPath = config.sops.secrets.koillection-db-password.path;
in
''
${config.services.postgresql.package}/bin/psql -tA <<'EOF'
DO $$
DECLARE password TEXT;
BEGIN
password := trim(both from replace(pg_read_file('${passwordPath}'), E'\n', '''));
EXECUTE format('ALTER ROLE ${serviceDB} WITH PASSWORD '''%s''';', password);
END $$;
EOF
'';
services = {
postgresql = {
enable = true;
enableTCPIP = true;
ensureDatabases = [ serviceDB ];
ensureUsers = [
{
name = serviceDB;
ensureDBOwnership = true;
}
];
authentication = ''
host ${serviceDB} ${serviceDB} 10.88.0.0/16 scram-sha-256
'';
};
};
nodes =
let
extraConfigLoc = ''
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
'';
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName extraConfigLoc; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName extraConfigLoc; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.41. Atuin
{ lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "atuin"; port = 8888; }) servicePort serviceName serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.${serviceName} = {
enable = true;
host = "0.0.0.0";
port = servicePort;
# openFirewall = true;
openRegistration = false;
};
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.42. Radicale
{ lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "radicale"; port = 8000; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
inherit (config.swarselsystems) sopsFile;
cfg = config.services.${serviceName};
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
sops = {
secrets.radicale-user = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
templates =
let
inherit (config.repo.secrets.local.radicale) user1;
in
{
"radicale-users" = {
content = ''
${user1}:${config.sops.placeholder.radicale-user}
'';
owner = serviceUser;
group = serviceGroup;
mode = "0440";
};
};
};
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.${serviceName} = {
enable = true;
settings = {
server = {
hosts = [
"0.0.0.0:${builtins.toString servicePort}"
"[::]:${builtins.toString servicePort}"
];
};
auth =
{
type = "htpasswd";
htpasswd_filename = config.sops.templates.radicale-users.path;
htpasswd_encryption = "autodetect";
};
storage = {
filesystem_folder = "/Vault/data/radicale/collections";
};
};
rights = {
# all: match authenticated users only
root = {
user = ".+";
collection = "";
permissions = "R";
};
principal = {
user = ".+";
collection = "{user}";
permissions = "RW";
};
calendars = {
user = ".+";
collection = "{user}/[^/]+";
permissions = "rw";
};
};
};
systemd.tmpfiles.settings."10-radicale" = {
"${cfg.settings.storage.filesystem_folder}" = {
d = {
group = serviceGroup;
user = serviceUser;
mode = "0750";
};
};
};
# networking.firewall.allowedTCPPorts = [ servicePort ];
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 16; maxBodyUnit = "M"; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 16; maxBodyUnit = "M"; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.43. croc
{ self, lib, config, pkgs, dns, globals, confLib, ... }:
let
inherit (confLib.gen { name = "croc"; proxy = config.node.name; }) serviceName serviceDomain proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome dnsServer;
servicePorts = [
9009
9010
9011
9012
9013
];
inherit (config.swarselsystems) sopsFile;
cfg = config.services.croc;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
nodes.${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
sops = {
secrets = {
croc-password = { inherit sopsFile; };
};
templates = {
"croc-env" = {
content = ''
CROC_PASS="${config.sops.placeholder.croc-password}"
'';
};
};
};
topology.self.services.${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
globals.services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome;
};
services.${serviceName} = {
enable = true;
ports = servicePorts;
pass = config.sops.secrets.croc-password.path;
openFirewall = true;
};
systemd.services = {
${serviceName} = {
serviceConfig = {
ExecStart = lib.mkForce "${pkgs.croc}/bin/croc ${lib.optionalString cfg.debug "--debug"} relay --ports ${
lib.concatMapStringsSep "," toString cfg.ports}";
EnvironmentFile = [
config.sops.templates.croc-env.path
];
};
};
};
# ports are opened on the firewall for croc, no nginx config
};
}
4.4.3.44. microbin
{ self, lib, config, dns, globals, confLib, ... }:
let
inherit (confLib.gen { name = "microbin"; port = 8777; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
inherit (config.swarselsystems) sopsFile;
cfg = config.services.${serviceName};
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
users = {
groups.${serviceGroup} = { };
users.${serviceUser} = {
isSystemUser = true;
group = serviceGroup;
};
};
sops = {
secrets = {
microbin-admin-username = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
microbin-admin-password = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
microbin-uploader-password = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
templates = {
"microbin-env" = {
content = ''
MICROBIN_ADMIN_USERNAME="${config.sops.placeholder.microbin-admin-username}"
MICROBIN_ADMIN_PASSWORD="${config.sops.placeholder.microbin-admin-password}"
MICROBIN_UPLOADER_PASSWORD="${config.sops.placeholder.microbin-uploader-password}"
'';
owner = serviceUser;
group = serviceGroup;
mode = "0440";
};
};
};
topology.self.services.${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.${serviceName} = {
enable = true;
passwordFile = config.sops.templates.microbin-env.path;
dataDir = "/var/lib/microbin";
settings = {
MICROBIN_HIDE_LOGO = true;
MICROBIN_PORT = servicePort;
MICROBIN_EDITABLE = true;
MICROBIN_HIDE_HEADER = true;
MICROBIN_HIDE_FOOTER = true;
MICROBIN_NO_LISTING = false;
MICROBIN_HIGHLIGHTSYNTAX = true;
MICROBIN_BIND = "0.0.0.0";
MICROBIN_PRIVATE = true;
MICROBIN_PUBLIC_PATH = "https://${serviceDomain}";
MICROBIN_READONLY = true;
MICROBIN_SHOW_READ_STATS = true;
MICROBIN_TITLE = "~SwarselScratch~";
MICROBIN_THREADS = 1;
MICROBIN_GC_DAYS = 30;
MICROBIN_ENABLE_BURN_AFTER = true;
MICROBIN_QR = true;
MICROBIN_ETERNAL_PASTA = true;
MICROBIN_ENABLE_READONLY = true;
MICROBIN_DEFAULT_EXPIRY = "1week";
MICROBIN_NO_FILE_UPLOAD = false;
MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB = 256;
MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB = 1024;
MICROBIN_DISABLE_UPDATE_CHECKING = true;
MICROBIN_DISABLE_TELEMETRY = true;
MICROBIN_LIST_SERVER = false;
};
};
systemd.services = {
${serviceName} = {
serviceConfig = {
DynamicUser = lib.mkForce false;
User = serviceUser;
Group = serviceGroup;
};
};
};
# networking.firewall.allowedTCPPorts = [ servicePort ];
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{ directory = cfg.dataDir; user = serviceUser; group = serviceGroup; mode = "0700"; }
];
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 1; maxBodyUnit = "G"; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 1; maxBodyUnit = "G"; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.45. shlink
{ self, lib, config, dns, globals, confLib, ... }:
let
inherit (confLib.gen { name = "shlink"; port = 8081; dir = "/var/lib/shlink"; }) servicePort serviceName serviceDomain serviceDir serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
containerRev = "sha256:1a697baca56ab8821783e0ce53eb4fb22e51bb66749ec50581adc0cb6d031d7a";
inherit (config.swarselsystems) sopsFile;
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
swarselmodules.server = {
podman = true;
};
sops = {
secrets = {
shlink-api = { inherit sopsFile; };
};
templates = {
"shlink-env" = {
content = ''
INITIAL_API_KEY=${config.sops.placeholder.shlink-api}
'';
};
};
};
virtualisation.oci-containers.containers.${serviceName} = {
image = "shlinkio/shlink@${containerRev}";
environment = {
"DEFAULT_DOMAIN" = serviceDomain;
"PORT" = "${builtins.toString servicePort}";
"USE_HTTPS" = "false";
"DEFAULT_SHORT_CODES_LENGTH" = "4";
"WEB_WORKER_NUM" = "1";
"TASK_WORKER_NUM" = "1";
};
environmentFiles = [
config.sops.templates.shlink-env.path
];
ports = [ "${builtins.toString servicePort}:${builtins.toString servicePort}" ];
volumes = [
"${serviceDir}/data:/etc/shlink/data"
];
};
systemd.tmpfiles.settings."11-shlink" = builtins.listToAttrs (
map
(path: {
name = "${serviceDir}/${path}";
value = {
d = {
group = "root";
user = "1001";
mode = "0750";
};
};
}) [
"data"
"data/cache"
"data/locks"
"data/log"
"data/proxies"
]
);
# networking.firewall.allowedTCPPorts = [ servicePort ];
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{ directory = serviceDir; }
{ directory = "/var/lib/containers"; }
];
topology.self.services.${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.46. slink
Deployment notes:
- enable user:
podman exec -it slink slink user:activate --email=<mail> - make user admin:
podman exec -it slink slink user:grant:role --email=<mail> ROLE_ADMIN - finally, disable new user registration in web ui
{ self, lib, config, dns, globals, confLib, ... }:
let
inherit (confLib.gen { name = "slink"; port = 3000; dir = "/var/lib/slink"; }) servicePort serviceName serviceDomain serviceDir serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
containerRev = "sha256:98b9442696f0a8cbc92f0447f54fa4bad227af5dcfd6680545fedab2ed28ddd9";
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
swarselmodules.server = {
podman = true;
};
virtualisation.oci-containers.containers.${serviceName} = {
image = "anirdev/slink@${containerRev}";
environment = {
"ORIGIN" = "https://${serviceDomain}";
"TZ" = config.repo.secrets.common.location.timezone;
"STORAGE_PROVIDER" = "local";
"IMAGE_MAX_SIZE" = "50M";
"USER_APPROVAL_REQUIRED" = "true";
};
ports = [ "${builtins.toString servicePort}:${builtins.toString servicePort}" ];
volumes = [
"${serviceDir}/var/data:/app/var/data"
"${serviceDir}/images:/app/slink/images"
];
};
systemd.tmpfiles.settings."12-slink" = builtins.listToAttrs (
map
(path: {
name = "${serviceDir}/${path}";
value = {
d = {
group = "root";
user = "root";
mode = "0750";
};
};
}) [
"var/data"
"images"
]
);
# networking.firewall.allowedTCPPorts = [ servicePort ];
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{ directory = serviceDir; }
];
topology.self.services.${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/shlink.png";
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
nodes =
let
genNginx = toAddress: extraConfig: {
upstreams = {
${serviceName} = {
servers = {
"${toAddress}:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
oauth2 = {
enable = true;
allowedGroups = [ "slink_access" ];
};
inherit extraConfig;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
};
"/image" = {
proxyPass = "http://${serviceName}";
setOauth2Headers = false;
bypassAuth = true;
};
};
};
};
};
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = genNginx serviceAddress "";
${homeWebProxy}.services.nginx = lib.mkIf isHome (genNginx homeServiceAddress nginxAccessRules);
};
};
}
4.4.3.47. Snipe-IT (currently unused)
{ lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "snipeit"; port = 80; }) servicePort serviceName serviceUser serviceGroup serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy webProxyIf homeProxyIf dnsServer homeServiceAddress nginxAccessRules;
# sopsFile = config.node.secretsDir + "/secrets2.yaml";
inherit (config.swarselsystems) sopsFile;
serviceDB = "snipeit";
mysqlPort = 3306;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
sops = {
secrets = {
snipe-it-appkey = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
};
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.snipe-it = {
enable = true;
appKeyFile = config.sops.secrets.snipe-it-appkey.path;
appURL = "https://${serviceDomain}";
hostName = serviceDomain;
user = serviceUser;
group = serviceGroup;
dataDir = "/Vault/data/snipeit";
database = {
user = serviceUser;
port = mysqlPort;
name = serviceDB;
host = "localhost";
createLocally = true;
};
};
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.48. Homebox
{ self, lib, pkgs, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "homebox"; port = 7745; }) servicePort serviceName serviceDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
topology.self.services.${serviceName} = {
name = "Homebox";
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.${serviceName} = {
enable = true;
package = pkgs.dev.homebox;
database.createLocally = true;
settings = {
HBOX_WEB_PORT = builtins.toString servicePort;
HBOX_OPTIONS_ALLOW_REGISTRATION = "false";
HBOX_STORAGE_CONN_STRING = "file:///Vault/data/homebox";
HBOX_STORAGE_PREFIX_PATH = ".data";
};
};
# networking.firewall.allowedTCPPorts = [ servicePort ];
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.49. OPKSSH
{ lib, config, globals, confLib, ... }:
let
inherit (confLib.gen { name = "opkssh"; user = "opksshuser"; group = "opksshuser"; }) serviceName serviceUser serviceGroup;
kanidmDomain = globals.services.kanidm.domain;
inherit (config.swarselsystems) mainUser;
inherit (config.repo.secrets.local) persons;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
services.${serviceName} = {
enable = true;
user = serviceUser;
group = serviceGroup;
providers = {
kanidm = {
lifetime = "oidc";
issuer = "https://${kanidmDomain}/oauth2/openid/${serviceName}";
clientId = serviceName;
};
};
authorizations = [
{
user = mainUser;
principal = builtins.head persons.${mainUser}.mailAddresses;
inherit (config.services.opkssh.providers.kanidm) issuer;
}
];
};
};
}
4.4.3.50. Garage
Garage acts as my s3 endpoint. I use it on two of my servers:
- Winters (Server: ASRock J4105-ITX): General s3 storage
- Belchsfactory (OCI): s3 storage for nix binary cache (used by Attic (nix binary cache))
Generate the admin token using openssl rand -base64 32.
Generate the rpc token using openssl rand -hex 32.
If a website is to be deployed using a s3 bucket, add the corresponding files in one of two ways:
either 1) use vhost addressing: aws s3 cp <local file> s3://<path to file; no bucket identifier needed> --endpoint-url https://<bucket>.<garage domain> --region swarsel
or 2) use classic path addressing aws s3 cp <local file> s3://<bucket>/<path to file> --endpoint-url https://<garage domain> --region swarsel
# inspired by https://github.com/atropos112/nixos/blob/7fef652006a1c939f4caf9c8a0cb0892d9cdfe21/modules/garage.nix
{ self, lib, pkgs, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "garage"; port = 3900; domain = config.repo.secrets.common.services.domains."garage-${config.node.name}"; }) servicePort serviceName specificServiceName serviceDomain subDomain baseDomain serviceAddress proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
cfg = lib.recursiveUpdate config.services.${serviceName} config.swarselsystems.server.${serviceName};
inherit (config.swarselsystems) sopsFile mainUser;
# needs SSD
metadata_dir = "/var/lib/garage/meta";
# metadata_dir = if config.swarselsystems.isCloud then "/var/lib/garage/meta" else "/Vault/data/garage/meta";
garageRpcPort = 3901;
garageWebPort = 3902;
garageAdminPort = 3903;
garageK2VPort = 3904;
adminDomain = "${subDomain}-admin.${baseDomain}";
webDomain = "${subDomain}-web.${baseDomain}";
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
swarselsystems.server.${serviceName} = {
data_dir = {
path = lib.mkOption {
type = lib.types.str;
description = "Directory where Garage stores its metadata";
};
capacity = lib.mkOption {
type = lib.types.str;
};
};
buckets = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of buckets to create";
};
keys = lib.mkOption {
type = lib.types.attrsOf (lib.types.listOf lib.types.str);
default = { };
description = "Keys and their associated buckets. Each key gets full access (read/write/owner) to its listed buckets.";
example = {
my_key_name = [ "bucket1" "bucket2" ];
my_other_key = [ "bucket2" "bucket3" ];
};
};
};
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
assertions = [
{
assertion = config.swarselsystems.server.${serviceName}.buckets != [ ];
message = "If Garage is enabled, at least one bucket must be specified in swarselsystems.server.${serviceName}.buckets";
}
{
assertion = builtins.length (lib.attrsToList config.swarselsystems.server.${serviceName}.keys) > 0;
message = "If Garage is enabled, at least one key must be specified in swarselsystems.server.${serviceName}.keys";
}
{
assertion =
let
allKeyBuckets = lib.flatten (lib.attrValues config.swarselsystems.server.${serviceName}.keys);
invalidBuckets = builtins.filter (bucket: !(lib.elem bucket config.swarselsystems.server.${serviceName}.buckets)) allKeyBuckets;
in
invalidBuckets == [ ];
message = "All buckets referenced in keys must exist in the buckets list";
}
];
# networking.firewall.allowedTCPPorts = [ servicePort 3901 3902 3903 3904 ];
topology.self.services.${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
sops = {
secrets.garage-admin-token = { inherit sopsFile; };
secrets.garage-rpc-secret = { inherit sopsFile; };
};
# DynamicUser cannot read above secrets
systemd.services.${serviceName}.serviceConfig = {
DynamicUser = false;
ProtectHome = lib.mkForce false;
};
environment = {
persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{ directory = "/var/lib/garage"; }
(lib.mkIf config.swarselsystems.isCloud { directory = config.swarselsystems.server.${serviceName}.data_dir.path; })
];
systemPackages = [
cfg.package
];
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort 3901 3902 3903 3904 ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort 3901 3902 3903 3904 ];
};
};
services.${specificServiceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
services.${serviceName} = {
enable = true;
package = pkgs.garage_2;
settings = {
data_dir = [ config.swarselsystems.server.${serviceName}.data_dir ];
inherit metadata_dir;
db_engine = "lmdb";
block_size = "128M";
use_local_tz = false;
disable_scrub = true;
replication_factor = 1;
compression_level = "none";
rpc_bind_addr = "[::]:${builtins.toString garageRpcPort}";
# we are not joining our nodes, just use the private ipv4
rpc_public_addr = "${globals.networks.${config.swarselsystems.server.netConfigName}.hosts.${config.node.name}.ipv4}:${builtins.toString garageRpcPort}";
rpc_secret_file = config.sops.secrets.garage-rpc-secret.path;
s3_api = {
s3_region = mainUser;
api_bind_addr = "[::]:${builtins.toString servicePort}";
root_domain = ".${serviceDomain}";
};
s3_web = {
bind_addr = "[::]:${builtins.toString garageWebPort}";
root_domain = ".${config.repo.secrets.common.services.domains."garage-web-${config.node.name}"}";
add_host_to_metrics = true;
};
admin = {
api_bind_addr = "[::]:${builtins.toString garageAdminPort}";
admin_token_file = config.sops.secrets.garage-admin-token.path;
};
k2v_api = {
api_bind_addr = "[::]:${builtins.toString garageK2VPort}";
};
};
};
systemd.services = {
garage-buckets = {
description = "Create Garage buckets";
after = [ "garage.service" ];
wants = [ "garage.service" ];
wantedBy = [ "multi-user.target" ];
path = [ cfg.package pkgs.gawk pkgs.coreutils ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "root";
Group = "root";
};
script = ''
garage status
# Checking repeatedly with garage status until getting 0 exit code
while ! garage status >/dev/null 2>&1; do
echo "Garage not yet operational, waiting..."
echo "Current garage status output:"
garage status 2>&1 || true
echo "---"
sleep 5
done
# Now we check if garage status shows any failed nodes by checking for ==== FAILED NODES ====
while garage status | grep -q "==== FAILED NODES ===="; do
echo "Garage has failed nodes, waiting..."
echo "Current garage status output:"
garage status 2>&1 || true
echo "---"
sleep 5
done
echo "Garage is operational, proceeding with bucket management."
# Get list of existing buckets
existing_buckets=$(garage bucket list | tail -n +2 | awk '{print $3}' | grep -v '^$' || true)
# Create buckets that should exist
${lib.concatMapStringsSep "\n" (bucket: ''
if [[ "$(garage bucket info ${lib.escapeShellArg bucket} 2>&1 >/dev/null)" == *"Bucket not found"* ]]; then
echo "Creating bucket ${lib.escapeShellArg bucket}"
garage bucket create ${lib.escapeShellArg bucket}
else
echo "Bucket ${lib.escapeShellArg bucket} already exists"
fi
'')
cfg.buckets}
# Remove buckets that shouldn't exist
for bucket in $existing_buckets; do
should_exist=false
${lib.concatMapStringsSep "\n" (bucket: ''
if [[ "$bucket" == ${lib.escapeShellArg bucket} ]]; then
should_exist=true
fi
'')
cfg.buckets}
if [[ "$should_exist" == "false" ]]; then
echo "Removing bucket $bucket"
garage bucket delete --yes "$bucket"
fi
done
'';
};
garage-keys = {
description = "Create Garage keys and set permissions";
after = [ "garage-buckets.service" ];
wants = [ "garage-buckets.service" ];
requires = [ "garage-buckets.service" ];
wantedBy = [ "multi-user.target" ];
path = [ cfg.package pkgs.gawk pkgs.coreutils ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "root";
Group = "root";
};
script = ''
garage key list
echo "Managing keys..."
# Get list of existing keys
existing_keys=$(garage key list | tail -n +2 | awk '{print $3}' | grep -v '^$' || true)
# Create keys that should exist
${lib.concatStringsSep "\n" (lib.mapAttrsToList (keyName: _: ''
if [[ "$(garage key info ${lib.escapeShellArg keyName} 2>&1)" == *"0 matching keys"* ]]; then
echo "Creating key ${lib.escapeShellArg keyName}"
garage key create ${lib.escapeShellArg keyName}
else
echo "Key ${lib.escapeShellArg keyName} already exists"
fi
'')
cfg.keys)}
# Set up key permissions for buckets
${lib.concatStringsSep "\n" (lib.mapAttrsToList (
keyName: buckets:
lib.concatMapStringsSep "\n" (bucket: ''
echo "Granting full access to key ${lib.escapeShellArg keyName} for bucket ${lib.escapeShellArg bucket}"
garage bucket allow --read --write --owner --key ${lib.escapeShellArg keyName} ${lib.escapeShellArg bucket}
'')
buckets
)
cfg.keys)}
# Remove permissions from buckets that are no longer associated with keys
${lib.concatStringsSep "\n" (lib.mapAttrsToList (keyName: buckets: ''
# Get current buckets this key has access to
current_buckets=$(garage key info ${lib.escapeShellArg keyName} | grep -A 1000 "==== BUCKETS FOR THIS KEY ====" | tail -n +3 | awk '{print $3}' | grep -v '^$' || true)
# Remove access from buckets not in the desired list
for current_bucket in $current_buckets; do
should_have_access=false
${lib.concatMapStringsSep "\n" (bucket: ''
if [[ "$current_bucket" == ${lib.escapeShellArg bucket} ]]; then
should_have_access=true
fi
'')
buckets}
if [[ "$should_have_access" == "false" ]]; then
echo "Removing access for key ${lib.escapeShellArg keyName} from bucket $current_bucket"
garage bucket deny --key ${lib.escapeShellArg keyName} $current_bucket
fi
done
'')
cfg.keys)}
# Remove keys that shouldn't exist
for key in $existing_keys; do
should_exist=false
${lib.concatStringsSep "\n" (lib.mapAttrsToList (keyName: _: ''
if [[ "$key" == ${lib.escapeShellArg keyName} ]]; then
should_exist=true
fi
'')
cfg.keys)}
if [[ "$should_exist" == "false" ]]; then
echo "Removing key $key"
garage key delete --yes "$key"
fi
done
'';
};
};
nodes =
let
genNginx = toAddress: extraConfig: {
upstreams = {
${serviceName} = {
servers = {
"${toAddress}:${builtins.toString servicePort}" = { };
};
};
"${serviceName}Web" = {
servers = {
"${toAddress}:${builtins.toString garageWebPort}" = { };
};
};
"${serviceName}Admin" = {
servers = {
"${toAddress}:${builtins.toString garageAdminPort}" = { };
};
};
};
virtualHosts = {
"${adminDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
oauth2.enable = false;
inherit extraConfig;
locations = {
"/" = {
proxyPass = "http://${serviceName}Admin";
};
};
};
"*.${webDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
oauth2.enable = false;
inherit extraConfig;
locations = {
"/" = {
proxyPass = "http://${serviceName}Web";
};
};
};
"${serviceDomain}" = {
serverAliases = [ "*.${serviceDomain}" ];
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
oauth2.enable = false;
inherit extraConfig;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 0;
client_body_timeout 600s;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
proxy_request_buffering off;
'';
};
};
};
};
};
in
{
${dnsServer}.swarselsystems.server.dns.${baseDomain}.subdomainRecords = {
"${subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
"${subDomain}-admin" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
"${subDomain}-web" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
"*.${subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
"*.${subDomain}-web" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = genNginx serviceAddress "";
${homeWebProxy}.services.nginx = lib.mkIf isHome (genNginx homeServiceAddress nginxAccessRules);
};
};
}
4.4.3.51. Set host domain for dns
{ lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "dns-hostrecord"; proxy = config.node.name; }) serviceName proxyAddress4 proxyAddress6;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf (config.swarselmodules.server.${serviceName} && config.swarselsystems.isCloud) {
nodes.stoicclub.swarselsystems.server.dns.${globals.domains.main}.subdomainRecords = {
"server.${config.node.name}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
};
}
4.4.3.52. Set dns host entries for home servers
{ lib, config, globals, confLib, ... }:
let
inherit (confLib.gen { name = "dns-home"; }) serviceName;
inherit (confLib.static) homeProxy;
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
networking.hosts = {
${globals.networks.home-lan.vlans.services.hosts.${homeProxy}.ipv4} = [ "server.${homeProxy}.${globals.domains.main}" ];
${globals.networks.home-lan.vlans.services.hosts.${homeProxy}.ipv6} = [ "server.${homeProxy}.${globals.domains.main}" ];
};
};
}
4.4.3.53. nsd (dns)
{ self, lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "nsd"; port = 53; }) serviceName servicePort proxyAddress4 proxyAddress6;
inherit (config.swarselsystems) sopsFile;
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
swarselsystems.server.dns = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
subdomainRecords = lib.mkOption {
type = lib.types.attrsOf dns.lib.types.subzone;
default = { };
};
};
}
);
};
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
sops.secrets = {
tsig-key = { inherit sopsFile; };
};
# services.resolved.enable = false;
networking = {
# nameservers = [ "1.1.1.1" "8.8.8.8" ];
firewall = {
allowedUDPPorts = [ servicePort ];
allowedTCPPorts = [ servicePort ];
};
};
topology.self.services.${serviceName} = {
name = lib.toUpper serviceName;
icon = "${self}/files/topology-images/${serviceName}.png";
};
services.nsd = {
enable = true;
keys = {
"${globals.domains.main}.${proxyAddress4}" = {
algorithm = "hmac-sha256";
keyFile = config.sops.secrets.tsig-key.path;
};
"${globals.domains.main}.${proxyAddress6}" = {
algorithm = "hmac-sha256";
keyFile = config.sops.secrets.tsig-key.path;
};
"${globals.domains.main}" = {
algorithm = "hmac-sha256";
keyFile = config.sops.secrets.tsig-key.path;
};
};
interfaces = [
"10.1.2.157"
"2603:c020:801f:a0cc::9d"
];
zones = {
"${globals.domains.main}" =
let
keyName4 = "${globals.domains.main}.${proxyAddress4}";
keyName6 = "${globals.domains.main}.${proxyAddress6}";
keyName = "${globals.domains.main}";
transferList = [
"213.239.242.238 ${keyName4}"
"2a01:4f8:0:a101::a:1 ${keyName6}"
"213.133.100.103 ${keyName4}"
"2a01:4f8:0:1::5ddc:2 ${keyName6}"
"193.47.99.3 ${keyName4}"
"2001:67c:192c::add:a3 ${keyName6}"
];
in
{
outgoingInterface = "2603:c020:801f:a0cc::9d";
notify = transferList ++ [
"216.218.130.2 ${keyName}"
];
provideXFR = transferList ++ [
"216.218.133.2 ${keyName}"
"2001:470:600::2 ${keyName}"
];
# dnssec = true;
data = dns.lib.toString "${globals.domains.main}" (import ./site1.nix { inherit config globals dns proxyAddress4 proxyAddress6; });
};
};
};
};
}
4.4.3.53.1. nsd (dns) - site1
{ config, globals, dns, proxyAddress4, proxyAddress6, ... }:
with dns.lib.combinators; {
SOA = {
nameServer = "soa";
adminEmail = "admin@${globals.domains.main}"; # this option is not parsed as domain (we cannot just write "admin")
serial = 2026010201; # update this on changes for secondary dns
};
useOrigin = false;
NS = [
"soa"
"srv"
] ++ globals.domains.externalDns;
CAA = [
{
issuerCritical = false;
tag = "issue";
value = "letsencrypt.org";
}
{
issuerCritical = false;
tag = "issuewild";
value = "letsencrypt.org";
}
{
issuerCritical = false;
tag = "iodef";
value = "mailto:${config.repo.secrets.common.dnsMail}";
}
];
A = [ config.repo.secrets.local.dns.homepage-ip ];
SRV = [
{
service = "_matrix";
proto = "_tcp";
port = 443;
target = "${globals.services.matrix.subDomain}";
priority = 10;
weight = 5;
}
{
service = "_submissions";
proto = "_tcp";
port = 465;
target = "${globals.services.mailserver.subDomain}";
priority = 5;
weight = 0;
ttl = 3600;
}
{
service = "_submission";
proto = "_tcp";
port = 587;
target = "${globals.services.mailserver.subDomain}";
priority = 5;
weight = 0;
ttl = 3600;
}
{
service = "_imap";
proto = "_tcp";
port = 143;
target = "${globals.services.mailserver.subDomain}";
priority = 5;
weight = 0;
ttl = 3600;
}
{
service = "_imaps";
proto = "_tcp";
port = 993;
target = "${globals.services.mailserver.subDomain}";
priority = 5;
weight = 0;
ttl = 3600;
}
];
MX = [
{
preference = 10;
exchange = "${globals.services.mailserver.subDomain}";
}
];
DKIM = [
{
selector = "mail";
k = "rsa";
p = config.repo.secrets.local.dns.mailserver.dkim-public;
ttl = 10800;
}
];
TXT = [
(with spf; strict [ "a:${globals.services.mailserver.subDomain}.${globals.domains.main}" ])
"google-site-verification=${config.repo.secrets.local.dns.google-site-verification}"
];
DMARC = [
{
p = "none";
ttl = 10800;
}
];
subdomains = config.swarselsystems.server.dns.${globals.domains.main}.subdomainRecords // {
"www".CNAME = [ "${globals.domains.main}." ];
"_acme-challenge".CNAME = [ "${config.repo.secrets.local.dns.acme-challenge-domain}." ];
"soa" = host proxyAddress4 proxyAddress6;
"srv" = host proxyAddress4 proxyAddress6;
};
}
4.4.3.54. Minecraft
{ self, lib, config, pkgs, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "minecraft"; port = 25565; dir = "/opt/minecraft"; proxy = config.node.name; }) serviceName servicePort serviceDir serviceDomain proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome dnsServer;
inherit (config.swarselsystems) mainUser;
worldName = "${mainUser}craft";
in
{
options.swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselmodules.server.${serviceName} {
nodes.${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
topology.self.services.${serviceName} = {
name = "Minecraft";
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
globals.services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome;
};
networking.firewall.allowedTCPPorts = [ servicePort ];
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{ directory = serviceDir; mode = "0755"; }
];
systemd.services.minecraft-swarselcraft = {
description = "Minecraft Server";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
User = "root";
WorkingDirectory = "${serviceDir}/${worldName}";
ExecStart = "${lib.getExe pkgs.temurin-jre-bin-17} @user_jvm_args.txt @libraries/net/minecraftforge/forge/1.20.1-47.2.20/unix_args.txt nogui";
Restart = "always";
RestartSec = 30;
StandardInput = "null";
};
wantedBy = [ "multi-user.target" ];
};
};
}
4.4.3.55. Mailserver
When changing the hashed passwords, dovecot needs to be restarted manually, it will not happen upon rebuild.
{ self, lib, config, globals, dns, confLib, ... }:
let
inherit (config.swarselsystems) sopsFile;
inherit (confLib.gen { name = "mailserver"; dir = "/var/lib/dovecot"; user = "virtualMail"; group = "virtualMail"; port = 443; }) serviceName serviceDir servicePort serviceUser serviceGroup serviceAddress serviceDomain proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome webProxy homeWebProxy dnsServer homeServiceAddress nginxAccessRules;
inherit (config.repo.secrets.local.mailserver) user1 alias1_1 alias1_2 alias1_3 alias1_4 user2 alias2_1 alias2_2 alias2_3 user3;
baseDomain = globals.domains.main;
roundcubeDomain = config.repo.secrets.common.services.domains.roundcube;
endpointAddress4 = globals.hosts.${config.node.name}.wanAddress4 or null;
endpointAddress6 = globals.hosts.${config.node.name}.wanAddress6 or null;
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
globals.services = {
${serviceName} = {
domain = serviceDomain;
proxyAddress4 = endpointAddress4;
proxyAddress6 = endpointAddress6;
};
roundcube = {
domain = roundcubeDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
topology.self.services = lib.listToAttrs (map
(service:
lib.nameValuePair "${service}" {
name = lib.swarselsystems.toCapitalized service;
info = lib.mkIf (service == "postfix" || service == "roundcube") (if service == "postfix" then "https://${serviceDomain}" else "https://${roundcubeDomain}");
icon = "${self}/files/topology-images/${service}.png";
}
)
[ "postfix" "dovecot" "rspamd" "clamav" "roundcube" ]);
sops.secrets = {
user1-hashed-pw = { inherit sopsFile; owner = serviceUser; };
user2-hashed-pw = { inherit sopsFile; owner = serviceUser; };
user3-hashed-pw = { inherit sopsFile; owner = serviceUser; };
};
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{ directory = "/var/vmail"; user = serviceUser; group = serviceGroup; mode = "0770"; }
{ directory = "/var/sieve"; user = serviceUser; group = serviceGroup; mode = "0770"; }
{ directory = "/var/dkim"; user = "rspamd"; group = "rspamd"; mode = "0700"; }
{ directory = serviceDir; user = serviceUser; group = serviceGroup; mode = "0700"; }
# { directory = "/var/lib/postgresql"; user = "postgres"; group = "postgres"; mode = "0750"; }
{ directory = "/var/lib/rspamd"; user = "rspamd"; group = "rspamd"; mode = "0700"; }
{ directory = "/var/lib/roundcube"; user = "roundcube"; group = "roundcube"; mode = "0700"; }
{ directory = "/var/lib/redis-rspamd"; user = "redis-rspamd"; group = "redis-rspamd"; mode = "0700"; }
{ directory = "/var/lib/postfix"; user = "root"; group = "root"; mode = "0755"; }
{ directory = "/var/lib/knot-resolver"; user = "knot-resolver"; group = "knot-resolver"; mode = "0770"; }
];
mailserver = {
enable = true;
stateVersion = 3;
fqdn = serviceDomain;
domains = [ baseDomain ];
indexDir = "${serviceDir}/indices";
openFirewall = true;
certificateScheme = "acme";
dmarcReporting.enable = true;
enableSubmission = true;
enableSubmissionSsl = true;
enableImapSsl = true;
loginAccounts = {
"${user1}@${baseDomain}" = {
hashedPasswordFile = config.sops.secrets.user1-hashed-pw.path;
aliases = [
"${alias1_1}@${baseDomain}"
"${alias1_2}@${baseDomain}"
"${alias1_3}@${baseDomain}"
"${alias1_4}@${baseDomain}"
];
};
"${user2}@${baseDomain}" = {
hashedPasswordFile = config.sops.secrets.user2-hashed-pw.path;
aliases = [
"${alias2_1}@${baseDomain}"
"${alias2_2}@${baseDomain}"
"${alias2_3}@${baseDomain}"
];
sendOnly = true;
};
"${user3}@${baseDomain}" = {
hashedPasswordFile = config.sops.secrets.user3-hashed-pw.path;
aliases = [
"@${baseDomain}"
];
catchAll = [
baseDomain
];
};
};
};
services.roundcube = {
enable = true;
# this is the url of the vhost, not necessarily the same as the fqdn of
# the mailserver
hostName = roundcubeDomain;
extraConfig = ''
$config['imap_host'] = "ssl://${config.mailserver.fqdn}";
$config['smtp_host'] = "ssl://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
'';
configureNginx = true;
};
# the rest of the ports are managed by snm
networking.firewall.allowedTCPPorts = [ 80 servicePort ];
services.nginx = {
virtualHosts = {
"${roundcubeDomain}" = {
useACMEHost = globals.domains.main;
enableACME = false;
forceSSL = true;
acmeRoot = null;
locations = {
"/".recommendedSecurityHeaders = false;
"~ ^/(SQL|bin|config|logs|temp|vendor)/".recommendedSecurityHeaders = false;
"~ ^/(CHANGELOG.md|INSTALL|LICENSE|README.md|SECURITY.md|UPGRADING|composer.json|composer.lock)".recommendedSecurityHeaders = false;
"~* \\.php(/|$)".recommendedSecurityHeaders = false;
};
};
};
};
nodes =
let
extraConfigLoc = ''
proxy_ssl_server_name on;
proxy_ssl_name ${roundcubeDomain};
'';
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host endpointAddress4 endpointAddress6;
"${globals.services.roundcube.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceName extraConfigLoc; serviceDomain = roundcubeDomain; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceName extraConfigLoc; serviceDomain = roundcubeDomain; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.56. Attic (nix binary cache)
Generate the attic server token using openssl genrsa -traditional 4096 | base64 -w0
$ attic login local http://localhost:8080 eyJ… ✍️ Configuring server "local"
$ attic cache create hello ✨ Created cache "hello" on "local"
{ lib, config, pkgs, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "attic"; port = 8091; }) serviceName serviceDir servicePort serviceAddress serviceDomain proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
inherit (config.swarselsystems) mainUser isPublic sopsFile;
serviceDB = "atticd";
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
topology.self.services.${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
info = "https://${serviceDomain}";
# attic does not have a logo
};
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
sops = lib.mkIf (!isPublic) {
secrets = {
attic-server-token = { inherit sopsFile; };
attic-garage-access-key = { inherit sopsFile; };
attic-garage-secret-key = { inherit sopsFile; };
};
templates = {
"attic.env" = {
content = ''
ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=${config.sops.placeholder.attic-server-token}
AWS_ACCESS_KEY_ID=${config.sops.placeholder.attic-garage-access-key}
AWS_SECRET_ACCESS_KEY=${config.sops.placeholder.attic-garage-secret-key}
'';
};
};
};
# networking.firewall.allowedTCPPorts = [ servicePort ];
services.atticd = {
enable = true;
# NOTE: remove once https://github.com/zhaofengli/attic/pull/268 is merged
package = pkgs.attic-server.overrideAttrs
(oldAttrs: {
patches = (oldAttrs.patches or [ ]) ++ [
(pkgs.writeText "remove-s3-checksums.patch" ''
diff --git a/server/src/storage/s3.rs b/server/src/storage/s3.rs
index 1d5719f3..036f3263 100644
--- a/server/src/storage/s3.rs
+++ b/server/src/storage/s3.rs
@@ -278,10 +278,6 @@ impl StorageBackend for S3Backend {
CompletedPart::builder()
.set_e_tag(part.e_tag().map(str::to_string))
.set_part_number(Some(part_number as i32))
- .set_checksum_crc32(part.checksum_crc32().map(str::to_string))
- .set_checksum_crc32_c(part.checksum_crc32_c().map(str::to_string))
- .set_checksum_sha1(part.checksum_sha1().map(str::to_string))
- .set_checksum_sha256(part.checksum_sha256().map(str::to_string))
.build()
})
.collect::<Vec<_>>();
'')
];
});
environmentFile = config.sops.templates."attic.env".path;
settings = {
listen = "[::]:${builtins.toString servicePort}";
api-endpoint = "https://${serviceDomain}/";
allowed-hosts = [
serviceDomain
];
require-proof-of-possession = false;
compression = {
type = "zstd";
level = 3;
};
database.url = "postgresql:///atticd?host=/run/postgresql";
storage =
if config.swarselmodules.server.garage then {
type = "s3";
region = mainUser;
bucket = serviceName;
# attic must be patched to never serve pre-signed s3 urls directly
# otherwise it will redirect clients to this localhost endpoint
endpoint = "http://127.0.0.1:3900"; # garage port
} else {
type = "local";
path = serviceDir;
};
garbage-collection = {
interval = "1 day";
default-retention-period = "3 months";
};
chunking = {
nar-size-threshold = if config.swarselmodules.server.garage then 0 else 64 * 1024; # garage using s3
min-size = 16 * 1024;
avg-size = 64 * 1024;
max-size = 256 * 1024;
};
};
};
services.postgresql = {
enable = true;
enableTCPIP = true;
ensureDatabases = [ serviceDB ];
ensureUsers = [
{
name = serviceDB;
ensureDBOwnership = true;
}
];
};
systemd.services.atticd = lib.mkIf config.swarselmodules.server.garage {
requires = [ "garage.service" ];
after = [ "garage.service" ];
};
nodes =
let
extraConfigLoc = ''
client_body_timeout 600s;
proxy_connect_timeout 600s;
proxy_send_timeout 600s;
proxy_read_timeout 600s;
proxy_request_buffering off;
'';
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName extraConfigLoc; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName extraConfigLoc; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.57. Hydra
Need to create user manually:
$ hydra-create-user alice –full-name 'Alice Q. User' \ –email-address 'alice@example.org' –password-prompt –role admin
{ inputs, lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "hydra"; port = 8002; }) serviceName servicePort serviceUser serviceGroup serviceAddress serviceDomain proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied webProxy homeWebProxy dnsServer homeProxyIf webProxyIf homeServiceAddress nginxAccessRules;
inherit (config.swarselsystems) sopsFile;
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
sops = {
secrets = {
nixbuild-net-key = { mode = "0600"; };
hydra-pw = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
templates = {
"hydra-env" = {
content = ''
HYDRA_PW="${config.sops.placeholder.hydra-pw}"
'';
owner = serviceUser;
group = serviceGroup;
mode = "0440";
};
};
};
services.hydra = {
enable = true;
package = inputs.hydra.packages.${config.node.arch}.hydra;
port = servicePort;
hydraURL = "https://${serviceDomain}";
listenHost = "*";
notificationSender = "hydra@${globals.domains.main}";
minimumDiskFreeEvaluator = 20; # 20G
minimumDiskFree = 20; # 20G
useSubstitutes = true;
smtpHost = globals.services.mailserver.domain;
buildMachinesFiles = [
"/etc/nix/machines"
];
extraConfig = ''
using_frontend_proxy 1
'';
};
systemd.services.hydra-user-setup = {
description = "Create admin user for Hydra";
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = "hydra";
EnvironmentFile = [
config.sops.templates.hydra-env.path
];
};
wantedBy = [ "multi-user.target" ];
requires = [ "hydra-init.service" ];
after = [ "hydra-init.service" ];
environment = lib.mkForce config.systemd.services.hydra-init.environment;
script = ''
set -eu
if [ ! -e ~hydra/.user-setup-done ]; then
/run/current-system/sw/bin/hydra-create-user admin --full-name 'admin' --email-address 'admin@${globals.domains.main}' --password "$HYDRA_PW" --role admin
touch ~hydra/.user-setup-done
fi
'';
};
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
];
nix = {
settings.builders-use-substitutes = true;
distributedBuilds = true;
buildMachines = [
{
hostName = "localhost";
protocol = null;
system = config.node.arch;
supportedFeatures = [ "kvm" "nixos-test" "big-parallel" "benchmark" ];
maxJobs = 4;
}
];
};
# networking.firewall.allowedTCPPorts = [ servicePort ];
programs.ssh = {
extraConfig = ''
StrictHostKeyChecking no
'';
};
nodes =
let
extraConfigLoc = ''
proxy_set_header X-Request-Base /hydra;
'';
in
{
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName extraConfigLoc; maxBody = 0; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName extraConfigLoc; maxBody = 0; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.3.58. Kea DHCP
This is the dhcp config that runs on my router.
{ self, lib, config, globals, confLib, ... }:
let
inherit (confLib.gen { name = "kea"; dir = "/var/lib/private/kea"; }) serviceName serviceDir;
inherit (confLib.static) homeDnsServer;
dhcpX = intX:
let
x = builtins.toString intX;
in
{
enable = true;
settings = {
reservations-out-of-pool = true;
lease-database = {
name = "/var/lib/kea/dhcp${x}.leases";
persist = true;
type = "memfile";
};
valid-lifetime = 86400;
renew-timer = 3600;
interfaces-config = {
interfaces = map (name: "me-${name}") (builtins.attrNames globals.networks.home-lan.vlans);
service-sockets-max-retries = -1;
};
"subnet${x}" = lib.flip lib.mapAttrsToList globals.networks.home-lan.vlans (
vlanName: vlanCfg: {
inherit (vlanCfg) id;
interface = "me-${vlanName}";
subnet = vlanCfg."cidrv${x}";
rapid-commit = lib.mkIf (intX == 6) true;
pools = [
{
pool = "${lib.net.cidr.host 100 vlanCfg."cidrv${x}"} - ${lib.net.cidr.host (-6) vlanCfg."cidrv${x}"}";
}
];
pd-pools = lib.mkIf (intX == 6) [
{
prefix = builtins.replaceStrings [ "::" ] [ ":0:0:100::" ] (lib.head (lib.splitString "/" vlanCfg.cidrv6));
prefix-len = 56;
delegated-len = 64;
}
];
option-data =
lib.optional (intX == 4)
{
name = "routers";
data = vlanCfg.hosts.hintbooth."ipv${x}";
}
# Advertise DNS server for VLANS that have internet access
++
lib.optional
(lib.elem vlanName globals.general.internetVLANs)
{
name = if (intX == 4) then "domain-name-servers" else "dns-servers";
data = globals.networks.home-lan.vlans.services.hosts.${homeDnsServer}."ipv${x}";
};
reservations = lib.concatLists (
lib.forEach (builtins.attrValues vlanCfg.hosts) (
hostCfg:
lib.optional (hostCfg.mac != null) {
hw-address = lib.mkIf (intX == 4) hostCfg.mac;
duid = lib.mkIf (intX == 6) "00:03:00:01:${hostCfg.mac}"; # 00:03 = duid type 3; 00:01 = ethernet
ip-address = lib.mkIf (intX == 4) hostCfg."ipv${x}";
ip-addresses = lib.mkIf (intX == 6) [ hostCfg."ipv${x}" ];
prefixes = lib.mkIf (intX == 6) [
"${builtins.replaceStrings ["::"] [":0:0:${builtins.toString (256 + hostCfg.id)}::"] (lib.head (lib.splitString "/" vlanCfg.cidrv6))}/64"
];
}
)
);
}
);
};
};
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{ directory = serviceDir; mode = "0700"; }
];
topology = {
extractors.kea.enable = false;
self.services.${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
icon = "${self}/files/topology-images/${serviceName}.png";
};
};
services.kea = {
dhcp4 = dhcpX 4;
dhcp6 = dhcpX 6;
};
};
}
4.4.3.59. nftables (firewall)
This is the dhcp config that runs on my router.
{ lib, config, confLib, ... }:
let
inherit (confLib.gen { name = "nftables"; }) serviceName;
in
{
options = {
swarselmodules.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
};
config = lib.mkIf config.swarselmodules.${serviceName} {
networking.nftables = {
stopRuleset = lib.mkDefault ''
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state invalid drop
ct state {established, related} accept
iifname lo accept
meta l4proto ipv6-icmp accept
meta l4proto icmp accept
ip protocol igmp accept
tcp dport ${toString (lib.head config.services.openssh.ports)} accept
}
chain forward {
type filter hook forward priority filter; policy drop;
}
chain output {
type filter hook output priority filter; policy accept;
}
}
'';
firewall = {
enable = true;
localZoneName = "local";
snippets = {
nnf-common.enable = false;
nnf-conntrack.enable = true;
nnf-drop.enable = true;
nnf-loopback.enable = true;
nnf-ssh.enable = true;
nnf-dhcpv6.enable = true;
};
rules.untrusted-to-local = {
from = [ "untrusted" ];
to = [ "local" ];
inherit (config.networking.firewall)
allowedTCPPorts
allowedTCPPortRanges
allowedUDPPorts
allowedUDPPortRanges
;
};
rules.icmp-and-igmp = {
after = [
"ct"
"ssh"
];
from = "all";
to = [ "local" ];
extraLines = [
"meta l4proto ipv6-icmp accept"
"meta l4proto icmp accept"
"ip protocol igmp accept"
];
};
};
};
};
}
4.4.3.60. Firezone
Firezone provides VPN access to my services at home - this works separately from the Wireguard mesh that I run between my server nodes.
This has some state:
- OIDC setup:
Settings > Identity Providers > Kanidm > Edit:- Note the UUIDs in the URLs under "Ensure the OAuth application has the following redirect URLs whitelisted:":
- the first one is
accountId, the second is theexternalId, update them in the Kanidm config
- the first one is
- Note the UUIDs in the URLs under "Ensure the OAuth application has the following redirect URLs whitelisted:":
- Relay setup:
Relays > Relays > Deploy:- note FIREZONETOKEN and add it as the firezone-relay-token secret
- Gateway setup:
Sites > Home > Deploy Gateway:- note FIREZONETOKEN and add it as the firezone-gateway-token secret
- After initial deployment, setup the user account under
Actors > Add Actor:- use OIDC login
{ self, lib, pkgs, config, globals, confLib, dns, nodes, ... }:
let
inherit (confLib.gen { name = "firezone"; dir = "/var/lib/private/firezone"; }) serviceName serviceDir serviceAddress serviceDomain proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied homeProxy webProxy homeWebProxy homeProxyIf webProxyIf idmServer dnsServer homeServiceAddress nginxAccessRules;
inherit (config.swarselsystems) sopsFile;
apiPort = 8081;
webPort = 8080;
relayPort = 3478;
domainPort = 9003;
homeServices = lib.attrNames (lib.filterAttrs (_: serviceCfg: serviceCfg.isHome) globals.services);
homeDomains = map (name: globals.services.${name}.domain) homeServices;
allow = group: resource: {
"${group}@${resource}" = {
inherit group resource;
description = "Allow ${group} access to ${resource}";
};
};
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy} = {
allowedTCPPorts = [ apiPort webPort domainPort ];
allowedUDPPorts = [ relayPort ];
allowedUDPPortRanges = [
{
from = config.services.firezone.relay.lowestPort;
to = config.services.firezone.relay.highestPort;
}
];
};
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy} = {
allowedTCPPorts = [ apiPort webPort domainPort ];
allowedUDPPorts = [ relayPort ];
allowedUDPPortRanges = [
{
from = config.services.firezone.relay.lowestPort;
to = config.services.firezone.relay.highestPort;
}
];
};
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
topology.self.services.${serviceName} = {
name = lib.swarselsystems.toCapitalized serviceName;
info = "https://${serviceDomain}";
icon = "${self}/files/topology-images/${serviceName}.png";
};
sops = {
secrets = {
kanidm-firezone-client = { inherit sopsFile; mode = "0400"; };
firezone-relay-token = { inherit sopsFile; mode = "0400"; };
firezone-smtp-password = { inherit sopsFile; mode = "0440"; };
firezone-adapter-config = { inherit sopsFile; mode = "0440"; };
};
};
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{ directory = serviceDir; mode = "0700"; }
];
services.firezone = {
server = {
enable = true;
enableLocalDB = true;
smtp = {
inherit (config.repo.secrets.local.firezone.mail) from username;
host = globals.services.mailserver.domain;
port = 465;
implicitTls = true;
passwordFile = config.sops.secrets.firezone-smtp-password.path;
};
provision = {
enable = true;
accounts.main = {
name = "Home";
relayGroups.relays.name = "Relays";
gatewayGroups.home.name = "Home";
actors.admin = {
type = "account_admin_user";
name = "Admin";
email = "admin@${globals.domains.main}";
};
groups.anyone = {
name = "anyone";
members = [
"admin"
];
};
auth.oidc =
let
client_id = "firezone";
in
{
name = "Kanidm";
adapter = "openid_connect";
adapter_config = {
scope = "openid email profile";
response_type = "code";
inherit client_id;
discovery_document_uri = "https://${globals.services.kanidm.domain}/oauth2/openid/${client_id}/.well-known/openid-configuration";
clientSecretFile = config.sops.secrets.kanidm-firezone-client.path;
};
};
resources =
lib.genAttrs homeDomains
(domain: {
type = "dns";
name = domain;
address = domain;
gatewayGroups = [ "home" ];
filters = [
{ protocol = "icmp"; }
{
protocol = "tcp";
ports = [
443
80
];
}
{
protocol = "udp";
ports = [ 443 ];
}
];
})
// {
"home.vlan-services.v4" = {
type = "cidr";
name = "home.vlan-services.v4";
address = globals.networks.home-lan.vlans.services.cidrv4;
gatewayGroups = [ "home" ];
};
"home.vlan-services.v6" = {
type = "cidr";
name = "home.vlan-services.v6";
address = globals.networks.home-lan.vlans.services.cidrv6;
gatewayGroups = [ "home" ];
};
};
policies =
{ }
// allow "everyone" "home.vlan-services.v4"
// allow "anyone" "home.vlan-services.v4"
// allow "everyone" "home.vlan-services.v6"
// allow "anyone" "home.vlan-services.v6"
// lib.mergeAttrsList (map (domain: allow "everyone" domain) homeDomains)
// lib.mergeAttrsList (map (domain: allow "anyone" domain) homeDomains);
};
};
domain = {
settings.ERLANG_DISTRIBUTION_PORT = domainPort;
package = pkgs.dev.firezone-server-domain;
};
api = {
externalUrl = "https://${serviceDomain}/api/";
address = "0.0.0.0";
port = apiPort;
package = pkgs.dev.firezone-server-api;
};
web = {
externalUrl = "https://${serviceDomain}/";
address = "0.0.0.0";
port = webPort;
package = pkgs.dev.firezone-server-web;
};
};
relay = {
enable = true;
port = relayPort;
inherit (config.node) name;
apiUrl = "wss://${serviceDomain}/api/";
tokenFile = config.sops.secrets.firezone-relay-token.path;
publicIpv4 = proxyAddress4;
publicIpv6 = proxyAddress6;
openFirewall = lib.mkIf (!isProxied) true;
package = pkgs.dev.firezone-relay;
};
};
# systemd.services.firezone-initialize =
# let
# generateSecrets =
# let
# requiredSecrets = lib.filterAttrs (_: v: v == null) cfg.settingsSecret;
# in
# ''
# mkdir -p secrets
# chmod 700 secrets
# ''
# + lib.concatLines (
# lib.forEach (builtins.attrNames requiredSecrets) (secret: ''
# if [[ ! -e secrets/${secret} ]]; then
# echo "Generating ${secret}"
# # Some secrets like TOKENS_KEY_BASE require a value >=64 bytes.
# head -c 64 /dev/urandom | base64 -w 0 > secrets/${secret}
# chmod 600 secrets/${secret}
# fi
# '')
# );
# loadSecretEnvironment =
# component:
# let
# relevantSecrets = lib.subtractLists (builtins.attrNames cfg.${component}.settings) (
# builtins.attrNames cfg.settingsSecret
# );
# in
# lib.concatLines (
# lib.forEach relevantSecrets (
# secret:
# ''export ${secret}=$(< ${
# if cfg.settingsSecret.${secret} == null then
# "secrets/${secret}"
# else
# "\"$CREDENTIALS_DIRECTORY/${secret}\""
# })''
# )
# );
# in
# {
# script = lib.mkForce ''
# mkdir -p "$TZDATA_DIR"
# # Generate and load secrets
# ${generateSecrets}
# ${loadSecretEnvironment "domain"}
# echo "Running migrations"
# ${lib.getExe cfg.domain.package} eval "Domain.Release.migrate(manual: true)"
# '';
# };
nodes =
let
genNginx = toAddress: extraConfig: {
upstreams = {
${serviceName} = {
servers."${toAddress}:${builtins.toString webPort}" = { };
};
"${serviceName}-api" = {
servers."${toAddress}:${builtins.toString apiPort}" = { };
};
};
virtualHosts = {
${serviceDomain} = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
inherit extraConfig;
locations = {
"/" = {
# The trailing slash is important to strip the location prefix from the request
proxyPass = "http://${serviceName}/";
proxyWebsockets = true;
};
"/api/" = {
# The trailing slash is important to strip the location prefix from the request
proxyPass = "http://${serviceName}-api/";
proxyWebsockets = true;
};
};
};
};
};
in
{
${homeProxy} =
let
nodeCfg = nodes.${homeProxy}.config;
nodePkgs = nodes.${homeProxy}.pkgs;
in
{
sops.secrets.firezone-gateway-token = { inherit (nodeCfg.swarselsystems) sopsFile; mode = "0400"; };
networking.nftables = {
firewall = {
zones.firezone.interfaces = [ "tun-firezone" ];
rules = {
# masquerade firezone traffic
masquerade-firezone = {
from = [ "firezone" ];
to = [ "vlan-services" ];
# masquerade = true; NOTE: custom rule below for ip4 + ip6
late = true; # Only accept after any rejects have been processed
verdict = "accept";
};
# forward firezone traffic
forward-incoming-firezone-traffic = {
from = [ "firezone" ];
to = [ "vlan-services" ];
verdict = "accept";
};
# FIXME: is this needed? conntrack should take care of it and we want to masquerade anyway
forward-outgoing-firezone-traffic = {
from = [ "vlan-services" ];
to = [ "firezone" ];
verdict = "accept";
};
};
};
chains.postrouting = {
masquerade-firezone = {
after = [ "hook" ];
late = true;
rules =
lib.forEach
[
"firezone"
]
(
zone:
lib.concatStringsSep " " [
"meta protocol { ip, ip6 }"
(lib.head nodeCfg.networking.nftables.firewall.zones.${zone}.ingressExpression)
(lib.head nodeCfg.networking.nftables.firewall.zones.vlan-services.egressExpression)
"masquerade random"
]
);
};
};
};
boot.kernel.sysctl = {
"net.core.wmem_max" = 16777216;
"net.core.rmem_max" = 134217728;
};
services.firezone.gateway = {
enable = true;
# logLevel = "trace";
inherit (nodeCfg.node) name;
apiUrl = "wss://${globals.services.firezone.domain}/api/";
tokenFile = nodeCfg.sops.secrets.firezone-gateway-token.path;
package = nodePkgs.stable25_05.firezone-gateway; # newer versions of firezone-gateway are not compatible with server package
};
topology.self.services."${serviceName}-gateway" = {
name = lib.swarselsystems.toCapitalized "${serviceName} Gateway";
icon = "${self}/files/topology-images/${serviceName}.png";
};
};
${idmServer} =
let
nodeCfg = nodes.${idmServer}.config;
accountId = "6b3c6ba7-5240-4684-95ce-f40fdae45096";
externalId = "08d714e9-1ab9-4133-a39d-00e843a960cc";
in
{
sops.secrets.kanidm-firezone = { inherit (nodeCfg.swarselsystems) sopsFile; owner = "kanidm"; group = "kanidm"; mode = "0440"; };
services.kanidm.provision = {
groups."firezone.access" = { };
systems.oauth2.firezone = {
displayName = "Firezone VPN";
# NOTE: state: both uuids are runtime values
originUrl = [
"https://${globals.services.firezone.domain}/${accountId}/sign_in/providers/${externalId}/handle_callback"
"https://${globals.services.firezone.domain}/${accountId}/settings/identity_providers/openid_connect/${externalId}/handle_callback"
];
originLanding = "https://${globals.services.firezone.domain}/";
basicSecretFile = nodeCfg.sops.secrets.kanidm-firezone.path;
preferShortUsername = true;
scopeMaps."firezone.access" = [
"openid"
"email"
"profile"
];
};
};
};
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = genNginx serviceAddress "";
${homeWebProxy}.services.nginx = lib.mkIf isHome (genNginx homeServiceAddress nginxAccessRules);
};
};
}
4.4.3.61. Adguardhome
{ lib, config, globals, dns, confLib, ... }:
let
inherit (confLib.gen { name = "adguardhome"; port = 3000; }) serviceName servicePort serviceAddress serviceDomain proxyAddress4 proxyAddress6;
inherit (confLib.static) isHome isProxied homeProxyIf webProxy webProxyIf homeWebProxy dnsServer homeDnsServer homeServiceAddress nginxAccessRules;
homeServices = lib.attrNames (lib.filterAttrs (_: serviceCfg: serviceCfg.isHome) globals.services);
homeDomains = map (name: globals.services.${name}.domain) homeServices;
in
{
options = {
swarselmodules.server.${serviceName} = lib.mkEnableOption "enable ${serviceName} on server";
};
config = lib.mkIf config.swarselmodules.server.${serviceName} {
globals = {
networks = {
${webProxyIf}.hosts = lib.mkIf isProxied {
${config.node.name}.firewallRuleForNode.${webProxy}.allowedTCPPorts = [ servicePort ];
};
${homeProxyIf}.hosts = lib.mkIf isHome {
${config.node.name}.firewallRuleForNode.${homeWebProxy}.allowedTCPPorts = [ servicePort ];
};
};
services.${serviceName} = {
domain = serviceDomain;
inherit proxyAddress4 proxyAddress6 isHome serviceAddress;
homeServiceAddress = lib.mkIf isHome homeServiceAddress;
};
};
networking.firewall = {
allowedTCPPorts = [ 53 ];
allowedUDPPorts = [ 53 ];
};
services.adguardhome = {
enable = true;
mutableSettings = false;
host = "0.0.0.0";
port = servicePort;
settings = {
dns = {
bind_hosts = [
globals.networks.home-lan.vlans.services.hosts.${homeDnsServer}.ipv4
globals.networks.home-lan.vlans.services.hosts.${homeDnsServer}.ipv6
];
ratelimit = 300;
upstream_dns = [
"https://dns.cloudflare.com/dns-query"
"https://dns.google/dns-query"
"https://doh.mullvad.net/dns-query"
];
bootstrap_dns = [
"1.1.1.1"
"2606:4700:4700::1111"
"8.8.8.8"
"2001:4860:4860::8844"
];
dhcp.enabled = false;
};
filtering.rewrites = map
(domain: {
inherit domain;
# FIXME: change to homeWebProxy once that is setup
answer = globals.networks.home-lan.vlans.services.hosts.${homeWebProxy}.ipv4;
# answer = globals.hosts.${webProxy}.wanAddress4;
enabled = true;
})
homeDomains;
filters = [
{
name = "AdGuard DNS filter";
url = "https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt";
enabled = true;
}
{
name = "AdAway Default Blocklist";
url = "https://adaway.org/hosts.txt";
enabled = true;
}
{
name = "OISD (Big)";
url = "https://big.oisd.nl";
enabled = true;
}
];
user_rules = config.repo.secrets.local.adguardUserRules;
};
};
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{
directory = "/var/lib/private/AdGuardHome";
mode = "0700";
}
];
nodes = {
${dnsServer}.swarselsystems.server.dns.${globals.services.${serviceName}.baseDomain}.subdomainRecords = {
"${globals.services.${serviceName}.subDomain}" = dns.lib.combinators.host proxyAddress4 proxyAddress6;
};
${webProxy}.services.nginx = confLib.genNginx { inherit serviceAddress servicePort serviceDomain serviceName; proxyWebsockets = true; oauth2 = true; oauth2Groups = [ "adguardhome_access" ]; };
${homeWebProxy}.services.nginx = lib.mkIf isHome (confLib.genNginx { inherit servicePort serviceDomain serviceName; proxyWebsockets = true; oauth2 = true; oauth2Groups = [ "adguardhome_access" ]; extraConfig = nginxAccessRules; serviceAddress = homeServiceAddress; });
};
};
}
4.4.4. Darwin
This section is to be used for darwin modules, in case I can ever be bothered to actually write them.
4.4.4.1. Imports
This section sets up all the imports that are used in the home-manager section.
{ self, lib, config, outputs, globals, withHomeManager, ... }:
let
macUser = globals.user.work;
in
{
imports = [
];
options.swarselmodules.optional.darwin = lib.mkEnableOption "optional darwin settings";
config = lib.mkIf config.swarselmodules.optional.darwin
{
nix.settings.experimental-features = "nix-command flakes";
nixpkgs = {
hostPlatform = "x86_64-darwin";
overlays = [ outputs.overlays.default ];
config = {
allowUnfree = true;
};
};
system.stateVersion = 4;
} // lib.optionalAttrs withHomeManager {
home-manager.users."${macUser}".imports = [
"${self}/modules/home/darwin"
];
};
}
4.4.5. TODO Optional
These sets of configuration do not need to be deployed on every host, for a multitude of reasons.
- The gaming set is not needed on weak machines, and also not on my work machine.
- The VirtualBox package takes forever to build, and I do not need virtual machines on every host.
- There are some hosts that I do not want to autologin to.
nswitch-rcmis a tool I wrote for easy payload flashing of a Nintendo Switch in RCM mode. However, that is not needed on every machine.- The work profile is only used on my work laptop.
TODO: evaluate whether I should keep using this structure.
# @ future me: dont panic, this file is not read in by readNix
{ lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/nixos/optional";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/nixos/optional";
}
4.4.5.1. Niri
Auto login for the initial session.
{ inputs, lib, config, pkgs, ... }:
let
moduleName = "niri";
in
{
imports = [
inputs.niri-flake.nixosModules.niri
];
options.swarselmodules.${moduleName} = lib.mkEnableOption "${moduleName} settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
environment.systemPackages = with pkgs; [
wl-clipboard
wayland-utils
libsecret
cage
gamescope
xwayland-satellite-unstable
];
programs.niri = {
enable = true;
package = pkgs.niri-unstable; # the actual niri that will be installed and used
};
} // {
niri-flake.cache.enable = true;
programs.niri = {
package = null;
};
};
}
4.4.5.2. gaming
This opens a few gaming ports and installs the steam configuration suite for gaming. There are more options in Gaming (home-manager side).
{ self, lib, pkgs, config, withHomeManager, ... }:
{
config = {
programs.steam = {
enable = true;
package = pkgs.steam;
extraCompatPackages = [
pkgs.proton-ge-bin
];
};
# specialisation = {
# gaming.configuration = {
# networking = {
# firewall.enable = lib.mkForce false;
# firewall = {
# allowedUDPPorts = [ 4380 27036 14242 34197 ]; # 34197: factorio; 4380 27036 14242: barotrauma;
# allowedTCPPorts = [ ]; # 34197: factorio; 4380 27036 14242: barotrauma; 51820: wireguard
# allowedTCPPortRanges = [
# { from = 27015; to = 27030; } # barotrauma
# { from = 27036; to = 27037; } # barotrauma
# ];
# allowedUDPPortRanges = [
# { from = 27000; to = 27031; } # barotrauma
# { from = 58962; to = 58964; } # barotrauma
# ];
# };
# };
# hardware.xone.enable = true;
# environment.systemPackages = [
# pkgs.linuxKernel.packages.linux_6_12.xone
# ];
# };
# };
} // lib.optionalAttrs withHomeManager {
home-manager.users."${config.swarselsystems.mainUser}" = {
imports = [
"${self}/modules/home/optional/gaming.nix"
];
};
};
}
4.4.5.3. VirtualBox
This sets the VirtualBox configuration. Guest should not be enabled if not direly needed, it will make rebuilds unbearably slow. I only use this privately to run an old editor that does not run well under wine, so I put it into it's own specialisation.
{ lib, config, pkgs, ... }:
{
config = {
# specialisation = {
# VBox.configuration = {
virtualisation.virtualbox = {
host = {
enable = true;
enableKvm = true;
addNetworkInterface = lib.mkIf config.virtualisation.virtualbox.host.enableKvm false;
package = pkgs.stable.virtualbox;
enableExtensionPack = true;
};
# leaving this here for future notice. setting guest.enable = true will make 'restarting sysinit-reactivation.target' take till timeout on nixos-rebuild switch
guest = {
enable = false;
};
};
# run an older kernel to provide compatibility with windows vm
# boot = {
# kernelPackages = lib.mkForce pkgs.stable24_05.linuxPackages;
# # kernelParams = [
# # "amd_iommu=on"
# # ];
# };
# fixes the issue of running together with QEMU
# NOTE: once you start a QEMU VM (use kvm) VirtualBox will fail to start VMs
# boot.kernelParams = [ "kvm.enable_virt_at_load=0" ];
# };
# };
};
}
4.4.5.4. VmWare
This sets the VirtualBox configuration. Guest should not be enabled if not direly needed, it will make rebuilds unbearably slow.
_:
{
config = {
virtualisation.vmware.host.enable = true;
virtualisation.vmware.guest.enable = true;
};
}
4.4.5.5. nswitch-rcm
This smashes Atmosphere 1.3.2 on the switch, which is what I am currenty using.
{ pkgs, ... }:
{
config = {
services.nswitch-rcm = {
enable = true;
package = pkgs.fetchurl {
url = "https://github.com/Atmosphere-NX/Atmosphere/releases/download/1.3.2/fusee.bin";
hash = "sha256-5AXzNsny45SPLIrvWJA9/JlOCal5l6Y++Cm+RtlJppI=";
};
};
};
}
4.4.5.6. Framework
This holds configuration that is specific to framework laptops.
{ self, lib, config, withHomeManager, ... }:
{
config = {
services = {
fwupd = {
enable = true;
# framework also uses lvfs-testing, but I do not want to use it
extraRemotes = [ "lvfs" ];
};
udev.extraRules = ''
# disable Wakeup on Framework Laptop 16 Keyboard (ANSI)
ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0012", ATTR{power/wakeup}="disabled"
# disable Wakeup on Framework Laptop 16 Numpad Module
ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0014", ATTR{power/wakeup}="disabled"
# disable Wakeup on Framework Laptop 16 Trackpad
ACTION=="add", SUBSYSTEM=="i2c", DRIVERS=="i2c_hid_acpi", ATTRS{name}=="PIXA3854:00", ATTR{power/wakeup}="disabled"
'';
};
hardware.fw-fanctrl = {
enable = true;
config = {
defaultStrategy = "lazy";
};
};
} // lib.optionalAttrs withHomeManager {
home-manager.users."${config.swarselsystems.mainUser}" = {
imports = [
"${self}/modules/home/optional/framework.nix"
];
};
};
}
4.4.5.7. AMD CPU
_:
{
config = {
hardware = {
cpu.amd.updateMicrocode = true;
};
};
}
4.4.5.8. AMD GPU
_:
{
config = {
hardware = {
amdgpu = {
opencl.enable = true;
initrd.enable = true;
# amdvlk = {
# enable = true;
# support32Bit.enable = true;
# };
};
};
};
}
4.4.5.9. Hibernation
{ lib, config, ... }:
{
options.swarselsystems = {
hibernation = {
offset = lib.mkOption {
type = lib.types.int;
default = 0;
};
resumeDevice = lib.mkOption {
type = lib.types.str;
default = "/dev/disk/by-label/nixos";
};
};
};
config = {
boot = {
kernelParams = [
"resume_offset=${builtins.toString config.swarselsystems.hibernation.offset}"
# "mem_sleep_default=deep"
];
inherit (config.swarselsystems.hibernation) resumeDevice;
};
systemd.services."systemd-suspend-then-hibernate".aliases = [ "systemd-suspend.service" ];
powerManagement.enable = true;
systemd.sleep.extraConfig = ''
HibernateDelaySec=120m
SuspendState=freeze
'';
};
}
4.4.5.10. work
Options that I need specifically at work. There are more options at Work (home-manager side).
When setting up a new machine:
- setup the work VPN: - using the laptop certificate `.pem` as User cert and private key (CA cert: none) - vpn gateway is found in `nixosConfig.repo.secrets.local.work.vpnGateway`
{ self, lib, pkgs, config, withHomeManager, ... }:
let
inherit (config.swarselsystems) mainUser homeDir;
iwd = config.networking.networkmanager.wifi.backend == "iwd";
owner = mainUser;
sopsFile = self + /secrets/work/secrets.yaml;
in
{
options.swarselsystems = {
hostName = lib.mkOption {
type = lib.types.str;
default = config.node.name;
};
fqdn = lib.mkOption {
type = lib.types.str;
default = "";
};
};
config = {
sops =
let
secretNames = [
"vcuser"
"vcpw"
"govcuser"
"govcpw"
"govcurl"
"govcdc"
"govcds"
"govchost"
"govcnetwork"
"govcpool"
"baseuser"
"basepw"
];
in
{
secrets = builtins.listToAttrs (
map
(name: {
inherit name;
value = { inherit owner sopsFile; };
})
secretNames
);
templates = {
"network-manager-work.env".content = ''
BASEUSER=${config.sops.placeholder.baseuser}
BASEPASS=${config.sops.placeholder.basepw}
'';
};
};
boot.initrd = {
systemd.enable = lib.mkForce true; # make sure we are using initrd systemd even when not using Impermanence
luks = {
# disable "support" since we use systemd-cryptenroll
# make sure yubikeys are enrolled using
# sudo systemd-cryptenroll --fido2-device=auto --fido2-with-user-verification=no --fido2-with-user-presence=true --fido2-with-client-pin=no /dev/nvme0n1p2
yubikeySupport = false;
fido2Support = false;
};
};
programs = {
browserpass.enable = true;
_1password.enable = true;
_1password-gui = {
enable = true;
package = pkgs._1password-gui-beta;
polkitPolicyOwners = [ "${mainUser}" ];
};
};
networking = {
inherit (config.swarselsystems) hostName fqdn;
networkmanager = {
wifi.scanRandMacAddress = false;
ensureProfiles = {
environmentFiles = [
"${config.sops.templates."network-manager-work.env".path}"
];
profiles = {
VBC = {
"802-1x" = {
eap = if (!iwd) then "ttls;" else "peap;";
identity = "$BASEUSER";
password = "$BASEPASS";
phase2-auth = "mschapv2";
};
connection = {
id = "VBC";
type = "wifi";
autoconnect-priority = "500";
uuid = "3988f10e-6451-381f-9330-a12e66f45051";
secondaries = "48d09de4-0521-47d7-9bd5-43f97e23ff82"; # vpn uuid
};
ipv4 = { method = "auto"; };
ipv6 = {
# addr-gen-mode = "default";
addr-gen-mode = "stable-privacy";
method = "auto";
};
proxy = { };
wifi = {
cloned-mac-address = "permanent";
mac-address = "E8:65:38:52:63:FF";
mac-address-randomization = "1";
mode = "infrastructure";
band = "a";
ssid = "VBC";
};
wifi-security = {
# auth-alg = "open";
key-mgmt = "wpa-eap";
};
};
};
};
};
firewall = {
enable = lib.mkDefault true;
trustedInterfaces = [ "virbr0" ];
};
search = [
"vbc.ac.at"
"clip.vbc.ac.at"
"imp.univie.ac.at"
];
};
virtualisation = {
docker.enable = lib.mkIf (!config.virtualisation.podman.dockerCompat) true;
spiceUSBRedirection.enable = true;
libvirtd = {
enable = true;
qemu = {
package = pkgs.qemu_kvm;
runAsRoot = true;
swtpm.enable = true;
vhostUserPackages = with pkgs; [ virtiofsd ];
# ovmf = {
# enable = true;
# packages = [
# (pkgs.OVMFFull.override {
# secureBoot = true;
# tpmSupport = true;
# }).fd
# ];
# };
};
};
};
environment.systemPackages = with pkgs; [
remmina
# gp-onsaml-gui
stable24_11.python39
qemu
packer
gnumake
libisoburn
govc
terraform
opentofu
# dev.terragrunt
terragrunt
graphviz
azure-cli
# vm
virt-manager
virt-viewer
virtiofsd
spice
spice-gtk
spice-protocol
virtio-win
win-spice
powershell
gh
];
services = {
spice-vdagentd.enable = true;
openssh = {
enable = true;
extraConfig = ''
'';
};
syncthing = {
settings = {
"winters" = {
id = "O7RWDMD-AEAHPP7-7TAVLKZ-BSWNBTU-2VA44MS-EYGUNBB-SLHKB3C-ZSLMOAA";
};
"moonside@oracle" = {
id = "VPCDZB6-MGVGQZD-Q6DIZW3-IZJRJTO-TCC3QUQ-2BNTL7P-AKE7FBO-N55UNQE";
};
folders = {
"Documents" = {
path = "${homeDir}/Documents";
devices = [ "moonside@oracle" ];
id = "hgr3d-pfu3w";
};
};
};
};
# ACTION=="remove", ENV{PRODUCT}=="3/1050/407/110", RUN+="${pkgs.kanshi}/bin/kanshictl switch laptoponly"
udev.extraRules = ''
# lock screen when yubikey removed
ACTION=="remove", ENV{PRODUCT}=="3/1050/407/110", RUN+="${pkgs.systemd}/bin/systemctl suspend"
'';
};
# cgroups v1 is required for centos7 dockers
# specialisation = {
# cgroup_v1.configuration = {
# boot.kernelParams = [
# "SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1"
# "systemd.unified_cgroup_hierarchy=0"
# ];
# };
# };
} // lib.optionalAttrs withHomeManager {
home-manager.users."${config.swarselsystems.mainUser}" = {
imports = [
"${self}/modules/home/optional/work.nix"
];
};
};
}
4.4.5.11. Uni
{ self, config, withHomeManager, ... }:
{
config = {} // lib.optionalAttrs withHomeManager {
home-manager.users."${config.swarselsystems.mainUser}" = {
imports = [
"${self}/modules/home/optional/work.nix"
];
};
};
}
4.4.5.12. microvm-host
Some standard options that should be set for every microvm host.
{ config, lib, ... }:
{
config = lib.mkIf (config.guests != { }) {
systemd.tmpfiles.settings."15-microvms" = builtins.listToAttrs (
map
(path: {
name = "${lib.optionalString config.swarselsystems.isImpermanence "/persist"}/microvms/${path}";
value = {
d = {
group = "kvm";
user = "microvm";
mode = "0750";
};
};
}) (builtins.attrNames config.guests)
);
};
}
4.4.5.13. microvm-guest
Some standard options that should be set for every microvm guest. We set the default
{ self, lib, config, inputs, microVMParent, nodes, ... }:
{
imports = [
inputs.disko.nixosModules.disko
inputs.home-manager.nixosModules.home-manager
inputs.impermanence.nixosModules.impermanence
inputs.lanzaboote.nixosModules.lanzaboote
inputs.microvm.nixosModules.microvm
inputs.nix-index-database.nixosModules.nix-index
inputs.nix-minecraft.nixosModules.minecraft-servers
inputs.nix-topology.nixosModules.default
inputs.nswitch-rcm-nix.nixosModules.nswitch-rcm
inputs.simple-nixos-mailserver.nixosModules.default
inputs.sops.nixosModules.sops
inputs.stylix.nixosModules.stylix
inputs.swarsel-nix.nixosModules.default
inputs.nixos-nftables-firewall.nixosModules.default
(inputs.nixos-extra-modules + "/modules/interface-naming.nix")
"${self}/modules/shared/meta.nix"
];
config = {
_module.args.dns = inputs.dns;
nix.settings.experimental-features = [
"nix-command"
"flakes"
];
systemd.services."systemd-networkd".environment.SYSTEMD_LOG_LEVEL = "debug";
# NOTE: this is needed, we dont import sevrer network module for microvms
globals.hosts.${config.node.name}.isHome = true;
fileSystems."/persist".neededForBoot = lib.mkForce true;
systemd.network.networks."10-vlan-services" = {
dhcpV6Config = {
WithoutRA = "solicit";
# duid-en is nice in principle, but I already have MAC info anyways for reservations
DUIDType = "link-layer";
};
# networkConfig = {
# IPv6PrivacyExtensions = "no";
# IPv6AcceptRA = false;
# };
ipv6AcceptRAConfig = {
DHCPv6Client = "always";
};
};
microvm = {
shares = [
{
tag = "persist";
source = "${lib.optionalString nodes.${microVMParent}.config.swarselsystems.isImpermanence "/persist"}/microvms/${config.networking.hostName}";
mountPoint = "/persist";
proto = "virtiofs";
}
];
# mount the writeable overlay so that we can use nix shells inside the microvm
volumes = [
{
image = "/tmp/nix-store-overlay-${config.networking.hostName}.img";
autoCreate = true;
mountPoint = config.microvm.writableStoreOverlay;
size = 1024;
}
];
};
};
}
4.4.5.14. systemd-networkd (base)
This set of options enables the network of the system to be managed by systemd-networkd:
networking.useNetworkdhas the effect that options fromnetworking.*are not performed using network scripts but rather usingsystemd-networkd.systemd.network.enableenables the actual management of networks using thesystemd-networkdinterface.
{ lib, config, ... }:
{
networking = {
useDHCP = lib.mkForce false;
useNetworkd = true;
dhcpcd.enable = lib.mkIf (!config.swarselsystems.isMicroVM) false;
renameInterfacesByMac = lib.mkIf (!config.swarselsystems.isMicroVM) (lib.mapAttrs (_: v: if (v ? mac) then v.mac else "") (
config.repo.secrets.local.networking.networks or { }
));
};
systemd.network.enable = true;
}
4.4.5.15. systemd-networkd (server base)
Some standard options that should be set vor every microvm guest. We set the default
{ self, lib, config, globals, ... }:
let
inherit (config.swarselsystems) isCrypted localVLANs;
inherit (globals.general) routerServer;
isRouter = config.node.name == routerServer;
ifName = config.swarselsystems.server.localNetwork;
in
{
imports = [
"${self}/modules/nixos/optional/systemd-networkd-base.nix"
];
boot.initrd.systemd.network = lib.mkIf (isCrypted && ((localVLANs == [ ]) || isRouter)) {
enable = true;
networks."10-${ifName}" = config.systemd.network.networks."10-${ifName}";
};
systemd = {
network = {
wait-online.enable = false;
networks =
let
netConfig = config.repo.secrets.local.networking;
in
{
"10-${ifName}" = lib.mkIf (isRouter || (localVLANs == [ ])) {
# address = lib.optionals (isRouter || (localVLANs == [ ])) [
address = [
"${globals.networks.${config.swarselsystems.server.netConfigName}.hosts.${config.node.name}.cidrv4}"
"${globals.networks.${config.swarselsystems.server.netConfigName}.hosts.${config.node.name}.cidrv6}"
];
routes = [
{
Gateway = netConfig.defaultGateway6;
GatewayOnLink = true;
}
{
Gateway = netConfig.defaultGateway4;
GatewayOnLink = true;
}
];
networkConfig = {
IPv6PrivacyExtensions = true;
IPv6AcceptRA = false;
};
matchConfig.MACAddress = netConfig.networks.${config.swarselsystems.server.localNetwork}.mac;
linkConfig.RequiredForOnline = "routable";
};
};
};
};
}
4.4.5.16. TODO systemd-networkd (server home)
This sets up the networking framework that is needed for a server that manages its VLAN interfaces using systemd-networkd. A host that is not both in the home network and using VLANs should rather be using systemd-networkd (server base) only.
We will differentiate between a host that uses microvms versus a host that is not using them.
For a host with microvms the general idea is as follows:
- For each local VLAN we create a VLAN here - we also create a macvlan interface for the hosts which is bound to the respective VLAN interface; also binding to that VLAN interface are the macvtap devices that are being created by the microvm module.
- normally, a guest using macvtap is not reachable by the host unless using a switch that supports hairpin-mode. However, consumers of the same VLAN can still communicate, which is realized using the macvlan
me-*interface. - even then, the kernel will only route requests when the underlying interface is up. In the case that no physical ports are used, this means that the bridge interface would effectively not work (even when administratively set to UP using
activationPolicy) - the aforementionedvethtakes care of that problem.- this is really only a consideration for the Router (because if the interface to the router is missing on the hosts, there will be no connectivity anyways) and is hence implemented there
- normally, a guest using macvtap is not reachable by the host unless using a switch that supports hairpin-mode. However, consumers of the same VLAN can still communicate, which is realized using the macvlan
The principle is the same for a host without microvms, but we do not need the local me-* interfaces and can ignore macvtap config.
A VLAN can also be used as the initrd network - this is however disabled for the router host. For that host, we need to connect from the FritzBox! side in case we need to reboot it (TODO: fix interface naming lan/wan which blocks this)
{ self,lib, config, globals, ... }:
let
inherit (globals.general) routerServer;
inherit (config.swarselsystems) withMicroVMs isCrypted initrdVLAN;
isRouter = config.node.name == routerServer;
localVLANsList = config.swarselsystems.localVLANs;
localVLANs = lib.genAttrs localVLANsList (x: globals.networks.home-lan.vlans.${x});
in
{
imports = [
"${self}/modules/nixos/optional/systemd-networkd-server.nix"
];
config = {
assertions = [
{
assertion = ((localVLANsList != []) && (initrdVLAN != null)) || (localVLANsList == []) || (!isCrypted);
message = "This host uses VLANs and disk encryption, thus a VLAN must be specified for initrd or disk encryption must be removed.";
}
];
boot.initrd = lib.mkIf (isCrypted && (localVLANsList != []) && (!isRouter)) {
availableKernelModules = [ "8021q" ];
kernelModules = [ "8021q" ]; # at least summers needs this to actually find the interfaces
systemd.network = {
enable = true;
netdevs."30-vlan-${initrdVLAN}" = {
netdevConfig = {
Kind = "vlan";
Name = "vlan-${initrdVLAN}";
};
vlanConfig.Id = globals.networks.home-lan.vlans.${initrdVLAN}.id;
};
networks = {
"10-lan" = {
matchConfig.Name = "lan";
# This interface should only be used from attached vlans.
# So don't acquire a link local address and only wait for
# this interface to gain a carrier.
networkConfig.LinkLocalAddressing = "no";
linkConfig.RequiredForOnline = "carrier";
vlan = [ "vlan-${initrdVLAN}" ];
};
"30-vlan-${initrdVLAN}" = {
address = [
globals.networks.home-lan.vlans.${initrdVLAN}.hosts.${config.node.name}.cidrv4
globals.networks.home-lan.vlans.${initrdVLAN}.hosts.${config.node.name}.cidrv6
];
matchConfig.Name = "vlan-${initrdVLAN}";
networkConfig = {
IPv6PrivacyExtensions = "yes";
};
linkConfig.RequiredForOnline = "routable";
};
};
};
};
topology.self.interfaces = (lib.mapAttrs'
(vlanName: _:
lib.nameValuePair "vlan-${vlanName}" {
network = lib.mkForce vlanName;
}
)
localVLANs) // (lib.mapAttrs'
(vlanName: _:
lib.nameValuePair "me-${vlanName}" {
network = lib.mkForce vlanName;
}
)
localVLANs);
systemd.network = {
netdevs = lib.flip lib.concatMapAttrs localVLANs (
vlanName: vlanCfg: {
"30-vlan-${vlanName}" = {
netdevConfig = {
Kind = "vlan";
Name = "vlan-${vlanName}";
};
vlanConfig.Id = vlanCfg.id;
};
# Create a MACVTAP for ourselves too, so that we can communicate with
# our guests on the same interface.
"40-me-${vlanName}" = lib.mkIf withMicroVMs {
netdevConfig = {
Name = "me-${vlanName}";
Kind = "macvlan";
};
extraConfig = ''
[MACVLAN]
Mode=bridge
'';
};
}
);
networks = {
"10-lan" = lib.mkIf (!isRouter) {
matchConfig.Name = "lan";
# This interface should only be used from attached vlans.
# So don't acquire a link local address and only wait for
# this interface to gain a carrier.
networkConfig.LinkLocalAddressing = "no";
linkConfig.RequiredForOnline = "carrier";
vlan = map (name: "vlan-${name}") (builtins.attrNames localVLANs);
};
# Remaining macvtap interfaces should not be touched.
"90-macvtap-ignore" = lib.mkIf withMicroVMs {
matchConfig.Kind = "macvtap";
linkConfig.ActivationPolicy = "manual";
linkConfig.Unmanaged = "yes";
};
}
// lib.flip lib.concatMapAttrs localVLANs (
vlanName: vlanCfg:
let
me = {
address = [
vlanCfg.hosts.${config.node.name}.cidrv4
vlanCfg.hosts.${config.node.name}.cidrv6
];
gateway = lib.optionals (vlanName == "services") [ vlanCfg.hosts.${routerServer}.ipv4 vlanCfg.hosts.${routerServer}.ipv6 ];
matchConfig.Name = "${if withMicroVMs then "me" else "vlan"}-${vlanName}";
networkConfig.IPv6PrivacyExtensions = "yes";
linkConfig.RequiredForOnline = "routable";
};
in
{
"30-vlan-${vlanName}" = if (!withMicroVMs) then me else {
matchConfig.Name = "vlan-${vlanName}";
# This interface should only be used from attached macvlans.
# So don't acquire a link local address and only wait for
# this interface to gain a carrier.
networkConfig.LinkLocalAddressing = "no";
networkConfig.MACVLAN = "me-${vlanName}";
linkConfig.RequiredForOnline = if isRouter then "no" else "carrier";
};
"40-me-${vlanName}" = lib.mkIf withMicroVMs (lib.mkDefault me);
}
);
};
};
}
4.4.5.17. nix-topology node config
Hold standard options for nix-topology per config
{ lib, config, globals, confLib, ... }:
let
inherit (confLib.static) webProxy;
in
{
topology.self = {
icon = lib.mkIf config.swarselsystems.isCloud "devices.cloud-server";
interfaces = {
wan = lib.mkIf (config.swarselsystems.isCloud && config.swarselsystems.server.localNetwork == "wan") { };
lan = lib.mkIf (config.swarselsystems.isCloud && config.swarselsystems.server.localNetwork == "lan") { };
wgProxy = lib.mkIf (config.swarselsystems.server.wireguard ? wgHome) {
addresses = [ globals.networks."${webProxy}-wg.hosts".${config.node.name}.ipv4 ];
renderer.hidePhysicalConnections = true;
virtual = true;
type = "wireguard";
};
wgHome = lib.mkIf (config.swarselsystems.server.wireguard ? wgHome) {
addresses = [ globals.networks.home-wgHome.hosts.${config.node.name}.ipv4 ];
renderer.hidePhysicalConnections = true;
virtual = true;
type = "wireguard";
};
};
};
}
4.5. Home-manager
The general structure here is the same as in the NixOS section.
# @ future me: dont panic, this file is not read in by readNix
{ lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/home";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/home";
}
4.5.1. Steps to setup/upgrade home-manager only
Steps to get a home-manager only setup up and running:
- (Optional) Install openssh-server
- Set hostname to the name specified in the home-manager configuration
- Install nix, either:
- (if upgrading existing nix) Install nix version matching with version that `nix-plugins` is compiled against: `nix-env --install --file '<nixpkgs>' cacert -I nixpkgs=channel:nixpkgs-unstable --attr nixVersions.nix_x_yy`
- (or installing nix freshly):
- Grab the link to the install script of the needed nix version from https://releases.nixos.org/?prefix=nix, e.g. https://releases.nixos.org/nix/nix-2.30.1/install
- `bash <(curl -L https://releases.nixos.org/nix/nix-x-yy-y/install) --daemon`
- add the following to /etc/nix/nix.conf to become a trusted user: `trusted-users = @wheel root swarsel`
- For the first build:
1) Clone dotfile repo & change into it
2) `nix --extra-experimental-features 'nix-command flakes' develop`
3) `home-manager --extra-experimental-features 'nix-command flakes' switch --flake .#$(hostname) --show-trace`
4.5.2. TODO Common
TODO: split this into actual common and client sections
4.5.2.1. Imports
This section sets up all the imports that are used in the home-manager section.
{ lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/home/common";
sharedNames = lib.swarselsystems.readNix "modules/shared";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/home/common" ++
lib.swarselsystems.mkImports sharedNames "modules/shared";
}
4.5.2.2. Mirror home-manager shared options (automatically active)
{ lib, config, nixosConfig ? null, ... }:
let
# mirrorAttrs = lib.mapAttrs (_: v: lib.mkDefault v) nixosConfig.swarselsystems;
mkDefaultCommonAttrs = base: defaults:
lib.mapAttrs (_: v: lib.mkDefault v)
(lib.filterAttrs (k: _: base ? ${k}) defaults);
in
{
# config.swarselsystems = mirrorAttrs;
config.swarselsystems = lib.mkIf (nixosConfig != null) (mkDefaultCommonAttrs config.swarselsystems (nixosConfig.swarselsystems or {}));
}
4.5.2.3. General home-manager-settings (nix)
Again, we adapt nix to our needs, enable the home-manager command for non-NixOS machines (NixOS machines are using it as a module) and setting user information that I always keep the same.
{ self, outputs, lib, pkgs, config, globals, confLib, ... }:
let
inherit (config.swarselsystems) mainUser flakePath isNixos isLinux;
inherit (confLib.getConfig.repo.secrets.common) atticPublicKey;
in
{
options.swarselmodules.general = lib.mkEnableOption "general nix settings";
config =
let
nix-version = "2_30";
in
lib.mkIf config.swarselmodules.general {
nix = lib.mkIf (!config.swarselsystems.isNixos) {
package = lib.mkForce pkgs.nixVersions."nix_${nix-version}";
# extraOptions = ''
# plugin-files = ${pkgs.dev.nix-plugins}/lib/nix/plugins
# extra-builtins-file = ${self + /nix/extra-builtins.nix}
# '';
extraOptions =
let
nix-plugins = pkgs.nix-plugins.override {
nixComponents = pkgs.nixVersions."nixComponents_${nix-version}";
};
in
''
plugin-files = ${nix-plugins}/lib/nix/plugins
extra-builtins-file = ${self + /nix/extra-builtins.nix}
'';
settings = {
experimental-features = [
"nix-command"
"flakes"
"ca-derivations"
"cgroups"
"pipe-operators"
];
substituters = [
"https://${globals.services.attic.domain}/${mainUser}"
];
trusted-public-keys = [
atticPublicKey
];
trusted-users = [
"@wheel"
"${mainUser}"
(lib.mkIf ((config.swarselmodules ? server) ? ssh-builder) "builder")
];
connect-timeout = 5;
bash-prompt-prefix = lib.mkIf config.swarselsystems.isClient "[33m$SHLVL:\\w [0m";
bash-prompt = lib.mkIf config.swarselsystems.isClient "$(if [[ $? -gt 0 ]]; then printf \"[31m\"; else printf \"[32m\"; fi)λ [0m";
fallback = true;
min-free = 128000000;
max-free = 1000000000;
auto-optimise-store = true;
warn-dirty = false;
max-jobs = 1;
use-cgroups = lib.mkIf isLinux true;
};
};
nixpkgs = lib.mkIf (!isNixos) {
overlays = [
outputs.overlays.default
(final: prev:
let
additions = final: _: import "${self}/pkgs/config" {
inherit self config lib;
pkgs = final;
homeConfig = config;
};
in
additions final prev
)
];
config = {
allowUnfree = true;
};
};
programs = {
# home-manager.enable = lib.mkIf (!isNixos) true;
man = {
enable = true;
generateCaches = true;
};
};
targets.genericLinux.enable = lib.mkIf (!isNixos) true;
home = {
username = lib.mkDefault mainUser;
homeDirectory = lib.mkDefault "/home/${mainUser}";
stateVersion = lib.mkDefault "23.05";
keyboard.layout = "us";
sessionVariables = {
FLAKE = "/home/${mainUser}/.dotfiles";
};
extraOutputsToInstall = [
"doc"
"info"
"devdoc"
];
packages = lib.mkIf (!isNixos) [
(pkgs.symlinkJoin {
name = "home-manager";
buildInputs = [ pkgs.makeWrapper ];
paths = [ pkgs.home-manager ];
postBuild = ''
wrapProgram $out/bin/home-manager \
--append-flags '--flake ${flakePath}#$(hostname)'
'';
})
];
};
};
}
4.5.2.4. nixGL
This integrates nixGL into home-manager. NixGL provies OpenGL and Vulkan APIs to nix installed utilities. This is needed for graphical applications on non-NixOS systems.
to get the info for the secondary gpu, use `lspci -nn | grep VGA` It can be set to either:
- a number, selecting the n-th non-default GPU
- a PCI bus id in the form
pci-XXX_YY_ZZ_U - a PCI id in the form
vendor_id:device_id
{ lib, config, inputs, ... }:
{
options.swarselmodules.nixgl = lib.mkEnableOption "nixgl settings";
options.swarselsystems = {
isSecondaryGpu = lib.mkEnableOption "device has a secondary GPU";
SecondaryGpuCard = lib.mkOption {
type = lib.types.str;
default = "";
};
};
config = lib.mkIf config.swarselmodules.nixgl {
nixGL = lib.mkIf (!config.swarselsystems.isNixos) {
inherit (inputs.nixgl) packages;
defaultWrapper = lib.mkDefault "mesa";
vulkan.enable = lib.mkDefault false;
prime = lib.mkIf config.swarselsystems.isSecondaryGpu {
card = config.swarselsystems.secondaryGpuCard;
installScript = "mesa";
};
offloadWrapper = lib.mkIf config.swarselsystem.isSecondaryGpu "mesaPrime";
installScripts = [
"mesa"
"mesaPrime"
];
};
};
}
4.5.2.5. Installed packages
Here are defined some packages that I would like to use across all my machines. Most of these should not require further setup. Notably the cura package is severely outdated on nixpkgs, so I just fetch a more recent AppImage and run that instead.
Also, I define some useful shell scripts here.
Programming languages and default lsp's are defined here: System Packages
4.5.2.5.1. Packaged
This holds packages that I can use as provided, or with small modifications (as in the texlive package that needs special configuration).
{ lib, config, pkgs, ... }:
{
options.swarselmodules.packages = lib.mkEnableOption "packages settings";
config = lib.mkIf config.swarselmodules.packages {
home.packages = with pkgs; [
# audio stuff
spek # spectrum analyzer
losslessaudiochecker
ffmpeg_7-full
flac
mediainfo
picard-tools
audacity
sox
# stable.feishin # does not work with oauth2-proxy
calibre
# printing
cups
simple-scan
cura-appimage
# ssh login using idm
opkssh
# cache
attic-client
# dict
(aspellWithDicts (dicts: with dicts; [ de en en-computers en-science ]))
# browser
stable24_11.vieb
mgba
# utilities
util-linux
nmap
lsof
nvd
nix-output-monitor
hyprpicker # color picker
findutils
units
vim
sshfs
fuse
# ventoy
poppler-utils
vdhcoapp
# nix
alejandra
nixpkgs-fmt
deadnix
statix
nix-tree
nix-diff
nix-visualize
nix-init
nix-inspect
(nixpkgs-review.override { nix = config.nix.package; })
manix
# shellscripts
shfmt
# local file sharing
wormhole-rs
croc
# b2 backup @backblaze
restic
# "big" programs
# obs-studio
gimp
inkscape
zoom-us
# nomacs
libreoffice-qt
xournalpp
# obsidian
# spotify
# vesktop # discord client
# nextcloud-client # enables a systemd service that I do not want
# spotify-player
# element-desktop
nicotine-plus
stable25_05.transmission_3
mktorrent
hugo
# kyria
qmk
qmk-udev-rules
# firefox related
tridactyl-native
# mako related
mako
libnotify
# general utilities
unrar
# samba
cifs-utils
zbar # qr codes
readline
autotiling
brightnessctl
libappindicator-gtk3
sqlite
speechd
networkmanagerapplet
psmisc # kill etc
lm_sensors
# jq # used for searching the i3 tree in check<xxx>.sh files
# specifically needed for anki
# mpv
# anki-bin
# dirvish file previews
fd
imagemagick
# poppler
ffmpegthumbnailer
mediainfo
gnutar
unzip
#nautilus
nautilus
xfce.tumbler
libgsf
# wayland stuff
wtype
wl-mirror
wl-clipboard
wf-recorder
kanshi
# screenshotting tools
grim
slurp
# the following packages are used (in some way) by waybar
# playerctl
pavucontrol
# stable.pamixer
# gnome.gnome-clocks
# wlogout
# jdiskreport
# monitor
#keychain
qalculate-gtk
gcr # needed for gnome-secrets to work
seahorse
# sops-related
sops
ssh-to-age
# mail related packages
mu
# latex and related packages
(texlive.combine {
inherit (pkgs.texlive) scheme-full
dvisvgm dvipng# for preview and export as html
wrapfig amsmath ulem hyperref capt-of;
})
# font stuff
nerd-fonts.fira-mono
nerd-fonts.fira-code
nerd-fonts.symbols-only
noto-fonts-color-emoji
font-awesome_5
noto-fonts
noto-fonts-cjk-sans
];
};
}
4.5.2.5.2. Self-defined
This is just a separate container for derivations defined in Packages. This is a good idea so that I do not lose track of package names I have defined myself, as this was once a problem in the past already.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.ownpackages = lib.mkEnableOption "own packages settings";
config = lib.mkIf config.swarselmodules.ownpackages {
home.packages = with pkgs; lib.mkIf (!config.swarselsystems.isPublic) [
pass-fuzzel
cdw
cdb
cdr
bak
timer
e
swarselcheck
swarselcheck-niri
waybarupdate
opacitytoggle
fs-diff
github-notifications
hm-specialisation
t2ts
ts2t
vershell
eontimer
project
fhs
swarsel-bootstrap
swarsel-displaypower
swarsel-deploy
swarsel-instantiate
swarselzellij
sshrm
endme
git-replace
prstatus
swarsel-gens
swarsel-switch
swarsel-sops
];
};
}
4.5.2.6. sops
I use sops-nix to handle secrets that I want to have available on my machines at all times. Procedure to add a new machine:
- `ssh-keygen -t ed25519 -C "NAME sops"` in .ssh directory (or wherever) - name e.g. "sops"
- cat ~/.ssh/sops.pub | ssh-to-age | wl-copy
- add the output to .sops.yaml
- cp ~/.ssh/sops.pub ~/.dotfiles/secrets/public/NAME.pub
update entry for sops.age.sshKeyPaths
Since we are using the home-manager implementation here, we need to specify the runtime path.
At the same time, I want to avoid running the homeManager module of sops on a NixOS machine. Note that we cannot use
lib.mkIfin the lineconfig =…= as this would evaluate the blocks that are within; however, on a NixOS machine, there will be nosopsmodule in the homeManager scope. Hence we useoptionalAttrs. Also, we cannot make use ofconfig.swarselsystems.isNixosbecause that will lead to an infinite recursion. Hence, we take thetypearg that we passed during host declaration to make sure sops stays disabled. This is used in all places in the home-manager config that make use of sops-secrets.
{ self, config, lib, type, ... }:
let
inherit (config.swarselsystems) homeDir;
in
{
options.swarselmodules.sops = lib.mkEnableOption "sops settings";
config = lib.optionalAttrs (type != "nixos") {
sops = lib.mkIf (!config.swarselsystems.isNixos) {
age.sshKeyPaths = [ "${if config.swarselsystems.isImpermanence then "/persist" else ""}${homeDir}/.ssh/sops" ];
# defaultSopsFile = "${if config.swarselsystems.isImpermanence then "/persist" else ""}${homeDir}/.dotfiles/secrets/repo/common.yaml";
defaultSopsFile = self + "/secrets/repo/common.yaml";
validateSopsFiles = false;
};
};
}
4.5.2.7. Yubikey
{ lib, config, confLib, type, ... }:
let
inherit (config.swarselsystems) homeDir;
in
{
options.swarselmodules.yubikey = lib.mkEnableOption "yubikey settings";
config = lib.mkIf config.swarselmodules.yubikey ({
pam.yubico.authorizedYubiKeys = lib.mkIf (config.swarselsystems.isNixos && !config.swarselsystems.isPublic) {
ids = [
confLib.getConfig.repo.secrets.common.yubikeys.dev1
confLib.getConfig.secrets.common.yubikeys.dev2
];
};
} // lib.optionalAttrs (type != "nixos") {
sops.secrets = lib.mkIf (!config.swarselsystems.isPublic) {
u2f-keys = { path = "${homeDir}/.config/Yubico/u2f_keys"; };
};
});
}
4.5.2.8. SSH Machines
It is very convenient to have SSH aliases in place for machines that I use. This is mainly used for some server machines and some university clusters. We also enable agent forwarding to have our Yubikey SSH key accessible on the remote host.
{ lib, config, confLib, type, ... }:
{
options.swarselmodules.ssh = lib.mkEnableOption "ssh settings";
config = lib.mkIf config.swarselmodules.ssh ({
programs.ssh = {
enable = true;
enableDefaultConfig = false;
extraConfig = ''
SetEnv TERM=xterm-256color
ServerAliveInterval 20
'';
matchBlocks = {
"*" = {
forwardAgent = true;
addKeysToAgent = "no";
compression = false;
serverAliveInterval = 0;
serverAliveCountMax = 3;
hashKnownHosts = false;
userKnownHostsFile = "~/.ssh/known_hosts";
controlMaster = "no";
controlPath = "~/.ssh/master-%r@%n:%p";
controlPersist = "no";
};
} // confLib.getConfig.repo.secrets.common.ssh.hosts;
};
} // lib.optionalAttrs (type != "nixos") {
sops.secrets = lib.mkIf (!config.swarselsystems.isPublic && !config.swarselsystems.isNixos) {
builder-key = { path = "${config.home.homeDirectory}/.ssh/builder"; mode = "0600"; };
};
});
}
4.5.2.9. Theme (stylix)
These section allows home-manager to allow theme settings, and handles some other appearance-related settings like cursor styles. Interestingly, system icons (adwaita) still need to be setup on system-level, and will break if defined here.
This section has been notably empty ever since switching to stylix. Only Emacs is not allowed to be styled by it, because it becomes more ugly compared to my handcrafted setup.
theme is defined in stylix color scheme.
{ self, lib, config, vars, ... }:
{
options.swarselmodules.stylix = lib.mkEnableOption "stylix settings";
config = lib.mkIf config.swarselmodules.stylix {
stylix = lib.mkIf (!config.swarselsystems.isNixos && config.swarselmodules.stylix) (lib.recursiveUpdate
{
enable = true;
base16Scheme = "${self}/files/stylix/swarsel.yaml";
targets = vars.stylixHomeTargets;
}
vars.stylix);
};
}
4.5.2.10. Desktop Entries, MIME types (xdg)
Some programs lack a dmenu launcher - I define them myself here.
TODO: Non-NixOS machines (=sp3) should not use these by default, but instead the programs prefixed with "nixGL". I need to figure out how to automate this process, as it is not feasible to write desktop entries for all programs installed on that machine.
{ lib, config, ... }:
{
options.swarselmodules.desktop = lib.mkEnableOption "desktop settings";
config = lib.mkIf config.swarselmodules.desktop {
xdg.desktopEntries = {
cura = {
name = "Ultimaker Cura";
genericName = "Cura";
exec = "cura";
terminal = false;
categories = [ "Application" ];
};
teamsNoGpu = {
name = "Microsoft Teams (no GPU)";
genericName = "Teams (no GPU)";
exec = "teams-for-linux --disableGpu=true --trayIconEnabled=true";
terminal = false;
categories = [ "Application" ];
};
rustdesk-vbc = {
name = "Rustdesk VBC";
genericName = "rustdesk-vbc";
exec = "rustdesk-vbc";
terminal = false;
categories = [ "Application" ];
};
anki = {
name = "Anki Flashcards";
genericName = "Anki";
exec = "anki";
terminal = false;
categories = [ "Application" ];
};
element = {
name = "Element Matrix Client";
genericName = "Element";
exec = "element-desktop -enable-features=UseOzonePlatform -ozone-platform=wayland --disable-gpu-driver-bug-workarounds";
terminal = false;
categories = [ "Application" ];
};
emacsclient-newframe = {
name = "Emacs (Client, New Frame)";
genericName = "Emacs (Client, New Frame)";
exec = "emacsclient -r %u";
icon = "emacs";
terminal = false;
categories = [ "Development" "TextEditor" ];
};
};
xdg = {
configFile."mimeapps.list".force = true;
mimeApps = {
enable = true;
defaultApplications = {
"application/epub+zip" = [ "calibre-ebook-viewer.desktop" ];
"application/metalink+xml" = [ "emacsclient.desktop" ];
"application/msword" = [ "writer.desktop" ];
"application/pdf" = [ "org.gnome.Evince.desktop" ];
"application/sql" = [ "emacsclient.desktop" ];
"application/vnd.ms-excel" = [ "calc.desktop" ];
"application/vnd.ms-powerpoint" = [ "impress.desktop" ];
"application/x-extension-htm" = [ "firefox.desktop" ];
"application/x-extension-html" = [ "firefox.desktop" ];
"application/x-extension-shtml" = [ "firefox.desktop" ];
"application/x-extension-xht" = [ "firefox.desktop" ];
"application/x-extension-xhtml" = [ "firefox.desktop" ];
"application/xhtml+xml" = [ "firefox.desktop" ];
"audio/flac" = [ "mpv.desktop" ];
"audio/mp3" = [ "mpv.desktop" ];
"audio/ogg" = [ "mpv.desktop" ];
"audio/wav" = [ "mpv.desktop" ];
"image/gif" = [ "imv.desktop" ];
"image/jpeg" = [ "imv.desktop" ];
"image/png" = [ "imv.desktop" ];
"image/svg" = [ "imv.desktop" ];
"image/vnd.adobe.photoshop" = [ "gimp.desktop" ];
"image/vnd.dxf" = [ "org.inkscape.Inkscape.desktop" ];
"image/webp" = [ "firefox.desktop" ];
"text/csv" = [ "emacsclient.desktop" ];
"text/html" = [ "firefox.desktop" ];
"text/plain" = [ "emacsclient.desktop" ];
"video/3gp" = [ "umpv.desktop" ];
"video/flv" = [ "umpv.desktop" ];
"video/mkv" = [ "umpv.desktop" ];
"video/mp4" = [ "umpv.desktop" ];
"x-scheme-handler/chrome" = [ "firefox.desktop" ];
"x-scheme-handler/http" = [ "firefox.desktop" ];
"x-scheme-handler/https" = [ "firefox.desktop" ];
};
associations = {
added = {
"application/x-zerosize" = [ "emacsclient.desktop" ];
"application/epub+zip" = [ "calibre-ebook-viewer.desktop" ];
};
};
};
};
};
}
4.5.2.11. Linking dotfiles (Symlinks home.file)
This section should be used in order to symlink already existing configuration files using `home.file` and setting session variables using `home.sessionVariables`.
As for the `home.sessionVariables`, it should be noted that environment variables that are needed at system start should NOT be loaded here, but instead in `programs.zsh.config.extraSessionCommands` (in the home-manager programs section). This is also where all the wayland related variables are stored.
Also, we link some files to the users XDG configuration home: Also in firefox `about:config > toolkit.legacyUserProfileCustomizations.stylesheets` to true.
{ self, lib, config, ... }:
{
options.swarselmodules.symlink = lib.mkEnableOption "symlink settings";
config = lib.mkIf config.swarselmodules.symlink {
home.file = {
"init.el" = lib.mkDefault {
source = self + /files/emacs/init.el;
target = ".emacs.d/init.el";
};
"early-init.el" = {
source = self + /files/emacs/early-init.el;
target = ".emacs.d/early-init.el";
};
# on NixOS, Emacs does not find the aspell dicts easily. Write the configuration manually
".aspell.conf" = {
source = self + /files/config/.aspell.conf;
target = ".aspell.conf";
};
".gitmessage" = {
source = self + /files/git/.gitmessage;
target = ".gitmessage";
};
};
xdg.configFile = {
"tridactyl/tridactylrc".source = self + /files/firefox/tridactyl/tridactylrc;
"tridactyl/themes/base16-codeschool.css".source = self + /files/firefox/tridactyl/themes/base16-codeschool.css;
"tridactyl/themes/swarsel.css".source = self + /files/firefox/tridactyl/themes/swarsel.css;
# "swayidle/config".source = self + /files/swayidle/config;
};
};
}
4.5.2.12. Sourcing environment variables
Sets environment variables. Here I am only setting the EDITOR variable, most variables are set in the Sway section.
{ lib, config, confLib, globals, ... }:
let
inherit (confLib.getConfig.repo.secrets.common.mail) address1 address2 address3 address4 allMailAddresses;
inherit (confLib.getConfig.repo.secrets.common.calendar) source1 source1-name source2 source2-name source3 source3-name;
inherit (confLib.getConfig.repo.secrets.common) fullName openrouterApi instaDomain sportDomain;
inherit (config.swarselsystems) isPublic homeDir;
DISPLAY = ":0";
in
{
options.swarselmodules.env = lib.mkEnableOption "env settings";
config = lib.mkIf config.swarselmodules.env {
home.sessionVariables = {
inherit DISPLAY;
EDITOR = "e -w";
} // (lib.optionalAttrs (!isPublic) { });
systemd.user.sessionVariables = {
DOCUMENT_DIR_PRIV = lib.mkForce "${homeDir}/Documents/Private";
FLAKE = "${config.home.homeDirectory}/.dotfiles";
} // lib.optionalAttrs (!isPublic) {
SWARSEL_DOMAIN = globals.domains.main;
SWARSEL_RSS_DOMAIN = globals.services.freshrss.domain;
SWARSEL_MUSIC_DOMAIN = globals.services.navidrome.domain;
SWARSEL_FILES_DOMAIN = globals.services.nextcloud.domain;
SWARSEL_INSTA_DOMAIN = instaDomain;
SWARSEL_SPORT_DOMAIN = sportDomain;
SWARSEL_MAIL1 = address1;
SWARSEL_MAIL2 = address2;
SWARSEL_MAIL3 = address3;
SWARSEL_MAIL4 = address4;
SWARSEL_CAL1 = source1;
SWARSEL_CAL1NAME = source1-name;
SWARSEL_CAL2 = source2;
SWARSEL_CAL2NAME = source2-name;
SWARSEL_CAL3 = source3;
SWARSEL_CAL3NAME = source3-name;
SWARSEL_FULLNAME = fullName;
SWARSEL_MAIL_ALL = lib.mkDefault allMailAddresses;
GITHUB_NOTIFICATION_TOKEN_PATH = confLib.getConfig.sops.secrets.github-notifications-token.path;
OPENROUTER_API_KEY = openrouterApi;
};
};
}
4.5.2.13. General Programs: bottom, imv, less, lesspipe, sioyek, bat, carapace, wlogout, swayr, yt-dlp, mpv, jq, nix-index, ripgrep, pandoc, fzf, zoxide, timidity
This section is for programs that require no further configuration. zsh Integration is enabled by default for these.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.programs = lib.mkEnableOption "programs settings";
config = lib.mkIf config.swarselmodules.programs {
programs = {
bat = {
enable = true;
extraPackages = [
pkgs.bat-extras.batdiff
pkgs.bat-extras.batman
pkgs.bat-extras.batwatch
] ++ [
pkgs.stable.bat-extras.batgrep
];
# extraPackages = with pkgs.bat-extras; [ batdiff batman batgrep batwatch ];
};
bottom.enable = true;
carapace.enable = true;
fzf = {
enable = true;
enableBashIntegration = false;
enableZshIntegration = false;
};
imv.enable = true;
jq.enable = true;
less.enable = true;
lesspipe.enable = true;
mpv.enable = true;
pandoc.enable = true;
rclone.enable = true;
ripgrep.enable = true;
sioyek.enable = true;
swayr.enable = true;
timidity.enable = true;
wlogout = {
enable = true;
layout = [
{
label = "lock";
action = "loginctl lock-session";
text = "Lock";
keybind = "l";
circular = true;
}
{
label = "hibernate";
action = "systemctl hibernate";
text = "Hibernate";
keybind = "h";
circular = true;
}
{
label = "logout";
action = "loginctl terminate-user $USER";
text = "Logout";
keybind = "u";
circular = true;
}
{
label = "shutdown";
action = "systemctl poweroff";
text = "Shutdown";
keybind = "p";
circular = true;
}
{
label = "suspend";
action = "systemctl suspend";
text = "Suspend";
keybind = "s";
circular = true;
}
{
label = "reboot";
action = "systemctl reboot";
text = "Reboot";
keybind = "r";
circular = true;
}
];
};
yt-dlp.enable = true;
zoxide = {
enable = true;
enableZshIntegration = true;
options = [
"--cmd cd"
];
};
};
};
}
4.5.2.14. nix-index
nix-index provides a way to find out which packages are provided by which derivations. By default it also comes with a replacement for command-not-found.sh, however, the implementation is based on a channel based setup. I like consistency, so I replace the command with one that provides a flakes-based output. This also uses the nix-index-with-full-db from the nix-index-database input thanks to its overlay.
{ self, lib, config, pkgs, ... }:
{
options.swarselmodules.nix-index = lib.mkEnableOption "nix-index settings";
config = lib.mkIf config.swarselmodules.nix-index {
programs.nix-index =
let
commandNotFound = pkgs.runCommandLocal "command-not-found.sh" { } ''
mkdir -p $out/etc/profile.d
substitute ${self + /files/scripts/command-not-found.sh} \
$out/etc/profile.d/command-not-found.sh \
--replace-fail @nix-locate@ ${pkgs.nix-index}/bin/nix-locate \
--replace-fail @tput@ ${pkgs.ncurses}/bin/tput
'';
in
{
enable = true;
package = pkgs.symlinkJoin {
name = "nix-index";
paths = [ commandNotFound ];
};
};
programs.nix-index-database.comma.enable = true;
};
}
4.5.2.15. nix-your-shell
{ lib, config, ... }:
let
moduleName = "nix-your-shell";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
programs.${moduleName} = {
enable = true;
enableZshIntegration = true;
};
};
}
4.5.2.16. password-store
Enables password store with the pass-otp extension which allows me to store and generate one-time-passwords.
{ lib, config, pkgs, ... }:
{
options.swarselmodules.passwordstore = lib.mkEnableOption "passwordstore settings";
config = lib.mkIf config.swarselmodules.passwordstore {
programs.password-store = {
enable = true;
settings = {
PASSWORD_STORE_DIR = "$HOME/.local/share/password-store";
};
package = pkgs.pass.withExtensions (exts: [ exts.pass-otp ]);
};
};
}
4.5.2.17. direnv
Enables direnv, which I use for nearly all of my nix dev flakes.
{ lib, config, ... }:
{
options.swarselmodules.direnv = lib.mkEnableOption "direnv settings";
config = lib.mkIf config.swarselmodules.direnv {
programs.direnv = {
enable = true;
silent = true;
nix-direnv.enable = true;
};
};
}
4.5.2.18. eza
Eza provides me with a better ls command and some other useful aliases.
{ lib, config, ... }:
{
options.swarselmodules.eza = lib.mkEnableOption "eza settings";
config = lib.mkIf config.swarselmodules.eza {
programs.eza = {
enable = true;
icons = "auto";
git = true;
extraOptions = [
"-l"
"--group-directories-first"
];
};
};
}
4.5.2.19. atuin
{ lib, config, globals, ... }:
let
atuinDomain = globals.services.atuin.domain;
in
{
options.swarselmodules.atuin = lib.mkEnableOption "atuin settings";
config = lib.mkIf config.swarselmodules.atuin {
programs.atuin = {
enable = true;
enableZshIntegration = true;
enableBashIntegration = true;
settings = {
auto_sync = true;
sync_frequency = "5m";
sync_address = "https://${atuinDomain}";
};
};
};
}
4.5.2.20. git
Here I set up my git config, automatic signing of commits, useful aliases for my ost used commands (for when I am not using Magit) as well as a git template defined in Linking dotfiles.
{ lib, config, globals, minimal, confLib, ... }:
let
inherit (confLib.getConfig.repo.secrets.common.mail) address1;
inherit (confLib.getConfig.repo.secrets.common) fullName;
gitUser = globals.user.name;
in
{
options.swarselmodules.git = lib.mkEnableOption "git settings";
config = lib.mkIf config.swarselmodules.git {
programs.git = {
enable = true;
} // lib.optionalAttrs (!minimal) {
settings = {
alias = {
a = "add";
c = "commit";
cl = "clone";
co = "checkout";
b = "branch";
i = "init";
m = "merge";
s = "status";
r = "restore";
p = "pull";
pp = "push";
};
user = {
email = lib.mkIf (config.swarselsystems.isNixos && !config.swarselsystems.isPublic) (lib.mkDefault address1);
name = lib.mkIf (config.swarselsystems.isNixos && !config.swarselsystems.isPublic) fullName;
};
};
signing = {
key = "0x76FD3810215AE097";
signByDefault = true;
};
lfs.enable = true;
includes = [
{
contents = {
github = {
user = gitUser;
};
commit = {
template = "~/.gitmessage";
};
};
}
];
};
programs.difftastic.enable = lib.mkIf (!minimal) true;
};
}
4.5.2.21. Fuzzel
Here I only need to set basic layout options - the rest is being managed by stylix.
{ lib, config, ... }:
{
options.swarselmodules.fuzzel = lib.mkEnableOption "fuzzel settings";
config = lib.mkIf config.swarselmodules.fuzzel {
programs.fuzzel = {
enable = true;
settings = {
main = {
layer = "overlay";
lines = "10";
width = "40";
};
border.radius = "0";
};
};
};
}
4.5.2.22. Starship
Starship makes my zsh look cooler! I have symbols for most programming languages and toolchains, also I build my own powerline.
{ lib, config, ... }:
{
options.swarselmodules.starship = lib.mkEnableOption "starship settings";
config = lib.mkIf config.swarselmodules.starship {
programs.starship = {
enable = true;
enableZshIntegration = true;
settings = {
add_newline = false;
format = "$shlvl$character";
right_format = "$all";
command_timeout = 3000;
directory.substitutions = {
"Documents" = " ";
"Downloads" = " ";
"Music" = " ";
"Pictures" = " ";
};
git_status = {
style = "bg:#394260";
format = "[[($all_status$ahead_behind)](fg:#769ff0 bg:#394260)]($style) ";
};
character = {
success_symbol = "[λ](bold green)";
error_symbol = "[λ](bold red)";
};
shlvl = {
disabled = false;
symbol = "↳";
format = "[$symbol]($style) ";
repeat = true;
repeat_offset = 1;
style = "blue";
};
nix_shell = {
disabled = false;
heuristic = true;
format = "[$symbol$name]($style)";
symbol = " ";
};
aws.symbol = " ";
buf.symbol = " ";
c.symbol = " ";
conda.symbol = " ";
dart.symbol = " ";
directory.read_only = " ";
docker_context.symbol = " ";
elixir.symbol = " ";
elm.symbol = " ";
fossil_branch.symbol = " ";
git_branch.symbol = " ";
golang.symbol = " ";
guix_shell.symbol = " ";
haskell.symbol = " ";
haxe.symbol = " ";
hg_branch.symbol = " ";
hostname.ssh_symbol = " ";
java.symbol = " ";
julia.symbol = " ";
lua.symbol = " ";
memory_usage.symbol = " ";
meson.symbol = " ";
nim.symbol = " ";
nodejs.symbol = " ";
os.symbols = {
Alpaquita = " ";
Alpine = " ";
Amazon = " ";
Android = " ";
Arch = " ";
Artix = " ";
CentOS = " ";
Debian = " ";
DragonFly = " ";
Emscripten = " ";
EndeavourOS = " ";
Fedora = " ";
FreeBSD = " ";
Garuda = " ";
Gentoo = " ";
HardenedBSD = " ";
Illumos = " ";
Linux = " ";
Mabox = " ";
Macos = " ";
Manjaro = " ";
Mariner = " ";
MidnightBSD = " ";
Mint = " ";
NetBSD = " ";
NixOS = " ";
OpenBSD = " ";
openSUSE = " ";
OracleLinux = " ";
Pop = " ";
Raspbian = " ";
Redhat = " ";
RedHatEnterprise = " ";
Redox = " ";
Solus = " ";
SUSE = " ";
Ubuntu = " ";
Unknown = " ";
Windows = " ";
};
package.symbol = " ";
pijul_channel.symbol = " ";
python.symbol = " ";
rlang.symbol = " ";
ruby.symbol = " ";
rust.symbol = " ";
scala.symbol = " ";
};
};
};
}
4.5.2.23. Kitty
Kitty is the terminal emulator of choice for me, it is nice to configure using nix, fast, and has a nice style.
The theme is handled by stylix.
{ lib, config, ... }:
{
options.swarselmodules.kitty = lib.mkEnableOption "kitty settings";
config = lib.mkIf config.swarselmodules.kitty {
programs.kitty = {
enable = true;
keybindings = let
bindWithModifier = lib.mapAttrs' (key: lib.nameValuePair ("ctrl+shift" + key));
in bindWithModifier {
"page_up" = "scroll_page_up";
"up" = "scroll_page_up";
"page_down" = "scroll_page_down";
"down" = "scroll_page_down";
"w" = "no_op";
};
settings = {
cursor_blink_interval = 0;
disable_ligatures = "cursor";
enable_audio_bell = false;
notify_on_cmd_finish = "always 20";
open_url_with = "xdg-open";
scrollback_lines = 100000;
scrollback_pager_history_size = 512;
};
};
};
}
4.5.2.24. zsh
zsh is the most convenient shell for me and it happens to be super neat to configure within home manager.
Here we set some aliases (some of them should be shellApplications instead) as well as some zsh plugins like fzf-tab.
Concerning the shell extensions, zle <widget-name> will run an existing widget and zle -N <function_name> will make a function available for use. The my- functions all remove . / and : from the WORDCHARS so that functions will stop there. The keycodes can be found using showkeys -a
Regarding initContent:
To specify the order, use lib.mkOrder.
Common order values:
- 500 (mkBefore: Early initialization (replaces initExtraFirst
- 550: Before completion initialization (replaces initExtraBeforeCompInit
- 1000 (default: General configuration (replaces initExtra
- 1500 (mkAfter: Last to run configuration
To specify both content in Early initialization and General configuration, use lib.mkMerge:
let zshConfigEarlyInit = lib.mkOrder 500 "do something"; zshConfig = lib.mkOrder 1000 "do something"; in lib.mkMerge [ zshConfigEarlyInit zshConfig ];
Currently I only use it as before with initExtra though.
{ self, config, pkgs, lib, minimal, globals, confLib, type, ... }:
let
inherit (config.swarselsystems) flakePath isNixos;
crocDomain = globals.services.croc.domain;
in
{
options.swarselmodules.zsh = lib.mkEnableOption "zsh settings";
options.swarselsystems = {
shellAliases = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
};
};
config = lib.mkIf config.swarselmodules.zsh
({
programs.zsh = {
enable = true;
}
// lib.optionalAttrs (!minimal) {
shellAliases = lib.recursiveUpdate
{
hg = "history | grep";
hmswitch = lib.mkIf (!isNixos) "${lib.getExe pkgs.home-manager} --flake ${flakePath}#$(hostname) switch |& nom";
nswitch = lib.mkIf isNixos "cd ${flakePath}; swarsel-deploy $(hostname) switch; cd -;";
ntest = lib.mkIf isNixos "cd ${flakePath}; swarsel-deploy $(hostname) test; cd -;";
nboot = lib.mkIf isNixos "cd ${flakePath}; swarsel-deploy $(hostname) boot; cd -;";
ndry = lib.mkIf isNixos "cd ${flakePath}; swarsel-deploy $(hostname) dry-activate; cd -;";
magit = "emacsclient -nc -e \"(magit-status)\"";
config = "git --git-dir=$HOME/.cfg/ --work-tree=$HOME";
g = "git";
c = "git --git-dir=$FLAKE/.git --work-tree=$FLAKE/";
passpush = "cd ~/.local/share/password-store; git add .; git commit -m 'pass file changes'; git push; cd -;";
passpull = "cd ~/.local/share/password-store; git pull; cd -;";
hotspot = "nmcli connection up local; nmcli device wifi hotspot;";
youtube-dl = "yt-dlp";
cat-orig = "cat";
# cdr = "cd \"$( (find $DOCUMENT_DIR_WORK $DOCUMENT_DIR_PRIV -maxdepth 1 && echo $FLAKE) | fzf )\"";
cdr = "source cdr";
nix-ldd-ldd = "LD_LIBRARY_PATH=$NIX_LD_LIBRARY_PATH ldd";
nix-ldd = "LD_LIBRARY_PATH=$NIX_LD_LIBRARY_PATH ldd";
nix-ldd-locate = "nix-locate --minimal --top-level -w ";
nix-store-search = "ls /nix/store | grep";
fs-diff = "sudo mount -o subvol=/ /dev/mapper/cryptroot /mnt ; fs-diff";
lt = "eza -las modified --total-size";
boot-diff = "nix store diff-closures /run/*-system";
gen-diff = "nix profile diff-closures --profile /nix/var/nix/profiles/system";
cc = "wl-copy";
build-topology = "nix build --override-input topologyPrivate ${self}/files/topology/private .#topology.x86_64-linux.config.output";
build-iso = "nix build --print-out-paths .#live-iso";
nix-review-local = "nix run nixpkgs#nixpkgs-review -- rev HEAD";
nix-review-post = "nix run nixpkgs#nixpkgs-review -- pr --post-result --systems linux";
}
config.swarselsystems.shellAliases;
autosuggestion.enable = true;
enableCompletion = true;
syntaxHighlighting.enable = true;
autocd = false;
cdpath = [
"~/.dotfiles"
# "~/Documents/GitHub"
];
defaultKeymap = "emacs";
dirHashes = {
dl = "$HOME/Downloads";
gh = "$HOME/Documents/GitHub";
};
history = {
expireDuplicatesFirst = true;
append = true;
ignoreSpace = true;
ignoreDups = true;
path = "${config.home.homeDirectory}/.histfile";
save = 100000;
size = 100000;
};
historySubstringSearch = {
enable = true;
searchDownKey = "^[OB";
searchUpKey = "^[OA";
};
plugins = [
# {
# name = "fzf-tab";
# src = pkgs.zsh-fzf-tab;
# }
];
initContent = ''
my-forward-word() {
local WORDCHARS=$WORDCHARS
WORDCHARS="''${WORDCHARS//:}"
WORDCHARS="''${WORDCHARS//\/}"
WORDCHARS="''${WORDCHARS//.}"
zle forward-word
}
zle -N my-forward-word
# ctrl + right
bindkey "^[[1;5C" my-forward-word
# shift + right
bindkey "^[[1;2C" forward-word
my-backward-word() {
local WORDCHARS=$WORDCHARS
WORDCHARS="''${WORDCHARS//:}"
WORDCHARS="''${WORDCHARS//\/}"
WORDCHARS="''${WORDCHARS//.}"
zle backward-word
}
zle -N my-backward-word
# ctrl + left
bindkey "^[[1;5D" my-backward-word
# shift + left
bindkey "^[[1;2D" backward-word
my-backward-delete-word() {
local WORDCHARS=$WORDCHARS
WORDCHARS="''${WORDCHARS//:}"
WORDCHARS="''${WORDCHARS//\/}"
WORDCHARS="''${WORDCHARS//.}"
zle backward-delete-word
}
zle -N my-backward-delete-word
# ctrl + del
bindkey '^H' my-backward-delete-word
'';
sessionVariables = lib.mkIf (!config.swarselsystems.isPublic) {
CROC_RELAY = crocDomain;
CROC_PASS = "$(cat ${confLib.getConfig.sops.secrets.croc-password.path or ""})";
GITHUB_TOKEN = "$(cat ${confLib.getConfig.sops.secrets.github-nixpkgs-review-token.path or ""})";
QT_QPA_PLATFORM_PLUGIN_PATH = "${pkgs.libsForQt5.qt5.qtbase.bin}/lib/qt-${pkgs.libsForQt5.qt5.qtbase.version}/plugins";
# QTWEBENGINE_CHROMIUM_FLAGS = "--no-sandbox";
};
};
} // lib.optionalAttrs (type != "nixos") {
sops.secrets = lib.mkIf (!config.swarselsystems.isPublic) {
croc-password = { };
github-nixpkgs-review-token = { };
};
});
}
4.5.2.25. bash
{ config, lib, ... }:
{
options.swarselmodules.bash = lib.mkEnableOption "bash settings";
config = lib.mkIf config.swarselmodules.bash {
programs.bash = {
enable = true;
# needed for remote builders
bashrcExtra = lib.mkIf (!config.swarselsystems.isNixos) ''
export PATH="/nix/var/nix/profiles/default/bin:$PATH"
'';
historyFile = "${config.home.homeDirectory}/.histfile";
historySize = 100000;
historyFileSize = 100000;
historyControl = [
"ignoreboth"
];
};
};
}
4.5.2.26. zellij
4.5.2.26.1. Main config
{ self, lib, config, pkgs, ... }:
{
options.swarselmodules.zellij = lib.mkEnableOption "zellij settings";
config = lib.mkIf config.swarselmodules.zellij {
programs.zellij = {
enable = true;
enableZshIntegration = true;
settings = {
pane_frames = false;
simplified_ui = false;
default_shell = "zsh";
copy_on_select = true;
on_force_close = "detach";
show_startup_tips = false;
support_kitty_keyboard_protocol = true;
default_layout = "swarsel";
layout_dir = "${config.home.homeDirectory}/.config/zellij/layouts";
theme_dir = "${config.home.homeDirectory}/.config/zellij/themes";
scrollback_lines_to_serialize = config.programs.kitty.settings.scrollback_lines;
session_serialization = true;
copy_command =
if pkgs.stdenv.hostPlatform.isLinux then
"wl-copy"
else if pkgs.stdenv.hostPlatform.isDarwin then
"pbcopy"
else
"";
ui.pane_frames = {
rounded_corners = true;
hide_session_name = true;
};
plugins = {
tab-bar.path = "tab-bar";
status-bar.path = "status-bar";
strider.path = "strider";
compact-bar.path = "compact-bar";
# configuration.path = "configuration";
# filepicker.path = "strider";
# plugin-manager.path = "plugin-manager";
# session-manager.path = "session-manager";
# welcome-screen.path = "session-manager";
};
};
};
home.packages = with pkgs; [
zjstatus
];
xdg.configFile = {
# "zellij/config.kdl".text = import "${self}/files/zellij/config.kdl.nix" { inherit config; };
"zellij/layouts/swarsel.kdl".text = import "${self}/files/zellij/layouts/swarsel.kdl.nix" { inherit config pkgs; };
};
};
}
4.5.2.26.2. Keybinds
{ lib, config, ... }:
{
config = lib.mkIf config.swarselmodules.zellij {
programs.zellij = {
settings.keybinds = {
_props.clear-defaults = true;
locked = {
_children = [
{
bind = {
_args = [ "Ctrl g" ];
_children = [{ SwitchToMode._args = [ "normal" ]; }];
};
}
];
};
pane = {
_children = [
{
bind = {
_args = [ "Ctrl p" ];
_children = [{ SwitchToMode._args = [ "normal" ]; }];
};
}
{
bind = {
_args = [ "left" ];
_children = [{ MoveFocus._args = [ "left" ]; }];
};
}
{
bind = {
_args = [ "down" ];
_children = [{ MoveFocus._args = [ "down" ]; }];
};
}
{
bind = {
_args = [ "up" ];
_children = [{ MoveFocus._args = [ "up" ]; }];
};
}
{
bind = {
_args = [ "right" ];
_children = [{ MoveFocus._args = [ "right" ]; }];
};
}
{
bind = {
_args = [ "h" ];
_children = [{ MoveFocus._args = [ "left" ]; }];
};
}
{
bind = {
_args = [ "j" ];
_children = [{ MoveFocus._args = [ "down" ]; }];
};
}
{
bind = {
_args = [ "k" ];
_children = [{ MoveFocus._args = [ "up" ]; }];
};
}
{
bind = {
_args = [ "l" ];
_children = [{ MoveFocus._args = [ "right" ]; }];
};
}
{
bind = {
_args = [ "d" ];
_children = [
{ NewPane._args = [ "down" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "e" ];
_children = [
{ TogglePaneEmbedOrFloating = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "f" ];
_children = [
{ ToggleFocusFullscreen = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "n" ];
_children = [
{ NewPane = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "p" ];
_children = [{ SwitchFocus = { }; }];
};
}
{
bind = {
_args = [ "f12" ];
_children = [
{ ToggleFloatingPanes = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
];
};
tab = {
_children = [
{
bind = {
_args = [ "Ctrl t" ];
_children = [{ SwitchToMode._args = [ "normal" ]; }];
};
}
{
bind = {
_args = [ "left" ];
_children = [{ GoToPreviousTab = { }; }];
};
}
{
bind = {
_args = [ "down" ];
_children = [{ GoToNextTab = { }; }];
};
}
{
bind = {
_args = [ "up" ];
_children = [{ GoToPreviousTab = { }; }];
};
}
{
bind = {
_args = [ "right" ];
_children = [{ GoToNextTab = { }; }];
};
}
{
bind = {
_args = [ "1" ];
_children = [
{ GoToTab._args = [ 1 ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "2" ];
_children = [
{ GoToTab._args = [ 2 ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "3" ];
_children = [
{ GoToTab._args = [ 3 ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "4" ];
_children = [
{ GoToTab._args = [ 4 ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "5" ];
_children = [
{ GoToTab._args = [ 5 ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "6" ];
_children = [
{ GoToTab._args = [ 6 ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "7" ];
_children = [
{ GoToTab._args = [ 7 ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "8" ];
_children = [
{ GoToTab._args = [ 8 ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "9" ];
_children = [
{ GoToTab._args = [ 9 ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "h" ];
_children = [{ GoToPreviousTab = { }; }];
};
}
{
bind = {
_args = [ "j" ];
_children = [{ GoToNextTab = { }; }];
};
}
{
bind = {
_args = [ "k" ];
_children = [{ GoToPreviousTab = { }; }];
};
}
{
bind = {
_args = [ "l" ];
_children = [{ GoToNextTab = { }; }];
};
}
{
bind = {
_args = [ "n" ];
_children = [
{ NewTab = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "r" ];
_children = [
{ SwitchToMode._args = [ "renametab" ]; }
{ TabNameInput._args = [ 0 ]; }
];
};
}
{
bind = {
_args = [ "s" ];
_children = [
{ ToggleActiveSyncTab = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "x" ];
_children = [
{ CloseTab = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
];
};
resize = {
_children = [
{
bind = {
_args = [ "Ctrl n" ];
_children = [{ SwitchToMode._args = [ "normal" ]; }];
};
}
{
bind = {
_args = [ "left" ];
_children = [{ Resize._args = [ "Increase left" ]; }];
};
}
{
bind = {
_args = [ "down" ];
_children = [{ Resize._args = [ "Increase down" ]; }];
};
}
{
bind = {
_args = [ "up" ];
_children = [{ Resize._args = [ "Increase up" ]; }];
};
}
{
bind = {
_args = [ "right" ];
_children = [{ Resize._args = [ "Increase right" ]; }];
};
}
{
bind = {
_args = [ "+" ];
_children = [{ Resize._args = [ "Increase" ]; }];
};
}
{
bind = {
_args = [ "-" ];
_children = [{ Resize._args = [ "Decrease" ]; }];
};
}
{
bind = {
_args = [ "=" ];
_children = [{ Resize._args = [ "Increase" ]; }];
};
}
{
bind = {
_args = [ "H" ];
_children = [{ Resize._args = [ "Decrease left" ]; }];
};
}
{
bind = {
_args = [ "J" ];
_children = [{ Resize._args = [ "Decrease down" ]; }];
};
}
{
bind = {
_args = [ "K" ];
_children = [{ Resize._args = [ "Decrease up" ]; }];
};
}
{
bind = {
_args = [ "L" ];
_children = [{ Resize._args = [ "Decrease right" ]; }];
};
}
{
bind = {
_args = [ "h" ];
_children = [{ Resize._args = [ "Increase left" ]; }];
};
}
{
bind = {
_args = [ "j" ];
_children = [{ Resize._args = [ "Increase down" ]; }];
};
}
{
bind = {
_args = [ "k" ];
_children = [{ Resize._args = [ "Increase up" ]; }];
};
}
{
bind = {
_args = [ "l" ];
_children = [{ Resize._args = [ "Increase right" ]; }];
};
}
];
};
move = {
_children = [
{
bind = {
_args = [ "Ctrl h" ];
_children = [{ SwitchToMode._args = [ "normal" ]; }];
};
}
{
bind = {
_args = [ "left" ];
_children = [{ MovePane._args = [ "left" ]; }];
};
}
{
bind = {
_args = [ "down" ];
_children = [{ MovePane._args = [ "down" ]; }];
};
}
{
bind = {
_args = [ "up" ];
_children = [{ MovePane._args = [ "up" ]; }];
};
}
{
bind = {
_args = [ "right" ];
_children = [{ MovePane._args = [ "right" ]; }];
};
}
{
bind = {
_args = [ "h" ];
_children = [{ MovePane._args = [ "left" ]; }];
};
}
{
bind = {
_args = [ "j" ];
_children = [{ MovePane._args = [ "down" ]; }];
};
}
{
bind = {
_args = [ "k" ];
_children = [{ MovePane._args = [ "up" ]; }];
};
}
{
bind = {
_args = [ "l" ];
_children = [{ MovePane._args = [ "right" ]; }];
};
}
];
};
scroll = {
_children = [
{
bind = {
_args = [ "e" ];
_children = [
{ EditScrollback = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "s" ];
_children = [
{ SwitchToMode._args = [ "entersearch" ]; }
{ SearchInput._args = [ 0 ]; }
];
};
}
];
};
search = {
_children = [
{
bind = {
_args = [ "c" ];
_children = [{ SearchToggleOption._args = [ "CaseSensitivity" ]; }];
};
}
{
bind = {
_args = [ "n" ];
_children = [{ Search._args = [ "down" ]; }];
};
}
{
bind = {
_args = [ "o" ];
_children = [{ SearchToggleOption._args = [ "WholeWord" ]; }];
};
}
{
bind = {
_args = [ "p" ];
_children = [{ Search._args = [ "up" ]; }];
};
}
{
bind = {
_args = [ "w" ];
_children = [{ SearchToggleOption._args = [ "Wrap" ]; }];
};
}
];
};
session = {
_children = [
{
bind = {
_args = [ "Ctrl o" ];
_children = [{ SwitchToMode._args = [ "normal" ]; }];
};
}
{
bind = {
_args = [ "c" ];
_children = [
{
LaunchOrFocusPlugin._args = [ "configuration" ];
LaunchOrFocusPlugin._children = [
{ floating._args = [ true ]; }
{ move_to_focused_tab._args = [ true ]; }
];
}
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "p" ];
_children = [
{
LaunchOrFocusPlugin._args = [ "plugin-manager" ];
LaunchOrFocusPlugin._children = [
{ floating._args = [ true ]; }
{ move_to_focused_tab._args = [ true ]; }
];
}
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "w" ];
_children = [
{
LaunchOrFocusPlugin._args = [ "session-manager" ];
LaunchOrFocusPlugin._children = [
{ floating._args = [ true ]; }
{ move_to_focused_tab._args = [ true ]; }
];
}
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
];
};
"shared_except \"locked\"" = {
_children = [
{
bind = {
_args = [ "Alt left" ];
_children = [{ MoveFocusOrTab._args = [ "left" ]; }];
};
}
{
bind = {
_args = [ "Alt down" ];
_children = [{ MoveFocus._args = [ "down" ]; }];
};
}
{
bind = {
_args = [ "Alt up" ];
_children = [{ MoveFocus._args = [ "up" ]; }];
};
}
{
bind = {
_args = [ "Alt right" ];
_children = [{ MoveFocusOrTab._args = [ "right" ]; }];
};
}
{
bind = {
_args = [ "Alt +" ];
_children = [{ Resize._args = [ "Increase" ]; }];
};
}
{
bind = {
_args = [ "Alt -" ];
_children = [{ Resize._args = [ "Decrease" ]; }];
};
}
{
bind = {
_args = [ "Alt =" ];
_children = [{ Resize._args = [ "Increase" ]; }];
};
}
{
bind = {
_args = [ "Alt r" ];
_children = [
{
WriteChars._args = [ "source cdr" ];
}
{
WriteChars._args = [ "\n" ];
}
];
};
}
{
bind = {
_args = [ "Alt f" ];
_children = [{ ToggleFloatingPanes = { }; }];
};
}
{
bind = {
_args = [ "Ctrl g" ];
_children = [{ SwitchToMode._args = [ "locked" ]; }];
};
}
{
bind = {
_args = [ "Alt h" ];
_children = [{ MoveFocusOrTab._args = [ "left" ]; }];
};
}
{
bind = {
_args = [ "Alt i" ];
_children = [{ MoveTab._args = [ "left" ]; }];
};
}
{
bind = {
_args = [ "Alt j" ];
_children = [{ MoveFocus._args = [ "down" ]; }];
};
}
{
bind = {
_args = [ "Alt k" ];
_children = [{ MoveFocus._args = [ "up" ]; }];
};
}
{
bind = {
_args = [ "Alt p" ];
_children = [{ NewPane = { }; }];
};
}
{
bind = {
_args = [ "Alt n" ];
_children = [{ NewTab = { }; }];
};
}
];
};
"shared_except \"locked\" \"move\"" = {
_children = [
{
bind = {
_args = [ "Ctrl h" ];
_children = [{ SwitchToMode._args = [ "move" ]; }];
};
}
];
};
"shared_except \"locked\" \"session\"" = {
_children = [
{
bind = {
_args = [ "Ctrl o" ];
_children = [{ SwitchToMode._args = [ "session" ]; }];
};
}
];
};
"shared_except \"locked\" \"scroll\" \"search\" \"tmux\"" = {
_children = [
{
bind = {
_args = [ "Ctrl b" ];
_children = [{ SwitchToMode._args = [ "tmux" ]; }];
};
}
];
};
"shared_except \"locked\" \"scroll\" \"search\"" = {
_children = [
{
bind = {
_args = [ "Ctrl s" ];
_children = [{ SwitchToMode._args = [ "scroll" ]; }];
};
}
];
};
"shared_except \"locked\" \"tab\"" = {
_children = [
{
bind = {
_args = [ "Ctrl t" ];
_children = [{ SwitchToMode._args = [ "tab" ]; }];
};
}
];
};
"shared_except \"locked\" \"pane\"" = {
_children = [
{
bind = {
_args = [ "Ctrl p" ];
_children = [{ SwitchToMode._args = [ "pane" ]; }];
};
}
];
};
"shared_except \"locked\" \"resize\"" = {
_children = [
{
bind = {
_args = [ "Ctrl n" ];
_children = [{ SwitchToMode._args = [ "resize" ]; }];
};
}
];
};
"shared_except \"normal\" \"locked\" \"entersearch\"" = {
_children = [
{
bind = {
_args = [ "enter" ];
_children = [{ SwitchToMode._args = [ "normal" ]; }];
};
}
];
};
"shared_except \"normal\" \"locked\" \"entersearch\" \"renametab\" \"renamepane\"" = {
_children = [
{
bind = {
_args = [ "esc" ];
_children = [{ SwitchToMode._args = [ "normal" ]; }];
};
}
];
};
"shared_among \"pane\" \"tmux\"" = {
_children = [
{
bind = {
_args = [ "x" ];
_children = [
{ CloseFocus = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
];
};
"shared_among \"scroll\" \"search\"" = {
_children = [
{
bind = {
_args = [ "PageDown" ];
_children = [{ PageScrollDown = { }; }];
};
}
{
bind = {
_args = [ "PageUp" ];
_children = [{ PageScrollUp = { }; }];
};
}
{
bind = {
_args = [ "left" ];
_children = [{ PageScrollUp = { }; }];
};
}
{
bind = {
_args = [ "down" ];
_children = [{ ScrollDown = { }; }];
};
}
{
bind = {
_args = [ "up" ];
_children = [{ ScrollUp = { }; }];
};
}
{
bind = {
_args = [ "right" ];
_children = [{ PageScrollDown = { }; }];
};
}
{
bind = {
_args = [ "Ctrl b" ];
_children = [{ PageScrollUp = { }; }];
};
}
{
bind = {
_args = [ "Ctrl c" ];
_children = [
{ ScrollToBottom = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "d" ];
_children = [{ HalfPageScrollDown = { }; }];
};
}
{
bind = {
_args = [ "Ctrl f" ];
_children = [{ PageScrollDown = { }; }];
};
}
{
bind = {
_args = [ "h" ];
_children = [{ PageScrollUp = { }; }];
};
}
{
bind = {
_args = [ "j" ];
_children = [{ ScrollDown = { }; }];
};
}
{
bind = {
_args = [ "k" ];
_children = [{ ScrollUp = { }; }];
};
}
{
bind = {
_args = [ "l" ];
_children = [{ PageScrollDown = { }; }];
};
}
{
bind = {
_args = [ "Ctrl s" ];
_children = [{ SwitchToMode._args = [ "normal" ]; }];
};
}
{
bind = {
_args = [ "u" ];
_children = [{ HalfPageScrollUp = { }; }];
};
}
];
};
entersearch = {
_children = [
{
bind = {
_args = [ "Ctrl c" ];
_children = [{ SwitchToMode._args = [ "scroll" ]; }];
};
}
{
bind = {
_args = [ "esc" ];
_children = [{ SwitchToMode._args = [ "scroll" ]; }];
};
}
{
bind = {
_args = [ "enter" ];
_children = [{ SwitchToMode._args = [ "search" ]; }];
};
}
];
};
renametab = {
_children = [
{
bind = {
_args = [ "esc" ];
_children = [
{ UndoRenameTab = { }; }
{ SwitchToMode._args = [ "tab" ]; }
];
};
}
];
};
"shared_among \"renametab\" \"renamepane\"" = {
_children = [
{
bind = {
_args = [ "Ctrl c" ];
_children = [{ SwitchToMode._args = [ "normal" ]; }];
};
}
];
};
renamepane = {
_children = [
{
bind = {
_args = [ "esc" ];
_children = [
{ UndoRenamePane = { }; }
{ SwitchToMode._args = [ "pane" ]; }
];
};
}
];
};
"shared_among \"session\" \"tmux\"" = {
_children = [
{
bind = {
_args = [ "d" ];
_children = [{ Detach = { }; }];
};
}
];
};
tmux = {
_children = [
{
bind = {
_args = [ "left" ];
_children = [
{ MoveFocus._args = [ "left" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "down" ];
_children = [
{ MoveFocus._args = [ "down" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "up" ];
_children = [
{ MoveFocus._args = [ "up" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "right" ];
_children = [
{ MoveFocus._args = [ "right" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "space" ];
_children = [{ NextSwapLayout = { }; }];
};
}
{
bind = {
_args = [ "\"" ];
_children = [
{ NewPane._args = [ "down" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "%" ];
_children = [
{ NewPane._args = [ "right" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "," ];
_children = [{ SwitchToMode._args = [ "renametab" ]; }];
};
}
{
bind = {
_args = [ "[" ];
_children = [{ SwitchToMode._args = [ "scroll" ]; }];
};
}
{
bind = {
_args = [ "Ctrl b" ];
_children = [
{ Write._args = [ 2 ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "c" ];
_children = [
{ NewTab = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "h" ];
_children = [
{ MoveFocus._args = [ "left" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "j" ];
_children = [
{ MoveFocus._args = [ "down" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "k" ];
_children = [
{ MoveFocus._args = [ "up" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "l" ];
_children = [
{ MoveFocus._args = [ "right" ]; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "n" ];
_children = [
{ GoToNextTab = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "o" ];
_children = [{ FocusNextPane = { }; }];
};
}
{
bind = {
_args = [ "p" ];
_children = [
{ GoToPreviousTab = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
{
bind = {
_args = [ "z" ];
_children = [
{ ToggleFocusFullscreen = { }; }
{ SwitchToMode._args = [ "normal" ]; }
];
};
}
];
};
};
};
};
}
4.5.2.27. tmux
{ lib, config, pkgs, ... }:
let
tmux-super-fingers = pkgs.tmuxPlugins.mkTmuxPlugin
{
pluginName = "tmux-super-fingers";
version = "unstable-2023-01-06";
src = pkgs.fetchFromGitHub {
owner = "artemave";
repo = "tmux_super_fingers";
rev = "2c12044984124e74e21a5a87d00f844083e4bdf7";
sha256 = "sha256-cPZCV8xk9QpU49/7H8iGhQYK6JwWjviL29eWabuqruc=";
};
};
in
{
options.swarselmodules.tmux = lib.mkEnableOption "tmux settings";
config = lib.mkIf config.swarselmodules.tmux {
home.packages = with pkgs; [
lsof
sesh
];
programs.tmux = {
enable = true;
shell = "${pkgs.zsh}/bin/zsh";
terminal = "tmux-256color";
historyLimit = 100000;
plugins = with pkgs;
[
tmuxPlugins.tmux-thumbs
{
plugin = tmux-super-fingers;
extraConfig = "set -g @super-fingers-key f";
}
tmuxPlugins.sensible
# must be before continuum edits right status bar
{
plugin = tmuxPlugins.catppuccin;
extraConfig = ''
set -g @catppuccin_flavour 'frappe'
set -g @catppuccin_window_tabs_enabled on
set -g @catppuccin_date_time "%H:%M"
'';
}
{
plugin = tmuxPlugins.resurrect;
extraConfig = ''
set -g @resurrect-strategy-vim 'session'
set -g @resurrect-strategy-nvim 'session'
set -g @resurrect-capture-pane-contents 'on'
'';
}
{
plugin = tmuxPlugins.continuum;
extraConfig = ''
set -g @continuum-restore 'on'
set -g @continuum-boot 'on'
set -g @continuum-save-interval '10'
'';
}
tmuxPlugins.better-mouse-mode
tmuxPlugins.yank
];
extraConfig = ''
set -g default-terminal "tmux-256color"
set -ag terminal-overrides ",xterm-256color:RGB"
set-option -g prefix C-a
unbind-key C-b
bind-key C-a send-prefix
set -g mouse on
# Open new split at cwd of current split
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
# Use vim keybindings in copy mode
set-window-option -g mode-keys vi
# v in copy mode starts making selection
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel
# Escape turns on copy mode
bind Escape copy-mode
set-option -g status-position top
# make Prefix p paste the buffer.
unbind p
bind p paste-buffer
'';
};
};
}
4.5.2.28. Mail
Normally I use 4 mail accounts - here I set them all up. Three of them are Google accounts (sadly), which are a chore to setup. The last is just a sender account that I setup SMTP for here.
{ lib, config, globals, confLib, type, ... }:
let
inherit (confLib.getConfig.repo.secrets.common.mail) address1 address2 address2-name address3 address3-name address4;
inherit (confLib.getConfig.repo.secrets.common) fullName;
inherit (config.swarselsystems) xdgDir;
in
{
options.swarselmodules.mail = lib.mkEnableOption "mail settings";
config = lib.mkIf config.swarselmodules.mail
({
programs = {
mbsync = {
enable = true;
};
msmtp = {
enable = true;
};
mu = {
enable = true;
};
};
services.mbsync = {
enable = true;
};
# this is needed so that mbsync can use the passwords from sops
systemd.user.services.mbsync.Unit.After = [ "sops-nix.service" ];
programs.thunderbird = {
enable = true;
profiles.default = {
isDefault = true;
withExternalGnupg = true;
settings = {
"mail.identity.default.archive_enabled" = true;
"mail.identity.default.archive_keep_folder_structure" = true;
"mail.identity.default.compose_html" = false;
"mail.identity.default.protectSubject" = true;
"mail.identity.default.reply_on_top" = 1;
"mail.identity.default.sig_on_reply" = false;
"mail.identity.default.sig_bottom" = false;
"gfx.webrender.all" = true;
"gfx.webrender.enabled" = true;
};
};
settings = {
"mail.server.default.allow_utf8_accept" = true;
"mail.server.default.max_articles" = 1000;
"mail.server.default.check_all_folders_for_new" = true;
"mail.show_headers" = 1;
"mail.identity.default.auto_quote" = true;
"mail.identity.default.attachPgpKey" = true;
"mailnews.default_sort_order" = 2;
"mailnews.default_sort_type" = 18;
"mailnews.default_view_flags" = 0;
"mailnews.sort_threads_by_root" = true;
"mailnews.headers.showMessageId" = true;
"mailnews.headers.showOrganization" = true;
"mailnews.headers.showReferences" = true;
"mailnews.headers.showUserAgent" = true;
"mail.imap.expunge_after_delete" = true;
"mail.server.default.delete_model" = 2;
"mail.warn_on_delete_from_trash" = false;
"mail.warn_on_shift_delete" = false;
"toolkit.telemetry.enabled" = false;
"toolkit.telemetry.rejected" = true;
"toolkit.telemetry.prompted" = 2;
"app.update.auto" = false;
"privacy.donottrackheader.enabled" = true;
};
};
xdg.mimeApps.defaultApplications = {
"x-scheme-handler/mailto" = [ "thunderbird.desktop" ];
"x-scheme-handler/mid" = [ "thunderbird.desktop" ];
"message/rfc822" = [ "thunderbird.desktop" ];
};
accounts = lib.mkIf (config.swarselsystems.isNixos && !config.swarselsystems.isPublic) {
email =
let
defaultSettings = {
imap = {
host = "imap.gmail.com";
port = 993;
tls.enable = true; # SSL/TLS
};
smtp = {
host = "smtp.gmail.com";
port = 465;
tls.enable = true; # SSL/TLS
};
thunderbird = {
enable = true;
profiles = [ "default" ];
};
mu.enable = true;
msmtp = {
enable = true;
};
mbsync = {
enable = true;
create = "maildir";
expunge = "both";
patterns = [ "*" "![Gmail]*" "[Gmail]/Sent Mail" "[Gmail]/Starred" "[Gmail]/All Mail" ];
extraConfig = {
channel = {
Sync = "All";
};
account = {
Timeout = 120;
PipelineDepth = 1;
AuthMechs = "LOGIN";
};
};
};
};
in
{
maildirBasePath = "Mail";
accounts = {
swarsel = {
imap = {
host = globals.services.mailserver.domain;
port = 993;
tls.enable = true; # SSL/TLS
};
smtp = {
host = globals.services.mailserver.domain;
port = 465;
tls.enable = true; # SSL/TLS
};
thunderbird = {
enable = true;
profiles = [ "default" ];
};
address = address4;
userName = address4;
realName = fullName;
passwordCommand = "cat ${confLib.getConfig.sops.secrets.address4-token.path}";
mu.enable = true;
msmtp = {
enable = true;
};
mbsync = {
enable = true;
create = "maildir";
expunge = "both";
patterns = [ "*" ];
extraConfig = {
channel = {
Sync = "All";
};
account = {
Timeout = 120;
PipelineDepth = 1;
AuthMechs = "LOGIN";
};
};
};
};
leon = lib.recursiveUpdate
{
primary = true;
address = address1;
userName = address1;
realName = fullName;
passwordCommand = "cat ${confLib.getConfig.sops.secrets.address1-token.path}";
gpg = {
key = "0x76FD3810215AE097";
signByDefault = true;
};
}
defaultSettings;
nautilus = lib.recursiveUpdate
{
primary = false;
address = address2;
userName = address2;
realName = address2-name;
passwordCommand = "cat ${confLib.getConfig.sops.secrets.address2-token.path}";
}
defaultSettings;
mrswarsel = lib.recursiveUpdate
{
primary = false;
address = address3;
userName = address3;
realName = address3-name;
passwordCommand = "cat ${confLib.getConfig.sops.secrets.address3-token.path}";
}
defaultSettings;
};
};
};
} // lib.optionalAttrs (type != "nixos") {
sops.secrets = lib.mkIf (!config.swarselsystems.isPublic && !config.swarselsystems.isNixos) {
address1-token = { path = "${xdgDir}/secrets/address1-token"; };
address2-token = { path = "${xdgDir}/secrets/address2-token"; };
address3-token = { path = "${xdgDir}/secrets/address3-token"; };
address4-token = { path = "${xdgDir}/secrets/address4-token"; };
};
});
}
4.5.2.29. Home-manager: Emacs
By using the emacs-overlay NixOS module, I can install all Emacs packages that I want to use right through NixOS. This is done by passing my init.el file to the configuration which will then be parsed upon system rebuild, looking for use-package sections in the Elisp code. Also I define here the style of Emacs that I want to run - I am going with native Wayland Emacs here (emacs-pgtk). All of the nice options such as tree-sitter support are enabled by default, so I do not need to adjust the build process.
Lastly, I am defining some more packages here that the parser has problems finding. Also there are some packages that are not in ELPA or MELPA that I still want to use, like calfw and fast-scroll, so I build them here.
{ self, lib, config, pkgs, globals, inputs, type, ... }:
let
inherit (config.swarselsystems) homeDir mainUser isPublic isNixos;
inherit (config.repo.secrets.common.emacs) radicaleUser;
in
{
options.swarselmodules.emacs = lib.mkEnableOption "emacs settings";
config = lib.mkIf config.swarselmodules.emacs ({
# needed for elfeed
# enable emacs overlay for bleeding edge features
# also read init.el file and install use-package packages
home.activation.setupEmacsOrgFiles =
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
set -eu
if [ ! -d ${homeDir}/Org ]; then
${pkgs.coreutils}/bin/install -d -m 0755 ${homeDir}/Org
${pkgs.coreutils}/bin/chown ${mainUser}:syncthing ${homeDir}/Org
fi
# create dummy files to make Emacs calendar work
# these have low modified dates and should be marked as sync-conflicts
for file in "Tasks" "Archive" "Journal"; do
if [ ! -f ${homeDir}/Org/"$file".org ]; then
${pkgs.coreutils}/bin/touch --time=access --time=modify -t 197001010000.00 ${homeDir}/Org/"$file".org
${pkgs.coreutils}/bin/chown ${mainUser}:syncthing ${homeDir}/Org/"$file".org
fi
done
# when the configuration is build again, these sync-conflicts will be cleaned up
for file in $(find ${homeDir}/Org/ -name "*sync-conflict*"); do
${pkgs.coreutils}/bin/rm "$file"
done
'';
programs.emacs = {
enable = true;
package = pkgs.emacsWithPackagesFromUsePackage {
config = self + /files/emacs/init.el;
package = pkgs.emacs-git-pgtk;
alwaysEnsure = true;
alwaysTangle = true;
extraEmacsPackages = epkgs: [
epkgs.mu4e
epkgs.use-package
epkgs.lsp-bridge
epkgs.doom-themes
epkgs.vterm
# pkgs.stable.emacs.pkgs.elpaPackages.tramp # use the unstable version from elpa
epkgs.treesit-grammars.with-all-grammars
# build the rest of the packages myself
# org-calfw is severely outdated on MELPA and throws many warnings on emacs startup
# build the package from the haji-ali fork, which is well-maintained
(epkgs.trivialBuild rec {
pname = "eglot-booster";
version = "main-29-10-2024";
src = pkgs.fetchFromGitHub {
owner = "jdtsmith";
repo = "eglot-booster";
rev = "e6daa6bcaf4aceee29c8a5a949b43eb1b89900ed";
hash = "sha256-PLfaXELkdX5NZcSmR1s/kgmU16ODF8bn56nfTh9g6bs=";
};
packageRequires = [ epkgs.jsonrpc epkgs.eglot ];
})
(inputs.nixpkgs-dev.legacyPackages.${pkgs.system}.emacsPackagesFor pkgs.emacs-git-pgtk).calfw
# epkgs.calfw
# (epkgs.trivialBuild rec {
# pname = "calfw";
# version = "1.0.0-20231002";
# src = pkgs.fetchFromGitHub {
# owner = "haji-ali";
# repo = "emacs-calfw";
# rev = "bc99afee611690f85f0cd0bd33300f3385ddd3d3";
# hash = "sha256-0xMII1KJhTBgQ57tXJks0ZFYMXIanrOl9XyqVmu7a7Y=";
# };
# packageRequires = [ epkgs.howm ];
# })
(epkgs.trivialBuild rec {
pname = "fast-scroll";
version = "1.0.0-20191016";
src = pkgs.fetchFromGitHub {
owner = "ahungry";
repo = "fast-scroll";
rev = "3f6ca0d5556fe9795b74714304564f2295dcfa24";
hash = "sha256-w1wmJW7YwXyjvXJOWdN2+k+QmhXr4IflES/c2bCX3CI=";
};
packageRequires = [ ];
})
];
};
};
services.emacs = {
enable = true;
socketActivation.enable = false;
startWithUserSession = "graphical";
};
} // lib.optionalAttrs (type != "nixos") {
sops = lib.mkIf (!isPublic && !isNixos) {
secrets = {
fever-pw = { path = "${homeDir}/.emacs.d/.fever"; };
emacs-radicale-pw = { };
github-forge-token = { };
};
templates = {
authinfo = {
path = "${homeDir}/.emacs.d/.authinfo";
content = ''
machine ${globals.services.radicale.domain} login ${radicaleUser} password ${config.sops.placeholder.emacs-radicale-pw}
machine api.github.com login ${mainUser}^forge password ${config.sops.placeholder.github-forge-token}
'';
};
};
};
});
}
4.5.2.30. Waybar
Again I am just using the first bar option here that I was able to find good understandable documentation for. Of note is that the `cpu` section's `format` is not defined here, but in section 1 (since not every machine has the same number of cores)
This section is mostly used to deliver the correct information to Waybar. AMD systems have changing hwmon paths that can be specifically set here. Also the cpu count can be set here for Waybars cpu module, but 8 is usually a good setting to show
These are explicit waybar options. Laptops do not need the battery module. However, this leads to a slight problem with theming: my waybar modules alternate their background-color between black and grey. The battery module is usually on grey background. If I were to simply delete that, I would now have two modules on black background. To avoid this, I define a pseudo-module custom/pseudobat that simply shows a static image and calls wlogout on right click. This wastes a little bit of screen space, but that is a price I am willing to pay for consistency.
The rest of the related configuration is found here:
- [BROKEN LINK: h:a9530c81-1976-442b-b597-0b4bed6baf25]
- waybarupdate
{ self, config, lib, pkgs, type, ... }:
let
inherit (config.swarselsystems) xdgDir;
generateIcons = n: lib.concatStringsSep " " (builtins.map (x: "{icon" + toString x + "}") (lib.range 0 (n - 1)));
modulesLeft = [
"custom/outer-left-arrow-dark"
"mpris"
"custom/left-arrow-light"
"network"
"custom/vpn"
"custom/left-arrow-dark"
"pulseaudio"
"custom/left-arrow-light"
];
modulesRight = [
"custom/left-arrow-dark"
"group/hardware"
"custom/left-arrow-light"
"clock#2"
"custom/left-arrow-dark"
"clock#1"
];
in
{
options.swarselmodules.waybar = lib.mkEnableOption "waybar settings";
options.swarselsystems = {
cpuCount = lib.mkOption {
type = lib.types.int;
default = 8;
};
temperatureHwmon = {
isAbsolutePath = lib.mkEnableOption "absolute temperature path";
path = lib.mkOption {
type = lib.types.str;
default = "";
};
input-filename = lib.mkOption {
type = lib.types.str;
default = "";
};
};
waybarModules = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = modulesLeft ++ [
"custom/pseudobat"
] ++ modulesRight;
};
cpuString = lib.mkOption {
type = lib.types.str;
default = generateIcons config.swarselsystems.cpuCount;
description = "The generated icons string for use by Waybar.";
internal = true;
};
};
config = lib.mkIf config.swarselmodules.waybar ({
swarselsystems = {
waybarModules = lib.mkIf config.swarselsystems.isLaptop (modulesLeft ++ [
"battery"
] ++ modulesRight);
};
services.playerctld.enable = true;
programs.waybar = {
enable = true;
systemd = {
enable = false;
# target = "sway-session.target";
inherit (config.wayland.systemd) target;
};
settings = {
mainBar = {
ipc = true;
id = "bar-0";
# mode = "hide";
# mode = "overlay";
# passthrough = false;
# start_hidden = true;
layer = "top";
position = "top";
modules-left = [ "sway/workspaces" "niri/workspaces" "custom/outer-right-arrow-dark" "niri/window" "sway/window" ];
modules-center = [ "sway/mode" "privacy" "custom/github" "custom/configwarn" "custom/nix-updates" ];
"sway/mode" = {
format = "<span style=\"italic\" font-weight=\"bold\">{}</span>";
};
"niri/window" = {
format = "<span style=\"italic\" font-weight=\"bold\">{title} ({app_id})</span>";
};
modules-right = config.swarselsystems.waybarModules;
"custom/pseudobat" = lib.mkIf (!config.swarselsystems.isLaptop) {
format = "";
on-click-right = "${pkgs.wlogout}/bin/wlogout -p layer-shell";
};
"custom/configwarn" = {
exec = "${pkgs.waybarupdate}/bin/waybarupdate";
interval = 60;
};
"custom/scratchpad-indicator" = {
interval = 3;
exec = "${pkgs.swayfx}/bin/swaymsg -t get_tree | ${pkgs.jq}/bin/jq 'recurse(.nodes[]) | first(select(.name==\"__i3_scratch\")) | .floating_nodes | length | select(. >= 1)'";
format = "{} ";
on-click = "${pkgs.swayfx}/bin/swaymsg 'scratchpad show'";
on-click-right = "${pkgs.swayfx}/bin/swaymsg 'move scratchpad'";
};
"custom/github" = {
format = "{} ";
return-type = "json";
interval = 60;
exec = "${pkgs.github-notifications}/bin/github-notifications";
on-click = "${pkgs.xdg-utils}/bin/xdg-open https://github.com/notifications";
};
idle_inhibitor = {
format = "{icon}";
format-icons = {
activated = "";
deactivated = "";
};
};
"group/hardware" = {
orientation = "inherit";
drawer = {
"transition-left-to-right" = false;
};
modules = [
"tray"
"temperature"
"power-profiles-daemon"
"custom/left-arrow-light"
"custom/left-arrow-dark"
"custom/scratchpad-indicator"
"custom/left-arrow-light"
"disk"
"custom/left-arrow-dark"
"memory"
"custom/left-arrow-light"
"cpu"
"custom/left-arrow-dark"
"backlight/slider"
"idle_inhibitor"
];
};
"backlight/slider" = {
min = 0;
max = 100;
orientation = "horizontal";
device = "intel_backlight";
};
power-profiles-daemon = {
format = "{icon}";
tooltip-format = "Power profile: {profile}\nDriver: {driver}";
tooltip = true;
format-icons = {
"default" = "";
"performance" = "";
"balanced" = "";
"power-saver" = "";
};
};
temperature = {
hwmon-path = lib.mkIf (!config.swarselsystems.temperatureHwmon.isAbsolutePath) config.swarselsystems.temperatureHwmon.path;
hwmon-path-abs = lib.mkIf config.swarselsystems.temperatureHwmon.isAbsolutePath config.swarselsystems.temperatureHwmon.path;
input-filename = lib.mkIf config.swarselsystems.temperatureHwmon.isAbsolutePath config.swarselsystems.temperatureHwmon.input-filename;
critical-threshold = 80;
format-critical = " {temperatureC}°C";
format = " {temperatureC}°C";
};
mpris = {
format = "{player_icon} {title} <small>[{position}/{length}]</small>";
format-paused = "{player_icon} <i>{title} <small>[{position}/{length}]</small></i>";
player-icons = {
"default" = "▶ ";
"mpv" = "🎵 ";
"spotify" = " ";
};
status-icons = {
"paused" = " ";
};
interval = 1;
title-len = 20;
artist-len = 20;
album-len = 10;
};
"custom/left-arrow-dark" = {
format = "";
tooltip = false;
};
"custom/outer-left-arrow-dark" = {
format = "";
tooltip = false;
};
"custom/left-arrow-light" = {
format = "";
tooltip = false;
};
"custom/right-arrow-dark" = {
format = "";
tooltip = false;
};
"custom/outer-right-arrow-dark" = {
format = "";
tooltip = false;
};
"custom/right-arrow-light" = {
format = "";
tooltip = false;
};
"sway/workspaces" = {
disable-scroll = true;
format = "{name}";
};
"clock#1" = {
min-length = 8;
interval = 1;
format = "{:%H:%M:%S}";
# on-click-right= "gnome-clocks";
tooltip-format = "<big>{:%Y %B}</big>\n<tt><small>{calendar}</small></tt>";
};
"clock#2" = {
format = "{:%d. %B %Y}";
# on-click-right= "gnome-clocks";
tooltip-format = "<big>{:%Y %B}</big>\n<tt><small>{calendar}</small></tt>";
};
pulseaudio = {
format = "{icon} {volume:2}%";
format-bluetooth = "{icon} {volume}%";
format-muted = "MUTE";
format-icons = {
headphones = "";
default = [
""
""
];
};
scroll-step = 1;
on-click = "${pkgs.pamixer}/bin/pamixer -t";
on-click-right = "${pkgs.pavucontrol}/bin/pavucontrol";
};
memory = {
interval = 5;
format = " {}%";
tooltip-format = "Memory: {used:0.1f}G/{total:0.1f}G\nSwap: {swapUsed}G/{swapTotal}G";
};
cpu = {
format = config.swarselsystems.cpuString;
min-length = 6;
interval = 5;
format-icons = [ "▁" "▂" "▃" "▄" "▅" "▆" "▇" "█" ];
# on-click-right= "com.github.stsdc.monitor";
on-click-right = "${pkgs.kitty}/bin/kitty -o confirm_os_window_close=0 btm";
};
"custom/vpn" = {
format = "()";
exec = "echo '{\"class\": \"connected\"}'";
exec-if = "${pkgs.toybox}/bin/test -d /proc/sys/net/ipv4/conf/tun0";
return-type = "json";
interval = 5;
};
battery = {
states = {
"warning" = 60;
"error" = 30;
"critical" = 15;
};
interval = 5;
format = "{icon} {capacity}%";
format-charging = "{capacity}% ";
format-plugged = "{capacity}% ";
format-icons = [
""
""
""
""
""
];
on-click-right = "wlogout -p layer-shell";
};
disk = {
interval = 30;
format = "Disk {percentage_used:2}%";
path = "/";
states = {
"warning" = 80;
"critical" = 90;
};
tooltip-format = "{used} used out of {total} on {path} ({percentage_used}%)\n{free} free on {path} ({percentage_free}%)";
};
tray = {
icon-size = 20;
};
network = {
interval = 5;
format-wifi = "{signalStrength}% ";
format-ethernet = "";
format-linked = "{ifname} (No IP) ";
format-disconnected = "Disconnected ⚠";
format-alt = "{ifname}: {ipaddr}/{cidr}";
tooltip-format-ethernet = "{ifname} via {gwaddr}: {essid} {ipaddr}/{cidr}\n\n⇡{bandwidthUpBytes} ⇣{bandwidthDownBytes}";
tooltip-format-wifi = "{ifname} via {gwaddr}: {essid} {ipaddr}/{cidr} \n{signaldBm}dBm @ {frequency}MHz\n\n⇡{bandwidthUpBytes} ⇣{bandwidthDownBytes}";
};
};
};
style = builtins.readFile (self + /files/waybar/style.css);
};
} // lib.optionalAttrs (type != "nixos") {
sops.secrets = lib.mkIf (!config.swarselsystems.isPublic && !config.swarselsystems.isNixos) {
github-notifications-token = { path = "${xdgDir}/secrets/github-notifications-token"; };
};
});
}
4.5.2.31. Firefox
Setting up firefox along with some policies that are important to me (mostly disabling telemetry related stuff as well as Pocket). I also enable some integrations that enable super useful packages, namely tridactyl and browserpass.
Also, using NUR with rycee's firefox addons, it is very convenient for me to add firefox addons here that will be automatically installed.
Also, I setup some search aliases for functions I often use, such as NixOS options search (@no)
I used to build the firefox addon bypass-paywalls-clean myself here, but the maintainer always deletes old packages, and it became a chore for me to maintain here, so I no longer do that.
{ config, pkgs, lib, vars, ... }:
{
options.swarselmodules.firefox = lib.mkEnableOption "firefox settings";
config = lib.mkIf config.swarselmodules.firefox {
programs.zsh.sessionVariables = {
MOZ_DISABLE_RDD_SANDBOX = "1";
};
programs.firefox = {
enable = true;
package = pkgs.firefox; # uses overrides
policies = {
# CaptivePortal = false;
AppAutoUpdate = false;
BackgroundAppUpdate = false;
DisableBuiltinPDFViewer = true;
DisableFirefoxStudies = true;
DisablePocket = true;
DisableFirefoxScreenshots = true;
DisableTelemetry = true;
DisableFirefoxAccounts = false;
DisableProfileImport = true;
DisableProfileRefresh = true;
DisplayBookmarksToolbar = "always";
DontCheckDefaultBrowser = true;
NoDefaultBookmarks = true;
OfferToSaveLogins = false;
OfferToSaveLoginsDefault = false;
PasswordManagerEnabled = false;
DisableMasterPasswordCreation = true;
ExtensionUpdate = false;
EnableTrackingProtection = {
Value = true;
Locked = true;
Cryptomining = true;
Fingerprinting = true;
EmailTracking = true;
# Exceptions = ["https://example.com"]
};
PDFjs = {
Enabled = false;
EnablePermissions = false;
};
Handlers = {
mimeTypes."application/pdf".action = "saveToDisk";
};
extensions = {
pdf = {
action = "useHelperApp";
ask = true;
handlers = [
{
name = "GNOME Document Viewer";
path = "${pkgs.evince}/bin/evince";
}
];
};
};
FirefoxHome = {
Search = true;
TopSites = true;
SponsoredTopSites = false;
Highlights = true;
Pocket = false;
SponsoredPocket = false;
Snippets = false;
Locked = true;
};
FirefoxSuggest = {
WebSuggestions = false;
SponsoredSuggestions = false;
ImproveSuggest = false;
Locked = true;
};
SanitizeOnShutdown = {
Cache = true;
Cookies = false;
Downloads = true;
FormData = true;
History = false;
Sessions = false;
SiteSettings = false;
OfflineApps = true;
Locked = true;
};
SearchEngines = {
PreventInstalls = true;
Remove = [
"Bing" # Fuck you
];
};
UserMessaging = {
ExtensionRecommendations = false; # Don’t recommend extensions while the user is visiting web pages
FeatureRecommendations = false; # Don’t recommend browser features
Locked = true; # Prevent the user from changing user messaging preferences
MoreFromMozilla = false; # Don’t show the “More from Mozilla” section in Preferences
SkipOnboarding = true; # Don’t show onboarding messages on the new tab page
UrlbarInterventions = false; # Don’t offer suggestions in the URL bar
WhatsNew = false; # Remove the “What’s New” icon and menuitem
};
ExtensionSettings = {
"3rdparty".Extensions = {
# https://github.com/gorhill/uBlock/blob/master/platform/common/managed_storage.json
"uBlock0@raymondhill.net".adminSettings = {
userSettings = rec {
uiTheme = "dark";
uiAccentCustom = true;
uiAccentCustom0 = "#0C8084";
cloudStorageEnabled = lib.mkForce false;
importedLists = [
"https://filters.adtidy.org/extension/ublock/filters/3.txt"
"https://github.com/DandelionSprout/adfilt/raw/master/LegitimateURLShortener.txt"
];
externalLists = lib.concatStringsSep "\n" importedLists;
};
selectedFilterLists = [
"CZE-0"
"adguard-generic"
"adguard-annoyance"
"adguard-social"
"adguard-spyware-url"
"easylist"
"easyprivacy"
"https://github.com/DandelionSprout/adfilt/raw/master/LegitimateURLShortener.txt"
"plowe-0"
"ublock-abuse"
"ublock-badware"
"ublock-filters"
"ublock-privacy"
"ublock-quick-fixes"
"ublock-unbreak"
"urlhaus-1"
];
};
};
};
};
profiles = {
default = lib.recursiveUpdate
{
id = 0;
isDefault = true;
settings = {
"browser.startup.homepage" = "https://lobste.rs";
};
}
vars.firefox;
};
};
};
}
4.5.2.32. Services
Services that can be defined through home-manager should be defined here.
4.5.2.32.1. gnome-keyring
Used for storing sessions in e.g. Nextcloud
{ lib, config, ... }:
{
options.swarselmodules.gnome-keyring = lib.mkEnableOption "gnome keyring settings";
config = lib.mkIf config.swarselmodules.gnome-keyring {
services.gnome-keyring = lib.mkIf (!config.swarselsystems.isNixos) {
enable = true;
};
};
}
4.5.2.32.2. KDE Connect
This enables phone/computer communication, including sending clipboard, files etc. Sadly on Wayland many of the features are broken (like remote control).
{ lib, config, ... }:
{
options.swarselmodules.kdeconnect = lib.mkEnableOption "kdeconnect settings";
config = lib.mkIf config.swarselmodules.kdeconnect {
services.kdeconnect = {
enable = true;
indicator = true;
};
};
}
4.5.2.32.3. Mako
Desktop notifications!
The `extraConfig` section here CANNOT be reindented. This has something to do with how nix handles multiline strings, when indented Mako will fail to start. This might be a mako bug as well.
{ lib, config, ... }:
{
options.swarselmodules.mako = lib.mkEnableOption "mako settings";
config = lib.mkIf config.swarselmodules.mako {
services.mako = {
enable = true;
settings = {
border-radius = 15;
border-size = 1;
default-timeout = 5000;
ignore-timeout = false;
icons = 1;
layer = "overlay";
sort = "-time";
height = 150;
width = 300;
"urgency=low" = {
border-color = lib.mkForce "#cccccc";
};
"urgency=normal" = {
border-color = lib.mkForce "#d08770";
};
"urgency=high" = {
border-color = lib.mkForce "#bf616a";
default-timeout = 3000;
};
"category=mpd" = {
default-timeout = 2000;
group-by = "category";
};
"mode=do-not-disturb" = {
invisible = true;
};
};
};
};
}
4.5.2.32.4. SwayOSD
{ lib, pkgs, config, ... }:
{
options.swarselmodules.swayosd = lib.mkEnableOption "swayosd settings";
config = lib.mkIf config.swarselmodules.swayosd {
services.swayosd = {
enable = true;
package = pkgs.dev.swayosd;
topMargin = 0.5;
};
};
}
4.5.2.32.5. yubikey-touch-detector
{ lib, config, pkgs, ... }:
{
options.swarselmodules.yubikeytouch = lib.mkEnableOption "yubikey touch detector service settings";
config = lib.mkIf config.swarselmodules.yubikeytouch {
systemd.user.services.yubikey-touch-detector = {
Unit = {
Description = "Detects when your YubiKey is waiting for a touch";
Requires = [ "yubikey-touch-detector.socket" ];
};
Service = {
ExecStart = "${pkgs.yubikey-touch-detector}/bin/yubikey-touch-detector --libnotify";
EnvironmentFile = "-%E/yubikey-touch-detector/service.conf";
};
Install = {
Also = [ "yubikey-touch-detector.socket" ];
WantedBy = [ "default.target" ];
};
};
systemd.user.sockets.yubikey-touch-detector = {
Unit = {
Description = "Unix socket activation for YubiKey touch detector service";
};
Socket = {
ListenStream = "%t/yubikey-touch-detector.socket";
RemoveOnStop = true;
};
Install = {
WantedBy = [ "sockets.target" ];
};
};
};
}
4.5.2.32.6. blueman-applet
{ lib, config, ... }:
{
options.swarselmodules.blueman-applet = lib.mkEnableOption "enable blueman applet for tray";
config = lib.mkIf config.swarselmodules.blueman-applet {
services.blueman-applet.enable = true;
};
}
4.5.2.32.7. network-manager-applet
{ lib, config, ... }:
{
options.swarselmodules.nm-applet = lib.mkEnableOption "enable network manager applet for tray";
config = lib.mkIf config.swarselmodules.nm-applet {
services.network-manager-applet.enable = true;
xsession.preferStatusNotifierItems = true; # needed for indicator icon to show
};
}
4.5.2.32.8. obsidian service for tray
{ lib, config, ... }:
{
options.swarselmodules.obsidian-tray = lib.mkEnableOption "enable obsidian applet for tray";
config = lib.mkIf config.swarselmodules.obsidian-tray {
systemd.user.services.obsidian-applet = {
Unit = {
Description = "Obsidian applet";
Requires = [ "tray.target" ];
After = [
"graphical-session.target"
"tray.target"
];
PartOf = [ "graphical-session.target" ];
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${lib.getExe config.programs.obsidian.package}";
};
};
};
}
4.5.2.32.9. anki service for tray
Sets up a systemd user service for anki that does not stall the shutdown process. Note that the outcommented ExecStart does not work because the home-manager anki package builds a separate anki package that - I think - cannot be referenced as no such expression exists in the module.
{ lib, config, ... }:
{
options.swarselmodules.anki-tray = lib.mkEnableOption "enable anki applet for tray";
config = lib.mkIf config.swarselmodules.anki-tray {
systemd.user.services.anki-applet = {
Unit = {
Description = "Anki applet";
Requires = [ "tray.target" ];
After = [
"graphical-session.target"
"tray.target"
];
PartOf = [ "graphical-session.target" ];
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
Service = {
# ExecStart = "${lib.getExe config.programs.anki.package}";
Type = "simple";
ExecStart = "/etc/profiles/per-user/${config.swarselsystems.mainUser}/bin/anki";
Environment = [
"QT_QPA_PLATFORM=xcb"
];
TimeoutStopSec = "2s";
KillMode = "mixed";
KillSignal = "SIGTERM";
SendSIGKILL = "yes";
};
};
};
}
4.5.2.32.10. element service for tray
{ lib, config, pkgs, ... }:
{
options.swarselmodules.element-tray = lib.mkEnableOption "enable element applet for tray";
config = lib.mkIf config.swarselmodules.element-tray {
systemd.user.services.element-applet = {
Unit = {
Description = "Element applet";
Requires = [ "tray.target" ];
After = [
"graphical-session.target"
"tray.target"
];
PartOf = [ "graphical-session.target" ];
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${pkgs.element-desktop}/bin/element-desktop --hidden --enable-features=useozoneplatform --ozone-platform=wayland --disable-gpu-driver-bug-workarounds";
};
};
};
}
4.5.2.32.11. vesktop service for tray
{ lib, config, pkgs, ... }:
{
options.swarselmodules.vesktop-tray = lib.mkEnableOption "enable vesktop applet for tray";
config = lib.mkIf config.swarselmodules.vesktop-tray {
systemd.user.services.vesktop-applet = {
Unit = {
Description = "Vesktop applet";
Requires = [ "tray.target" ];
After = [
"graphical-session.target"
"tray.target"
];
PartOf = [ "graphical-session.target" ];
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${pkgs.vesktop}/bin/vesktop --start-minimized --enable-speech-dispatcher --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime";
};
};
};
}
4.5.2.32.12. firezone service for tray
{ lib, config, pkgs, ... }:
{
options.swarselmodules.firezone-tray = lib.mkEnableOption "enable firezone applet for tray";
config = lib.mkIf config.swarselmodules.firezone-tray {
systemd.user.services.firezone-applet = {
Unit = {
Description = "Firezone applet";
Requires = [
"tray.target"
];
After = [
"graphical-session.target"
"tray.target"
];
PartOf = [ "graphical-session.target" ];
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${pkgs.firezone-gui-client}/bin/firezone-client-gui";
};
};
};
}
4.5.2.32.13. syncthing service for tray
{ lib, config, pkgs, ... }:
{
options.swarselmodules.syncthing-tray = lib.mkEnableOption "enable syncthing applet for tray";
config = lib.mkIf config.swarselmodules.syncthing-tray {
home.activation.setupSyncthingIni =
let
syncthingApiEnvVarName = "SYNCTHING_API_KEY";
syncthingIni = {
file = "${config.home.homeDirectory}/.config/syncthingtray.ini";
content = ''
[General]
v=2.0.2
[qt]
customfont=false
customicontheme=false
customlocale=false
custompalette=false
customstylesheet=false
customwidgetstyle=false
font="Cantarell,11,-1,5,400,0,0,0,0,0,0,0,0,0,0,1"
icontheme=hicolor
iconthemepath=
locale=en_US
palette="@Variant(\0\0\0\x44\x1\x1\xff\xff\xa0\xa0\xb3\xb3\xc5\xc5\0\0\x1\x1\xff\xff jj\x86\x86\0\0\x1\x1\xff\xff\0\0::ff\0\0\x1\x1\xff\xff\0\0::ff\0\0\x1\x1\xff\xff\x1d\x1d%%,,\0\0\x1\x1\xff\xff\x1d\x1d%%,,\0\0\x1\x1\xff\xff\xa0\xa0\xb3\xb3\xc5\xc5\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xa0\xa0\xb3\xb3\xc5\xc5\0\0\x1\x1\xff\xff\x1d\x1d%%,,\0\0\x1\x1\xff\xff\x17\x17\x1d\x1d##\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1\xff\xff\0\0::ff\0\0\x1\x1\xff\xff\xa0\xa0\xb3\xb3\xc5\xc5\0\0\x1\x1\xff\xff^^\xc4\xc4\xff\xff\0\0\x1\x1\xff\xff\xc0\xc0nn\xce\xce\0\0\x1\x1\xff\xff\x17\x17\x1d\x1d##\0\0\x1\x1\xff\xff^^\xc4\xc4\xff\xff\0\0\x1\x1\xff\xff jj\x86\x86\0\0\x1\x1\xff\xff\0\0::ff\0\0\x1\x1\xff\xff\0\0::ff\0\0\x1\x1\xff\xff\x1d\x1d%%,,\0\0\x1\x1\xff\xff\x1d\x1d%%,,\0\0\x1\x1\xff\xff^^\xc4\xc4\xff\xff\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff^^\xc4\xc4\xff\xff\0\0\x1\x1\xff\xff\x1d\x1d%%,,\0\0\x1\x1\xff\xff\x17\x17\x1d\x1d##\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x1\xff\xff\0\0::ff\0\0\x1\x1\x66\x66\xa0\xa0\xb3\xb3\xc5\xc5\0\0\x1\x1\xff\xff^^\xc4\xc4\xff\xff\0\0\x1\x1\xff\xff\xc0\xc0nn\xce\xce\0\0\x1\x1\xff\xff\x17\x17\x1d\x1d##\0\0\x1\x1\xff\xff\xa0\xa0\xb3\xb3\xc5\xc5\0\0\x1\x1\xff\xff jj\x86\x86\0\0\x1\x1\xff\xff\0\0::ff\0\0\x1\x1\xff\xff\0\0::ff\0\0\x1\x1\xff\xff\x1d\x1d%%,,\0\0\x1\x1\xff\xff\x1d\x1d%%,,\0\0\x1\x1\xff\xff\xa0\xa0\xb3\xb3\xc5\xc5\0\0\x1\x1\xff\xff\xff\xff\xff\xff\xff\xff\0\0\x1\x1\xff\xff\xa0\xa0\xb3\xb3\xc5\xc5\0\0\x1\x1\xff\xff\x1d\x1d%%,,\0\0\x1\x1\xff\xff\x17\x17\x1d\x1d##\0\0\x1\x1\xff\xff\0\0\0\0\0\0\0\0\x1\x2\xff\xffP\x14\xff\xff\x65\x65\0\0\x1\x1\xff\xff\xa0\xa0\xb3\xb3\xc5\xc5\0\0\x1\x1\xff\xff^^\xc4\xc4\xff\xff\0\0\x1\x1\xff\xff\xc0\xc0nn\xce\xce\0\0\x1\x1\xff\xff\x17\x17\x1d\x1d##\0\0)"
plugindir=
stylesheetpath=
trpath=
widgetstyle=
[startup]
considerForReconnect=false
considerLauncherForReconnect=false
showButton=false
showLauncherButton=false
stopOnMetered=false
stopServiceOnMetered=false
syncthingArgs="serve --no-browser --logflags=3"
syncthingAutostart=false
syncthingPath=syncthing
syncthingUnit=syncthing.service
systemUnit=false
useLibSyncthing=false
[tray]
connections\1\apiKey=@ByteArray(''$${syncthingApiEnvVarName})
connections\1\authEnabled=falsex
connections\1\autoConnect=true
connections\1\devStatsPollInterval=60000
connections\1\diskEventLimit=200
connections\1\errorsPollInterval=30000
connections\1\httpsCertPath=${config.home.homeDirectory}/.config/syncthing/https-cert.pem
connections\1\label=Primary instance
connections\1\localPath=
connections\1\longPollingTimeout=0
connections\1\password=
connections\1\pauseOnMetered=false
connections\1\reconnectInterval=30000
connections\1\requestTimeout=0
connections\1\statusComputionFlags=123
connections\1\syncthingUrl=http://${config.services.syncthing.guiAddress}
connections\1\trafficPollInterval=5000
connections\1\userName=
connections\size=1
dbusNotifications=true
distinguishTrayIcons=false
frameStyle=16
ignoreInavailabilityAfterStart=15
notifyOnDisconnect=true
notifyOnErrors=true
notifyOnLauncherErrors=true
notifyOnLocalSyncComplete=false
notifyOnNewDeviceConnects=false
notifyOnNewDirectoryShared=false
notifyOnRemoteSyncComplete=false
positioning\assumedIconPos=@Point(0 0)
positioning\useAssumedIconPosition=false
positioning\useCursorPos=true
preferIconsFromTheme=false
showDownloads=false
showSyncthingNotifications=true
showTabTexts=true
showTraffic=true
statusIcons="#ff26b6db,#ff0882c8,#ffffffff;#ffdb3c26,#ffc80828,#ffffffff;#ffc9ce3b,#ffebb83b,#ffffffff;#ff2d9d69,#ff2d9d69,#ffffffff;#ff26b6db,#ff0882c8,#ffffffff;#ff26b6db,#ff0882c8,#ffffffff;#ffa9a9a9,#ff58656c,#ffffffff;#ffa9a9a9,#ff58656c,#ffffffff;#ffa9a9a9,#ff58656c,#ffffffff"
statusIconsRenderSize=@Size(32 32)
statusIconsStrokeWidth=0
tabPos=1
trayIcons="#ff26b6db,#ff0882c8,#ffffffff;#ffdb3c26,#ffc80828,#ffffffff;#ffc9ce3b,#ffebb83b,#ffffffff;#ff2d9d69,#ff2d9d69,#ffffffff;#ff26b6db,#ff0882c8,#ffffffff;#ff26b6db,#ff0882c8,#ffffffff;#ffa9a9a9,#ff58656c,#ffffffff;#ffa9a9a9,#ff58656c,#ffffffff;#ffa9a9a9,#ff58656c,#ffffffff"
trayIconsRenderSize=@Size(32 32)
trayIconsStrokeWidth=0
trayMenuSize=@Size(575 475)
usePaletteForStatusIcons=false
usePaletteForTrayIcons=false
windowType=0
[webview]
customCommand=
disabled=false
mode=0
'';
};
in
lib.hm.dag.entryAfter [ "writeBoundary" ] ''
set -eu
if [ ! -f ${syncthingIni.file} ]; then
cat >${syncthingIni.file} <<'EOF'
${syncthingIni.content}
EOF
export ${syncthingApiEnvVarName}=$(cat /run/syncthing-init/api_key)
${lib.getExe pkgs.envsubst} -i ${syncthingIni.file} -o ${syncthingIni.file}
unset ${syncthingApiEnvVarName}
fi
'';
};
}
4.5.2.32.14. TODO attic store push service
Normally, I want to push all nix build artifacts to my main cache automatically, which is realized here. Note that authentication to the cache must be done manually once on client nodes. TODO: fix that
{ lib, config, pkgs, ... }:
{
options.swarselmodules.attic-store-push = lib.mkEnableOption "enable automatic attic store push";
config = lib.mkIf config.swarselmodules.attic-store-push {
systemd.user.services.attic-store-push = {
Unit = {
Description = "Attic store pusher";
Requires = [ "graphical-session.target" ];
After = [ "graphical-session.target" ];
PartOf = [ "graphical-session.target" ];
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${lib.getExe pkgs.attic-client} watch-store ${config.swarselsystems.mainUser}:${config.swarselsystems.mainUser}";
};
};
};
}
4.5.2.33. Sway
I am currently using SwayFX, which adds some nice effects to sway, like rounded corners and hiding the separator between title and content of a window.
Currently, I am too lazy to explain every option here, but most of it is very self-explaining in any case.
{ config, lib, vars, confLib, ... }:
let
eachOutput = _: monitor: {
inherit (monitor) name;
value = builtins.removeAttrs monitor [ "mode" "name" "scale" "transform" "position" ];
};
in
{
options.swarselmodules.sway = lib.mkEnableOption "sway settings";
options.swarselsystems = {
inputs = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
default = { };
};
monitors = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
default = { };
};
keybindings = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
};
startup = lib.mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.str);
default = [
# { command = "nextcloud --background"; }
# { command = "vesktop --start-minimized --enable-speech-dispatcher --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime"; }
# { command = "element-desktop --hidden --enable-features=useozoneplatform --ozone-platform=wayland --disable-gpu-driver-bug-workarounds"; }
# { command = "anki"; }
# { command = "obsidian"; }
# { command = "nm-applet"; }
# { command = "feishin"; }
];
};
kyria = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
default = {
"36125:53060:splitkb.com_splitkb.com_Kyria_rev3" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
"7504:24926:Kyria_Keyboard" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
};
internal = true;
};
standardinputs = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
default = lib.recursiveUpdate (lib.recursiveUpdate config.swarselsystems.touchpad config.swarselsystems.kyria) config.swarselsystems.inputs;
internal = true;
};
touchpad = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
default = { };
internal = true;
};
swayfxConfig = lib.mkOption {
type = lib.types.str;
default = ''
blur enable
blur_xray disable
blur_passes 1
blur_radius 1
shadows enable
corner_radius 2
titlebar_separator disable
default_dim_inactive 0.02
'';
internal = true;
};
};
config = lib.mkIf config.swarselmodules.sway {
swarselsystems = {
touchpad = lib.mkIf config.swarselsystems.isLaptop {
"type:touchpad" = {
dwt = "enabled";
tap = "enabled";
natural_scroll = "enabled";
middle_emulation = "enabled";
drag_lock = "disabled";
};
};
swayfxConfig = lib.mkIf (!config.swarselsystems.isNixos) " ";
};
wayland.windowManager.sway = {
enable = true;
# checkConfig = false; # delete this line once SwayFX is fixed upstream
package = lib.mkIf config.swarselsystems.isNixos null;
systemd = {
enable = true;
xdgAutostart = true;
variables = [
"DISPLAY"
"WAYLAND_DISPLAY"
"SWAYSOCK"
"XDG_CURRENT_DESKTOP"
"XDG_SESSION_TYPE"
"NIXOS_OZONE_WL"
"XCURSOR_THEME"
"XCURSOR_SIZE"
];
};
wrapperFeatures = {
base = true;
gtk = true;
};
config = rec {
modifier = "Mod4";
# terminal = "kitty";
menu = "fuzzel";
bars = [{
command = "waybar";
mode = "hide";
hiddenState = "hide";
position = "top";
extraConfig = "modifier Mod4";
}];
keybindings =
let
inherit (config.wayland.windowManager.sway.config) modifier;
in
lib.recursiveUpdate
{
"${modifier}+0" = "workspace 10:十";
"${modifier}+1" = "workspace 1:一";
"${modifier}+2" = "workspace 2:二";
"${modifier}+3" = "workspace 3:三";
"${modifier}+4" = "workspace 4:四";
"${modifier}+5" = "workspace 5:五";
"${modifier}+6" = "workspace 6:六";
"${modifier}+7" = "workspace 7:七";
"${modifier}+8" = "workspace 8:八";
"${modifier}+9" = "workspace 9:九";
"${modifier}+Ctrl+Shift+c" = "reload";
"${modifier}+Ctrl+Shift+e" = "move container to workspace 13:E";
"${modifier}+Ctrl+Shift+f" = "move container to workspace 16:F";
"${modifier}+Ctrl+Shift+l" = "move container to workspace 15:L";
"${modifier}+Ctrl+Shift+m" = "move container to workspace 11:M";
"${modifier}+Ctrl+Shift+r" = "exec swarsel-displaypower";
"${modifier}+Ctrl+Shift+s" = "move container to workspace 12:S";
"${modifier}+Ctrl+Shift+t" = "move container to workspace 14:T";
"${modifier}+Ctrl+e" = "workspace 13:E";
"${modifier}+Ctrl+f" = "workspace 16:F";
"${modifier}+Ctrl+l" = "workspace 15:L";
"${modifier}+Ctrl+m" = "workspace 11:M";
"${modifier}+Ctrl+p" = "exec 1password --quick-acces";
"${modifier}+Ctrl+s" = "workspace 12:S";
"${modifier}+Ctrl+t" = "workspace 14:T";
"${modifier}+Down" = "focus down";
"${modifier}+Escape" = "exec wlogout";
"${modifier}+F12" = "scratchpad show";
"${modifier}+Left" = "focus left";
"${modifier}+Return" = "exec swarselzellij";
"${modifier}+Right" = "focus right";
"${modifier}+Shift+0" = "move container to workspace 10:十";
"${modifier}+Shift+1" = "move container to workspace 1:一";
"${modifier}+Shift+2" = "move container to workspace 2:二";
"${modifier}+Shift+3" = "move container to workspace 3:三";
"${modifier}+Shift+4" = "move container to workspace 4:四";
"${modifier}+Shift+5" = "move container to workspace 5:五";
"${modifier}+Shift+6" = "move container to workspace 6:六";
"${modifier}+Shift+7" = "move container to workspace 7:七";
"${modifier}+Shift+8" = "move container to workspace 8:八";
"${modifier}+Shift+9" = "move container to workspace 9:九";
"${modifier}+Shift+Down" = "move down 40px";
"${modifier}+Shift+Escape" = "exec kitty -o confirm_os_window_close=0 btm";
"${modifier}+Shift+F12" = "move scratchpad";
"${modifier}+Shift+Left" = "move left 40px";
"${modifier}+Shift+Right" = "move right 40px";
"${modifier}+Shift+Space" = "floating toggle";
"${modifier}+Shift+Up" = "move up 40px";
"${modifier}+Shift+a" = "exec emacsclient -cF '((name . \"Emacs Popup Anchor\"))' -e '(prot-window-popup-swarsel/open-calendar)'";
"${modifier}+Shift+c" = "exec qalculate-gtk";
"${modifier}+Shift+e" = "exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -b 'Yes, exit sway' 'swaymsg exit'";
"${modifier}+Shift+f" = "exec swaymsg fullscreen";
"${modifier}+Shift+m" = "exec emacsclient -cF '((name . \"Emacs Popup Anchor\"))' -e '(prot-window-popup-mu4e)'";
"${modifier}+Shift+o" = "exec pass-fuzzel --otp --type";
"${modifier}+Shift+p" = "exec pass-fuzzel --type";
"${modifier}+Shift+s" = "exec slurp | grim -g - Pictures/Screenshots/$(date +'screenshot_%Y-%m-%d-%H%M%S.png')";
"${modifier}+Shift+t" = "exec opacitytoggle";
"${modifier}+Shift+v" = "exec wf-recorder -g '$(slurp -f %o -or)' -f ~/Videos/screenrecord_$(date +%Y-%m-%d-%H%M%S).mkv";
"${modifier}+Space" = "exec fuzzel";
"${modifier}+Up" = "focus up";
"${modifier}+a" = "exec swarselcheck -s";
"${modifier}+c" = "exec emacsclient -cF '((name . \"Emacs Popup Anchor\"))' -e '(prot-window-popup-org-capture)'";
"${modifier}+d" = "exec swarselcheck -d";
"${modifier}+e" = "exec emacsclient -nquc -a emacs -e \"(dashboard-open)\"";
"${modifier}+f" = "exec firefox";
"${modifier}+h" = "exec hyprpicker | wl-copy";
"${modifier}+m" = "exec swaymsg workspace back_and_forth";
"${modifier}+o" = "exec pass-fuzzel --otp";
"${modifier}+p" = "exec pass-fuzzel";
"${modifier}+q" = "kill";
"${modifier}+r" = "mode resize";
"${modifier}+s" = "exec grim -g \"$(slurp)\" -t png - | wl-copy -t image/png";
"${modifier}+t" = "exec emacsclient -cF '((name . \"Emacs Popup Anchor\"))' -e '(prot-window-popup-org-agenda)'";
"${modifier}+w" = "exec swarselcheck -e";
"${modifier}+x" = "exec swarselcheck -k";
# "${modifier}+Escape" = "mode $exit";
# "${modifier}+Return" = "exec kitty";
"XF86AudioRaiseVolume" = "exec swayosd-client --output-volume raise";
"XF86AudioLowerVolume" = "exec swayosd-client --output-volume lower";
"XF86AudioMute" = "exec swayosd-client --output-volume mute-toggle";
"XF86MonBrightnessUp" = "exec swayosd-client --brightness raise";
"XF86MonBrightnessDown" = "exec swayosd-client --brightness lower";
"XF86Display" = "exec wl-mirror eDP-1";
# "--no-repeat Super_L" = "exec killall -SIGUSR1 .waybar-wrapped";
# "${modifier}+z" = "exec killall -SIGUSR1 .waybar-wrapped";
}
config.swarselsystems.keybindings;
modes = {
resize = {
Down = "resize grow height 10 px or 10 ppt";
Escape = "mode default";
Left = "resize shrink width 10 px or 10 ppt";
Return = "mode default";
Right = "resize grow width 10 px or 10 ppt";
Up = "resize shrink height 10 px or 10 ppt";
Tab = "move position center, resize set width 50 ppt height 50 ppt";
};
};
defaultWorkspace = "workspace 1:一";
# output = {
# "${config.swarselsystems.sharescreen}" = {
# bg = "${self}/files/wallpaper/lenovowp.png ${config.stylix.imageScalingMode}";
# };
# "Philips Consumer Electronics Company PHL BDM3270 AU11806002320" = {
# bg = "${self}/files/wallpaper/standwp.png ${config.stylix.imageScalingMode}";
# };
# };
input = config.swarselsystems.standardinputs;
workspaceOutputAssign =
let
workplaceSets = lib.mapAttrs' eachOutput config.swarselsystems.monitors;
workplaceOutputs = map (key: lib.getAttr key workplaceSets) (lib.attrNames workplaceSets);
in
workplaceOutputs;
startup = config.swarselsystems.startup ++ [
{ command = "kitty -T kittyterm -o confirm_os_window_close=0 zellij attach --create kittyterm"; }
{ command = "sleep 60; kitty -T spotifytui -o confirm_os_window_close=0 spotify_player"; }
];
seat = {
"*" = {
hide_cursor = "when-typing enable";
};
};
window = {
border = 1;
titlebar = false;
};
assigns = {
"15:L" = [{ app_id = "teams-for-linux"; }];
};
floating = {
border = 1;
criteria = [
{ app_id = "qalculate-gtk"; }
{ app_id = "blueman"; }
{ app_id = "pavucontrol"; }
{ app_id = "syncthingtray"; }
{ app_id = "Element"; }
{ app_id = "1Password"; }
{ app_id = "com.nextcloud.desktopclient.nextcloud"; }
{ title = "(?:Open|Save) (?:File|Folder|As)"; }
{ title = "^Add$"; }
{ title = "^Picture-in-Picture$"; }
{ title = "Syncthing Tray"; }
{ title = "^Emacs Popup Frame$"; }
{ title = "^Emacs Popup Anchor$"; }
{ title = "^spotifytui$"; }
{ title = "^kittyterm$"; }
{ app_id = "vesktop"; }
{ window_role = "pop-up"; }
{ window_role = "bubble"; }
{ window_role = "dialog"; }
{ window_role = "task_dialog"; }
{ window_role = "menu"; }
{ window_role = "Preferences"; }
];
titlebar = false;
};
window = {
commands = [
{
command = "opacity 0.95";
criteria = {
class = ".*";
};
}
{
command = "opacity 1";
criteria = {
app_id = "at.yrlf.wl_mirror";
};
}
{
command = "opacity 1";
criteria = {
app_id = "Gimp-2.10";
};
}
{
command = "opacity 0.99";
criteria = {
app_id = "firefox";
};
}
{
command = "opacity 0.99";
criteria = {
app_id = "chromium-browser";
};
}
{
command = "sticky enable, shadows enable";
criteria = {
title = "^Picture-in-Picture$";
};
}
{
command = "resize set width 60 ppt height 60 ppt, opacity 0.99, sticky enable";
criteria = {
title = "^Emacs Popup Frame$";
};
}
{
command = "move container to scratchpad";
criteria = {
title = "^Emacs Popup Anchor$";
};
}
{
command = "resize set width 60 ppt height 60 ppt, opacity 0.8, sticky enable, border normal, move container to scratchpad";
criteria = {
title = "^kittyterm$";
};
}
{
command = "resize set width 60 ppt height 60 ppt, opacity 0.95, sticky enable, border normal, move container to scratchpad";
criteria = {
title = "^spotifytui$";
};
}
{
command = "resize set width 60 ppt height 60 ppt, sticky enable, move container to scratchpad";
criteria = {
class = "Spotify";
};
}
{
command = "resize set width 60 ppt height 60 ppt, sticky enable";
criteria = {
app_id = "vesktop";
};
}
{
command = "resize set width 60 ppt height 60 ppt, sticky enable";
criteria = {
class = "Element";
};
}
# {
# command = "resize set width 60 ppt height 60 ppt, sticky enable, move container to scratchpad";
# criteria = {
# app_id="^$";
# class="^$";
# };
# }
];
};
gaps = {
inner = 5;
};
};
extraSessionCommands = ''
export XDG_CURRENT_DESKTOP=sway;
export XDG_SESSION_DESKTOP=sway;
export _JAVA_AWT_WM_NONREPARENTING=1;
export GITHUB_NOTIFICATION_TOKEN_PATH=${confLib.getConfig.sops.secrets.github-notifications-token.path};
'' + vars.waylandExports;
# extraConfigEarly = "
# exec systemctl --user import-environment DISPLAY WAYLAND_DISPLAY SWAYSOCK
# exec hash dbus-update-activation-environment 2>/dev/null && dbus-update-activation-environment --systemd DISPLAY WAYLAND_DISPLAY SWAYSOCK
# ";
extraConfig =
let
inherit (config.wayland.windowManager.sway.config) modifier;
swayfxSettings = config.swarselsystems.swayfxConfig;
in
"
# exec_always autotiling
# set $exit \"exit: [s]leep, [l]ock, [p]oweroff, [r]eboot, [u]ser logout\"
# mode $exit {
# bindsym --to-code {
# s exec \"systemctl suspend\", mode \"default\"
# h exec \"systemctl hibernate\", mode \"default\"
# l exec \"swaylock --screenshots --clock --effect-blur 7x5 --effect-vignette 0.5:0.5 --fade-in 0.2 --daemonize\", mode \"default\
# p exec \"systemctl poweroff\"
# r exec \"systemctl reboot\"
# u exec \"swaymsg exit\"
# Return mode \"default\"
# Escape mode \"default\"
# ${modifier}+Escape mode \"default\"
# }
# }
exec systemctl --user import-environment
# exec swayidle -w
seat * hide_cursor 2000
exec_always kill -1 $(pidof kanshi)
bindswitch --locked lid:on exec kanshictl switch lidclosed
bindswitch --locked lid:off exec kanshictl switch lidopen
${swayfxSettings}
";
};
};
}
4.5.2.34. Kanshi
{ self, lib, pkgs, config, ... }:
{
options.swarselmodules.kanshi = lib.mkEnableOption "kanshi settings";
config = lib.mkIf config.swarselmodules.kanshi {
swarselsystems = {
monitors = {
homedesktop = rec {
name = "Philips Consumer Electronics Company PHL BDM3270 AU11806002320";
mode = "2560x1440";
scale = "1";
position = "0,0";
workspace = "11:M";
output = name;
};
};
};
services.kanshi = {
enable = true;
settings = [
{
# laptop screen
output = {
criteria = config.swarselsystems.sharescreen;
mode = config.swarselsystems.highResolution;
scale = 1.0;
};
}
{
# home main screen
output = {
criteria = "Philips Consumer Electronics Company PHL BDM3270 AU11806002320";
scale = 1.0;
mode = "2560x1440";
};
}
{
profile = {
name = "lidopen";
exec = [ "${pkgs.swaybg}/bin/swaybg --output '${config.swarselsystems.sharescreen}' --image ${config.swarselsystems.wallpaper} --mode ${config.stylix.imageScalingMode}" ];
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "enable";
scale = 1.0;
}
];
};
}
{
profile =
let
monitor = "Philips Consumer Electronics Company PHL BDM3270 AU11806002320";
in
{
name = "lidopen";
exec = [
"${pkgs.swaybg}/bin/swaybg --output '${config.swarselsystems.sharescreen}' --image ${config.swarselsystems.wallpaper} --mode ${config.stylix.imageScalingMode}"
"${pkgs.swaybg}/bin/swaybg --output '${monitor}' --image ${self}/files/wallpaper/standwp.png --mode ${config.stylix.imageScalingMode}"
];
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "enable";
scale = 1.7;
position = "2560,0";
}
{
criteria = monitor;
scale = 1.0;
mode = "2560x1440";
position = "0,0";
}
];
};
}
{
profile =
let
monitor = "Philips Consumer Electronics Company PHL BDM3270 AU11806002320";
in
{
name = "lidclosed";
exec = [ "${pkgs.swaybg}/bin/swaybg --output '${monitor}' --image ${self}/files/wallpaper/standwp.png --mode ${config.stylix.imageScalingMode}" ];
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "disable";
position = "2560,0";
}
{
criteria = monitor;
scale = 1.0;
mode = "2560x1440";
position = "0,0";
}
];
};
}
];
};
};
}
4.5.2.35. gpg-agent
Settings that are needed for the gpg-agent. Also we are enabling emacs support for unlocking my Yubikey here.
When setting up a new machine:
- setup gpgsm for signing of mails using S/MIME:
- `gpgsm --import ~/Certificates/<certname>.p12`
- `gpgsm --import ~/Certificates/harica-root.pem`
- `gpgsm --import ~/Certificates/harica-intermediate.pem`
- `gpgsm --list-keys --with-validation "HARICA Client RSA Root CA 2021"`
- trust the certificate and set passphrase
{ self, lib, config, pkgs, ... }:
let
inherit (config.swarselsystems) mainUser homeDir;
in
{
options.swarselmodules.gpgagent = lib.mkEnableOption "gpg agent settings";
config = lib.mkIf config.swarselmodules.gpgagent {
services.gpg-agent = {
enable = true;
verbose = true;
enableZshIntegration = true;
enableScDaemon = true;
enableSshSupport = true;
enableExtraSocket = true;
pinentry.package = pkgs.wayprompt;
pinentry.program = "pinentry-wayprompt";
# pinentry.package = pkgs.pinentry.gtk2;
defaultCacheTtl = 600;
maxCacheTtl = 7200;
extraConfig = ''
allow-loopback-pinentry
allow-emacs-pinentry
'';
sshKeys = [
"4BE7925262289B476DBBC17B76FD3810215AE097"
];
};
programs.gpg = {
enable = true;
publicKeys = [
{
source = "${self}/secrets/public/gpg/gpg-public-key-0x76FD3810215AE097.asc";
trust = 5;
}
];
};
# assure correct permissions
systemd.user.tmpfiles.settings."30-gpgagent".rules = {
"${homeDir}/.gnupg" = {
d = {
group = "users";
user = mainUser;
mode = "0700";
};
};
};
};
}
4.5.2.36. gammastep
This service changes the screen hue at night. I am not sure if that really does something, but I like the color anyways.
{ lib, config, confLib, ... }:
let
inherit (confLib.getConfig.repo.secrets.common.location) latitude longitude;
in
{
options.swarselmodules.gammastep = lib.mkEnableOption "gammastep settings";
config = lib.mkIf config.swarselmodules.gammastep {
services.gammastep = lib.mkIf (config.swarselsystems.isNixos && !config.swarselsystems.isPublic) {
enable = true;
provider = "manual";
inherit longitude latitude;
};
};
}
4.5.2.37. Spicetify
{ inputs, lib, config, pkgs, ... }:
let
moduleName = "spicetify";
spicePkgs = inputs.spicetify-nix.legacyPackages.${pkgs.stdenv.system};
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "${moduleName} settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
programs.spicetify = {
enable = true;
# spotifyPackage = pkgs.stable24_11.spotify;
spotifyPackage = pkgs.spotify;
enabledExtensions = with spicePkgs.extensions; [
fullAppDisplay
shuffle
hidePodcasts
fullAlbumDate
skipStats
history
];
};
};
}
4.5.2.38. Obsidian
{ lib, config, pkgs, confLib, ... }:
let
moduleName = "obsidian";
inherit (confLib.getConfig.repo.secrets.common.obsidian) userIgnoreFilters;
name = "Main";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} with settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
home.file = {
"${config.programs.obsidian.vaults.${name}.target}/.obsidian/app.json".force = true;
"${config.programs.obsidian.vaults.${name}.target}/.obsidian/appearance.json".force = true;
"${config.programs.obsidian.vaults.${name}.target}/.obsidian/core-plugins.json".force = true;
};
programs.obsidian = let
pluginSource = pkgs.nur.repos.swarsel;
in {
enable = true;
package = pkgs.obsidian;
defaultSettings = {
app = {
attachmentFolderPath = "attachments";
alwaysUpdateLinks = true;
spellcheck = false;
inherit userIgnoreFilters;
vimMode = false;
newFileLocation = "current";
};
hotkeys = {
"graph:open" = [ ];
"omnisearch:show-modal" = [
{
modifiers = [
"Mod"
];
key = "S";
}
];
"editor:save-file" = [ ];
"editor:delete-paragraph" = [ ];
};
corePlugins = [
"backlink"
"bookmarks"
"canvas"
"command-palette"
"daily-notes"
"editor-status"
"file-explorer"
"file-recovery"
"global-search"
"graph"
"note-composer"
"outgoing-link"
"outline"
"page-preview"
"properties"
"slides"
"switcher"
"tag-pane"
"templates"
"word-count"
];
# communityPlugins = with pkgs.swarsel-nix; [
communityPlugins = with pluginSource; [
advanced-tables
calendar
file-hider
linter
omnisearch
sort-and-permute-lines
tag-wrangler
tray
];
};
vaults = {
${name} = {
target = "./Obsidian/${name}";
settings = {
appearance = {
baseFontSize = lib.mkForce 19;
};
# communityPlugins = with pkgs.swarsel-nix; [
communityPlugins = with pluginSource; [
{
pkg = advanced-tables;
enable = true;
}
{
pkg = calendar;
enable = true;
}
{
pkg = sort-and-permute-lines;
enable = true;
}
{
pkg = tag-wrangler;
enable = true;
}
{
pkg = tray;
enable = true;
settings = {
launchOnStartup = false;
hideOnLaunch = true;
runInBackground = true;
hideTaskbarIcon = false;
createTrayIcon = true;
};
}
{
pkg = file-hider;
enable = true;
settings =
{
hidden = true;
hiddenList = [
"attachments"
"images"
"ltximg"
"logseq"
];
};
}
{
pkg = linter;
enable = true;
settings = {
auto-correct-common-misspellings = {
skip-words-with-multiple-capitals = true;
};
convert-bullet-list-markers = {
enabled = true;
};
};
}
{
pkg = omnisearch;
enable = true;
settings = {
hideExcluded = true;
};
}
];
};
};
};
};
};
}
4.5.2.39. Anki
{ lib, config, pkgs, globals, confLib, type, ... }:
let
moduleName = "anki";
inherit (config.swarselsystems) isPublic isNixos;
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName}
({
programs.anki = {
enable = true;
# # package = pkgs.anki;
hideBottomBar = true;
hideBottomBarMode = "always";
hideTopBar = true;
hideTopBarMode = "always";
reduceMotion = true;
spacebarRatesCard = true;
# videoDriver = "opengl";
sync = {
autoSync = false; # sync on profile close will delay system shutdown
syncMedia = true;
autoSyncMediaMinutes = 5;
url = "https://${globals.services.ankisync.domain}";
usernameFile = confLib.getConfig.sops.secrets.anki-user.path;
# this is not the password but the syncKey
# get it by logging in or out, saving preferences and then
# show details on the "settings wont be saved" dialog
keyFile = confLib.getConfig.sops.secrets.anki-pw.path;
};
addons =
let
minimize-to-tray = pkgs.anki-utils.buildAnkiAddon
(finalAttrs: {
pname = "minimize-to-tray";
version = "2.0.1";
src = pkgs.fetchFromGitHub {
owner = "simgunz";
repo = "anki21-addons_minimize-to-tray";
rev = finalAttrs.version;
sparseCheckout = [ "src" ];
hash = "sha256-xmvbIOfi9K0yEUtUNKtuvv2Vmqrkaa4Jie6J1s+FuqY=";
};
sourceRoot = "${finalAttrs.src.name}/src";
});
in
[
(minimize-to-tray.withConfig
{
config = {
hide_on_startup = "true";
};
})
];
};
} // lib.optionalAttrs (type != "nixos") {
sops = lib.mkIf (!isPublic && !isNixos) {
secrets = {
anki-user = { };
anki-pw = { };
};
};
});
}
4.5.2.40. Element-desktop
{ lib, config, globals, ... }:
let
moduleName = "element-desktop";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
programs.element-desktop = {
enable = true;
settings = {
default_server_config = {
"m.homeserver" = {
base_url = "https://${globals.services.matrix.domain}/";
};
};
UIFeature = {
feedback = false;
voip = false;
widgets = false;
shareSocial = false;
registration = false;
passwordReset = false;
deactivate = false;
};
};
};
};
}
4.5.2.41. Hexchat
{ lib, config, confLib, ... }:
let
moduleName = "hexchat";
inherit (confLib.getConfig.repo.secrets.common.irc) irc_nick1;
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
programs.${moduleName} = {
enable = true;
settings = {
inherit irc_nick1;
};
};
};
}
4.5.2.42. obs-studio
{ lib, config, ... }:
let
moduleName = "obs-studio";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
programs.${moduleName} = {
enable = true;
};
};
}
4.5.2.43. spotify-player
{ lib, config, ... }:
let
moduleName = "spotify-player";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
programs.${moduleName} = {
enable = true;
};
};
}
4.5.2.44. vesktop
{ lib, pkgs, config, ... }:
let
moduleName = "vesktop";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
programs.${moduleName} = {
enable = true;
package = pkgs.stable.vesktop;
settings = {
appBadge = false;
arRPC = false;
checkUpdates = false;
customTitleBar = false;
disableMinSize = true;
minimizeToTray = true;
tray = true;
staticTitle = true;
hardwareAcceleration = true;
discordBranch = "stable";
};
vencord = {
useSystem = true;
settings = {
autoUpdate = false;
autoUpdateNotification = false;
enableReactDevtools = false;
frameless = false;
transparent = false;
winCtrlQ = false;
notifyAboutUpdates = false;
useQuickCss = true;
disableMinSize = true;
winNativeTitleBar = false;
plugins = {
MessageLogger = {
enabled = true;
ignoreSelf = true;
};
ChatInputButtonAPI = {
enabled = false;
};
CommandsAPI = {
enabled = true;
};
MemberListDecoratorsAPI = {
enabled = false;
};
MessageAccessoriesAPI = {
enabled = true;
};
MessageDecorationsAPI = {
enabled = false;
};
MessageEventsAPI = {
enabled = false;
};
MessagePopoverAPI = {
enabled = false;
};
MessageUpdaterAPI = {
enabled = false;
};
ServerListAPI = {
enabled = false;
};
UserSettingsAPI = {
enabled = true;
};
FakeNitro = {
enabled = true;
};
};
};
};
};
};
}
4.5.2.45. batsignal
{ lib, config, ... }:
let
moduleName = "batsignal";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
services.${moduleName} = {
enable = true;
extraArgs = [
"-W"
" Consider charging the battery"
"-C"
" Battery is low; plug in charger now"
"-D"
" Device will lose power in a few seconds"
"-c"
"10"
"-d"
"5"
];
};
};
}
4.5.2.46. autotiling
{ lib, config, ... }:
let
moduleName = "autotiling";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
services.${moduleName} = {
enable = true;
systemdTarget = config.wayland.systemd.target;
};
};
}
4.5.2.47. swayidle
{ lib, config, pkgs, ... }:
let
moduleName = "swayidle";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
services.${moduleName} =
let
brightnessctl = "${lib.getExe pkgs.brightnessctl}";
swaylock = "${lib.getExe pkgs.swaylock-effects}";
suspend = "${pkgs.systemd}/bin/systemctl suspend";
in
{
enable = true;
systemdTarget = config.wayland.systemd.target;
extraArgs = [ "-w" ];
timeouts = [
{ timeout = 60; command = "${brightnessctl} -s; ${brightnessctl} set 80%-"; resumeCommand = "${brightnessctl} -r"; }
# { timeout = 300; command = "${lib.getExe pkgs.swaylock-effects} -f --screenshots --clock --effect-blur 7x5 --effect-vignette 0.5:0.5 --fade-in 0.2"; }
{ timeout = 300; command = "${swaylock} -f"; }
# { timeout = 600; command = ''${pkgs.sway}/bin/swaymsg "output * dpms off"; resumeCommand = "${pkgs.sway}/bin/swaymsg output * dpms on''; }
{ timeout = 600; command = "${suspend}"; }
];
events = [
# { event = "before-sleep"; command = "${lib.getExe pkgs.swaylock-effects} -f --screenshots --clock --effect-blur 7x5 --effect-vignette 0.5:0.5 --fade-in 0.2"; }
# { event = "after-resume"; command = "${swaylock} -f "; }
{ event = "before-sleep"; command = "${swaylock} -f "; }
{ event = "lock"; command = "${swaylock} -f "; }
];
};
};
}
4.5.2.48. swaylock
{ lib, config, pkgs, ... }:
let
moduleName = "swaylock";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
programs.${moduleName} = {
enable = true;
package = pkgs.swaylock-effects;
settings = {
screenshots = true;
clock = true;
effect-blur = "7x5";
effect-vignette = "0.5:0.5";
fade-in = "0.2";
};
};
};
}
4.5.2.49. opkssh
{ lib, config, globals, ... }:
let
moduleName = "opkssh";
in
{
options.swarselmodules.${moduleName} = lib.mkEnableOption "enable ${moduleName} and settings";
config = lib.mkIf config.swarselmodules.${moduleName} {
programs.${moduleName} = {
enable = true;
settings = {
default_provider = "kanidm";
providers = [
{
alias = "kanidm";
issuer = "https://${globals.services.kanidm.domain}/oauth2/openid/opkssh";
client_id = "opkssh";
scopes = "openid email profile";
redirect_uris = [
"http://localhost:3000/login-callback"
"http://localhost:10001/login-callback"
"http://localhost:11110/login-callback"
];
}
];
};
};
};
}
4.5.3. Server
This is again configuration that is mostly needed on servers. Most things should be done using the NixOS config instead, consider carefully if a home-manager config must be used.
4.5.3.1. Imports
This section sets up all the imports that are used in the home-manager section.
{ self, lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/home/server";
modulesPath = "${self}/modules";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/home/server" ++ [
"${modulesPath}/home/common/settings.nix"
];
}
4.5.3.2. Symlinking dotfiles
This section should be used in order to symlink already existing configuration files using `home.file` and setting session variables using `home.sessionVariables`.
As for the `home.sessionVariables`, it should be noted that environment variables that are needed at system start should NOT be loaded here, but instead in `programs.zsh.config.extraSessionCommands` (in the home-manager programs section). This is also where all the wayland related variables are stored.
{ self, lib, config, ... }:
{
options.swarselmodules.server.dotfiles = lib.mkEnableOption "server dotfiles settings";
config = lib.mkIf config.swarselmodules.server.dotfiles {
home.file = {
"init.el" = lib.mkForce {
source = self + /files/emacs/server.el;
target = ".emacs.d/init.el";
};
};
};
}
4.5.4. Darwin
Again, mostly a placeholder for future home-manager modules that run on darwin systems.
4.5.4.1. Imports
This section sets up all the imports that are used in the home-manager section.
{ self, ... }:
{
home.stateVersion = "23.05";
imports = [
"${self}/modules/home/common/settings.nix"
"${self}/modules/shared/options.nix"
"${self}/modules/shared/vars.nix"
];
}
4.5.5. Optional
Akin to the Optional NixOS modules.
{ lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/home/optional";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/home/optional";
}
4.5.5.1. Niri
{ inputs, config, pkgs, lib, vars, ... }:
{
imports = [
inputs.niri-flake.homeModules.niri
];
options.swarselmodules.niri = lib.mkEnableOption "niri settings";
config = lib.mkIf config.swarselmodules.niri
{
programs.niri = {
package = pkgs.niri-unstable; # which package to use for niri validation
settings = {
xwayland-satellite = {
enable = true;
path = "${lib.getExe pkgs.xwayland-satellite-unstable}";
};
prefer-no-csd = true;
layer-rules = [
{ matches = [{ namespace = "^notifications$"; }]; block-out-from = "screencast"; }
{ matches = [{ namespace = "^wallpaper$"; }]; place-within-backdrop = true; }
];
window-rules = [
{
matches = [{ app-id = ".*"; }];
opacity = 0.95;
default-column-width = { proportion = 0.5; };
shadow = {
enable = true;
draw-behind-window = true;
};
geometry-corner-radius = { top-left = 2.0; top-right = 2.0; bottom-left = 2.0; bottom-right = 2.0; };
}
{ matches = [{ app-id = "at.yrlf.wl_mirror"; }]; opacity = 1.0; }
{ matches = [{ app-id = "Gimp"; }]; opacity = 1.0; }
{ matches = [{ app-id = "firefox"; }]; opacity = 0.99; }
{ matches = [{ app-id = "^special.*"; }]; default-column-width = { proportion = 0.9; }; open-on-workspace = "Scratchpad"; }
{ matches = [{ app-id = "chromium-browser"; }]; opacity = 0.99; }
{ matches = [{ app-id = "^qalculate-gtk$"; }]; open-floating = true; }
{ matches = [{ app-id = "^blueman$"; }]; open-floating = true; }
{ matches = [{ app-id = "^pavucontrol$"; }]; open-floating = true; }
{ matches = [{ app-id = "^syncthingtray$"; }]; open-floating = true; }
{ matches = [{ app-id = "^Element$"; }]; open-floating = true; default-column-width = { proportion = 0.5; }; block-out-from = "screencast"; }
# { matches = [{ app-id = "^Element$"; }]; default-column-width = { proportion = 0.9; }; open-on-workspace = "Scratchpad"; block-out-from = "screencast"; }
{ matches = [{ app-id = "^vesktop$"; }]; open-floating = true; default-column-width = { proportion = 0.5; }; block-out-from = "screencast"; }
# { matches = [{ app-id = "^vesktop$"; }]; default-column-width = { proportion = 0.9; }; open-on-workspace = "Scratchpad"; block-out-from = "screencast"; }
{ matches = [{ app-id = "^com.nextcloud.desktopclient.nextcloud$"; }]; open-floating = true; }
{ matches = [{ title = ".*1Password.*"; }]; excludes = [{ app-id = "^firefox$"; } { app-id = "^emacs$"; } { app-id = "^kitty$"; }]; open-floating = true; block-out-from = "screencast"; }
{ matches = [{ title = "(?:Open|Save) (?:File|Folder|As)"; }]; open-floating = true; }
{ matches = [{ title = "^Add$"; }]; open-floating = true; }
{ matches = [{ title = "^Picture-in-Picture$"; }]; open-floating = true; }
{ matches = [{ title = "Syncthing Tray"; }]; open-floating = true; }
{ matches = [{ title = "^Emacs Popup Frame$"; }]; open-floating = true; }
{ matches = [{ title = "^Emacs Popup Anchor$"; }]; open-floating = true; }
{ matches = [{ app-id = "^spotifytui$"; }]; open-floating = true; default-column-width = { proportion = 0.5; }; }
{ matches = [{ app-id = "^kittyterm$"; }]; open-floating = true; default-column-width = { proportion = 0.5; }; }
];
environment = {
DISPLAY = ":0";
} // vars.waylandSessionVariables;
screenshot-path = "~/Pictures/Screenshots/screenshot_%Y-%m-%d-%H%M%S.png";
input = {
mod-key = "Super";
keyboard = {
xkb = {
layout = "us";
variant = "altgr-intl";
};
};
mouse = {
natural-scroll = false;
};
touchpad = {
enable = true;
tap = true;
tap-button-map = "left-right-middle";
natural-scroll = true;
scroll-method = "two-finger";
click-method = "clickfinger";
disabled-on-external-mouse = true;
drag = true;
drag-lock = false;
dwt = true;
dwtp = true;
};
};
cursor = {
hide-after-inactive-ms = 2000;
hide-when-typing = true;
};
layout = {
background-color = "transparent";
border = {
enable = true;
width = 1;
};
focus-ring = {
enable = false;
};
gaps = 5;
};
binds = with config.lib.niri.actions; let
sh = spawn "sh" "-c";
in
{
# "Mod+Super_L" = spawn "killall -SIGUSR1 .waybar-wrapped";
"Mod+z".action = spawn "killall -SIGUSR1 .waybar-wrapped";
"Mod+Shift+t".action = toggle-window-rule-opacity;
# "Mod+Escape".action = "mode $exit";
"Mod+m".action = focus-workspace-previous;
"Mod+Shift+Space".action = toggle-window-floating;
"Mod+Shift+f".action = toggle-windowed-fullscreen;
"Mod+q".action = close-window;
"Mod+f".action = spawn "firefox";
"Mod+Space".action = spawn "fuzzel";
"Mod+Shift+c".action = spawn "qalculate-gtk";
"Mod+Ctrl+p".action = spawn "1password" "--quick-acces";
"Mod+Shift+Escape".action = spawn "kitty" "-o" "confirm_os_window_close=0" "btm";
"Mod+h".action = sh ''hyprpicker | wl-copy'';
# "Mod+s".action = spawn "grim" "-g" "\"$(slurp)\"" "-t" "png" "-" "|" "wl-copy" "-t" "image/png";
# "Mod+s".action = screenshot { show-pointer = false; };
"Mod+s".action.screenshot = { show-pointer = false; };
# "Mod+Shift+s".action = spawn "slurp" "|" "grim" "-g" "-" "Pictures/Screenshots/$(date +'screenshot_%Y-%m-%d-%H%M%S.png')";
# "Mod+Shift+s".action = screenshot-window { write-to-disk = true; };
"Mod+Shift+s".action.screenshot-window = { write-to-disk = true; };
# "Mod+Shift+v".action = spawn "wf-recorder" "-g" "'$(slurp -f %o -or)'" "-f" "~/Videos/screenrecord_$(date +%Y-%m-%d-%H%M%S).mkv";
"Mod+e".action = sh "emacsclient -nquc -a emacs -e '(dashboard-open)'";
"Mod+c".action = sh "emacsclient -ce '(org-capture)'";
"Mod+t".action = sh "emacsclient -ce '(org-agenda)'";
"Mod+Shift+m".action = sh "emacsclient -ce '(mu4e)'";
"Mod+Shift+a".action = sh "emacsclient -ce '(swarsel/open-calendar)'";
"Mod+a".action = spawn "swarselcheck-niri" "-s";
"Mod+x".action = spawn "swarselcheck-niri" "-k";
"Mod+d".action = spawn "swarselcheck-niri" "-d";
"Mod+w".action = spawn "swarselcheck-niri" "-e";
"Mod+p".action = spawn "pass-fuzzel";
"Mod+o".action = spawn "pass-fuzzel" "--otp";
"Mod+Shift+p".action = spawn "pass-fuzzel" "--type";
"Mod+Shift+o".action = spawn "pass-fuzzel" "--otp" "--type";
"Mod+Left".action = focus-column-or-monitor-left;
"Mod+Right".action = focus-column-or-monitor-right;
"Mod+Down".action = focus-window-or-workspace-down;
"Mod+Up".action = focus-window-or-workspace-up;
"Mod+Shift+Left".action = move-column-left;
"Mod+Shift+Right".action = move-column-right;
"Mod+Shift+Down".action = move-window-down-or-to-workspace-down;
"Mod+Shift+Up".action = move-window-up-or-to-workspace-up;
# "Mod+Ctrl+Shift+c".action = "reload";
# "Mod+Ctrl+Shift+r".action = "exec swarsel-displaypower";
# "Mod+Shift+e".action = "exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -b 'Yes, exit sway' 'swaymsg exit'";
# "Mod+r".action = "mode resize";
# "Mod+Return".action = "exec kitty";
"Mod+Return".action = spawn "swarselzellij";
"XF86AudioRaiseVolume".action = spawn "swayosd-client" "--output-volume" "raise";
"XF86AudioLowerVolume".action = spawn "swayosd-client" "--output-volume" "lower";
"XF86AudioMute".action = spawn "swayosd-client" "--output-volume" "mute-toggle";
"XF86MonBrightnessUp".action = spawn "swayosd-client" "--brightness raise";
"XF86MonBrightnessDown".action = spawn "swayosd-client" "--brightness lower";
"XF86Display".action = spawn "wl-mirror" "eDP-1";
"Mod+Escape".action = spawn "wlogout";
"Mod+Equal".action = set-column-width "+10%";
"Mod+Minus".action = set-column-width "-10%";
"Mod+1".action = focus-workspace 1;
"Mod+2".action = focus-workspace 2;
"Mod+3".action = focus-workspace 3;
"Mod+4".action = focus-workspace 4;
"Mod+5".action = focus-workspace 5;
"Mod+6".action = focus-workspace 6;
"Mod+7".action = focus-workspace 7;
"Mod+8".action = focus-workspace 8;
"Mod+9".action = focus-workspace 9;
"Mod+0".action = focus-workspace 0;
"Mod+Shift+1".action = move-column-to-index 1;
"Mod+Shift+2".action = move-column-to-index 2;
"Mod+Shift+3".action = move-column-to-index 3;
"Mod+Shift+4".action = move-column-to-index 4;
"Mod+Shift+5".action = move-column-to-index 5;
"Mod+Shift+6".action = move-column-to-index 6;
"Mod+Shift+7".action = move-column-to-index 7;
"Mod+Shift+8".action = move-column-to-index 8;
"Mod+Shift+9".action = move-column-to-index 9;
"Mod+Shift+0".action = move-column-to-index 0;
};
spawn-at-startup = [
# { command = [ "vesktop" "--start-minimized" "--enable-speech-dispatcher" "--ozone-platform-hint=auto" "--enable-features=WaylandWindowDecorations" "--enable-wayland-ime" ]; }
# { command = [ "element-desktop" "--hidden" "--enable-features=UseOzonePlatform" "--ozone-platform=wayland" "--disable-gpu-driver-bug-workarounds" ]; }
# { command = [ "anki" ]; }
# { command = [ "obsidian" ]; }
# { command = [ "nm-applet" ]; }
{ command = [ "niri" "msg" "action" "focus-workspace" "2" ]; }
];
workspaces = {
# "01-Main" = {
# name = "Scratchpad";
# };
"99-Scratchpad" = {
name = "";
};
};
};
};
} // {
programs.niri = lib.mkIf (!config.swarselmodules.niri) {
package = null;
config = null;
settings = null;
};
};
}
4.5.5.2. Gaming
The rest of the settings is at gaming.
{ config, pkgs, confLib, ... }:
let
inherit (config.swarselsystems) isNixos;
in
{
config = {
# specialisation = {
# gaming.configuration = {
home.packages = with pkgs; [
# lutris
wine
protonplus
winetricks
libudev-zero
dwarfs
fuse-overlayfs
# steam
steam-run
patchelf
gamescope
vulkan-tools
moonlight-qt
ns-usbloader
quark-goldleaf
# gog games installing
heroic
# minecraft
prismlauncher # has overrides
temurin-bin-17
pokefinder
retroarch
flips
];
programs.lutris = {
enable = true;
extraPackages = with pkgs; [
winetricks
gamescope
umu-launcher
];
steamPackage = if isNixos then confLib.getConfig.programs.steam.package else pkgs.steam;
winePackages = with pkgs; [
wineWow64Packages.waylandFull
];
protonPackages = with pkgs; [
proton-ge-bin
];
};
# };
# };
};
}
4.5.5.3. Work (pizauth)
The rest of the settings is at work. Here, I am setting up the different firefox profiles that I need for the SSO sites that I need to access at work as well as a few ssh shorthands.
When setting up a new machine:
- setup pizauth for microsoft mail sync (account names are possibly `uni` and `work`): - `pizauth auth <account name, e.g. 'work'>` - `pizauth dump > ~/.pizauth.state`
{ self, config, pkgs, lib, vars, confLib, type, ... }:
let
inherit (config.swarselsystems) homeDir mainUser;
inherit (confLib.getConfig.repo.secrets.local.mail) allMailAddresses;
inherit (confLib.getConfig.repo.secrets.local.work) mailAddress;
certsSopsFile = self + /secrets/repo/certs.yaml;
in
{
options.swarselmodules.optional-work = lib.swarselsystems.mkTrueOption;
config = {
home = {
packages = with pkgs; [
stable.teams-for-linux
shellcheck
dig
docker
postman
# rclone
libguestfs-with-appliance
prometheus.cli
tigervnc
# openstackclient
vscode
dev.antigravity
rustdesk-vbc
];
sessionVariables = {
AWS_CA_BUNDLE = confLib.getConfig.sops.secrets.harica-root-ca.path;
};
};
systemd.user.sessionVariables = {
DOCUMENT_DIR_WORK = lib.mkForce "${homeDir}/Documents/Work";
} // lib.optionalAttrs (!config.swarselsystems.isPublic) {
SWARSEL_MAIL_ALL = lib.mkForce allMailAddresses;
SWARSEL_MAIL_WORK = lib.mkForce mailAddress;
};
accounts.email.accounts.work =
let
inherit (confLib.getConfig.repo.secrets.local.work) mailName;
in
{
primary = false;
address = mailAddress;
userName = mailAddress;
realName = mailName;
passwordCommand = "pizauth show work";
imap = {
host = "outlook.office365.com";
port = 993;
tls.enable = true; # SSL/TLS
};
smtp = {
host = "outlook.office365.com";
port = 587;
tls = {
enable = true; # SSL/TLS
useStartTls = true;
};
};
thunderbird = {
enable = true;
profiles = [ "default" ];
settings = id: {
"mail.smtpserver.smtp_${id}.authMethod" = 10; # oauth
"mail.server.server_${id}.authMethod" = 10; # oauth
# "toolkit.telemetry.enabled" = false;
# "toolkit.telemetry.rejected" = true;
# "toolkit.telemetry.prompted" = 2;
};
};
msmtp = {
enable = true;
extraConfig = {
auth = "xoauth2";
host = "outlook.office365.com";
protocol = "smtp";
port = "587";
tls = "on";
tls_starttls = "on";
from = "${mailAddress}";
user = "${mailAddress}";
passwordeval = "pizauth show work";
};
};
mu.enable = true;
mbsync = {
enable = true;
expunge = "both";
patterns = [ "INBOX" ];
extraConfig = {
account = {
AuthMechs = "XOAUTH2";
};
};
};
};
# wayland.windowManager.sway.config = {
# output = {
# "Applied Creative Technology Transmitter QUATTRO201811" = {
# bg = "${self}/files/wallpaper/navidrome.png ${config.stylix.imageScalingMode}";
# };
# "Hewlett Packard HP Z24i CN44250RDT" = {
# bg = "${self}/files/wallpaper/op6wp.png ${config.stylix.imageScalingMode}";
# };
# "HP Inc. HP 732pk CNC4080YL5" = {
# bg = "${self}/files/wallpaper/botanicswp.png ${config.stylix.imageScalingMode}";
# };
# };
# };
wayland.windowManager.sway =
let
inherit (confLib.getConfig.repo.secrets.local.work) user1 user1Long domain1 mailAddress;
in
{
config = {
keybindings =
let
inherit (config.wayland.windowManager.sway.config) modifier;
in
{
"${modifier}+Shift+d" = "exec ${pkgs.quickpass}/bin/quickpass work/adm/${user1}/${user1Long}@${domain1}";
"${modifier}+Shift+i" = "exec ${pkgs.quickpass}/bin/quickpass work/${mailAddress}";
};
};
};
stylix = {
targets.firefox.profileNames =
let
inherit (confLib.getConfig.repo.secrets.local.work) user1 user2 user3;
in
[
"${user1}"
"${user2}"
"${user3}"
"work"
];
};
programs =
let
inherit (confLib.getConfig.repo.secrets.local.work) user1 user1Long user2 user2Long user3 user3Long user4 path1 loc1 loc2 site1 site2 site3 site4 site5 site6 site7 lifecycle1 lifecycle2 domain1 domain2 gitMail clouds;
in
{
openstackclient = {
enable = true;
inherit clouds;
};
awscli = {
enable = true;
package = pkgs.stable24_05.awscli2;
# settings = {
# "default" = { };
# "profile s3-imagebuilder-prod" = { };
# };
# credentials = {
# "s3-imagebuilder-prod" = {
# aws_access_key_id = "5OYXY4879EJG9I91K1B6";
# credential_process = "${pkgs.pass}/bin/pass show work/awscli/s3-imagebuilder-prod/secret-key";
# };
# };
};
git.settings.user.email = lib.mkForce gitMail;
zsh = {
shellAliases = {
dssh = "ssh -l ${user1Long}";
cssh = "ssh -l ${user2Long}";
wssh = "ssh -l ${user3Long}";
};
cdpath = [
"~/Documents/Work"
];
dirHashes = {
d = "$HOME/.dotfiles";
w = "$HOME/Documents/Work";
s = "$HOME/.dotfiles/secrets";
pr = "$HOME/Documents/Private";
ac = path1;
};
sessionVariables = {
VSPHERE_USER = "$(cat ${confLib.getConfig.sops.secrets.vcuser.path})";
VSPHERE_PW = "$(cat ${confLib.getConfig.sops.secrets.vcpw.path})";
GOVC_USERNAME = "$(cat ${confLib.getConfig.sops.secrets.govcuser.path})";
GOVC_PASSWORD = "$(cat ${confLib.getConfig.sops.secrets.govcpw.path})";
GOVC_URL = "$(cat ${confLib.getConfig.sops.secrets.govcurl.path})";
GOVC_DATACENTER = "$(cat ${confLib.getConfig.sops.secrets.govcdc.path})";
GOVC_DATASTORE = "$(cat ${confLib.getConfig.sops.secrets.govcds.path})";
GOVC_HOST = "$(cat ${confLib.getConfig.sops.secrets.govchost.path})";
GOVC_RESOURCE_POOL = "$(cat ${confLib.getConfig.sops.secrets.govcpool.path})";
GOVC_NETWORK = "$(cat ${confLib.getConfig.sops.secrets.govcnetwork.path})";
};
};
ssh = {
matchBlocks = {
"${loc1}" = {
hostname = "${loc1}.${domain2}";
user = user4;
};
"${loc1}.stg" = {
hostname = "${loc1}.${lifecycle1}.${domain2}";
user = user4;
};
"${loc1}.staging" = {
hostname = "${loc1}.${lifecycle1}.${domain2}";
user = user4;
};
"${loc1}.dev" = {
hostname = "${loc1}.${lifecycle2}.${domain2}";
user = user4;
};
"${loc2}" = {
hostname = "${loc2}.${domain1}";
user = user1Long;
};
"${loc2}.stg" = {
hostname = "${loc2}.${lifecycle1}.${domain2}";
user = user1Long;
};
"${loc2}.staging" = {
hostname = "${loc2}.${lifecycle1}.${domain2}";
user = user1Long;
};
"*.${domain1}" = {
user = user1Long;
};
};
};
firefox = {
profiles =
let
isDefault = false;
in
{
"${user1}" = lib.recursiveUpdate
{
inherit isDefault;
id = 1;
settings = {
"browser.startup.homepage" = "${site1}|${site2}";
};
}
vars.firefox;
"${user2}" = lib.recursiveUpdate
{
inherit isDefault;
id = 2;
settings = {
"browser.startup.homepage" = "${site3}";
};
}
vars.firefox;
"${user3}" = lib.recursiveUpdate
{
inherit isDefault;
id = 3;
}
vars.firefox;
work = lib.recursiveUpdate
{
inherit isDefault;
id = 4;
settings = {
"browser.startup.homepage" = "${site4}|${site5}|${site6}|${site7}";
};
}
vars.firefox;
};
};
chromium = {
enable = true;
package = pkgs.chromium;
extensions = [
# 1password
"gejiddohjgogedgjnonbofjigllpkmbf"
# dark reader
"eimadpbcbfnmbkopoojfekhnkhdbieeh"
# ublock origin
"cjpalhdlnbpafiamejdnhcphjbkeiagm"
# i still dont care about cookies
"edibdbjcniadpccecjdfdjjppcpchdlm"
# browserpass
"naepdomgkenhinolocfifgehidddafch"
];
};
};
services = {
kanshi = {
settings = [
{
# seminary room
output = {
criteria = "Applied Creative Technology Transmitter QUATTRO201811";
scale = 1.0;
mode = "1280x720";
};
}
{
# work side screen
output = {
criteria = "HP Inc. HP 732pk CNC4080YL5";
scale = 1.0;
mode = "3840x2160";
transform = "270";
};
}
# {
# # work side screen
# output = {
# criteria = "Hewlett Packard HP Z24i CN44250RDT";
# scale = 1.0;
# mode = "1920x1200";
# transform = "270";
# };
# }
{
# work main screen
output = {
criteria = "HP Inc. HP Z32 CN41212T55";
scale = 1.0;
mode = "3840x2160";
};
}
{
profile = {
name = "lidopen";
exec = [
"${pkgs.swaybg}/bin/swaybg --output '${config.swarselsystems.sharescreen}' --image ${config.swarselsystems.wallpaper} --mode ${config.stylix.imageScalingMode}"
"${pkgs.swaybg}/bin/swaybg --output 'HP Inc. HP Z32 CN41212T55' --image ${self}/files/wallpaper/botanicswp.png --mode ${config.stylix.imageScalingMode}"
"${pkgs.swaybg}/bin/swaybg --output 'HP Inc. HP 732pk CNC4080YL5' --image ${self}/files/wallpaper/op6wp.png --mode ${config.stylix.imageScalingMode}"
];
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "enable";
scale = 1.5;
position = "2560,0";
}
{
criteria = "HP Inc. HP 732pk CNC4080YL5";
scale = 1.0;
mode = "3840x2160";
position = "-3440,-1050";
transform = "270";
}
{
criteria = "HP Inc. HP Z32 CN41212T55";
scale = 1.0;
mode = "3840x2160";
position = "-1280,0";
}
];
};
}
{
profile =
let
monitor = "Applied Creative Technology Transmitter QUATTRO201811";
in
{
name = "lidopen";
exec = [
"${pkgs.swaybg}/bin/swaybg --output '${config.swarselsystems.sharescreen}' --image ${config.swarselsystems.wallpaper} --mode ${config.stylix.imageScalingMode}"
"${pkgs.swaybg}/bin/swaybg --output '${monitor}' --image ${self}/files/wallpaper/navidrome.png --mode ${config.stylix.imageScalingMode}"
"${pkgs.kanshare}/bin/kanshare ${config.swarselsystems.sharescreen} '${monitor}'"
];
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "enable";
scale = 1.7;
position = "2560,0";
}
{
criteria = "Applied Creative Technology Transmitter QUATTRO201811";
scale = 1.0;
mode = "1280x720";
position = "10000,10000";
}
];
};
}
{
profile = {
name = "lidclosed";
exec = [
"${pkgs.swaybg}/bin/swaybg --output 'HP Inc. HP Z32 CN41212T55' --image ${self}/files/wallpaper/botanicswp.png --mode ${config.stylix.imageScalingMode}"
"${pkgs.swaybg}/bin/swaybg --output 'HP Inc. HP 732pk CNC4080YL5' --image ${self}/files/wallpaper/op6wp.png --mode ${config.stylix.imageScalingMode}"
];
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "disable";
}
{
criteria = "HP Inc. HP 732pk CNC4080YL5";
scale = 1.0;
mode = "3840x2160";
position = "-3440,-1050";
transform = "270";
}
{
criteria = "HP Inc. HP Z32 CN41212T55";
scale = 1.0;
mode = "3840x2160";
position = "-1280,0";
}
];
};
}
{
profile =
let
monitor = "Applied Creative Technology Transmitter QUATTRO201811";
in
{
name = "lidclosed";
exec = [
"${pkgs.swaybg}/bin/swaybg --output '${monitor}' --image ${self}/files/wallpaper/navidrome.png --mode ${config.stylix.imageScalingMode}"
];
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "disable";
}
{
criteria = "Applied Creative Technology Transmitter QUATTRO201811";
scale = 1.0;
mode = "1280x720";
position = "10000,10000";
}
];
};
}
];
};
};
systemd.user.services = {
pizauth.Service = {
ExecStartPost = [
"${pkgs.toybox}/bin/sleep 1"
"//bin/sh -c '${lib.getExe pkgs.pizauth} restore < ${homeDir}/.pizauth.state'"
];
};
teams-applet = {
Unit = {
Description = "teams applet";
Requires = [ "tray.target" ];
After = [
"graphical-session.target"
"tray.target"
];
PartOf = [ "graphical-session.target" ];
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${pkgs.stable.teams-for-linux}/bin/teams-for-linux --disableGpu=true --minimized=true --trayIconEnabled=true";
};
};
onepassword-applet = {
Unit = {
Description = "1password applet";
Requires = [ "tray.target" ];
After = [
"graphical-session.target"
"tray.target"
];
PartOf = [ "graphical-session.target" ];
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
Service = {
ExecStart = "${pkgs._1password-gui-beta}/bin/1password";
};
};
};
services.pizauth = {
enable = true;
extraConfig = ''
auth_notify_cmd = "if [[ \"$(notify-send -A \"Open $PIZAUTH_ACCOUNT\" -t 30000 'pizauth authorisation')\" == \"0\" ]]; then open \"$PIZAUTH_URL\"; fi";
error_notify_cmd = "notify-send -t 90000 \"pizauth error for $PIZAUTH_ACCOUNT\" \"$PIZAUTH_MSG\"";
token_event_cmd = "pizauth dump > ${homeDir}/.pizauth.state";
'';
accounts = {
work = {
authUri = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
tokenUri = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
clientId = "08162f7c-0fd2-4200-a84a-f25a4db0b584";
clientSecret = "TxRBilcHdC6WGBee]fs?QR:SJ8nI[g82";
scopes = [
"https://outlook.office365.com/IMAP.AccessAsUser.All"
"https://outlook.office365.com/SMTP.Send"
"offline_access"
];
loginHint = "${confLib.getConfig.repo.secrets.local.work.mailAddress}";
};
};
};
xdg =
let
inherit (confLib.getConfig.repo.secrets.local.work) user1 user2 user3;
in
{
mimeApps = {
defaultApplications = {
"x-scheme-handler/msteams" = [ "teams-for-linux.desktop" ];
};
};
desktopEntries =
let
terminal = false;
categories = [ "Application" ];
icon = "firefox";
in
{
firefox_work = {
name = "Firefox (work)";
genericName = "Firefox work";
exec = "firefox -p work";
inherit terminal categories icon;
};
"firefox_${user1}" = {
name = "Firefox (${user1})";
genericName = "Firefox ${user1}";
exec = "firefox -p ${user1}";
inherit terminal categories icon;
};
"firefox_${user2}" = {
name = "Firefox (${user2})";
genericName = "Firefox ${user2}";
exec = "firefox -p ${user2}";
inherit terminal categories icon;
};
"firefox_${user3}" = {
name = "Firefox (${user3})";
genericName = "Firefox ${user3}";
exec = "firefox -p ${user3}";
inherit terminal categories icon;
};
};
};
swarselsystems = {
startup = [
# { command = "nextcloud --background"; }
# { command = "vesktop --start-minimized --enable-speech-dispatcher --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime"; }
# { command = "element-desktop --hidden --enable-features=UseOzonePlatform --ozone-platform=wayland --disable-gpu-driver-bug-workarounds"; }
# { command = "anki"; }
# { command = "obsidian"; }
# { command = "nm-applet"; }
# { command = "feishin"; }
# { command = "teams-for-linux --disableGpu=true --minimized=true --trayIconEnabled=true"; }
# { command = "1password"; }
];
monitors = {
work_back_middle = rec {
name = "LG Electronics LG Ultra HD 0x000305A6";
mode = "2560x1440";
scale = "1";
position = "5120,0";
workspace = "1:一";
# output = "DP-10";
output = name;
};
work_front_left = rec {
name = "LG Electronics LG Ultra HD 0x0007AB45";
mode = "3840x2160";
scale = "1";
position = "5120,0";
workspace = "1:一";
# output = "DP-7";
output = name;
};
work_middle_middle_main = rec {
name = "HP Inc. HP Z32 CN41212T55";
mode = "3840x2160";
scale = "1";
position = "-1280,0";
workspace = "1:一";
# output = "DP-3";
output = name;
};
# work_middle_middle_main = rec {
# name = "HP Inc. HP 732pk CNC4080YL5";
# mode = "3840x2160";
# scale = "1";
# position = "-1280,0";
# workspace = "11:M";
# # output = "DP-8";
# output = name;
# };
work_middle_middle_side = rec {
name = "HP Inc. HP 732pk CNC4080YL5";
mode = "3840x2160";
transform = "270";
scale = "1";
position = "-3440,-1050";
workspace = "12:S";
# output = "DP-8";
output = name;
};
work_middle_middle_old = rec {
name = "Hewlett Packard HP Z24i CN44250RDT";
mode = "1920x1200";
transform = "270";
scale = "1";
position = "-2480,0";
workspace = "12:S";
# output = "DP-9";
output = name;
};
work_seminary = rec {
name = "Applied Creative Technology Transmitter QUATTRO201811";
mode = "1280x720";
scale = "1";
position = "10000,10000"; # i.e. this screen is inaccessible by moving the mouse
workspace = "14:T";
# output = "DP-4";
output = name;
};
};
inputs = {
"1133:45081:MX_Master_2S_Keyboard" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
# "2362:628:PIXA3854:00_093A:0274_Touchpad" = {
# dwt = "enabled";
# tap = "enabled";
# natural_scroll = "enabled";
# middle_emulation = "enabled";
# drag_lock = "disabled";
# };
"1133:50504:Logitech_USB_Receiver" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
"1133:45944:MX_KEYS_S" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
};
};
} // lib.optionalAttrs (type != "nixos") {
sops.secrets = lib.mkIf (!config.swarselsystems.isPublic && !config.swarselsystems.isNixos) {
harica-root-ca = {
sopsFile = certsSopsFile;
path = "${homeDir}/.aws/certs/harica-root.pem";
owner = mainUser;
};
};
};
}
4.5.5.4. Uni
{ confLib, ... }:
{
config = {
services.pizauth = {
enable = true;
accounts = {
uni = {
authUri = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
tokenUri = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
clientId = "08162f7c-0fd2-4200-a84a-f25a4db0b584";
clientSecret = "TxRBilcHdC6WGBee]fs?QR:SJ8nI[g82";
scopes = [
"https://outlook.office365.com/IMAP.AccessAsUser.All"
"https://outlook.office365.com/SMTP.Send"
"offline_access"
];
loginHint = "${confLib.getConfig.repo.secrets.local.uni.mailAddress}";
};
};
};
};
}
4.5.5.5. Framework
This holds configuration that is specific to framework laptops.
_:
{
config = {
swarselsystems = {
inputs = {
"12972:18:Framework_Laptop_16_Keyboard_Module_-_ANSI_Keyboard" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
};
};
};
}
4.6. Shared
This section is for modules that are to be used on NixOS and home-manager scopes alike. This is for example needed in order to allow me to define and set my own custom functions only once in the NixOS config and then mirror them into the corresponding home-manager option.
4.6.1. TODO Configuration options
These are my own configuration options that are used in multiple places throughout the configuration - for which reason I did not put them right where they are used for the first time.
TODO: check which of these can be replaced but builtin functions.
{ self, config, lib, ... }:
{
options.swarselsystems = {
proxyHost = lib.mkOption {
type = lib.types.str;
default = config.node.name;
};
isBastionTarget = lib.mkOption {
type = lib.types.bool;
default = false;
};
isCloud = lib.mkOption {
type = lib.types.bool;
default = false;
};
isServer = lib.mkOption {
type = lib.types.bool;
default = config.swarselsystems.isCloud;
};
isClient = lib.mkOption {
type = lib.types.bool;
default = config.swarselsystems.isLaptop;
};
isMicroVM = lib.mkOption {
type = lib.types.bool;
default = false;
};
isSwap = lib.mkOption {
type = lib.types.bool;
default = true;
};
writeGlobalNetworks = lib.mkOption {
type = lib.types.bool;
default = true;
};
swapSize = lib.mkOption {
type = lib.types.str;
default = "8G";
};
rootDisk = lib.mkOption {
type = lib.types.str;
default = "";
};
# @ future me: dont put this under server prefix
# home-manager would then try to import all swarselsystems.server.* options
localVLANs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
# @ future me: dont put this under server prefix
# home-manager would then try to import all swarselsystems.server.* options
initrdVLAN = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
};
mainUser = lib.mkOption {
type = lib.types.str;
default = "swarsel";
};
isCrypted = lib.mkEnableOption "uses full disk encryption";
withMicroVMs = lib.mkEnableOption "enable MicroVMs on this host";
isImpermanence = lib.mkEnableOption "use impermanence on this system";
isSecureBoot = lib.mkEnableOption "use secure boot on this system";
isLaptop = lib.mkEnableOption "laptop host";
isNixos = lib.mkEnableOption "nixos host";
isPublic = lib.mkEnableOption "is a public machine (no secrets)";
isDarwin = lib.mkEnableOption "darwin host";
isLinux = lib.mkEnableOption "whether this is a linux machine";
isBtrfs = lib.mkEnableOption "use btrfs filesystem";
sopsFile = lib.mkOption {
type = lib.types.either lib.types.str lib.types.path;
# default = (if config.swarselsystems.isImpermanence then "/persist" else "") + config.node.secretsDir + "/secrets.yaml";
default = config.node.secretsDir + "/secrets.yaml";
};
homeDir = lib.mkOption {
type = lib.types.str;
default = "/home/swarsel";
};
xdgDir = lib.mkOption {
type = lib.types.str;
default = "/run/user/1000";
};
flakePath = lib.mkOption {
type = lib.types.str;
default = "/home/swarsel/.dotfiles";
};
wallpaper = lib.mkOption {
type = lib.types.path;
default = "${self}/files/wallpaper/lenovowp.png";
};
sharescreen = lib.mkOption {
type = lib.types.str;
default = "";
};
lowResolution = lib.mkOption {
type = lib.types.str;
default = "";
};
highResolution = lib.mkOption {
type = lib.types.str;
default = "";
};
};
}
4.6.2. Variables (vars; holds firefox & stylix config parts)
At work I am using several services that are using SSO login - however, as I am using four different accounts at work, this becomes a chore here. Hence, I have defined multiple profiles in Work that are all practically using the same configuration. To save screen space, I template that profile here.
Set in firefox about:config > toolkit.legacyUserProfileCustomizations.stylesheets to true. This should in principle be set automatically using the below config, but it seems not to be working reliably.
For styling, I am using the stylix NixOS module, loaded by flake. This package is really great, as it adds nix expressions for basically everything. Ever since switching to this, I did not have to play around with theming anywhere else.
The full list of nerd-fonts can be found here: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/data/fonts/nerd-fonts/manifests/fonts.json
This is where the theme for the whole OS is defined. Originally, this noweb-ref section could not be copied to the general NixOS config since they are on different folder structure levels in the config, which would have made the flake impure. By now, I have found out that using the ${self} method for referencing the flake root, I could circumvent this problem. Also, the noweb-ref block could in general be replaced by a custom attribute set (see for example [BROKEN LINK: h:e7f98ad8-74a6-4860-a368-cce154285ff0]). The difference here was, for a long time, that this block is used in a NixOS and a home-manager-only configuration, verbatim. If I were to use an attribute set, I would have to duplicate this block once each for NixOS and home-manager. Alas, this block stays (for now). However, I learned how to use an attribute set in a custom home-manager module and pass it to both NixOS and home-manager configurations, which also removed the need for that use of it.
In short, the options defined here are passed to the modules systems using _modules.args - they can then be used by passing vars as an attribute in the input attribute set of a modules system file (=basically all files in this configuration)
{ self, lib, pkgs, ... }:
{
_module.args = {
vars = rec {
waylandSessionVariables = {
SDL_VIDEODRIVER = "wayland";
QT_WAYLAND_DISABLE_WINDOWDECORATION = "1";
QT_QPA_PLATFORM = "wayland-egl";
ANKI_WAYLAND = "1";
OBSIDIAN_USE_WAYLAND = "1";
MOZ_ENABLE_WAYLAND = "1";
};
waylandExports =
let
renderedWaylandExports = map (key: "export ${key}=${waylandSessionVariables.${key}};") (builtins.attrNames waylandSessionVariables);
in
builtins.concatStringsSep "\n" renderedWaylandExports;
stylix = {
polarity = "dark";
opacity.popups = 0.5;
cursor = {
package = pkgs.banana-cursor;
# package = pkgs.capitaine-cursors;
name = "Banana";
# name = "capitaine-cursors";
size = 16;
};
fonts = {
sizes = {
terminal = 10;
applications = 11;
};
serif = {
# package = (pkgs.nerdfonts.override { fonts = [ "FiraMono" "FiraCode"]; });
package = pkgs.cantarell-fonts;
# package = pkgs.montserrat;
name = "Cantarell";
# name = "FiraCode Nerd Font Propo";
# name = "Montserrat";
};
sansSerif = {
# package = (pkgs.nerdfonts.override { fonts = [ "FiraMono" "FiraCode"]; });
package = pkgs.cantarell-fonts;
# package = pkgs.montserrat;
name = "Cantarell";
# name = "FiraCode Nerd Font Propo";
# name = "Montserrat";
};
monospace = {
package = pkgs.nerd-fonts.fira-mono; # has overrides
name = "FiraCode Nerd Font Mono";
};
emoji = {
package = pkgs.noto-fonts-color-emoji;
name = "Noto Color Emoji";
};
};
};
stylixHomeTargets = {
emacs.enable = false;
waybar.enable = false;
sway.useWallpaper = false;
spicetify.enable = true;
firefox.profileNames = [ "default" ];
};
firefox = {
userChrome = builtins.readFile "${self}/files/firefox/chrome/userChrome.css";
extensions = {
packages = with pkgs.nur.repos.rycee.firefox-addons; [
tridactyl
tampermonkey
sidebery
browserpass
clearurls
darkreader
# enhancer-for-youtube
istilldontcareaboutcookies
translate-web-pages
ublock-origin
reddit-enhancement-suite
sponsorblock
web-archives
onepassword-password-manager
single-file
widegithub
enhanced-github
unpaywall
don-t-fuck-with-paste
# plasma-integration
noscript
# configure a shortcut 'ctrl+shift+c' with behaviour 'do nothing' in order to disable the dev console shortcut
(buildFirefoxXpiAddon {
pname = "shortkeys";
version = "4.0.2";
addonId = "Shortkeys@Shortkeys.com";
url = "https://addons.mozilla.org/firefox/downloads/file/3673761/shortkeys-4.0.2.xpi";
sha256 = "c6fe12efdd7a871787ac4526eea79ecc1acda8a99724aa2a2a55c88a9acf467c";
meta = with lib;
{
description = "Easily customizable custom keyboard shortcuts for Firefox. To configure this addon go to Addons (ctrl+shift+a) ->Shortkeys ->Options. Report issues here (please specify that the issue is found in Firefox): https://github.com/mikecrittenden/shortkeys";
mozPermissions = [
"tabs"
"downloads"
"clipboardWrite"
"browsingData"
"storage"
"bookmarks"
"sessions"
"<all_urls>"
];
platforms = platforms.all;
};
})
];
};
settings =
{
"extensions.autoDisableScopes" = 0;
"browser.bookmarks.showMobileBookmarks" = true;
"browser.autofocus" = false;
"toolkit.legacyUserProfileCustomizations.stylesheets" = true;
"browser.search.suggest.enabled" = false;
"browser.search.suggest.enabled.private" = false;
"browser.urlbar.suggest.searches" = false;
"browser.urlbar.showSearchSuggestionsFirst" = false;
"browser.topsites.contile.enabled" = false;
"browser.newtabpage.activity-stream.feeds.section.topstories" = false;
"browser.newtabpage.activity-stream.feeds.snippets" = false;
"browser.newtabpage.activity-stream.section.highlights.includePocket" = false;
"browser.newtabpage.activity-stream.section.highlights.includeBookmarks" = false;
"browser.newtabpage.activity-stream.section.highlights.includeDownloads" = false;
"browser.newtabpage.activity-stream.section.highlights.includeVisited" = false;
"browser.newtabpage.activity-stream.showSponsored" = false;
"browser.newtabpage.activity-stream.system.showSponsored" = false;
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
};
search = {
# default = "Kagi";
default = "google";
# privateDefault = "Kagi";
privateDefault = "google";
engines = {
"Kagi" = {
urls = [{
template = "https://kagi.com/search";
params = [
{ name = "q"; value = "{searchTerms}"; }
];
}];
icon = "https://kagi.com/favicon.ico";
updateInterval = 24 * 60 * 60 * 1000; # every day
definedAliases = [ "@k" ];
};
"Nix Packages" = {
urls = [{
template = "https://search.nixos.org/packages";
params = [
{ name = "type"; value = "packages"; }
{ name = "query"; value = "{searchTerms}"; }
];
}];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = [ "@np" ];
};
"NixOS Wiki" = {
urls = [{
template = "https://nixos.wiki/index.php?search={searchTerms}";
}];
icon = "https://nixos.wiki/favicon.png";
updateInterval = 24 * 60 * 60 * 1000; # every day
definedAliases = [ "@nw" ];
};
"NixOS Options" = {
urls = [{
template = "https://search.nixos.org/options";
params = [
{ name = "query"; value = "{searchTerms}"; }
];
}];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = [ "@no" ];
};
"Home Manager Options" = {
urls = [{
template = "https://home-manager-options.extranix.com/";
params = [
{ name = "query"; value = "{searchTerms}"; }
];
}];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = [ "@hm" "@ho" "@hmo" ];
};
"Confluence search" = {
urls = [{
template = "https://vbc.atlassian.net/wiki/search";
params = [
{ name = "text"; value = "{searchTerms}"; }
];
}];
definedAliases = [ "@c" "@cf" "@confluence" ];
};
"Jira search" = {
urls = [{
template = "https://vbc.atlassian.net/issues/";
params = [
{ name = "jql"; value = "textfields ~ \"{searchTerms}*\"&wildcardFlag=true"; }
];
}];
definedAliases = [ "@j" "@jire" ];
};
"google".metaData.alias = "@g";
};
force = true; # this is required because otherwise the search.json.mozlz4 symlink gets replaced on every firefox restart
};
};
};
};
}
4.6.3. Meta options (options only)
{ lib, ... }:
{
options = {
node = {
secretsDir = lib.mkOption {
description = "Path to the secrets directory for this node.";
type = lib.types.path;
default = ./.;
};
configDir = lib.mkOption {
description = "Path to the base directory for this node.";
type = lib.types.path;
default = ./.;
};
name = lib.mkOption {
type = lib.types.str;
};
arch = lib.mkOption {
type = lib.types.str;
};
type = lib.mkOption {
type = lib.types.str;
};
lockFromBootstrapping = lib.mkOption {
description = "Whether this host should be marked to not be bootstrapped again using swarsel-bootstrap.";
type = lib.types.bool;
};
};
};
}
4.6.4. Config Library (confLib)
{ self, config, lib, globals, inputs, outputs, minimal, nixosConfig ? null, ... }:
let
domainDefault = service: config.repo.secrets.common.services.domains.${service};
proxyDefault = config.swarselsystems.proxyHost;
addressDefault =
if
config.swarselsystems.proxyHost != config.node.name
then
if
config.swarselsystems.server.wireguard.interfaces.wgProxy.isClient
then
globals.networks."${config.swarselsystems.server.wireguard.interfaces.wgProxy.serverNetConfigPrefix}-wgProxy".hosts.${config.node.name}.ipv4
else
globals.networks.${config.swarselsystems.server.netConfigName}.hosts.${config.node.name}.ipv4
else
"localhost";
in
{
_module.args = {
confLib = rec {
getConfig = if nixosConfig == null then config else nixosConfig;
gen = { name ? "n/a", user ? name, group ? name, dir ? null, port ? null, domain ? (domainDefault name), address ? addressDefault, proxy ? proxyDefault }: rec {
servicePort = port;
serviceName = name;
specificServiceName = "${name}-${config.node.name}";
serviceUser = user;
serviceGroup = group;
serviceDomain = domain;
baseDomain = lib.swarselsystems.getBaseDomain domain;
subDomain = lib.swarselsystems.getSubDomain domain;
serviceDir = dir;
serviceAddress = address;
serviceProxy = proxy;
proxyAddress4 = globals.hosts.${proxy}.wanAddress4 or null;
proxyAddress6 = globals.hosts.${proxy}.wanAddress6 or null;
};
static = rec {
inherit (globals.hosts.${config.node.name}) isHome;
inherit (globals.general) homeProxy webProxy dnsServer homeDnsServer homeWebProxy idmServer oauthServer;
webProxyIf = "${webProxy}-wgProxy";
homeProxyIf = "home-wgHome";
isProxied = config.node.name != webProxy;
nginxAccessRules = ''
allow ${globals.networks.home-lan.vlans.home.cidrv4};
allow ${globals.networks.home-lan.vlans.home.cidrv6};
allow ${globals.networks.home-lan.vlans.services.hosts.${homeProxy}.ipv4};
allow ${globals.networks.home-lan.vlans.services.hosts.${homeProxy}.ipv6};
deny all;
'';
homeServiceAddress = lib.optionalString (config.swarselsystems.server.wireguard.interfaces ? wgHome) globals.networks."${config.swarselsystems.server.wireguard.interfaces.wgHome.serverNetConfigPrefix}-wgHome".hosts.${config.node.name}.ipv4;
};
mkMicrovm =
if config.swarselsystems.withMicroVMs then
(guestName: {
${guestName} = {
backend = "microvm";
autostart = true;
modules = [
(config.node.configDir + /guests/${guestName}/default.nix)
{
node.secretsDir = config.node.configDir + /secrets/${guestName};
node.configDir = config.node.configDir + /guests/${guestName};
networking.nftables.firewall = {
zones.untrusted.interfaces = lib.mkIf
(
lib.length config.guests.${guestName}.networking.links == 1
)
config.guests.${guestName}.networking.links;
};
}
"${self}/modules/nixos/optional/microvm-guest.nix"
"${self}/modules/nixos/optional/systemd-networkd-base.nix"
];
microvm = {
system = config.node.arch;
baseMac = config.repo.secrets.local.networking.networks.lan.mac;
interfaces.vlan-services = { };
};
extraSpecialArgs = {
inherit (inputs.self) nodes;
inherit (inputs.self.pkgs.${config.node.arch}) lib;
inherit inputs outputs minimal;
inherit (inputs) self;
withHomeManager = false;
microVMParent = config.node.name;
globals = inputs.self.globals.${config.node.arch};
};
};
}) else (_: { _ = { }; });
genNginx =
{ serviceAddress
, serviceName
, serviceDomain
, servicePort
, protocol ? "http"
, maxBody ? (-1)
, maxBodyUnit ? ""
, noSslVerify ? false
, proxyWebsockets ? false
, oauth2 ? false
, oauth2Groups ? [ ]
, extraConfig ? ""
, extraConfigLoc ? ""
}: {
upstreams = {
${serviceName} = {
servers = {
"${serviceAddress}:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
useACMEHost = globals.domains.main;
forceSSL = true;
acmeRoot = null;
oauth2 = {
enable = lib.mkIf oauth2 true;
allowedGroups = lib.mkIf (oauth2Groups != [ ]) oauth2Groups;
};
locations = {
"/" = {
proxyPass = "${protocol}://${serviceName}";
proxyWebsockets = lib.mkIf proxyWebsockets true;
extraConfig = lib.optionalString (maxBody != (-1)) ''
client_max_body_size ${builtins.toString maxBody}${maxBodyUnit};
'' + extraConfigLoc;
};
};
extraConfig = lib.optionalString noSslVerify ''
proxy_ssl_verify off;
'' + extraConfig;
};
};
};
};
};
}
4.6.5. Packages
This is the central station for self-defined packages. These are all referenced in default.nix. Wherever possible, I am keeping the shell version of these scripts in this file as well and then read it using builtin.readFile in the NixOS configurations. This lets me keep full control in this one file but also keep the separate files uncluttered.
Note: The structure of generating the packages was changed in commit 2cf03a3 refactor: package and module generation. That commit can be checked out in order to see a simpler version of achieving the same thing.
4.6.5.1. Packages (flake)
{ self, lib, pkgs, ... }:
let
mkPackages = names: pkgs: builtins.listToAttrs (map
(name: {
inherit name;
value = pkgs.callPackage "${self}/pkgs/flake/${name}" { inherit self name; };
})
names);
packageNames = lib.swarselsystems.readNix "pkgs/flake";
in
mkPackages packageNames pkgs
4.6.5.1.1. pass-fuzzel
This app allows me, in conjunction with my Yubikey, to quickly enter passwords when the need arises. Normal and TOTP passwords are supported, and they can either be printed directly or copied to the clipboard.
# Adapted from https://code.kulupu.party/thesuess/home-manager/src/branch/main/modules/river.nix shopt -s nullglob globstar otp=0 typeit=0 while :; do case ${1:-} in -t | --type) typeit=1 ;; -o | --otp) otp=1 ;; *) break ;; esac shift done export PASSWORD_STORE_DIR=~/.local/share/password-store prefix=${PASSWORD_STORE_DIR-~/.local/share/password-store} if [[ $otp -eq 0 ]]; then password_files=("$prefix"/**/*.gpg) else password_files=("$prefix"/otp/**/*.gpg) fi password_files=("${password_files[@]#"$prefix"/}") password_files=("${password_files[@]%.gpg}") password=$(printf '%s\n' "${password_files[@]}" | fuzzel --dmenu "$@") [[ -n $password ]] || exit if [[ $otp -eq 0 ]]; then if [[ $typeit -eq 0 ]]; then pass show -c "$password" &> /tmp/pass-fuzzel else pass show "$password" | { IFS= read -r pass printf %s "$pass" } | wtype - fi else if [[ $typeit -eq 0 ]]; then pass otp -c "$password" &> /tmp/pass-fuzzel else pass otp "$password" | { IFS= read -r pass printf %s "$pass" } | wtype - fi fi notify-send -u critical -a pass -t 1000 "Copied/Typed Password"
{ self, name, writeShellApplication, libnotify, pass, fuzzel, wtype }:
writeShellApplication {
inherit name;
runtimeInputs = [ libnotify (pass.withExtensions (exts: [ exts.pass-otp ])) fuzzel wtype ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.2. quickpass
shopt -s nullglob globstar notify-send "$(env | grep -E 'WAYLAND|SWAY')" password="$1" pass show "$password" | { IFS= read -r pass printf %s "$pass" } | wtype - notify-send -u critical -a pass -t 1000 "Typed Password"
{ self, name, writeShellApplication, libnotify, pass, wtype }:
writeShellApplication {
inherit name;
runtimeInputs = [ libnotify pass wtype ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.3. cura5
The version of cura used to be quite outdated in nixpkgs. I am fetching a newer AppImage here and use that instead.
# taken from https://github.com/NixOS/nixpkgs/issues/186570#issuecomment-1627797219
{ appimageTools, fetchurl, writeScriptBin, pkgs, ... }:
let
cura5 = appimageTools.wrapType2 rec {
pname = "cura5";
version = "5.9.0";
src = fetchurl {
url = "https://github.com/Ultimaker/Cura/releases/download/${version}/UltiMaker-Cura-${version}-linux-X64.AppImage";
hash = "sha256-STtVeM4Zs+PVSRO3cI0LxnjRDhOxSlttZF+2RIXnAp4=";
};
extraPkgs = pkgs: with pkgs; [ ];
};
in
writeScriptBin "cura" ''
#! ${pkgs.bash}/bin/bash
# AppImage version of Cura loses current working directory and treats all paths relative to $HOME.
# So we convert each of the files passed as argument to an absolute path.
# This fixes use cases like `cd /path/to/my/files; cura mymodel.stl anothermodel.stl`.
args=()
for a in "$@"; do
if [ -e "$a" ]; then
a="$(realpath "$a")"
fi
args+=("$a")
done
exec "${cura5}/bin/cura5" "''${args[@]}"
''
4.6.5.1.4. hm-specialisation
This script allows for quick git home-manager specialisation switching.
{ name, writeShellApplication, fzf, findutils, home-manager, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ fzf findutils home-manager ];
text = ''
genpath=$(home-manager generations | head -1 | awk '{print $7}')
dirs=$(find "$genpath/specialisation" -type l 2>/dev/null; [ -d "$genpath" ] && echo "$genpath")
"$(echo "$dirs" | fzf --prompt="Choose home-manager specialisation to activate")"/activate
'';
}
4.6.5.1.5. cdw
This script allows for quick git worktree switching.
{ name, writeShellApplication, fzf, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ fzf ];
text = ''
cd "$(git worktree list | fzf | awk '{print $1}')"
'';
}
4.6.5.1.6. cdb
This script allows for quick git branch switching.
{ name, writeShellApplication, fzf, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ fzf ];
text = ''
git checkout "$(git branch --list | grep -v "^\*" | fzf | awk '{print $1}')"
'';
}
4.6.5.1.7. prstatus
This script allows for quick checking of nixpkgs PR statuses.
{ name, writeShellApplication, curl, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ curl ];
text = ''
curl https://nixpkgs.molybdenum.software/api/v2/landings/"$1"
'';
}
4.6.5.1.8. bak
This script lets me quickly backup files by appending .bak to the filename.
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
text = ''
cp -r "$1"{,.bak}
'';
}
4.6.5.1.9. timer
This app starts a configuratble timer and uses TTS to say something once the timer runs out.
{ name, writeShellApplication, speechd, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ speechd ];
text = ''
sleep "$1"; while true; do spd-say "$2"; sleep 0.5; done;
'';
}
4.6.5.1.10. e
This is a shorthand for calling emacsclient mostly. Also, it hides the kittyterm scratchpad window that I sometimes use for calling a command quickly, in case it is on the screen. After emacs closes, the kittyterm window is then shown again if it was visible earlier.
wait=0 while :; do case ${1:-} in -w | --wait) wait=1 ;; *) break ;; esac shift done STR=$(swaymsg -t get_tree | jq -r 'recurse(.nodes[]) | select(.name == "__i3_scratch")' | grep kittyterm || true) if [ "$STR" == "" ]; then swaymsg '[title="kittyterm"]' scratchpad show emacsclient -c -a "" "$@" swaymsg '[title="kittyterm"]' scratchpad show else if [[ $wait -eq 0 ]]; then emacsclient -n -c -a "" "$@" else emacsclient -c -a "" "$@" fi fi
{ self, name, writeShellApplication, emacs30-pgtk, sway, jq }:
writeShellApplication {
inherit name;
runtimeInputs = [ emacs30-pgtk sway jq ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.11. command-not-found
The normal command-not-found.sh uses the outdated nix-shell commands as suggestions. This version supplies me with the more modern nixpkgs#<name> version.
# Adapted from https://github.com/bennofs/nix-index/blob/master/command-not-found.sh command_not_found_handle() { if [ -n "${MC_SID-}" ] || ! [ -t 1 ]; then >&2 echo "$1: command not found" return 127 fi echo -n "searching nix-index..." ATTRS=$(@nix-locate@ --minimal --no-group --type x --type s --whole-name --at-root "/bin/$1") case $(echo -n "$ATTRS" | grep -c "^") in 0) >&2 echo -ne "$(@tput@ el1)\r" >&2 echo "$1: command not found" ;; *) >&2 echo -ne "$(@tput@ el1)\r" >&2 echo "The program ‘$(@tput@ setaf 4)$1$(@tput@ sgr0)’ is currently not installed." >&2 echo "It is provided by the following derivation(s):" while read -r ATTR; do ATTR=${ATTR%.out} >&2 echo " $(@tput@ setaf 12)nixpkgs#$(@tput@ setaf 4)$ATTR$(@tput@ sgr0)" done <<< "$ATTRS" ;; esac return 127 } command_not_found_handler() { command_not_found_handle "$@" return $? }
4.6.5.1.12. swarselcheck
This app checks for different apps that I keep around in the scratchpad for quick viewing and hiding (messengers and music players mostly) and then behaves like the kittyterm hider that I described in e.
kitty=0 element=0 vesktop=0 spotifyplayer=0 while :; do case ${1:-} in -k | --kitty) kitty=1 ;; -e | --element) element=1 ;; -d | --vesktop) vesktop=1 ;; -s | --spotifyplayer) spotifyplayer=1 ;; *) break ;; esac shift done if [[ $kitty -eq 1 ]]; then STR=$(swaymsg -t get_tree | jq -r 'recurse(.nodes[]) | select(.name == "__i3_scratch")' | grep kittyterm || true) CHECK=$(swaymsg -t get_tree | grep kittyterm || true) if [ "$CHECK" == "" ]; then exec kitty --app-id kittyterm -T kittyterm -o confirm_os_window_close=0 zellij attach --create kittyterm & sleep 1 fi if [ "$STR" == "" ]; then exec swaymsg '[title="kittyterm"]' scratchpad show else exec swaymsg '[title="kittyterm"]' scratchpad show fi elif [[ $element -eq 1 ]]; then STR=$(swaymsg -t get_tree | grep Element || true) if [ "$STR" == "" ]; then exec element-desktop else exec swaymsg '[app_id=Element]' kill fi elif [[ $vesktop -eq 1 ]]; then STR=$(swaymsg -t get_tree | grep vesktop || true) if [ "$STR" == "" ]; then exec vesktop else exec swaymsg '[app_id=vesktop]' kill fi elif [[ $spotifyplayer -eq 1 ]]; then STR=$(swaymsg -t get_tree | jq -r 'recurse(.nodes[]) | select(.name == "__i3_scratch")' | grep spotifytui || true) CHECK=$(swaymsg -t get_tree | grep spotifytui || true) if [ "$CHECK" == "" ]; then exec kitty --add-id spotifytui -T spotifytui -o confirm_os_window_close=0 spotify_player & sleep 1 fi if [ "$STR" == "" ]; then exec swaymsg '[title="spotifytui"]' scratchpad show else exec swaymsg '[title="spotifytui"]' scratchpad show fi fi
{ self, name, writeShellApplication, kitty, element-desktop, vesktop, spotify-player, jq }:
writeShellApplication {
inherit name;
runtimeInputs = [ kitty element-desktop vesktop spotify-player jq ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.13. swarselcheck-niri
while :; do case ${1:-} in -k | --kitty) cmd=(sh -c 'kitty --app-id kittyterm -T kittyterm -o confirm_os_window_close=0 zellij attach --create kittyterm' '&') searchapp="kittyterm" ;; -e | --element) cmd=(element-desktop) searchapp="Element" ;; -d | --vesktop) cmd=(vesktop) searchapp="vesktop" ;; -s | --spotifyplayer) cmd=(sh -c 'kitty --add-id spotifytui -T spotifytui -o confirm_os_window_close=0 spotify_player' '&') searchapp="spotifytui" ;; *) break ;; esac shift done WIN_INFO=$(niri msg -j windows | jq --arg search "$searchapp" '.[] | select (.app_id | test($search)) | { id, is_focused, workspace_id }') ID=$(echo "$WIN_INFO" | jq -r '.id // empty') IS_FOCUSED=$(echo "$WIN_INFO" | jq -r '.is_focused // empty') TARGET_MONITOR=$(niri msg -j workspaces | jq --arg search "" '.[] | select (.name != null and (.name | test($search))) | { output }' | jq -r '.output // empty') CURRENT_WORKSPACE=$(niri msg -j workspaces | jq -r '.[] | select (.is_active == true) | .output // empty') if [ -z "$ID" ]; then niri msg action spawn -- "${cmd[@]}" elif [ "$IS_FOCUSED" ]; then niri msg action move-window-to-workspace "" --window-id "$ID" --focus false else niri msg action focus-monitor "$TARGET_MONITOR" && niri msg action move-window-to-workspace "$CURRENT_WORKSPACE" --window-id "$ID" && niri msg action focus-floating fi
{ self, name, writeShellApplication, kitty, element-desktop, vesktop, spotify-player, jq }:
writeShellApplication {
inherit name;
runtimeInputs = [ kitty element-desktop vesktop spotify-player jq ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.14. swarselzellij
# KITTIES=$(($(pgrep -P 1 kitty | wc -l) - 1)) # if ((KITTIES < 1)); then # exec kitty -o confirm_os_window_close=0 zellij attach --create main # else # exec kitty -o confirm_os_window_close=0 zellij attach --create "temp $KITTIES" # fi exec kitty -o confirm_os_window_close=0 zellij
{ self, name, writeShellApplication, kitty }:
writeShellApplication {
inherit name;
runtimeInputs = [ kitty ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.15. waybarupdate
This scripts checks if there are uncommited changes in either my dotfile repo, my university repo, or my passfile repo. In that case a warning will be shown in waybar.
CFG=$(git --git-dir="$HOME"/.dotfiles/.git --work-tree="$HOME"/.dotfiles/ status -s | wc -l) CSE=$(git --git-dir="$DOCUMENT_DIR_PRIV"/CSE_TUWIEN/.git --work-tree="$DOCUMENT_DIR_PRIV"/CSE_TUWIEN/ status -s | wc -l) PASS=$(($(git --git-dir="$HOME"/.local/share/password-store/.git --work-tree="$HOME"/.local/share/password-store/ status -s | wc -l) + $(git --git-dir="$HOME"/.local/share/password-store/.git --work-tree="$HOME"/.local/share/password-store/ diff origin/main..HEAD | wc -l))) if [[ $CFG != 0 ]]; then CFG_STR='CONFIG' else CFG_STR='' fi if [[ $CSE != 0 ]]; then CSE_STR=' CSE' else CSE_STR='' fi if [[ $PASS != 0 ]]; then PASS_STR=' PASS' else PASS_STR='' fi OUT="$CFG_STR""$CSE_STR""$PASS_STR" echo "$OUT"
{ self, name, writeShellApplication, git }:
writeShellApplication {
inherit name;
runtimeInputs = [ git ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.16. opacitytoggle
This app quickly toggles between 5% and 0% transparency.
if swaymsg opacity plus 0.01 -q; then swaymsg opacity 1 else swaymsg opacity 0.95 fi
{ self, name, writeShellApplication, sway }:
writeShellApplication {
inherit name;
runtimeInputs = [ sway ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.17. fs-diff
This utility is used to compare the current state of the root directory with the blanket state that is stored in /root-blank (the snapshot that is restored on each reboot of an impermanence machine). Using this, I can find files that I will lose once I reboot - if there are important files in that list, I can then easily add them to the persist options.
set -euo pipefail OLD_TRANSID=$(sudo btrfs subvolume find-new /mnt/root-blank 9999999) OLD_TRANSID=${OLD_TRANSID#transid marker was } sudo btrfs subvolume find-new "/mnt/root" "$OLD_TRANSID" | sed '$d' | cut -f17- -d' ' | sort | uniq | while read -r path; do path="/$path" if [ -L "$path" ]; then : # The path is a symbolic link, so is probably handled by NixOS already elif [ -d "$path" ]; then : # The path is a directory, ignore else echo "$path" fi done
{ self, name, writeShellApplication }:
writeShellApplication {
inherit name;
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.18. github-notifications
This utility checks if there are updated packages in nixpkgs-unstable. It does so by fully building the most recent configuration, which I do not love, but it has its merits once I am willing to switch to the newer version.
{ name, writeShellApplication, jq, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ jq ];
text = ''
count=$(curl -u Swarsel:"$(cat "$GITHUB_NOTIFICATION_TOKEN_PATH")" https://api.github.com/notifications | jq '. | length')
if [[ "$count" != "0" ]]; then
echo "{\"text\":\"$count\"}"
fi
'';
}
4.6.5.1.19. kanshare
This utility checks if there are updated packages in nixpkgs-unstable. It does so by fully building the most recent configuration, which I do not love, but it has its merits once I am willing to switch to the newer version.
{ name, writeShellApplication, wlr-randr, busybox, wl-mirror, mako, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ wlr-randr busybox wl-mirror mako ];
text = ''
makoctl mode -a do-not-disturb
wlr-randr | grep "$2" | cut -d" " -f1 | xargs -I{} wl-present mirror "$1" --fullscreen-output {}
makoctl mode -r do-not-disturb
'';
}
4.6.5.1.20. swarsel-bootstrap
This program sets up a new NixOS host remotely. It also takes care of secret management on the new host.
# highly inspired by https://github.com/EmergentMind/nix-config/blob/dev/files/scripts/bootstrap-nixos.sh set -eo pipefail target_hostname="" target_destination="" target_arch="" target_user="swarsel" ssh_port="22" persist_dir="" disk_encryption=0 disk_encryption_args="" no_disko_deps="false" temp=$(mktemp -d) function help_and_exit() { echo echo "Remotely installs SwarselSystem on a target machine including secret deployment." echo echo "USAGE: $0 -n <target_hostname> -d <target_destination> [OPTIONS]" echo echo "ARGS:" echo " -n <target_hostname> specify target_hostname of the target host to deploy the nixos config on." echo " -d <target_destination> specify ip or url to the target host." echo " -a <targeit_arch> specify the architecture of the target host." echo " target during install process." echo echo "OPTIONS:" echo " -u <target_user> specify target_user with sudo access. nix-config will be cloned to their home." echo " Default='${target_user}'." echo " --port <ssh_port> specify the ssh port to use for remote access. Default=${ssh_port}." echo " --debug Enable debug mode." echo " --no-disko-deps Upload only disk script and not dependencies (for use on low ram)." echo " -h | --help Print this help." exit 0 } function cleanup() { rm -rf "$temp" rm -rf /tmp/disko-password } trap cleanup exit function red() { echo -e "\x1B[31m[!] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[31m[!] $($2) \x1B[0m" fi } function green() { echo -e "\x1B[32m[+] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[32m[+] $($2) \x1B[0m" fi } function yellow() { echo -e "\x1B[33m[*] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[33m[*] $($2) \x1B[0m" fi } function yes_or_no() { echo -en "\x1B[32m[+] $* [y/n] (default: y): \x1B[0m" while true; do read -rp "" yn yn=${yn:-y} case $yn in [Yy]*) return 0 ;; [Nn]*) return 1 ;; esac done } function update_sops_file() { key_name=$1 key_type=$2 key=$3 if [ ! "$key_type" == "hosts" ] && [ ! "$key_type" == "users" ]; then red "Invalid key type passed to update_sops_file. Must be either 'hosts' or 'users'." exit 1 fi cd "${git_root}" SOPS_FILE=".sops.yaml" sed -i "{ # Remove any * and & entries for this host /[*&]$key_name/ d; # Inject a new age: entry # n matches the first line following age: and p prints it, then we transform it while reusing the spacing /age:/{n; p; s/\(.*- \*\).*/\1$key_name/}; # Inject a new hosts or user: entry /&$key_type/{n; p; s/\(.*- &\).*/\1$key_name $key/} }" $SOPS_FILE green "Updating .sops.yaml" cd - } while [[ $# -gt 0 ]]; do case "$1" in -n) shift target_hostname=$1 ;; -d) shift target_destination=$1 ;; -a) shift target_arch=$1 ;; -u) shift target_user=$1 ;; --port) shift ssh_port=$1 ;; --no-disko-deps) no_disko_deps="true" ;; --debug) set -x ;; -h | --help) help_and_exit ;; *) echo "Invalid option detected." help_and_exit ;; esac shift done if [[ $target_arch == "" || $target_destination == "" || $target_hostname == "" ]]; then red "error: target_arch, target_destination or target_hostname not set." help_and_exit fi LOCKED="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.node.lockFromBootstrapping)" if [[ $LOCKED == "true" ]]; then red "THIS SYSTEM IS LOCKED FROM BOOTSTRAPPING - set 'node.lockFromBootstrapping = lib.mkForce false;' to proceed" exit fi green "~SwarselSystems~ remote installer" green "Reading system information for $target_hostname ..." DISK="$(nix eval --raw ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.rootDisk)" green "Root Disk: $DISK" CRYPTED="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isCrypted)" if [[ $CRYPTED == "true" ]]; then green "Encryption: ✓" disk_encryption=1 disk_encryption_args=( --disk-encryption-keys /tmp/disko-password /tmp/disko-password ) else red "Encryption: X" disk_encryption=0 fi IMPERMANENCE="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isImpermanence)" if [[ $IMPERMANENCE == "true" ]]; then green "Impermanence: ✓" persist_dir="/persist" else red "Impermanence: X" persist_dir="" fi SWAP="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isSwap)" if [[ $SWAP == "true" ]]; then green "Swap: ✓" else red "Swap: X" fi SECUREBOOT="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isSecureBoot)" if [[ $SECUREBOOT == "true" ]]; then green "Secure Boot: ✓" else red "Secure Boot: X" fi ssh_cmd="ssh -oport=${ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t $target_user@$target_destination" # ssh_root_cmd=$(echo "$ssh_cmd" | sed "s|${target_user}@|root@|") # uses @ in the sed switch to avoid it triggering on the $ssh_key value ssh_root_cmd=${ssh_cmd/${target_user}@/root@} scp_cmd="scp -oport=${ssh_port} -o StrictHostKeyChecking=no" if [[ -z ${FLAKE} ]]; then FLAKE=/home/"$target_user"/.dotfiles fi if [ ! -d "$FLAKE" ]; then cd /home/"$target_user" yellow "Flake directory not found - cloning repository from GitHub" git clone git@github.com:Swarsel/.dotfiles.git || (yellow "Could not clone repository via SSH - defaulting to HTTPS" && git clone https://github.com/Swarsel/.dotfiles.git) FLAKE=/home/"$target_user"/.dotfiles fi cd "$FLAKE" rm install/flake.lock || true git_root=$(git rev-parse --show-toplevel) # ------------------------ green "Wiping known_hosts of $target_destination" sed -i "/$target_hostname/d; /$target_destination/d" ~/.ssh/known_hosts # ------------------------ green "Preparing a new ssh_host_ed25519_key pair for $target_hostname." # Create the directory where sshd expects to find the host keys install -d -m755 "$temp/$persist_dir/etc/ssh" # Generate host ssh key pair without a passphrase ssh-keygen -t ed25519 -f "$temp/$persist_dir/etc/ssh/ssh_host_ed25519_key" -C root@"$target_hostname" -N "" # Set the correct permissions so sshd will accept the key chmod 600 "$temp/$persist_dir/etc/ssh/ssh_host_ed25519_key" echo "Adding ssh host fingerprint at $target_destination to ~/.ssh/known_hosts" # This will fail if we already know the host, but that's fine ssh-keyscan -p "$ssh_port" "$target_destination" >> ~/.ssh/known_hosts || true # ------------------------ # when using luks, disko expects a passphrase on /tmp/disko-password, so we set it for now and will update the passphrase later # via the config if [ "$disk_encryption" -eq 1 ]; then while true; do green "Set disk encryption passphrase:" read -rs luks_passphrase green "Please confirm passphrase:" read -rs luks_passphrase_confirm if [[ $luks_passphrase == "$luks_passphrase_confirm" ]]; then echo "$luks_passphrase" > /tmp/disko-password $ssh_root_cmd "echo '$luks_passphrase' > /tmp/disko-password" break else red "Passwords do not match" fi done fi # ------------------------ green "Generating hardware-config.nix for $target_hostname and adding it to the nix-config." $ssh_root_cmd "nixos-generate-config --force --no-filesystems --root /mnt" mkdir -p "$FLAKE"/hosts/nixos/"$target_arch"/"$target_hostname" $scp_cmd root@"$target_destination":/mnt/etc/nixos/hardware-configuration.nix "${git_root}"/hosts/nixos/"$target_arch"/"$target_hostname"/hardware-configuration.nix # ------------------------ green "Generating hostkey for ssh initrd" $ssh_root_cmd "mkdir -p $temp/etc/secrets/initrd /etc/secrets/initrd" $ssh_root_cmd "ssh-keygen -t ed25519 -N '' -f $temp/etc/secrets/initrd/ssh_host_ed25519_key" $ssh_root_cmd "cp $temp/etc/secrets/initrd/ssh_host_ed25519_key /etc/secrets/initrd/ssh_host_ed25519_key" # ------------------------ green "Deploying minimal NixOS installation on $target_destination" if [[ $no_disko_deps == "true" ]]; then green "Building without disko dependencies (using custom kexec)" nix run github:nix-community/nixos-anywhere/1.10.0 -- "${disk_encryption_args[@]}" --no-disko-deps --ssh-port "$ssh_port" --extra-files "$temp" --flake ./install#"$target_hostname" --kexec "$(nix build --print-out-paths .#packages."$target_arch".swarsel-kexec)/swarsel-kexec-$target_arch.tar.gz" root@"$target_destination" else green "Building with disko dependencies (using nixos-images kexec)" nix run github:nix-community/nixos-anywhere/1.10.0 -- "${disk_encryption_args[@]}" --ssh-port "$ssh_port" --extra-files "$temp" --flake ./install#"$target_hostname" root@"$target_destination" fi echo "Updating ssh host fingerprint at $target_destination to ~/.ssh/known_hosts" ssh-keyscan -p "$ssh_port" "$target_destination" >> ~/.ssh/known_hosts || true # ------------------------ while true; do read -rp "Press Enter to continue once the remote host has finished booting." if nc -z "$target_destination" "${ssh_port}" 2> /dev/null; then green "$target_destination is booted. Continuing..." break else yellow "$target_destination is not yet ready." fi done # ------------------------ if [[ $SECUREBOOT == "true" ]]; then green "Setting up secure boot keys" $ssh_root_cmd "mkdir -p /var/lib/sbctl" read -ra scp_call <<< "${scp_cmd}" sudo "${scp_call[@]}" -r /var/lib/sbctl root@"$target_destination":/var/lib/ $ssh_root_cmd "sbctl enroll-keys --ignore-immutable --microsoft || true" fi # ------------------------ if [ -n "$persist_dir" ]; then $ssh_root_cmd "cp /etc/machine-id $persist_dir/etc/machine-id || true" $ssh_root_cmd "cp -R /etc/ssh/ $persist_dir/etc/ssh/ || true" fi # ------------------------ green "Generating an age key based on the new ssh_host_ed25519_key." target_key=$( ssh-keyscan -p "$ssh_port" -t ssh-ed25519 "$target_destination" 2>&1 | grep ssh-ed25519 | cut -f2- -d" " || ( red "Failed to get ssh key. Host down?" exit 1 ) ) host_age_key=$(nix shell nixpkgs#ssh-to-age.out -c sh -c "echo $target_key | ssh-to-age") if grep -qv '^age1' <<< "$host_age_key"; then red "The result from generated age key does not match the expected format." yellow "Result: $host_age_key" yellow "Expected format: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" exit 1 else echo "$host_age_key" fi green "Updating nix-secrets/.sops.yaml" update_sops_file "$target_hostname" "hosts" "$host_age_key" yellow ".sops.yaml has been updated. There may be superfluous entries, you might need to edit manually." if yes_or_no "Do you want to manually edit .sops.yaml now?"; then vim "${git_root}"/.sops.yaml fi green "Updating all secrets files to reflect updates .sops.yaml" sops updatekeys --yes --enable-local-keyservice "${git_root}"/hosts/nixos/"$target_arch"/"$target_hostname"/secrets/* || true # -------------------------- green "Making ssh_host_ed25519_key available to home-manager for user $target_user" sed -i "/$target_hostname/d; /$target_destination/d" ~/.ssh/known_hosts $ssh_root_cmd "mkdir -p /home/$target_user/.ssh; chown -R $target_user:users /home/$target_user/.ssh/" $scp_cmd root@"$target_destination":/etc/ssh/ssh_host_ed25519_key root@"$target_destination":/home/"$target_user"/.ssh/ssh_host_ed25519_key $ssh_root_cmd "chown $target_user:users /home/$target_user/.ssh/ssh_host_ed25519_key" # __________________________ if yes_or_no "Add ssh host fingerprints for git upstream repositories? (This is needed for building the full config)"; then green "Adding ssh host fingerprints for git{lab,hub}" $ssh_cmd "mkdir -p /home/$target_user/.ssh/; ssh-keyscan -t ssh-ed25519 gitlab.com github.com | tee /home/$target_user/.ssh/known_hosts" $ssh_root_cmd "mkdir -p /root/.ssh/; ssh-keyscan -t ssh-ed25519 gitlab.com github.com | tee /root/.ssh/known_hosts" fi # -------------------------- if yes_or_no "Do you want to copy your full nix-config and nix-secrets to $target_hostname?"; then green "Adding ssh host fingerprint at $target_destination to ~/.ssh/known_hosts" ssh-keyscan -p "$ssh_port" "$target_destination" >> ~/.ssh/known_hosts || true green "Copying full nix-config to $target_hostname" cd "${git_root}" just sync "$target_user" "$target_destination" if [ -n "$persist_dir" ]; then $ssh_root_cmd "cp -r /home/$target_user/.dotfiles $persist_dir/.dotfiles || true" $ssh_root_cmd "cp -r /home/$target_user/.ssh $persist_dir/.ssh || true" fi if yes_or_no "Do you want to rebuild immediately?"; then green "Building nix-config for $target_hostname" # yellow "Reminder: The password is 'setup'" $ssh_root_cmd "mkdir -p /root/.local/share/nix/; printf '{\"extra-substituters\":{\"https://nix-community.cachix.org\":true,\"https://nix-community.cachix.org https://cache.ngi0.nixos.org/\":true},\"extra-trusted-public-keys\":{\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\":true,\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= cache.ngi0.nixos.org-1:KqH5CBLNSyX184S9BKZJo1LxrxJ9ltnY2uAs5c/f1MA=\":true}}' | tee /root/.local/share/nix/trusted-settings.json" # $ssh_cmd -oForwardAgent=yes "cd .dotfiles && sudo nixos-rebuild --show-trace --flake .#$target_hostname switch" store_path=$(nix build --no-link --print-out-paths .#nixosConfigurations."$target_hostname".config.system.build.toplevel) green "Copying generation to $target_hostname" nix copy --to "ssh://root@$target_destination" "$store_path" # prev_system=$($ssh_root_cmd " readlink -e /nix/var/nix/profiles/system") green "Linking generation in bootloader" $ssh_root_cmd "/run/current-system/sw/bin/nix-env --profile /nix/var/nix/profiles/system --set $store_path" green "Setting generation to activate upon next boot" $ssh_root_cmd "$store_path/bin/switch-to-configuration boot" else echo green "NixOS was successfully installed!" echo "Post-install config build instructions:" echo "To copy nix-config from this machine to the $target_hostname, run the following command from ~/nix-config" echo "just sync $target_user $target_destination" echo "To rebuild, sign into $target_hostname and run the following command from ~/nix-config" echo "cd nix-config" # see above FIXME:(bootstrap) echo "sudo nixos-rebuild .pre-commit-config.yaml show-trace --flake .#$target_hostname switch" # echo "just rebuild" echo fi fi green "NixOS was successfully installed!" if yes_or_no "You can now commit and push the nix-config, which includes the hardware-configuration.nix for $target_hostname?"; then cd "${git_root}" deadnix hosts/nixos/"$target_arch"/"$target_hostname"/hardware-configuration.nix -qe nixpkgs--fmt hosts/nixos/"$target_arch"/"$target_hostname"/hardware-configuration.nix (.pre-commit-config.yaml mit run --all-files 2> /dev/null || true) && git add "$git_root/hosts/nixos/$target_arch/$target_hostname/hardware-configuration.nix" && git add "$git_root/.sops.yaml" && git add "$git_root/secrets" && (git commit -m "feat: deployed $target_hostname" || true) && git push fi if yes_or_no "Reboot now?"; then $ssh_root_cmd "reboot" fi rm -rf /tmp/disko-password
{ self, name, writeShellApplication, openssh }:
writeShellApplication {
inherit name;
runtimeInputs = [ openssh ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.21. swarsel-rebuild
set -eo pipefail target_config="hotel" target_arch="" target_user="swarsel" function help_and_exit() { echo echo "Builds SwarselSystem configuration." echo echo "USAGE: $0 [OPTIONS]" echo echo "ARGS:" echo " -n <target_config> specify nixos config to build." echo " Default: hotel" echo " -u <target_user> specify user to deploy for." echo " Default: swarsel" echo " -a <target_arch> specify target architecture." echo " -h | --help Print this help." exit 0 } function red() { echo -e "\x1B[31m[!] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[31m[!] $($2) \x1B[0m" fi } function green() { echo -e "\x1B[32m[+] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[32m[+] $($2) \x1B[0m" fi } function yellow() { echo -e "\x1B[33m[*] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[33m[*] $($2) \x1B[0m" fi } while [[ $# -gt 0 ]]; do case "$1" in -n) shift target_config=$1 ;; -a) shift target_arch=$1 ;; -u) shift target_user=$1 ;; -h | --help) help_and_exit ;; *) echo "Invalid option detected." help_and_exit ;; esac shift done if [[ $target_arch == "" ]]; then red "error: target_arch not set." help_and_exit fi cd /home/"$target_user" if [ ! -d /home/"$target_user"/.dotfiles ]; then green "Cloning repository from GitHub" git clone https://github.com/Swarsel/.dotfiles.git else red "A .dotfiles repository is in the way. Please (re-)move the repository and try again." exit 1 fi local_keys=$(ssh-add -L || true) pub_key=$(cat /home/"$target_user"/.dotfiles/secrets/public/ssh/yubikey.pub) read -ra pub_arr <<< "$pub_key" cd .dotfiles if [[ $local_keys != *"${pub_arr[1]}"* ]]; then yellow "The ssh key for this configuration is not available." green "Adjusting flake.nix so that the configuration is buildable" sed -i '/nix-secrets = {/,/^[[:space:]]*};/d' flake.nix sed -i '/vbc-nix = {/,/^[[:space:]]*};/d' flake.nix sed -i '/[[:space:]]*\/\/ (inputs.vbc-nix.overlays.default final prev)/d' overlays/default.nix rm modules/home/common/env.nix rm modules/home/common/gammastep.nix rm modules/home/common/git.nix rm modules/home/common/mail.nix rm modules/home/common/yubikey.nix rm modules/nixos/server/restic.nix rm hosts/nixos/aarch64-linux/milkywell/default.nix rm -rf modules/nixos/server rm -rf modules/home/server nix flake update vbc-nix git add . else green "Valid SSH key found! Continuing with installation" fi sudo nixos-generate-config --dir /home/"$target_user"/.dotfiles/hosts/nixos/"$target_arch"/"$target_config"/ git add /home/"$target_user"/.dotfiles/hosts/nixos/"$target_arch"/"$target_config"/hardware-configuration.nix green "Installing flake $target_config" sudo nixos-rebuild --show-trace --flake .#"$target_config" boot yellow "Please keep in mind that this is only a demo of the configuration. Things might break unexpectedly."
{ self, name, writeShellApplication, git }:
writeShellApplication {
inherit name;
runtimeInputs = [ git ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.22. swarsel-install
Autoformatting always puts the EOF with indentation, which makes shfmt check fail. When editing this block, unindent them manually.
set -eo pipefail target_config="hotel" target_hostname="hotel" target_user="swarsel" target_arch="" persist_dir="" target_disk="/dev/vda" disk_encryption=0 function help_and_exit() { echo echo "Locally installs SwarselSystem on this machine." echo echo "USAGE: $0 -n <target_config> -d <target_disk> [OPTIONS]" echo echo "ARGS:" echo " -n <target_config> specify the nixos config to deploy." echo " Default: hotel" echo " -d <target_disk> specify disk to install on." echo " Default: /dev/vda" echo " -u <target_user> specify user to deploy for." echo " Default: swarsel" echo " -a <target_arch> specify target architecture." echo " -h | --help Print this help." exit 0 } function red() { echo -e "\x1B[31m[!] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[31m[!] $($2) \x1B[0m" fi } function green() { echo -e "\x1B[32m[+] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[32m[+] $($2) \x1B[0m" fi } function yellow() { echo -e "\x1B[33m[*] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[33m[*] $($2) \x1B[0m" fi } while [[ $# -gt 0 ]]; do case "$1" in -n) shift target_config=$1 target_hostname=$1 ;; -u) shift target_user=$1 ;; -d) shift target_disk=$1 ;; -a) shift target_arch=$1 ;; -h | --help) help_and_exit ;; *) echo "Invalid option detected." help_and_exit ;; esac shift done function cleanup() { sudo rm -rf .cache/nix sudo rm -rf /root/.cache/nix } trap cleanup exit if [[ $target_arch == "" || $target_hostname == "" ]]; then red "error: target_arch or target_hostname not set." help_and_exit fi green "~SwarselSystems~ local installer" cd /home/"$target_user" sudo rm -rf /root/.cache/nix sudo rm -rf .cache/nix sudo rm -rf .dotfiles green "Cloning repository from GitHub" git clone https://github.com/Swarsel/.dotfiles.git local_keys=$(ssh-add -L || true) pub_key=$(cat /home/"$target_user"/.dotfiles/secrets/public/ssh/yubikey.pub) read -ra pub_arr <<< "$pub_key" cd .dotfiles if [[ $local_keys != *"${pub_arr[1]}"* ]]; then yellow "The ssh key for this configuration is not available." green "Adjusting flake.nix so that the configuration is buildable ..." sed -i '/vbc-nix = {/,/^[[:space:]]*};/d' flake.nix sed -i '/[[:space:]]*\/\/ (inputs.vbc-nix.overlays.default final prev)/d' overlays/default.nix nix flake update vbc-nix git add . else green "Valid SSH key found! Continuing with installation" fi green "Reading system information for $target_config ..." DISK="$(nix eval --raw ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.rootDisk)" green "Root Disk in config: $DISK - Root Disk passed in cli: $target_disk" CRYPTED="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isCrypted)" if [[ $CRYPTED == "true" ]]; then green "Encryption: ✓" disk_encryption=1 else red "Encryption: X" disk_encryption=0 fi IMPERMANENCE="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isImpermanence)" if [[ $IMPERMANENCE == "true" ]]; then green "Impermanence: ✓" persist_dir="/persist" else red "Impermanence: X" persist_dir="" fi SWAP="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isSwap)" if [[ $SWAP == "true" ]]; then green "Swap: ✓" else red "Swap: X" fi SECUREBOOT="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isSecureBoot)" if [[ $SECUREBOOT == "true" ]]; then green "Secure Boot: ✓" else red "Secure Boot: X" fi if [ "$disk_encryption" -eq 1 ]; then while true; do green "Set disk encryption passphrase:" read -rs luks_passphrase green "Please confirm passphrase:" read -rs luks_passphrase_confirm if [[ $luks_passphrase == "$luks_passphrase_confirm" ]]; then echo "$luks_passphrase" > /tmp/disko-password break else red "Passwords do not match" fi done fi green "Setting up disk ..." if [[ $target_config == "hotel" ]]; then sudo nix --experimental-features "nix-command flakes" run github:nix-community/disko/v1.10.0 -- --mode destroy,format,mount --flake .#"$target_config" --yes-wipe-all-disks --arg diskDevice "$target_disk" else sudo nix --experimental-features "nix-command flakes" run github:nix-community/disko/latest -- --mode destroy,format,mount --flake .#"$target_config" --yes-wipe-all-disks fi sudo mkdir -p /mnt/"$persist_dir"/home/"$target_user"/ sudo cp -r /home/"$target_user"/.dotfiles /mnt/"$persist_dir"/home/"$target_user"/ sudo chown -R 1000:100 /mnt/"$persist_dir"/home/"$target_user" green "Generating hardware configuration ..." sudo nixos-generate-config --root /mnt --no-filesystems --dir /home/"$target_user"/.dotfiles/hosts/nixos/"$target_arch"/"$target_config"/ git add /home/"$target_user"/.dotfiles/hosts/nixos/"$target_arch"/"$target_config"/hardware-configuration.nix sudo mkdir -p /root/.local/share/nix/ printf '{\"extra-substituters\":{\"https://nix-community.cachix.org\":true,\"https://nix-community.cachix.org https://cache.ngi0.nixos.org/\":true},\"extra-trusted-public-keys\":{\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\":true,\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= cache.ngi0.nixos.org-1:KqH5CBLNSyX184S9BKZJo1LxrxJ9ltnY2uAs5c/f1MA=\":true}}' | sudo tee /root/.local/share/nix/trusted-settings.json > /dev/null green "Installing flake $target_config" store_path=$(nix build --no-link --print-out-paths .#nixosConfigurationsMinimal."$target_config".config.system.build.toplevel) green "Linking generation in bootloader" sudo "/run/current-system/sw/bin/nix-env --profile /nix/var/nix/profiles/system --set $store_path" green "Setting generation to activate upon next boot" sudo "$store_path/bin/switch-to-configuration boot" green "Installation finished! Reboot to see changes"
{ self, name, writeShellApplication, git }:
writeShellApplication {
inherit name;
runtimeInputs = [ git ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.23. swarsel-postinstall
set -eo pipefail target_config="hotel" target_user="swarsel" function help_and_exit() { echo echo "Locally installs SwarselSystem on this machine." echo echo "USAGE: $0 -d <disk> [OPTIONS]" echo echo "ARGS:" echo " -d <disk> specify disk to install on." echo " -n <target_config> specify the nixos config to deploy." echo " Default: hotel" echo " Default: hotel" echo " -u <target_user> specify user to deploy for." echo " Default: swarsel" echo " -h | --help Print this help." exit 0 } function green() { echo -e "\x1B[32m[+] $1 \x1B[0m" if [ -n "${2-}" ]; then echo -e "\x1B[32m[+] $($2) \x1B[0m" fi } while [[ $# -gt 0 ]]; do case "$1" in -n) shift target_config=$1 ;; -u) shift target_user=$1 ;; -h | --help) help_and_exit ;; *) echo "Invalid option detected." help_and_exit ;; esac shift done function cleanup() { sudo rm -rf .cache/nix sudo rm -rf /root/.cache/nix } trap cleanup exit sudo rm -rf .cache/nix sudo rm -rf /root/.cache/nix green "~SwarselSystems~ remote post-installer" cd /home/"$target_user"/.dotfiles SECUREBOOT="$(nix eval ~/.dotfiles#nixosConfigurations."$target_config".config.swarselsystems.isSecureBoot)" if [[ $SECUREBOOT == "true" ]]; then green "Setting up secure boot keys" sudo mkdir -p /var/lib/sbctl sbctl create-keys || true sbctl enroll-keys --ignore-immutable --microsoft || true fi sudo nixos-rebuild --flake .#"$target_config" switch green "Post-install finished!"
{ self, name, writeShellApplication, git }:
writeShellApplication {
inherit name;
runtimeInputs = [ git ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.24. t2ts
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ ];
text = ''
date -d"$1" +%s
'';
}
4.6.5.1.25. ts2t
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ ];
text = ''
date -d @"$1" 2>/dev/null || date -r "$1"
'';
}
4.6.5.1.26. vershell
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ ];
text = ''
nix shell github:nixos/nixpkgs/"$1"#"$2";
'';
}
4.6.5.1.27. eontimer
{ lib
, python3
, fetchFromGitHub
, makeDesktopItem
, writeShellScript
, ...
}:
let
wrapper = writeShellScript "eontimer-wrapper" ''
export QT_QPA_PLATFORM=xcb
exec @out@/bin/EonTimer
'';
in
python3.pkgs.buildPythonApplication rec {
pname = "eontimer";
version = "3.0.0-rc.6";
pyproject = true;
src = fetchFromGitHub {
owner = "DasAmpharos";
repo = "EonTimer";
rev = version;
hash = "sha256-+XN/VGGlEg2gVncRZrWDOZ2bfxt8xyIu22F2wHlG6YI=";
};
build-system = [
python3.pkgs.setuptools
python3.pkgs.wheel
];
dependencies = with python3.pkgs; [
altgraph
certifi
charset-normalizer
idna
libsass
macholib
packaging
pillow
pipdeptree
platformdirs
pyinstaller
pyinstaller-hooks-contrib
pyside6
requests
setuptools
shiboken6
urllib3
];
nativeBuildInputs = [
python3.pkgs.pyinstaller
];
buildPhase = ''
runHook preBuild
pyinstaller --clean --noconfirm EonTimer.spec
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/bin
mkdir -p $out/share/applications
cp dist/EonTimer $out/bin/
install -Dm755 -T ${wrapper} $out/bin/eontimer
substituteInPlace $out/bin/eontimer --subst-var out
runHook postInstall
'';
postInstall = ''
install -Dm755 -t $out/share/applications ${
makeDesktopItem {
name = "eontimer";
desktopName = "EonTimer";
comment = "Start EonTimer";
exec = "eontimer";
}
}/share/applications/eontimer.desktop
'';
meta = {
description = "Pokémon RNG Timer";
homepage = "https://github.com/DasAmpharos/EonTimer";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ ];
mainProgram = "eon-timer";
};
}
4.6.5.1.28. project
set -euo pipefail if [ ! -d "$(pwd)/.git" ]; then git init fi nix flake init --template "$FLAKE"#"$1" direnv allow
{ self, name, writeShellApplication }:
writeShellApplication {
inherit name;
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.29. fhs
{ name, pkgs, ... }:
let
base = pkgs.appimageTools.defaultFhsEnvArgs;
in
pkgs.buildFHSEnv (base // {
inherit name;
targetPkgs = pkgs: (base.targetPkgs pkgs) ++ [ pkgs.pkg-config ];
profile = "export FHS=1";
runScript = "zsh";
extraOutputsToInstall = [ "dev" ];
})
4.6.5.1.30. swarsel-displaypower
A crude script to power on all displays that might be attached. Needed because sometimes displays do not awake from sleep.
swaymsg "output * power on" > /dev/null 2>&1 || true swaymsg "output * dpms on" > /dev/null 2>&1 || true
{ self, name, writeShellApplication, sway }:
writeShellApplication {
inherit name;
runtimeInputs = [ sway ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.31. swarsel-mgba
AppImage version of mgba in which the lua scripting works.
{ appimageTools, fetchurl, ... }:
let
pname = "mgba";
version = "0.10.4";
src = fetchurl {
url = "https://github.com/mgba-emu/mgba/releases/download/${version}/mGBA-${version}-appimage-x64.appimage";
hash = "sha256-rDihDfuA8DqxvCe6UeavCzpjeU+fSqUbFnyTNC2dc1I=";
};
appimageContents = appimageTools.extractType2 { inherit pname version src; };
in
appimageTools.wrapType2 {
inherit pname version src;
extraInstallCommands = ''
install -Dm444 ${appimageContents}/io.mgba.mGBA.desktop -t $out/share/applications
substituteInPlace $out/share/applications/io.mgba.mGBA.desktop \
--replace-fail 'Exec=mgba-qt %f' 'Exec=mgba'
cp -r ${appimageContents}/usr/share/icons $out/share
'';
}
4.6.5.1.32. swarsel-deploy
# heavily inspired from https://github.com/oddlama/nix-config/blob/d42cbde676001a7ad8a3cace156e050933a4dcc3/pkgs/deploy.nix
{ name, bc, nix-output-monitor, writeShellApplication, ... }:
writeShellApplication {
runtimeInputs = [ bc nix-output-monitor ];
inherit name;
text = ''
set -euo pipefail
shopt -s lastpipe # allow cmd | readarray
function die() {
echo "error: $*" >&2
exit 1
}
function show_help() {
echo 'Usage: deploy [OPTIONS] <host,...> [ACTION]'
echo "Builds, pushes and activates nixosConfigurations on target systems."
echo ""
echo 'ACTION:'
echo ' switch [default] Switch immediately to the new configuration and make it the boot default'
echo ' boot Make the configuration the new boot default'
echo " test Activate the configuration but don't make it the boot default"
echo " dry-activate Don't activate, just show what would be done"
echo ""
echo 'OPTIONS: [passed to nix build]'
}
function time_start() {
T_START=$(date +%s.%N)
}
function time_next() {
T_END=$(date +%s.%N)
T_LAST=$(${bc}/bin/bc <<< "scale=1; ($T_END - $T_START)/1")
T_START="$T_END"
}
USER_FLAKE_DIR=$(git rev-parse --show-toplevel 2> /dev/null || pwd) ||
die "Could not determine current working directory. Something went very wrong."
[[ -e "$USER_FLAKE_DIR/flake.nix" ]] ||
die "Could not determine location of your project's flake.nix. Please run this at or below your main directory containing the flake.nix."
cd "$USER_FLAKE_DIR"
[[ $# -gt 0 ]] || {
show_help
exit 1
}
OPTIONS=()
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
"help" | "--help" | "-help" | "-h")
show_help
exit 1
;;
-*) OPTIONS+=("$1") ;;
*) POSITIONAL_ARGS+=("$1") ;;
esac
shift
done
[[ ''${#POSITIONAL_ARGS[@]} -ge 1 ]] ||
die "Missing argument: <hosts...>"
[[ ''${#POSITIONAL_ARGS[@]} -le 2 ]] ||
die "Too many arguments given."
tr , '\n' <<< "''${POSITIONAL_ARGS[0]}" | sort -u | readarray -t HOSTS
ACTION="''${POSITIONAL_ARGS[1]-switch}"
# Expand flake paths for hosts definitions
declare -A TOPLEVEL_FLAKE_PATHS
for host in "''${HOSTS[@]}"; do
TOPLEVEL_FLAKE_PATHS["$host"]=".#nixosConfigurations.$host.config.system.build.toplevel"
done
time_start
# Get outputs of all derivations (should be cached)
declare -A TOPLEVEL_STORE_PATHS
for host in "''${HOSTS[@]}"; do
toplevel="''${TOPLEVEL_FLAKE_PATHS["$host"]}"
# Make sudo call to get prompt out of the way
sudo echo "[1;36m Building [m📦 [34m$host[m"
nix build --no-link "''${OPTIONS[@]}" --show-trace --log-format internal-json -v "$toplevel" |& ${nix-output-monitor}/bin/nom --json ||
die "Failed to get derivation path for $host from ''${TOPLEVEL_FLAKE_PATHS["$host"]}"
TOPLEVEL_STORE_PATHS["$host"]=$(nix build --no-link --print-out-paths "''${OPTIONS[@]}" "$toplevel")
time_next
echo "[1;32m Built [m✅ [34m$host[m [33m''${TOPLEVEL_STORE_PATHS["$host"]}[m [90min ''${T_LAST}s[m"
done
current_host=$(hostname)
for host in "''${HOSTS[@]}"; do
store_path="''${TOPLEVEL_STORE_PATHS["$host"]}"
if [ "$host" = "$current_host" ]; then
echo -e "\033[1;36m Running locally for $host... \033[m"
ssh_prefix="sudo"
else
echo -e "\033[1;36m Copying \033[m➡️ \033[34m$host\033[m"
nix copy --to "ssh://$host" "$store_path"
time_next
echo -e "\033[1;32m Copied \033[m✅ \033[34m$host\033[m \033[90min ''${T_LAST}s\033[m"
ssh_prefix="ssh $host --"
fi
echo -e "\033[1;36m Applying \033[m⚙️ \033[34m$host\033[m"
prev_system=$($ssh_prefix readlink -e /nix/var/nix/profiles/system)
$ssh_prefix /run/current-system/sw/bin/nix-env --profile /nix/var/nix/profiles/system --set "$store_path" ||
die "Failed to set system profile"
$ssh_prefix "$store_path"/bin/switch-to-configuration "$ACTION" ||
echo "Error while activating new system" >&2
if [[ -n $prev_system ]]; then
$ssh_prefix nvd --color always diff "$prev_system" "$store_path" || true
fi
time_next
echo -e "\033[1;32m Applied \033[m✅ \033[34m$host\033[m \033[90min ''${T_LAST}s\033[m"
done
'';
}
4.6.5.1.33. swarsel-build
{ name, nix-output-monitor, writeShellApplication, ... }:
writeShellApplication {
runtimeInputs = [ nix-output-monitor ];
inherit name;
text = ''
set -euo pipefail
[[ "$#" -ge 1 ]] \
|| { echo "usage: build <HOST>..." >&2; exit 1; }
HOSTS=()
for h in "$@"; do
HOSTS+=(".#nixosConfigurations.$h.config.system.build.toplevel")
done
nom build --no-link --print-out-paths --show-trace "''${HOSTS[@]}"
'';
}
4.6.5.1.34. swarsel-instantiate
This is a convenience function that calls nix-instantiate with a number of flags that I need in order to evaluate nix expressions in org-src blocks.
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
text = ''
set -euo pipefail
nix-instantiate --strict --eval --expr "let lib = import <nixpkgs/lib>; in $*"
'';
}
4.6.5.1.35. sshrm
This programs simply runs ssh-keygen on the last host that I tried to ssh into. I need this frequently when working with cloud-init usually.
HISTFILE="$HOME"/.histfile last_ssh_cmd=$(grep -E "ssh " "$HISTFILE" | sed -E 's/^: [0-9]+:[0-9]+;//' | grep "^ssh " | tail -1) host=$(echo "$last_ssh_cmd" | sed -E 's/.*ssh ([^@ ]+@)?([^ ]+).*/\2/') if [[ -n $host ]]; then echo "Removing SSH host key for: $host" ssh-keygen -R "$host" else echo "No valid SSH command found in history." fi
{ self, name, writeShellApplication, openssh }:
writeShellApplication {
inherit name;
runtimeInputs = [ openssh ];
text = builtins.readFile "${self}/files/scripts/${name}.sh";
}
4.6.5.1.36. endme
Sometimes my DE crashes after putting it to suspend - to be precise, it happens when I put it into suspend when I have multiple screens plugged in. I have never taken the time to debug the issue, but instead just switch to a different TTY and then use this script to kill the hanging session.
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
text = ''
set -euo pipefail
systemctl --user stop graphical-session.target
systemctl --user stop graphical-session-pre.target
'';
}
4.6.5.1.37. git-replace
This script allows for quick git replace of a string.
{ name, writeShellApplication, git, gnugrep, findutils, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ git gnugrep findutils ];
text = ''
function help_and_exit() {
echo
echo "Remotely installs SwarselSystem on a target machine including secret deployment."
echo
echo "USAGE: $0 [-f/-t} <from> <to>"
echo
echo "ARGS:"
echo " -f | --filenames Replace in filenames."
echo " -d | --directory Replace text in files within this directory."
echo " -r | --repo Replace text in files in the entire git repo."
echo " -h | --help Print this help."
exit 0
}
target_files=false
target_repo=false
target_dirs=false
while [[ $# -gt 0 ]]; do
case "$1" in
-f | --filenames)
shift
target_files=true
;;
-r | --repo)
shift
target_repo=rue
;;
-d | --directory)
shift
target_dirs=rue
;;
-h | --help) help_and_exit ;;
*)
echo "Invalid option detected."
help_and_exit
;;
esac
shift
done
if [[ $target_files == "true" ]]; then
for file in $(git ls-files | grep "$1" | sed -e "s/\($1[^/]*\).*/\1/" | uniq); do
git mv "$file" "''${file//$1/$2}"
done
fi
if [[ $target_repo == "true" ]]; then
git grep -l "$1" | xargs sed -i "s/$1/$2/g"
fi
if [[ $target_dirs == "true" ]]; then
grep -rl "$1" . | xargs sed -i "s/$1/$2/g"
fi
'';
}
4.6.5.1.38. gen-sops-guest
This script quickly generates a block in .sops.yaml for a guest host.
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
text = ''
if [ "$#" -lt 3 ]; then
echo "Usage: $0 <host> <arch_path> <service1> [service2 ...]" >&2
echo "Example: $0 hintbooth hosts/nixos/x86_64-linux adguardhome nginx" >&2
exit 1
fi
HOST="$1"
ARCH_PATH="$2"
shift 2
for service in "$@"; do
cat <<EOF
- path_regex: ''${ARCH_PATH}/''${HOST}/secrets/''${service}/[^/]+\.(yaml|json|env|ini|enc)\$
key_groups:
- pgp:
- *swarsel
age:
- *''${HOST}
- *''${HOST}-''${service}
EOF
done
'';
}
4.6.5.2. Packages (config)
{ self, lib, pkgs, config, homeConfig, ... }:
let
mkPackages = names: pkgs: builtins.listToAttrs (map
(name: {
inherit name;
value = pkgs.callPackage "${self}/pkgs/config/${name}" { inherit self name homeConfig config; };
})
names);
packageNames = lib.swarselsystems.readNix "pkgs/config";
in
mkPackages packageNames pkgs
4.6.5.2.1. cdr
{ name, homeConfig, writeShellApplication, fzf, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ fzf ];
text = ''
DOCUMENT_DIR_WORK=${homeConfig.systemd.user.sessionVariables.DOCUMENT_DIR_WORK or ""}
DOCUMENT_DIR_PRIV=${homeConfig.systemd.user.sessionVariables.DOCUMENT_DIR_PRIV}
FLAKE=${homeConfig.home.sessionVariables.FLAKE}
cd "$( (find "$DOCUMENT_DIR_WORK" "$DOCUMENT_DIR_PRIV" -maxdepth 1 && echo "$FLAKE") | fzf )"
'';
}
4.6.5.2.2. swarsel-gens
This script quickly lists all nix generatinos on the system
{ name, writeShellApplication, config, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ config.nix.package ];
text = ''
sudo nix-env --list-generations --profile /nix/var/nix/profiles/system
'';
}
4.6.5.2.3. swarsel-switch
This script quickly switches to another nix generation.
{ name, writeShellApplication, config, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ config.nix.package ];
text = ''
sudo nix-env --switch-generation "$1" -p /nix/var/nix/profiles/system && sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch
'';
}
4.6.5.2.4. swarsel-sops
{ name, sops, homeConfig, writeShellApplication, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ sops ];
text = ''
sops updatekeys ${homeConfig.swarselsystems.flakePath}/secrets/repo/*
sops updatekeys ${homeConfig.swarselsystems.flakePath}/secrets/nginx/*
sops updatekeys ${homeConfig.swarselsystems.flakePath}/secrets/work/*
sops updatekeys ${homeConfig.swarselsystems.flakePath}/hosts/*/*/*/secrets/*/secrets.yaml
'';
}
4.7. Profiles
In this section I define custom modules under the swarsel attribute. These are mostly used to define settings specific to a host. I keep these settings confined to either home-manager or nixos to maintain compatibility with non-NixOS machines.
Note: The structure of generating the packages was changed in commit 2cf03a3 refactor: package and module generation. That commit can be checked out in order to see a simpler version of achieving the same thing.
4.7.1. NixOS
Modules that need to be loaded on the NixOS level. Note that these will not be available on systems that are not running NixOS.
{ lib, ... }:
let
profileNames = lib.swarselsystems.readNix "profiles/nixos";
in
{
imports = lib.swarselsystems.mkImports profileNames "profiles/nixos";
}
4.7.1.1. Personal
{ lib, config, ... }:
{
options.swarselprofiles.personal = lib.mkEnableOption "is this a personal host";
config = lib.mkIf config.swarselprofiles.personal {
swarselmodules = {
# keyd = lib.mkDefault true;
appimage = lib.mkDefault true;
autologin = lib.mkDefault true;
blueman = lib.mkDefault true;
boot = lib.mkDefault true;
btrfs = lib.mkDefault true;
distrobox = lib.mkDefault true;
env = lib.mkDefault true;
firezone-client = lib.mkDefault true;
general = lib.mkDefault true;
gnome-keyring = lib.mkDefault true;
gvfs = lib.mkDefault true;
hardware = lib.mkDefault true;
home-manager = lib.mkDefault true;
impermanence = lib.mkDefault true;
interceptionTools = lib.mkDefault true;
keyboards = lib.mkDefault true;
lanzaboote = lib.mkDefault true;
ledger = lib.mkDefault true;
lid = lib.mkDefault true;
login = lib.mkDefault true;
lowBattery = lib.mkDefault false;
network = lib.mkDefault true;
networkDevices = lib.mkDefault true;
nftables = lib.mkDefault true;
nix-ld = lib.mkDefault true;
nvd = lib.mkDefault true;
packages = lib.mkDefault true;
pii = lib.mkDefault true;
pipewire = lib.mkDefault true;
ppd = lib.mkDefault true;
programs = lib.mkDefault true;
pulseaudio = lib.mkDefault true;
remotebuild = lib.mkDefault true;
security = lib.mkDefault true;
sops = lib.mkDefault true;
stylix = lib.mkDefault true;
sway = lib.mkDefault true;
swayosd = lib.mkDefault true;
syncthing = lib.mkDefault true;
systemdTimeout = lib.mkDefault true;
time = lib.mkDefault true;
users = lib.mkDefault true;
uwsm = lib.mkDefault true;
xdg-portal = lib.mkDefault true;
xserver = lib.mkDefault true;
yubikey = lib.mkDefault true;
zsh = lib.mkDefault true;
};
home-manager.users."${config.swarselsystems.mainUser}" = {
swarselprofiles = {
personal = lib.mkDefault true;
};
};
};
}
4.7.1.2. Minimal
{ lib, config, ... }:
{
options.swarselprofiles.minimal = lib.mkEnableOption "declare this a minimal host";
config = lib.mkIf config.swarselprofiles.minimal {
swarselmodules = {
general = lib.mkDefault true;
home-manager = lib.mkDefault true;
xserver = lib.mkDefault true;
lanzaboote = lib.mkDefault true;
time = lib.mkDefault true;
users = lib.mkDefault true;
impermanence = lib.mkDefault true;
security = lib.mkDefault true;
sops = lib.mkDefault true;
pii = lib.mkDefault true;
zsh = lib.mkDefault true;
yubikey = lib.mkDefault true;
autologin = lib.mkDefault true;
boot = lib.mkDefault true;
btrfs = lib.mkDefault true;
nftables = lib.mkDefault true;
server = {
ssh = lib.mkDefault true;
diskEncryption = lib.mkDefault true;
};
};
};
}
4.7.1.3. Hotel
{ lib, config, ... }:
{
options.swarselprofiles.hotel = lib.mkEnableOption "is this a hotel host";
config = lib.mkIf config.swarselprofiles.hotel {
swarselmodules = {
packages = lib.mkForce true;
general = lib.mkForce true;
home-manager = lib.mkForce true;
xserver = lib.mkForce true;
users = lib.mkForce true;
sops = lib.mkForce true;
env = lib.mkForce true;
security = lib.mkForce true;
systemdTimeout = lib.mkForce true;
hardware = lib.mkForce true;
pulseaudio = lib.mkForce true;
pipewire = lib.mkForce true;
network = lib.mkForce true;
time = lib.mkForce true;
stylix = lib.mkForce true;
programs = lib.mkForce true;
zsh = lib.mkForce true;
syncthing = lib.mkForce true;
blueman = lib.mkForce true;
networkDevices = lib.mkForce true;
gvfs = lib.mkForce true;
interceptionTools = lib.mkForce true;
swayosd = lib.mkForce true;
ppd = lib.mkForce true;
yubikey = lib.mkForce false;
ledger = lib.mkForce true;
keyboards = lib.mkForce true;
login = lib.mkForce true;
nix-ld = lib.mkForce true;
impermanence = lib.mkForce true;
nvd = lib.mkForce true;
gnome-keyring = lib.mkForce true;
sway = lib.mkForce true;
xdg-portal = lib.mkForce true;
distrobox = lib.mkForce true;
appimage = lib.mkForce true;
lid = lib.mkForce true;
lowBattery = lib.mkForce true;
lanzaboote = lib.mkForce true;
autologin = lib.mkForce true;
nftables = lib.mkDefault true;
};
};
}
4.7.1.4. Server
{ lib, config, ... }:
{
options.swarselprofiles.server = lib.mkEnableOption "is this a server";
config = lib.mkIf config.swarselprofiles.server {
swarselmodules = {
general = lib.mkDefault true;
lanzaboote = lib.mkDefault true;
pii = lib.mkDefault true;
home-manager = lib.mkDefault true;
xserver = lib.mkDefault true;
time = lib.mkDefault true;
users = lib.mkDefault true;
impermanence = lib.mkDefault true;
btrfs = lib.mkDefault true;
sops = lib.mkDefault true;
boot = lib.mkDefault true;
nftables = lib.mkDefault true;
server = {
general = lib.mkDefault true;
network = lib.mkDefault true;
diskEncryption = lib.mkDefault true;
packages = lib.mkDefault true;
ssh = lib.mkDefault true;
attic-setup = lib.mkDefault true;
dns-hostrecord = lib.mkDefault true;
};
};
};
}
4.7.1.5. MicroVM
{ lib, config, ... }:
{
options.swarselprofiles.microvm = lib.mkEnableOption "is this a server";
config = lib.mkIf config.swarselprofiles.microvm {
swarselsystems = {
isLinux = true;
isNixos = true;
};
swarselmodules = {
general = lib.mkDefault true;
pii = lib.mkDefault true;
xserver = lib.mkDefault true;
time = lib.mkDefault true;
users = lib.mkDefault true;
impermanence = lib.mkDefault true;
btrfs = lib.mkDefault true;
sops = lib.mkDefault true;
nftables = lib.mkDefault true;
server = {
general = lib.mkDefault true;
packages = lib.mkDefault true;
ssh = lib.mkDefault true;
wireguard = lib.mkDefault true;
dns-home = lib.mkDefault true;
};
};
};
}
4.7.1.6. Router
{ lib, config, ... }:
{
options.swarselprofiles.router = lib.mkEnableOption "enable the router profile";
config = lib.mkIf config.swarselprofiles.router {
swarselmodules = {
nftables = lib.mkDefault true;
server = {
router = lib.mkDefault true;
kea = lib.mkDefault true;
};
};
};
}
4.7.2. home-manager
This holds modules that are to be used on most hosts. These are also the most important options to configure, as these allow me easy access to monitor, keyboard, and other setups.
{ lib, ... }:
let
profileNames = lib.swarselsystems.readNix "profiles/home";
in
{
imports = lib.swarselsystems.mkImports profileNames "profiles/home";
}
4.7.2.1. Personal
{ lib, config, ... }:
{
options.swarselprofiles.personal = lib.mkEnableOption "is this a personal host";
config = lib.mkIf config.swarselprofiles.personal {
swarselmodules = {
anki = lib.mkDefault true;
anki-tray = lib.mkDefault true;
attic-store-push = lib.mkDefault true;
atuin = lib.mkDefault true;
autotiling = lib.mkDefault true;
batsignal = lib.mkDefault true;
blueman-applet = lib.mkDefault true;
desktop = lib.mkDefault true;
direnv = lib.mkDefault true;
element-desktop = lib.mkDefault true;
element-tray = lib.mkDefault true;
emacs = lib.mkDefault true;
env = lib.mkDefault true;
eza = lib.mkDefault true;
firefox = lib.mkDefault true;
firezone-tray = lib.mkDefault true;
fuzzel = lib.mkDefault true;
gammastep = lib.mkDefault true;
general = lib.mkDefault true;
git = lib.mkDefault true;
gnome-keyring = lib.mkDefault true;
gpgagent = lib.mkDefault true;
hexchat = lib.mkDefault true;
kanshi = lib.mkDefault true;
kdeconnect = lib.mkDefault true;
kitty = lib.mkDefault true;
mail = lib.mkDefault true;
mako = lib.mkDefault true;
nix-index = lib.mkDefault true;
nixgl = lib.mkDefault true;
nix-your-shell = lib.mkDefault true;
nm-applet = lib.mkDefault true;
obs-studio = lib.mkDefault true;
obsidian = lib.mkDefault true;
obsidian-tray = lib.mkDefault true;
opkssh = lib.mkDefault true;
ownpackages = lib.mkDefault true;
packages = lib.mkDefault true;
passwordstore = lib.mkDefault true;
programs = lib.mkDefault true;
sops = lib.mkDefault false;
spicetify = lib.mkDefault true;
spotify-player = lib.mkDefault true;
ssh = lib.mkDefault true;
starship = lib.mkDefault true;
stylix = lib.mkDefault true;
sway = lib.mkDefault true;
swayidle = lib.mkDefault true;
swaylock = lib.mkDefault true;
swayosd = lib.mkDefault true;
symlink = lib.mkDefault true;
tmux = lib.mkDefault true;
vesktop = lib.mkDefault true;
vesktop-tray = lib.mkDefault true;
syncthing-tray = lib.mkDefault true;
waybar = lib.mkDefault true;
yubikey = lib.mkDefault false;
yubikeytouch = lib.mkDefault true;
zellij = lib.mkDefault true;
zsh = lib.mkDefault true;
};
};
}
4.7.2.2. DGX Spark
{ lib, config, ... }:
{
options.swarselprofiles.dgxspark = lib.mkEnableOption "is this a dgx spark host";
config = lib.mkIf config.swarselprofiles.dgxspark {
swarselmodules = {
anki = lib.mkDefault false;
anki-tray = lib.mkDefault false;
atuin = lib.mkDefault true;
autotiling = lib.mkDefault false;
batsignal = lib.mkDefault false;
bash = lib.mkDefault true;
blueman-applet = lib.mkDefault true;
desktop = lib.mkDefault false;
direnv = lib.mkDefault true;
element-desktop = lib.mkDefault false;
element-tray = lib.mkDefault false;
emacs = lib.mkDefault false;
env = lib.mkDefault false;
eza = lib.mkDefault true;
firefox = lib.mkDefault true;
fuzzel = lib.mkDefault true;
gammastep = lib.mkDefault false;
general = lib.mkDefault true;
git = lib.mkDefault true;
gnome-keyring = lib.mkDefault false;
gpgagent = lib.mkDefault true;
hexchat = lib.mkDefault false;
kanshi = lib.mkDefault false;
kdeconnect = lib.mkDefault false;
kitty = lib.mkDefault true;
mail = lib.mkDefault false;
mako = lib.mkDefault false;
nix-index = lib.mkDefault true;
nixgl = lib.mkDefault true;
nix-your-shell = lib.mkDefault true;
nm-applet = lib.mkDefault true;
obs-studio = lib.mkDefault false;
obsidian = lib.mkDefault false;
obsidian-tray = lib.mkDefault false;
ownpackages = lib.mkDefault false;
packages = lib.mkDefault false;
passwordstore = lib.mkDefault false;
programs = lib.mkDefault false;
sops = lib.mkDefault true;
spicetify = lib.mkDefault false;
spotify-player = lib.mkDefault false;
ssh = lib.mkDefault false;
starship = lib.mkDefault true;
stylix = lib.mkDefault true;
sway = lib.mkDefault false;
swayidle = lib.mkDefault false;
swaylock = lib.mkDefault false;
swayosd = lib.mkDefault false;
symlink = lib.mkDefault false;
tmux = lib.mkDefault true;
vesktop = lib.mkDefault false;
vesktop-tray = lib.mkDefault false;
syncthing-tray = lib.mkDefault false;
waybar = lib.mkDefault false;
yubikey = lib.mkDefault false;
yubikeytouch = lib.mkDefault false;
zellij = lib.mkDefault true;
zsh = lib.mkDefault true;
};
};
}
4.7.2.3. Minimal
{ lib, config, ... }:
{
options.swarselprofiles.minimal = lib.mkEnableOption "is this a personal host";
config = lib.mkIf config.swarselprofiles.minimal {
swarselmodules = {
general = lib.mkDefault true;
sops = lib.mkDefault true;
kitty = lib.mkDefault true;
zsh = lib.mkDefault true;
git = lib.mkDefault true;
};
};
}
4.7.2.4. Hotel
{ lib, config, ... }:
{
options.swarselprofiles.hotel = lib.mkEnableOption "is this a hotel host";
config = lib.mkIf config.swarselprofiles.hotel {
swarselmodules = {
packages = lib.mkForce true;
ownpackages = lib.mkForce true;
general = lib.mkForce true;
nixgl = lib.mkForce true;
sops = lib.mkForce true;
yubikey = lib.mkForce false;
ssh = lib.mkForce true;
stylix = lib.mkForce true;
desktop = lib.mkForce true;
symlink = lib.mkForce true;
env = lib.mkForce false;
programs = lib.mkForce true;
nix-index = lib.mkForce true;
direnv = lib.mkForce true;
eza = lib.mkForce true;
git = lib.mkForce false;
fuzzel = lib.mkForce true;
starship = lib.mkForce true;
kitty = lib.mkForce true;
zsh = lib.mkForce true;
zellij = lib.mkForce true;
tmux = lib.mkForce true;
mail = lib.mkForce false;
emacs = lib.mkForce true;
waybar = lib.mkForce true;
firefox = lib.mkForce true;
gnome-keyring = lib.mkForce true;
kdeconnect = lib.mkForce true;
mako = lib.mkForce true;
swayosd = lib.mkForce true;
yubikeytouch = lib.mkForce true;
sway = lib.mkForce true;
kanshi = lib.mkForce true;
gpgagent = lib.mkForce true;
gammastep = lib.mkForce false;
};
};
}
4.7.2.5. Local Server
{ lib, config, ... }:
{
options.swarselprofiles.server.local = lib.mkEnableOption "is this a local server";
config = lib.mkIf config.swarselprofiles.server.local {
swarselmodules = {
general = lib.mkDefault true;
server = {
dotfiles = lib.mkDefault true;
};
};
};
}
5. Emacs
5.1. Initialization (early-init.el)
In this section I handle my early init file; it takes care of frame-setup for emacsclient buffers.
5.1.1. Increase startup performance
First, I use some advice from doomemacs regarding garbace collection; here I make sure that during startup, the garbace collectur will not run, which will improve startup times. Now, that might not really be needed since I will usually only start the emacs server once during startup and then not touch it again, however, since I am building my emacs configuration using NixOS, there is some merit to this since I will usually need to restart the server once I rebuild my configuration.
Also, inspired by a setting I have seen in protesilaos' configuration, I apply the same idea to the file-name-handler-alist and vc-handled-backends.
In the end, we need to restore those values to values that will work during normal operation. For that, I add a hook to the startup function that will revert the values once Emacs has finished initialization.
Also packed into the hook function is the line (fset 'epg-wait-for-status 'ignore). This line is needed at the end of the configuration in order to allow for my Yubikey to be used to encrypt and decrypt .gpg files. Without it, Emacs will just hang forever and basically crash.
;; -*- lexical-binding: t; -*- (defvar swarsel-file-name-handler-alist file-name-handler-alist) (defvar swarsel-vc-handled-backends vc-handled-backends) (setq gc-cons-threshold most-positive-fixnum gc-cons-percentage 0.6 file-name-handler-alist nil vc-handled-backends nil) (add-hook 'emacs-startup-hook (lambda () (progn (setq gc-cons-threshold (* 32 1024 1024) gc-cons-percentage 0.1 jit-lock-defer-time 0.05 read-process-output-max (* 1024 1024) file-name-handler-alist swarsel-file-name-handler-alist vc-handled-backends swarsel-vc-handled-backends) (fset 'epg-wait-for-status 'ignore) )))
5.1.2. Setup frames
Next, I will setup the basic frame for my emacs buffers. Note that I use a tiling window manager, so I do not need to hold myself up with sizing the windows myself. I also disable some GUI tools that I (like many others) do not find to be particularly useful. Also I inhibit many startup functions here, even though it does not affect me greatly since I use another solution for that.
We also make require immediate compilation of native code.
For the default-frame-alist, I used to also set '(right-divider-width . 4) and '(bottom-divider-width . 4), but I did not like the look of the divider bar and usually know my splits anyways, so this is no longer set.
(tool-bar-mode 0) (menu-bar-mode 0) (scroll-bar-mode 0) (setq frame-inhibit-implied-resize t ring-bell-function 'ignore use-dialog-box nil use-file-dialog nil use-short-answers t inhibit-startup-message t inhibit-splash-screen t inhibit-startup-screen t inhibit-x-resources t inhibit-startup-buffer-menu t inhibit-startup-echo-area-message user-login-name ; this needs to be set to the username or it will not have an effect comp-deferred-compilation nil ; compile all Elisp to native code immediately ) (setq-default left-margin-width 1 right-margin-width 1) (setq-default default-frame-alist (append (list '(undecorated . t) ; no title bar, borders etc. '(background-color . "#1D252C") ; load doom-citylight colors to avoid white flash '(foreground-color . "#A0B3C5") ; load doom-citylight colors to avoid white flash '(vertical-scroll-bars . nil) '(horizontal-scroll-bars . nil) '(internal-border-width . 5) '(tool-bar-lines . 0) '(menu-bar-lines . 0))))
5.1.3. Make C-i, C-m, C-[ available in graphic sessions
By default, emacs binds
C-ito theTABkeyC-mto theRETkeyC-[to theECSkey
These keybinds exist to make Emacs work well in terminal mode. However, most of the time I am using Emacs in a graphic session, and I would hence like to have these keybinds available for personal use.
NOTE: To use these keybinds, you need to enclose the binding in angled brackets (<>). Then they can be used normally
(add-hook 'after-make-frame-functions (lambda (frame) (with-selected-frame frame (when (display-graphic-p) (define-key input-decode-map (kbd "C-i") [DUMMY-i]) (define-key input-decode-map (kbd "C-[") [DUMMY-lsb]) (define-key input-decode-map (kbd "C-m") [DUMMY-m]) ))))
5.2. Personal settings
This section is used to define my own functions, own variables, and own keybindings.
5.2.1. Custom functions
In this section I define extra functions that I need. Some of these functions I wrote myself, some I found after internet reseach. For functions I found on the internet, I will link the original source I found it in.
5.2.1.1. Emacs/Evil state toggle
Since I am rebinding the C-z hotkey for emacs-evil-state toggling, I want to have a function that still lets me perform this action quickly.
We set a keybinding to this in Custom Keybindings.
;; -*- lexical-binding: t; -*- (defun swarsel/toggle-evil-state () (interactive) (if (or (evil-emacs-state-p) (evil-insert-state-p)) (evil-normal-state) (evil-emacs-state)))
5.2.1.2. Switching to last used buffer
I often find myself bouncing between two buffers when I do not want to use a window split. This function simply jumps to the last used buffer.
We set a keybinding to this in Custom Keybindings.
(defun swarsel/last-buffer () (interactive) (switch-to-buffer nil))
5.2.1.3. mu4e functions
I use these functions to let me switch between my main email accounts, as mu4e by itself has trouble doing so. mu4e-switch-account allows for manual choosing of the sender account, while mu4e-rfs--matching-address and mu4e-send-from-correct-address are used when replying to a mail; they switch the sender account to the one that received the mail.
By default, the sender email will not be changed after sending a mail; however, I want Emacs to always use my main address when not replying to another email. For that I use mu4e-restore-default.
Used here: mu4e
(defun swarsel/mu4e-switch-account () (interactive) (let ((account (completing-read "Select account: " mu4e-user-mail-address-list))) (setq user-mail-address account))) (defun swarsel/mu4e-rfs--matching-address () (cl-loop for to-data in (mu4e-message-field mu4e-compose-parent-message :to) for to-email = (pcase to-data (`(_ . email) email) (x (mu4e-contact-email x))) for to-name = (pcase to-data (`(_ . name) name) (x (mu4e-contact-name x))) when (mu4e-user-mail-address-p to-email) return (list to-name to-email))) (defun swarsel/mu4e-send-from-correct-address () (when mu4e-compose-parent-message (save-excursion (when-let ((dest (swarsel/mu4e-rfs--matching-address))) (cl-destructuring-bind (from-user from-addr) dest (setq user-mail-address from-addr) (when (and (boundp 'user-mail-address) (stringp user-mail-address) (string-equal user-mail-address (getenv "SWARSEL_MAIL_WORK"))) (mml-secure-message-sign-smime)) (message-position-on-field "From") (message-beginning-of-line) (delete-region (point) (line-end-position)) (insert (format "%s <%s>" (or from-user user-full-name) from-addr))))))) (defun swarsel/mu4e-restore-default () (setq user-mail-address (getenv "SWARSEL_MAIL4") user-full-name (getenv "SWARSEL_FULLNAME")))
5.2.1.4. Create non-existant directories when finding file
This function will check if a directory for which a file we want to open exists; if not, it will offer to create the directories for me.
(defun swarsel/with-buffer-name-prompt-and-make-subdirs () (let ((parent-directory (file-name-directory buffer-file-name))) (when (and (not (file-exists-p parent-directory)) (y-or-n-p (format "Directory `%s' does not exist! Create it? " parent-directory))) (make-directory parent-directory t)))) (add-to-list 'find-file-not-found-functions #'swarsel/with-buffer-name-prompt-and-make-subdirs)
5.2.1.5. [crux] Duplicate Lines
When programming, I like to be able to duplicate a line. There are easier functions than the one below, but they either
- screw with undo/redo
- move the cursor wildly
The below function avoids these problems. Originally I used the function duplicate-line found here: https://stackoverflow.com/questions/88399/how-do-i-duplicate-a-whole-line-in-emacs
However, this function does not work on regions. Later, I found a solution implemented by crux. I do not need the whole package, so I just extracted the three functions I needed from it.
We set a keybinding to this in Custom Keybindings.
(defun crux-get-positions-of-line-or-region () "Return positions (beg . end) of the current line or region." (let (beg end) (if (and mark-active (> (point) (mark))) (exchange-point-and-mark)) (setq beg (line-beginning-position)) (if mark-active (exchange-point-and-mark)) (setq end (line-end-position)) (cons beg end))) (defun crux-duplicate-current-line-or-region (arg) "Duplicates the current line or region ARG times. If there's no region, the current line will be duplicated. However, if there's a region, all lines that region covers will be duplicated." (interactive "p") (pcase-let* ((origin (point)) (`(,beg . ,end) (crux-get-positions-of-line-or-region)) (region (buffer-substring-no-properties beg end))) (dotimes (_i arg) (goto-char end) (newline) (insert region) (setq end (point))) (goto-char (+ origin (* (length region) arg) arg)))) (defun crux-duplicate-and-comment-current-line-or-region (arg) "Duplicates and comments the current line or region ARG times. If there's no region, the current line will be duplicated. However, if there's a region, all lines that region covers will be duplicated." (interactive "p") (pcase-let* ((origin (point)) (`(,beg . ,end) (crux-get-positions-of-line-or-region)) (region (buffer-substring-no-properties beg end))) (comment-or-uncomment-region beg end) (setq end (line-end-position)) (dotimes (_ arg) (goto-char end) (newline) (insert region) (setq end (point))) (goto-char (+ origin (* (length region) arg) arg))))
5.2.1.6. [prot] org-id-headings
These functions by protesilaos generate heading links in an org-file similar to the normal org-store-link approach when not using properties. This approach has a weakness however - if the heading name is changed, the link breaks. These functions generate a unique identifier for each heading which will not break and also works when exporting the file to html, for example.
(defun prot-org--id-get () "Get the CUSTOM_ID of the current entry. If the entry already has a CUSTOM_ID, return it as-is, else create a new one." (let* ((pos (point)) (id (org-entry-get pos "CUSTOM_ID"))) (if (and id (stringp id) (string-match-p "\\S-" id)) id (setq id (org-id-new "h")) (org-entry-put pos "CUSTOM_ID" id) id))) (declare-function org-map-entries "org") (defun prot-org-id-headlines () "Add missing CUSTOM_ID to all headlines in current file." (interactive) (org-map-entries (lambda () (prot-org--id-get)))) (defun prot-org-id-headline () "Add missing CUSTOM_ID to headline at point." (interactive) (prot-org--id-get))
5.2.1.7. Inhibit Messages in Echo Area
Emacs likes to send messages to the echo area; this is generally a good thing. However, it bothers me a lot when I am currently working in minibuffer where I receive an echo area message that is actually important and it is then overwritten by e.g. the mu4e update message. This section makes it possible to find the root function calling the message function and disabling it here.
Usage: Enable the (advice-add 'message :around #'who-called-me?) by running this code block, which will show a full trace of all messages being sent to the echo area:
(advice-add 'message :around #'who-called-me?)
Once the root function has been found, it can be disabled via advice=add as in the last block in this section. To disable the stack tracing, run (advice-remove 'message #'who-called-me?) or the following code block:
(advice-remove 'message #'who-called-me?)
Lastly, individual messages can be reenabled using the (advice-remove '<FUNCTION-NAME> #'suppress-messages) approach. Use this when you accidentally disabled a helpful message.
(defun suppress-messages (old-fun &rest args) (cl-flet ((silence (&rest args1) (ignore))) (advice-add 'message :around #'silence) (unwind-protect (apply old-fun args) (advice-remove 'message #'silence)))) (advice-add 'pixel-scroll-precision :around #'suppress-messages) (advice-add 'mu4e--server-filter :around #'suppress-messages) (advice-add 'org-unlogged-message :around #'suppress-messages) (advice-add 'magit-auto-revert-mode--init-kludge :around #'suppress-messages) (advice-add 'push-mark :around #'suppress-messages) (advice-add 'evil-insert :around #'suppress-messages) (advice-add 'evil-visual-char :around #'suppress-messages) ;; to reenable ;; (advice-remove 'timer-event-handler #'suppress-messages) (defun who-called-me? (old-fun format &rest args) (let ((trace nil) (n 1) (frame nil)) (while (setf frame (backtrace-frame n)) (setf n (1+ n) trace (cons (cadr frame) trace)) ) (apply old-fun (concat "<<%S>>\n" format) (cons trace args)))) ;; enable to get message backtrace, the first function shown in backtrace calls the other functions ;; (advice-add 'message :around #'who-called-me?) ;; disable to stop receiving backtrace (advice-remove 'message #'who-called-me?)
5.2.1.8. Move up one directory for find-file
I find it very annoying that the standard behavior for M-DEL only deletes one word when using find-file. This function makes it so that we always go up by one directory level instead.
This function was found here: https://www.reddit.com/r/emacs/comments/re31i6/how_to_go_up_one_directory_when_using_findfile_cx/
(defun up-directory (path) "Move up a directory in PATH without affecting the kill buffer." (interactive "p") (if (string-match-p "/." (minibuffer-contents)) (let ((end (point))) (re-search-backward "/.") (forward-char) (delete-region (point) end)))) (define-key minibuffer-local-filename-completion-map [C-backspace] #'up-directory)
5.2.1.9. Magit: List directories using vertico/consult
At work and when working on private projects, I often have to jump between several git repositories. This function fires up a picker that gets me to the magit overview page of that repository.
We set a keybinding to this in Custom Keybindings.
(defun swarsel/consult-magit-repos () (interactive) (require 'magit) (let* ((repos (magit-list-repos)) (repo (consult--read repos :prompt "Magit repo: " :require-match t :history 'my/consult-magit-repos-history :sort t))) (magit-status repo)))
5.2.1.10. org-mode: General setup
Sets up the basic settings that I want to have active in org-mode buffers.
Used here: General org-mode
(defun swarsel/org-mode-setup () (variable-pitch-mode 1) (add-hook 'org-tab-first-hook 'org-end-of-line) (visual-line-mode 1))
5.2.1.11. org-mode: Visual-fill column
This function sets the width of buffers in org-mode.
Used in: Centered org-mode Buffers
(defun swarsel/org-mode-visual-fill () (setq visual-fill-column-width 150 visual-fill-column-center-text t) (visual-fill-column-mode 1))
5.2.1.12. org-mode: Upon-save actions (Auto-tangle, export to html, formatting)
This section handles everything that shoudld happen when I save SwarselSystems.org. It:
- automatically tangles all configuration blocks in this file
- exports the configuration file as html for an easier reading experience with working links and index
- formats the generated
.nixfiles in accordance to theAlejandra-style.
We set a hook that runs everytime we save the file. It would be a bit more efficient to only export and format when we enter a magit window for instance (since especially the html export takes times), however, since I cannot be sure to only ever commit from magit (I do indeed sometimes use git from the command line), I prefer this approach.
(defun swarsel/run-formatting () (interactive) (let ((default-directory (expand-file-name "~/.dotfiles"))) (shell-command "nixpkgs-fmt . > /dev/null"))) (defun swarsel/org-babel-tangle-config () (interactive) (when (string-equal (buffer-file-name) swarsel-swarsel-org-filepath) ;; Dynamic scoping to the rescue (let ((org-confirm-babel-evaluate nil)) ;; (org-html-export-to-html) (org-babel-tangle) (swarsel/run-formatting) ))) (setq org-html-htmlize-output-type nil) ;; (add-hook 'org-mode-hook (lambda () (add-hook 'after-save-hook #'swarsel/org-babel-tangle-config)))
5.2.1.13. org-mode: Fold current heading
Normally emacs cycles between three states:
- fully folded
- One heading expanded
- All headings expanded
However, I want to be able to fold a single heading consistently.
We set a keybinding to this in Custom Keybindings.
(defun org-fold-outer () (interactive) (org-beginning-of-line) (if (string-match "^*+" (thing-at-point 'line t)) (outline-up-heading 1)) (outline-hide-subtree) )
5.2.1.14. corfu: Do not interrupt navigation
These three functions allow me to keep using the normal navigation keys even when a corfu completion pops up.
These functions are used here: Corfu
(defun swarsel/corfu-normal-return (&optional arg) (interactive) (corfu-quit) (newline) ) (defun swarsel/corfu-quit-and-up (&optional arg) (interactive) (corfu-quit) (evil-previous-visual-line)) (defun swarsel/corfu-quit-and-down (&optional arg) (interactive) (corfu-quit) (evil-next-visual-line))
5.2.1.15. Disable garbage collection while minibuffer is active
(defun swarsel/minibuffer-setup-hook () (setq gc-cons-threshold most-positive-fixnum)) (defun swarsel/minibuffer-exit-hook () (setq gc-cons-threshold (* 32 1024 1024))) (add-hook 'minibuffer-setup-hook #'swarsel/minibuffer-setup-hook) (add-hook 'minibuffer-exit-hook #'swarsel/minibuffer-exit-hook)
5.2.1.16. Insert link to another header in org file
When writing this file, I often want to refer to a different section of the file. One way to do this is to C-x O (consult-org-heading) to get to said heading, then C=c s (org-store-link), finally C-o (evil-jump-backward) to get back to the origin and insert the link using C-c C-l (org-insert-link).
These two scripts just let me do all of this in one step. I have styled the picker in a way that is similar to consult-org-heading.
We set a keybinding to this in Custom Keybindings.
(defun swarsel/org-colorize-outline (parents raw) (let* ((palette ["#58B6ED" "#8BD49C" "#33CED8" "#4B9CCC" "yellow" "orange" "salmon" "red"]) (n (length parents)) (colored-parents (cl-mapcar (lambda (p i) (propertize p 'face `(:foreground ,(aref palette (mod i (length palette))) :weight bold))) parents (number-sequence 0 (1- n))))) (concat (when parents (string-join colored-parents "/")) (when parents "/") (propertize raw 'face `(:foreground ,(aref palette (mod n (length palette))) :weight bold))))) (defun swarsel/org-insert-link-to-heading () (interactive) (let ((candidates '())) (org-map-entries (lambda () (let* ((raw (org-get-heading t t t t)) (parents (org-get-outline-path t)) (m (copy-marker (point))) (colored (swarsel/org-colorize-outline parents raw))) (push (cons colored m) candidates)))) (let* ((choice (completing-read "Heading: " (mapcar #'car candidates))) (marker (cdr (assoc choice candidates))) id raw-heading) (unless marker (user-error "No marker for heading??")) (save-excursion (goto-char marker) (setq id (prot-org--id-get)) (setq raw-heading (org-get-heading t t t t))) (insert (org-link-make-string (format "#%s" id) raw-heading)))))
5.2.2. Custom Keybindings
This defines a set of keybinds that I want to have available globally. I have one set of keys that is globally available through the C-SPC prefix. This set is used mostly for functions that I have trouble remembering the original keybind for, or that I just want to have gathered in a common space.
I also define some keybinds to some combinations directly. Those are used mostly for custom functions that I call often enough to warrant this.
;; Make ESC quit prompts (global-set-key (kbd "<escape>") 'keyboard-escape-quit) ;; Set up general keybindings (use-package general :config (general-create-definer swarsel/leader-keys :keymaps '(normal insert visual emacs) :prefix "SPC" :global-prefix "C-SPC") (swarsel/leader-keys "e" '(:ignore e :which-key "evil") "eo" '(evil-jump-backward :which-key "cursor jump backwards") "eO" '(evil-jump-forward :which-key "cursor jump forwards") "t" '(:ignore t :which-key "toggles") "ts" '(hydra-text-scale/body :which-key "scale text") "te" '(swarsel/toggle-evil-state :which-key "emacs/evil") "tl" '(display-line-numbers-mode :which-key "line numbers") "tp" '(evil-cleverparens-mode :wk "cleverparens") "to" '(olivetti-mode :wk "olivetti") "td" '(darkroom-tentative-mode :wk "darkroom") "tw" '((lambda () (interactive) (toggle-truncate-lines)) :which-key "line wrapping") "m" '(:ignore m :which-key "modes/programs") "mm" '((lambda () (interactive) (mu4e)) :which-key "mu4e") "mg" '((lambda () (interactive) (magit-list-repositories)) :which-key "magit-list-repos") "mc" '((lambda () (interactive) (swarsel/open-calendar)) :which-key "calendar") "mp" '(popper-toggle :which-key "popper") "md" '(dirvish :which-key "dirvish") "mr" '(bjm/elfeed-load-db-and-open :which-key "elfeed") "o" '(:ignore o :which-key "org") "op" '((lambda () (interactive) (org-present)) :which-key "org-present") "oa" '((lambda () (interactive) (org-agenda)) :which-key "org-agenda") "oa" '((lambda () (interactive) (org-refile)) :which-key "org-refile") "ob" '((lambda () (interactive) (org-babel-mark-block)) :which-key "Mark whole src-block") "ol" '((lambda () (interactive) (org-insert-link)) :which-key "insert link") "oc" '((lambda () (interactive) (org-store-link)) :which-key "copy (=store) link") "os" '(shfmt-region :which-key "format sh-block") "od" '((lambda () (interactive) (org-babel-demarcate-block)) :which-key "demarcate (split) src-block") "on" '(nixpkgs-fmt-region :which-key "format nix-block") "ot" '(swarsel/org-babel-tangle-config :which-key "tangle file") "oe" '(org-html-export-to-html :which-key "export to html") "c" '(:ignore c :which-key "capture") "ct" '((lambda () (interactive) (org-capture nil "tt")) :which-key "task") "l" '(:ignore l :which-key "links") "lc" '((lambda () (interactive) (progn (find-file swarsel-swarsel-org-filepath) (org-overview) )) :which-key "SwarselSystems.org") "le" '((lambda () (interactive) (progn (find-file swarsel-swarsel-org-filepath) (goto-char (org-find-exact-headline-in-buffer "Emacs") ) (org-overview) (org-cycle) )) :which-key "Emacs.org") "lr" '(swarsel/consult-magit-repos :which-key "List repos") "ln" '((lambda () (interactive) (progn (find-file swarsel-swarsel-org-filepath) (goto-char (org-find-exact-headline-in-buffer "System") ) (org-overview) (org-cycle))) :which-key "Nixos.org") "lp" '((lambda () (interactive) (projectile-switch-project)) :which-key "switch project") "lg" '((lambda () (interactive) (magit-list-repositories)) :which-key "list git repos") "h" '(:ignore h :which-key "help") "hy" '(yas-describe-tables :which-key "yas tables") "hb" '(embark-bindings :which-key "current key bindings") "h" '(:ignore t :which-key "describe") "he" 'view-echo-area-messages "hf" 'describe-function "hF" 'describe-face "hl" '(view-lossage :which-key "show command keypresses") "hL" 'find-library "hm" 'describe-mode "ho" 'describe-symbol "hk" 'describe-key "hK" 'describe-keymap "hp" 'describe-package "hv" 'describe-variable "hd" 'devdocs-lookup "w" '(:ignore t :which-key "window") "wl" 'windmove-right "w <right>" 'windmove-right "wh" 'windmove-left "w <left>" 'windmove-left "wk" 'windmove-up "w <up>" 'windmove-up "wj" 'windmove-down "w <down>" 'windmove-down "wr" 'winner-redo "wd" 'delete-window "w=" 'balance-windows-area "wD" 'kill-buffer-and-window "wu" 'winner-undo "wr" 'winner-redo "w/" 'evil-window-vsplit "w\\" 'evil-window-vsplit "w-" 'evil-window-split "wm" '(delete-other-windows :wk "maximize") "<right>" 'up-list "<left>" 'down-list )) ;; General often used hotkeys (general-define-key "C-M-a" (lambda () (interactive) (org-capture nil "a")) ; make new anki card "C-c d" 'crux-duplicate-current-line-or-region "C-c D" 'crux-duplicate-and-comment-current-line-or-region "<DUMMY-m>" 'swarsel/last-buffer "M-\\" 'indent-region "M-r" 'swarsel/consult-magit-repos "M-i" 'swarsel/org-insert-link-to-heading "<Paste>" 'yank "<Cut>" 'kill-region "<Copy>" 'kill-ring-save "<undo>" 'evil-undo "<redo>" 'evil-redo "C-S-c C-S-c" 'mc/edit-lines "C->" 'mc/mark-next-like-this "C-<" 'mc/mark-previous-like-this "C-c C-<" 'mc/mark-all-like-this )
5.2.3. Directory setup / File structure
In this section I setup some aliases that I use for various directories on my system. This is just to prevent setting the same stuff too often.
;; set Nextcloud directory for journals etc. (setq swarsel-emacs-directory "~/.emacs.d" swarsel-dotfiles-directory (getenv "FLAKE") swarsel-swarsel-org-filepath (expand-file-name "SwarselSystems.org" swarsel-dotfiles-directory) swarsel-tasks-org-file "Tasks.org" swarsel-archive-org-file "Archive.org" swarsel-work-projects-directory (getenv "DOCUMENT_DIR_WORK") swarsel-private-projects-directory (getenv "DOCUMENT_DIR_PRIV") )
5.2.4. Unclutter .emacs.d
In this section I move the custom.el out of it's standard location in .emacs.d. Firstly, I dislike using this file at all since I would rather have fully stateful configuration as commanded by this file. Secondly, this file is too easily permanently changed. Recently I figured out the last bits that I needed to remove from custom.el to no longer be reliant on it, so I now just write it to a temporary file (through make-temp=file) which will be cleaned on shutdown. However, I like to retain the custom framework because it is nice for testing out theme customizations, hence why I still load the file.
This section also sets the emacs directory to the ~/.cache/ directory which is useful for files that I do not want to have lying around in my .emacs.d.
;; Change the user-emacs-directory to keep unwanted things out of ~/.emacs.d (setq user-emacs-directory (expand-file-name "~/.cache/emacs/") url-history-file (expand-file-name "url/history" user-emacs-directory)) ;; Use no-littering to automatically set common paths to the new user-emacs-directory (use-package no-littering) (setq custom-file (make-temp-file "emacs-custom-")) (load custom-file t)
5.2.5. Move backup files to another location
Many people dislike the Emacs backup files; I do enjoy them, but have to admit that they clutter the filesystem a little too much. Also, I rarely need to access these over different sessions. Hence I move them to /tmp - if Emacs unexpectedly crashes, the files can be recovered, but the backup files will not gather everywhere and will be deleted upon shutdown.
(let ((backup-dir "~/tmp/emacs/backups") (auto-saves-dir "~/tmp/emacs/auto-saves/")) (dolist (dir (list backup-dir auto-saves-dir)) (when (not (file-directory-p dir)) (make-directory dir t))) (setq backup-directory-alist `(("." . ,backup-dir)) auto-save-file-name-transforms `((".*" ,auto-saves-dir t)) auto-save-list-file-prefix (concat auto-saves-dir ".saves-") tramp-backup-directory-alist `((".*" . ,backup-dir)) tramp-auto-save-directory auto-saves-dir)) (setq backup-by-copying t ; Don't delink hardlinks delete-old-versions t ; Clean up the backups version-control t ; Use version numbers on backups, kept-new-versions 5 ; keep some new versions kept-old-versions 2) ; and some old ones, too
5.3. General init.el setup + UI
In this general section I have settings that I either consider to be integral to my experience when using Emacs or have no other section that I feel they belong to.
5.3.1. General setup
Here I set up some things that are too minor to put under other categories.
- Firstly we disable to having to type `yes` and `no` and switch it to `y` and `n`.
- We also enable the marking of trailing whitespaces.
- Also, make emacs highlight the current line globally
- Emacs defaults to pausing all display redrawing on any input. This may have been useful previously, but is not necessary nowadays.
- I also disable the suspend-frame function, as I never use it and it is quite confusing when accidentally hitting the keys for it.
;; use UTF-8 everywhere (set-language-environment "UTF-8") ;; (profiler-start 'cpu) ;; set default font size (defvar swarsel/default-font-size 130) (setq swarsel-standard-font "FiraCode Nerd Font Mono" swarsel-alt-font "FiraCode Nerd Font Mono") ;; (defalias 'yes-or-no-p 'y-or-n-p) ;;(setq-default show-trailing-whitespace t) (add-hook 'before-save-hook 'delete-trailing-whitespace) (global-hl-line-mode 1) ;; (setq redisplay-dont-pause t) ;; obsolete (setq blink-cursor-mode nil) ;; blink-cursor is an unexpected source of slowdown (global-subword-mode 1) ; Iterate through CamelCase words (setq blink-matching-paren nil) ;; this makes the cursor jump around annoyingly (delete-selection-mode 1) (setq vc-follow-symlinks t) (setq require-final-newline t) (winner-mode 1) (setq load-prefer-newer t) (setq-default bidi-paragraph-direction 'left-to-right bidi-display-reordering 'left-to-right bidi-inhibit-bpa t) (global-so-long-mode) (setq process-adaptive-read-buffering nil) ;; not sure if this is a good idea (setq fast-but-imprecise-scrolling t redisplay-skip-fontification-on-input t inhibit-compacting-font-caches t) (setq idle-update-delay 1.0 which-func-update-delay 1.0) (setq undo-limit 80000000 evil-want-fine-undo t auto-save-default t password-cache-expiry nil ) (setq browse-url-browser-function 'browse-url-firefox) ;; (setenv "DISPLAY" ":0") ;; needed for firefox ;; disable a keybind that does more harm than good (global-set-key [remap suspend-frame] (lambda () (interactive) (message "This keybinding is disabled (was 'suspend-frame')"))) (setq visible-bell nil) (setq initial-major-mode 'fundamental-mode initial-scratch-message nil) (add-hook 'prog-mode-hook 'display-line-numbers-mode) ;; (add-hook 'text-mode-hook 'display-line-numbers-mode) ;; (global-visual-line-mode 1)
5.3.2. Mark all themes as safe
Normally when switching themes in emacs, the user will be warned that themes can run malicious code. I only run one theme really and deem it safe. It is however annoying to be asked this on every new system and it also creates lines in custom.el to answer that query, so here I declare all themes as safe.
(setq custom-safe-themes t)
5.3.3. Show less compilation warnings
When Emacs compiles stuff, it often shows a bunch of warnings that I do not need to deal with. Here we silence those. Some will be disabled completely, and some only when we have native compilation available (which should be most of the time, however).
This is really not needed anymore ever since I started managing my emacs packages with nix, but I still keep this around in case I ever move away from it.
(setq byte-compile-warnings '(not free-vars unresolved noruntime lexical make-local)) ;; Make native compilation silent and prune its cache. (when (native-comp-available-p) (setq native-comp-async-report-warnings-errors 'silent) ; Emacs 28 with native compilation (setq native-compile-prune-cache t)) ; Emacs 29
5.3.4. Better garbage collection
This sets up automatic garbage collection when the frame is unused. There is a lot of discussion on whether it is smart to tamper with garbage collection - in my eyes it is worth running this, because I often times switch away from Emacs for a while when researching. That times can be then used to run GC.
(setq garbage-collection-messages nil) (defmacro k-time (&rest body) "Measure and return the time it takes evaluating BODY." `(let ((time (current-time))) ,@body (float-time (time-since time)))) ;; When idle for 15sec run the GC no matter what. (defvar k-gc-timer (run-with-idle-timer 15 t (lambda () ;; (message "Garbage Collector has run for %.06fsec" (k-time (garbage-collect))))) ;; )
5.3.5. Indentation
Here I define several options related to indentation; I first make it so that only whitespace will be used instead of tab characters for indentation, and I also set a small standard indent.
We set tab-always-indent to 'complete in order to indent first and then do completion if there are any. Also we make it so that python will not complain about missing indentation info.
Lastly, I load the highlight-indent-guides package. This adds a neat visual indicator of the indentation level, which is useful for languages like python.
(setq-default indent-tabs-mode nil tab-width 2) (setq tab-always-indent 'complete) (setq python-indent-guess-indent-offset-verbose nil) (use-package highlight-indent-guides :hook (prog-mode . highlight-indent-guides-mode) :init (setq highlight-indent-guides-method 'column) (setq highlight-indent-guides-responsive 'top) ) (with-eval-after-load 'highlight-indent-guides (set-face-attribute 'highlight-indent-guides-even-face nil :background "gray10") (set-face-attribute 'highlight-indent-guides-odd-face nil :background "gray20") (set-face-attribute 'highlight-indent-guides-stack-even-face nil :background "gray40") (set-face-attribute 'highlight-indent-guides-stack-odd-face nil :background "gray50")) ;; (use-package aggressive-indent) ;; (global-aggressive-indent-mode 1)
5.3.6. Scrolling
By default, emacs scrolls half a page when reaching the bottom of the buffer. This is extremely annoying. This sets up more granular scrolling that allows scrolling with a mouse wheel or the two-finger touchscreen gesture. This now also works in buffers with a very small frame.
(setq mouse-wheel-scroll-amount '(1 ((shift) . 5) ((meta) . 0.5) ((control) . text-scale)) mouse-drag-copy-region nil make-pointer-invisible t mouse-wheel-progressive-speed t mouse-wheel-follow-mouse t) (setq-default scroll-preserve-screen-position t scroll-conservatively 1 scroll-margin 0 next-screen-context-lines 0) (pixel-scroll-precision-mode 1)
5.3.7. Evil
5.3.7.1. General evil
This setups up evil, which brings vim-like keybindings to emacs. In the same location, I also unbind the C-z key (I am very unhappy with this implementation, but it is the only thing that works consistently so far) to make it available for cape later.
Also, I setup initial modes for several major-modes depending on what I deem fit.
;; Emulate vim in emacs (use-package evil :init (setq evil-want-integration t) ; loads evil (setq evil-want-keybinding nil) ; loads "helpful bindings" for other modes (setq evil-want-C-u-scroll t) ; scrolling using C-u (setq evil-want-C-i-jump nil) ; jumping with C-i (setq evil-want-Y-yank-to-eol t) ; give Y some utility (setq evil-shift-width 2) ; uniform indent (setq evil-respect-visual-line-mode nil) ; i am torn on this one (setq evil-split-window-below t) (setq evil-vsplit-window-right t) :config (evil-mode 1) ;; make normal mode respect wrapped lines (define-key evil-normal-state-map (kbd "j") 'evil-next-visual-line) (define-key evil-normal-state-map (kbd "<down>") 'evil-next-visual-line) (define-key evil-normal-state-map (kbd "k") 'evil-previous-visual-line) (define-key evil-normal-state-map (kbd "<up>") 'evil-previous-visual-line) (define-key evil-normal-state-map (kbd "C-z") nil) (define-key evil-insert-state-map (kbd "C-z") nil) (define-key evil-visual-state-map (kbd "C-z") nil) (define-key evil-motion-state-map (kbd "C-z") nil) (define-key evil-operator-state-map (kbd "C-z") nil) (define-key evil-replace-state-map (kbd "C-z") nil) (define-key global-map (kbd "C-z") nil) (evil-set-undo-system 'undo-tree) ;; Don't use evil-mode in these contexts, or use it in a specific mode (evil-set-initial-state 'messages-buffer-mode 'emacs) (evil-set-initial-state 'dashboard-mode 'emacs) (evil-set-initial-state 'dired-mode 'emacs) (evil-set-initial-state 'cfw:details-mode 'emacs) (evil-set-initial-state 'Custom-mode 'emacs) ; god knows why this mode is in uppercase (evil-set-initial-state 'mu4e-headers-mode 'normal) (evil-set-initial-state 'python-inferior-mode 'normal) (add-hook 'org-capture-mode-hook 'evil-insert-state) (add-to-list 'evil-buffer-regexps '("COMMIT_EDITMSG" . insert)))
5.3.7.2. evil-collection
This gives support for many different modes, and works beautifully out of the box.
(use-package evil-collection :after evil :config (evil-collection-init) (setq forge-add-default-bindings nil))
5.3.7.3. evil-snipe
This package changes the char-search commands like f by showing the results in a more visual manner. It also gives a 2-character search using s and S.
;; enables 2-char inline search (use-package evil-snipe :after evil :demand :config (evil-snipe-mode +1) ;; replace 1-char searches (f&t) with this better UI (evil-snipe-override-mode +1))
5.3.7.4. evil-cleverparens
This helps keeping parentheses balanced which is useful when writing in languages like Elisp. I do not activate this by default, as most languages do not profit from this enough in my eyes.
;; for parentheses-heavy languades modify evil commands to keep balance of parantheses (use-package evil-cleverparens)
5.3.7.5. evil-surround
This minor-mode adds functionality for doing better surround-commands; for example ci[ will let you change the word within square brackets.
;; enables surrounding text with S (use-package evil-surround :config (global-evil-surround-mode 1))
5.3.7.6. evil-visual-mark-mode
This makes it so that when setting a mark in evil mode (using m <key>), it creates a visual marker at that place that reminds me what the key for that marker position is (the marker is of course not part of the text of the document, and is hence not saved).
(use-package evil-visual-mark-mode :config (evil-visual-mark-mode))
5.3.7.7. evil-textobj-tree-sitter
This adds support for tree-sitter objects. This allows for the following chords:
- "…af" around function
- "…if" inside function
(use-package evil-textobj-tree-sitter) ;; bind `function.outer`(entire function block) to `f` for use in things like `vaf`, `yaf` (define-key evil-outer-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.outer")) ;; bind `function.inner`(function block without name and args) to `f` for use in things like `vif`, `yif` (define-key evil-inner-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.inner")) ;; You can also bind multiple items and we will match the first one we can find (define-key evil-outer-text-objects-map "a" (evil-textobj-tree-sitter-get-textobj ("if_statement.outer" "conditional.outer" "loop.outer") '((python-mode . ((if_statement.outer) @if_statement.outer)) (python-ts-mode . ((if_statement.outer) @if_statement.outer)))))
5.3.7.8. evil-numbers
A very simple package that brings back the vim possibility of incrementing/decrementing numbers. I do not need it often, but it is nice to have.
(use-package evil-numbers)
5.3.8. ispell
This sets up a wordlist that is, for example, used in completions. When coding, I do not really need this, but it is sometimes useful when writing prose.
;; set the NixOS wordlist by hand (setq ispell-alternate-dictionary (getenv "WORDLIST"))
5.3.9. Font Configuration
Here I define my fonts to be used. Honestly I do not understand the face-attributes and pitches of emacs all too well. It seems this configuration works fine, but I might have to revisit this at some point in the future.
(dolist (face '(default fixed-pitch)) (set-face-attribute face nil :font "FiraCode Nerd Font Mono")) (add-to-list 'default-frame-alist '(font . "FiraCode Nerd Font Mono")) (set-face-attribute 'default nil :height 100) (set-face-attribute 'fixed-pitch nil :height 1.0) (set-face-attribute 'variable-pitch nil :family "IBM Plex Sans" :weight 'regular :height 1.06)
5.3.10. Theme
I have grown to love the doom-citylights theme and have modeled my whole system after it. Also solaire-mode is a nice mode that inverts the alt-faces with the normal faces for specific 'minor' buffers (like Help-buffers).
(use-package solaire-mode :custom (solaire-global-mode +1)) (use-package doom-themes :hook (server-after-make-frame . (lambda () (load-theme 'doom-city-lights t))) :config (load-theme 'doom-city-lights t) (doom-themes-treemacs-config) (doom-themes-org-config))
5.3.11. Icons
This section loads the base icons used in my configuration. I am using nerd-icons over all-the-icons since the former seems to have more integrations with different packages than the latter.
Used in:
(use-package nerd-icons)
5.3.12. Variable Pitch Mode
This minor mode allows mixing fixed and variable pitch fonts within the same buffer.
(use-package mixed-pitch :custom (mixed-pitch-set-height nil) (mixed-pitch-variable-pitch-cursor nil) :hook (text-mode . mixed-pitch-mode))
5.3.13. Modeline
Here I set up the modeline with some information that I find useful. I was using the doom modeline for a while. Most informations I disabled for it, except for the cursor information (row + column) as well as a widget for mu4e and git information.
I have currently disabled this in favor of mini-modeline, which saves more screen space and holds only the information I really need.
(use-package doom-modeline :init ;; (doom-modeline-mode) ;; (column-number-mode) :custom ((doom-modeline-height 22) (doom-modeline-indent-info nil) (doom-modeline-buffer-encoding nil)))
5.3.14. mini-modeline
I have found that the doom-modeline, while very useful, consumes too much screen space for my liking. This modeline takes a more minimalistic approach. The only information that is shown is:
- the line number
- state of the file (whether it is saved etc.)
- the name of the file
- the percentage of the cursor in the file
- the major mode of the file
- the current evil mode
This is really the perfect solution for me, but it might not be for everyone.
(use-package mini-modeline :after smart-mode-line :config (mini-modeline-mode t) (setq mini-modeline-display-gui-line nil) (setq mini-modeline-enhance-visual nil) (setq mini-modeline-truncate-p nil) (setq mini-modeline-l-format nil) (setq mini-modeline-right-padding 5) (setq window-divider-mode t) (setq window-divider-default-places t) (setq window-divider-default-bottom-width 1) (setq window-divider-default-right-width 1) (setq mini-modeline-r-format '("%e" mode-line-front-space mode-line-mule-info mode-line-client mode-line-modified mode-line-remote mode-line-frame-identification mode-line-buffer-identification " " mode-line-position " " mode-name evil-mode-line-tag )) ) (use-package smart-mode-line :config (sml/setup) (add-to-list 'sml/replacer-regexp-list '("^~/Documents/Work/" ":WK:")) (add-to-list 'sml/replacer-regexp-list '("^~/Documents/Private/" ":PR:")) (add-to-list 'sml/replacer-regexp-list '("^~/.dotfiles/" ":D:") t) )
5.3.15. Helper Modes
5.3.15.1. Vertico, Orderless, Marginalia, Consult, Embark
This set of packages uses the default emacs completion framework and works together to provide a very nice user experience.
5.3.15.1.1. vertico
Vertico simply provides a vertically stacking completion framework.
(setq read-buffer-completion-ignore-case t read-file-name-completion-ignore-case t completion-ignore-case t) (use-package vertico :custom (vertico-scroll-margin 0) (vertico-count 10) (vertico-resize t) (vertico-cycle t) :init (vertico-mode) (vertico-mouse-mode))
5.3.15.1.2. vertico-directory
This package allows for Ido-like directory navigation.
(use-package vertico-directory :ensure nil :after vertico :bind (:map vertico-map ("RET" . vertico-directory-enter) ("C-DEL" . vertico-directory-delete-word) ("DEL" . vertico-directory-delete-char)) ;; Tidy shadowed file names :hook (rfn-eshadow-update-overlay . vertico-directory-tidy))
5.3.15.1.3. orderless
Orderless allows for fuzzy matching.
When first installing orderless, I often times faced the problem, that when editing long files and calling consult-line, Emacs would hang when changing a search term in the middle (e.g. from servicse.xserver to servic.xserver in order to fix the typo). The below orderless rules have a more strict matching that has a positive impact on performance.
(use-package orderless :config (orderless-define-completion-style orderless+initialism (orderless-matching-styles '(orderless-initialism orderless-literal orderless-regexp))) (setq completion-styles '(orderless) completion-category-defaults nil completion-category-overrides '((file (styles partial-completion orderless+initialism)) (buffer (styles orderless+initialism)) (consult-multi (styles orderless+initialism)) (command (styles orderless+initialism)) (eglot (styles orderless+initialism)) (variable (styles orderless+initialism)) (symbol (styles orderless+initialism))) orderless-matching-styles '(orderless-literal orderless-regexp)))
5.3.15.1.4. consult
Consult provides better implementations for several user functions, e.g. consult-line or consult-outline.
The big winner here are the convenient keybinds being setup here for general use. Also, I setup vim-navigation for minibuffer completions. consult-buffer is set twice because I am still used to that weird C-M-j command that I chose for ivy-switch-buffer when I first started using Emacs. I want to move to the other command but for now it is not feasible to delete the other one.
(use-package consult :config (setq consult-fontify-max-size 1024) :bind (("C-x b" . consult-buffer) ("C-c <C-m>" . consult-global-mark) ("C-c C-a" . consult-org-agenda) ("C-x O" . consult-org-heading) ("C-M-j" . consult-buffer) ("C-s" . consult-line) ("M-g M-g" . consult-goto-line) ("M-g i" . consult-imenu) ("M-s M-s" . consult-line-multi) :map minibuffer-local-map ("C-j" . next-line) ("C-k" . previous-line)))
5.3.15.1.5. embark
Embark allows acting on the results in the minibuffer while the completion is still ongoing - this is extremely useful since it allows to, for example, read the documentation for several functions without closing the help search. It can also collect the results of a grep operation into a seperate buffer that edits the result in their original location.
I have stripped down the embark keybinds heavily. It is very useful to me even in it's current state, but it quickly becomes overwhelming. embark-dwim acts on a candidate without closing the minibuffer, which is very useful. embark-act lets the user choose from all actions, but has an overwhelming interface.
(use-package embark :bind (("C-." . embark-act) ("M-." . embark-dwim) ("C-h B" . embark-bindings) ("C-c c" . embark-collect)) :custom (prefix-help-command #'embark-prefix-help-command) (embark-quit-after-action '((t . nil))) :config (add-to-list 'display-buffer-alist '("\\`\\*Embark Collect \\(Live\\|Completions\\)\\*" nil (window-parameters (mode-line-format . none)))))
5.3.15.1.6. embark-consult
Provides previews for embark.
(use-package embark-consult :after (embark consult) :demand t ; only necessary if you have the hook below ;; if you want to have consult previews as you move around an ;; auto-updating embark collect buffer :hook (embark-collect-mode . consult-preview-at-point-mode))
5.3.15.1.7. marginalia
Marginalia adds more information to completion results.
I set the annotation-mode of marginalia to heavy. This gives even more information on the stuff that you are looking at. One thing I am missing from ivy is the highlighting on mode-commands based on the current state of the mode. Also, I do not understand all the shorthands used by marginalia yet.
(use-package marginalia :after vertico :bind (:map minibuffer-local-map ("M-A" . marginalia-cycle)) :init (marginalia-mode) ;; (setq marginalia-annotators '(marginalia-annotators-heavy marginalia-annotators-light nil)) )
5.3.15.1.8. nerd-icons-completion
As stated above, this simply provides nerd-icons to the completion framework. It is originally enabled here: Icons
(use-package nerd-icons-completion :after (marginalia nerd-icons) :hook (marginalia-mode . nerd-icons-completion-marginalia-setup) :init (nerd-icons-completion-mode))
5.3.15.2. Helpful + which-key: Better help defaults
This pair of packages provides information on keybinds in addition to function names, which makes it easier to remember keybinds (which-key). The helpful package provides a better Help framework for Emacs. For some reason, the Help windows are always being focused by the cursor even though I have set help-window-select to nil. I do not understand why.
(use-package which-key :init (which-key-mode) :diminish which-key-mode :config (setq which-key-idle-delay 0.3)) (use-package helpful :bind (("C-h f" . helpful-callable) ("C-h v" . helpful-variable) ("C-h k" . helpful-key) ("C-h C-." . helpful-at-point)) :config (setq help-window-select nil))
5.3.16. Ligatures
Personally, I think ligatures are fancy. With this mode, they stay 'cursorable'. However, I do not need them in all modes, so I only use them in programming modes.
(use-package ligature :init (global-ligature-mode t) :config (ligature-set-ligatures 'prog-mode '("|||>" "<|||" "<==>" "<!--" "####" "~~>" "***" "||=" "||>" ":::" "::=" "=:=" "===" "==>" "=!=" "=>>" "=<<" "=/=" "!==" "!!." ">=>" ">>=" ">>>" ">>-" ">->" "->>" "-->" "---" "-<<" "<~~" "<~>" "<*>" "<||" "<|>" "<$>" "<==" "<=>" "<=<" "<->" "<--" "<-<" "<<=" "<<-" "<<<" "<+>" "</>" "###" "#_(" "..<" "..." "+++" "/==" "///" "_|_" "www" "&&" "^=" "~~" "~@" "~=" "~>" "~-" "**" "*>" "*/" "||" "|}" "|]" "|=" "|>" "|-" "{|" "[|" "]#" "::" ":=" ":>" ":<" "$>" "==" "=>" "!=" "!!" ">:" ">=" ">>" ">-" "-~" "-|" "->" "--" "-<" "<~" "<*" "<|" "<:" "<$" "<=" "<>" "<-" "<<" "<+" "</" "#{" "#[" "#:" "#=" "#!" "##" "#(" "#?" "#_" "%%" ".=" ".." ".?" "+>" "++" "?:" "?=" "?." "??" "/*" "/=" "/>" "//" "__" "~~" "(*" "*)" "\\\\" "://" ";;")))
5.3.17. Popup (popper) + Shackle Buffers
The popper package allows to declare different buffers as 'popup-type', which sort of acts like a scratchpad. It can be toggled at any time using popper-toggle and the resulting frame can be freely customized (with shackle) to a certain size. It is also possible to prevent a buffer from appearing - I do this for example to the *Warnings* buffer, since usually I am not interested in it's output.
popper-echo-mode shows all buffers that are currently stored as a popup in the echo area when a popup is opened - this is useful since you can cycle between all popup buffers.
(use-package popper :bind (("M-[" . popper-toggle)) :init (setq popper-reference-buffers '("\\*Messages\\*" ("\\*Warnings\\*" . hide) "Output\\*$" "\\*Async Shell Command\\*" "\\*Async-native-compile-log\\*" help-mode helpful-mode "*Occur*" "*scratch*" "*julia*" "*Python*" "*rustic-compilation*" "*cargo-run*" ;; ("*tex-shell*" . hide) (compilation-mode . hide))) (popper-mode +1) (popper-echo-mode +1)) (use-package shackle :config (setq shackle-rules '(("*Messages*" :select t :popup t :align right :size 0.3) ("*Warnings*" :ignore t :popup t :align right :size 0.3) ("*Occur*" :select t :popup t :align below :size 0.2) ("*scratch*" :select t :popup t :align below :size 0.2) ("*Python*" :select t :popup t :align below :size 0.2) ("*rustic-compilation*" :select t :popup t :align below :size 0.4) ("*cargo-run*" :select t :popup t :align below :size 0.2) ("*tex-shell*" :ignore t :popup t :align below :size 0.2) (helpful-mode :select t :popup t :align right :size 0.35) (help-mode :select t :popup t :align right :size 0.4))) (shackle-mode 1))
5.3.18. Indicate first and last line of buffer
This places little angled indicators on the fringe of a window which indicate buffer boundaries. This is not super useful, but makes use of a space that I want to keep for aesthetic reasons anyways and makes it a bit more useful in the process.
(setq-default indicate-buffer-boundaries t)
5.3.19. Authentication
This defines the authentication sources used by org-calfw (Calendar) and Forge.
This file is written using home-manager sops in Home-manager: Emacs
;; (setq auth-sources '( "~/.emacs.d/.caldav" "~/.emacs.d/.authinfo.gpg") ;; auth-source-cache-expiry nil) ; default is 2h (setq auth-sources '( "~/.emacs.d/.authinfo") auth-source-cache-expiry nil)
5.4. Modules
This section houses all configuration bits that are related to a specific package that is not fundamental to my Emacs experience.
At some point this will receive further sorting, but for now this is good enough.
5.4.1. Org Mode
org-mode is probably my most-used mode in Emcas. It acts as my organizer, config management tool and calender even.
Note that nearly all headings within the Org-mode heading are coded within the use-package setup, so be very careful about moving stuff about here.
5.4.1.1. General org-mode
This sets up the basic org-mode. I wrote a function to handle some of the initial org-mode behaviour in org-mode setup. This part of the configuration mostly makes some aesthetic changes, enables neat LaTeX and points Emacs to some files that it needs for org-mode
(defun swarsel/org-agenda-done-and-archive () "Mark TODO at point as DONE, archive it, and save all agenda files." (interactive) (let ((org-archive-location "~/Org/Archive.org::Archive")) (org-agenda-todo "DONE") (org-agenda-archive) (dolist (buf (buffer-list)) (with-current-buffer buf (when (and buffer-file-name (string-prefix-p (expand-file-name "~/Org/") (file-truename buffer-file-name)) (derived-mode-p 'org-mode)) (save-buffer)))))) (with-eval-after-load 'org-agenda (define-key org-agenda-mode-map (kbd "C-a") #'swarsel/org-agenda-done-and-archive)) (use-package org ;;:diminish (org-indent-mode) :hook (org-mode . swarsel/org-mode-setup) ;; :mode "\\.nix\\'" :bind (("C-<tab>" . org-fold-outer) ("C-c s" . org-store-link)) :config (setq org-ellipsis " ⤵" org-link-descriptive t org-hide-emphasis-markers t) (setq org-startup-folded t) (setq org-support-shift-select t) (setq org-agenda-start-with-log-mode t) (setq org-log-done 'time) (setq org-log-into-drawer t) (setq org-startup-with-inline-images t) (setq org-export-headline-levels 6) (setq org-image-actual-width nil) (setq org-format-latex-options '(:foreground "White" :background default :scale 2.0 :html-foreground "Black" :html-background "Transparent" :html-scale 1.0 :matchers ("begin" "$1" "$" "$$" "\\(" "\\["))) (setq org-agenda-files '("/home/swarsel/Org/Tasks.org" "/home/swarsel/Org/Archive.org" )) (setq org-capture-templates '(("t" "Todo" entry (file+headline "~/Org/Tasks.org" "Inbox") "* TODO %?\n %i\n %a") ("j" "Journal" entry (file+datetree "~/Org/Journal.org") "* %?\nEntered on %U\n %i\n %a"))) (setq org-refile-targets '((swarsel-archive-org-file :maxlevel . 1) (swarsel-tasks-org-file :maxlevel . 1))) )
5.4.1.2. org-appear
This package makes emphasis-markers appear when the cursor moves over them. Very useful as I enjoy the clean look of not always seeing them, but it is annoying not to be able to edit them properly.
(use-package org-appear :hook (org-mode . org-appear-mode) :init (setq org-appear-autolinks t) (setq org-appear-autokeywords t) (setq org-appear-autoentities t) (setq org-appear-autosubmarkers t))
5.4.1.3. Centered org-mode Buffers
I like org-mode buffers to be centered, as I do not find that enormous lines are of big use.
Function definition in: Visual-fill column
(use-package visual-fill-column :hook (org-mode . swarsel/org-mode-visual-fill))
5.4.1.4. Fix headings not folding sometimes
There is a weird bug in org-mode that makes it so that headings were not folding correctly sometimes. This setting seems to fix it.
(setq org-fold-core-style 'overlays)
5.4.1.5. Babel
org-babel allows to run blocks in other programming languages within an org-mode buffer, similar to what e.g. jupyterhub offers for python.
It also offers a very useful utility of exporting org-mode buffers to different formats; the feature I enjoy most is what makes this file useful: the tangling functionality.
5.4.1.5.1. Language Configuration
- This configures the languages that babel recognizes.
(setq org-src-preserve-indentation nil) (org-babel-do-load-languages 'org-babel-load-languages '((emacs-lisp . t) (python . t) (js . t) (shell . t) )) (push '("conf-unix" . conf-unix) org-src-lang-modes) (setq org-export-with-broken-links 'mark) (setq org-confirm-babel-evaluate nil) ;; tangle is too slow, try to speed it up (defadvice org-babel-tangle-single-block (around inhibit-redisplay activate protect compile) "inhibit-redisplay and inhibit-message to avoid flicker." (let ((inhibit-redisplay t) (inhibit-message t)) ad-do-it)) (defadvice org-babel-tangle (around time-it activate compile) "Display the execution time" (let ((tim (current-time))) ad-do-it (message "org-tangle took %f sec" (float-time (time-subtract (current-time) tim)))))
5.4.1.5.2. old easy structure templates (org-tempo)
org 9.2 changed the way structure templates work. This brings back the old way it worked.
Usage: Type
<, followed by one of the below keywords and pressTAB. The corresponding source block should appear.(require 'org-tempo) (add-to-list 'org-structure-template-alist '("sh" . "src shell")) (add-to-list 'org-structure-template-alist '("el" . "src emacs-lisp")) (add-to-list 'org-structure-template-alist '("py" . "src python :results output")) (add-to-list 'org-structure-template-alist '("nix" . "src nix-ts :tangle")) (add-to-list 'org-structure-template-alist '("ne" . "bash :exports both"))
5.4.1.6. aucTex
This provides several utilities for LaTeX in Emacs, including many completions and convenience functions for math-mode.
(use-package auctex) (setq TeX-auto-save t) (setq TeX-save-query nil) (setq TeX-parse-self t) (setq-default TeX-engine 'luatex) (setq-default TeX-master nil) (add-hook 'LaTeX-mode-hook 'visual-line-mode) (add-hook 'LaTeX-mode-hook 'flyspell-mode) (add-hook 'LaTeX-mode-hook 'LaTeX-math-mode) (add-hook 'LaTeX-mode-hook 'reftex-mode) (setq LaTeX-electric-left-right-brace t) (setq font-latex-fontify-script nil) (setq TeX-electric-sub-and-superscript t) ;; (setq reftex-plug-into-AUCTeX t)
5.4.1.7. org-download
This package allows to download and copy images into org-mode buffers. Sadly it does not work in a very stable manner - if you copy images that are also links to another page (like is often the case in a Google image search), Emacs might crash from this.
5.4.1.8. org-fragtog
This package automatically toggles LaTeX-fragments in org-files. It seems to also work in markdown-files which is a nice addition, as my Obsidian notes are held in markdown.
(use-package org-fragtog)
(add-hook 'org-mode-hook 'org-fragtog-mode)
(add-hook 'markdown-mode-hook 'org-fragtog-mode)
5.4.1.9. org-modern
This just makes org-mode a little bit more beautiful, mostly by making the begin_src and end_src tags in source-blocks turn into more beautiful icons, as well as hiding #+ tags before them, as well as in the properties section of the file.
(use-package org-modern :config (setq org-modern-block-name '((t . t) ("src" "»" "∥"))) :hook (org-mode . org-modern-mode))
5.4.1.10. Presentations
Recently I have grown fond of holding presentations using Emacs.
When holding presentations, I think it is important to not have too many distractions on your slides. org-present just shows a plain background, is very responsive, and it is still an org buffer (so you can e.g. run source block codes while in the presentation).
(use-package org-present :bind (:map org-present-mode-keymap ("q" . org-present-quit) ("<left>" . swarsel/org-present-prev) ("<up>" . 'ignore) ("<down>" . 'ignore) ("<right>" . swarsel/org-present-next)) :hook ((org-present-mode . swarsel/org-present-start) (org-present-mode-quit . swarsel/org-present-end)) ) (use-package hide-mode-line) (defun swarsel/org-present-start () (setq-local face-remapping-alist '((default (:height 1.5) variable-pitch) (header-line (:height 4.0) variable-pitch) (org-document-title (:height 1.75) org-document-title) (org-code (:height 1.55) org-code) (org-verbatim (:height 1.55) org-verbatim) (org-block (:height 1.25) org-block) (org-block-begin-line (:height 0.7) org-block) )) (dolist (face '((org-level-1 . 1.1) (org-level-2 . 1.2) (org-level-3 . 1.2) (org-level-4 . 1.2) (org-level-5 . 1.2) (org-level-6 . 1.2) (org-level-7 . 1.2) (org-level-8 . 1.2))) (set-face-attribute (car face) nil :font swarsel-alt-font :weight 'medium :height (cdr face))) (setq header-line-format " ") (setq visual-fill-column-width 90) (setq indicate-buffer-boundaries nil) (setq inhibit-message nil) ;; (breadcrumb-mode 0) (org-display-inline-images) (global-hl-line-mode 0) ;; (display-line-numbers-mode 0) (org-modern-mode 0) (evil-insert-state 1) (beginning-of-buffer) (org-present-read-only) ;; (org-present-hide-cursor) (swarsel/org-present-slide) ) (defun swarsel/org-present-end () (setq-local face-remapping-alist '((default variable-pitch default))) (dolist (face '((org-level-1 . 1.1) (org-level-2 . 0.9) (org-level-3 . 0.9) (org-level-4 . 0.9) (org-level-5 . 0.9) (org-level-6 . 0.9) (org-level-7 . 0.9) (org-level-8 . 0.9))) (set-face-attribute (car face) nil :font swarsel-alt-font :weight 'medium :height (cdr face))) (setq header-line-format nil) (setq visual-fill-column-width 150) (setq indicate-buffer-boundaries t) (setq inhibit-message nil) ;; (breadcrumb-mode 1) (global-hl-line-mode 1) ;; (display-line-numbers-mode 1) (org-remove-inline-images) (org-modern-mode 1) (evil-normal-state 1) ;; (org-present-show-cursor) ) (defun swarsel/org-present-slide () (org-overview) (org-show-entry) (org-show-children) ) (defun swarsel/org-present-prev () (interactive) (org-present-prev) (swarsel/org-present-slide)) (defun swarsel/org-present-next () (interactive) (unless (eobp) (org-next-visible-heading 1) (org-fold-show-entry)) (when (eobp) (org-present-next) (swarsel/org-present-slide) )) (defun clojure-leave-clojure-mode-function () ) (add-hook 'buffer-list-update-hook #'clojure-leave-clojure-mode-function) (add-hook 'org-present-mode-hook 'swarsel/org-present-start) (add-hook 'org-present-mode-quit-hook 'swarsel/org-present-end) (add-hook 'org-present-after-navigate-functions 'swarsel/org-present-slide)
5.4.1.11. Render markdown blocks as body to expand noweb blocks
I have written this function to allow me to get a preview of the information that is gathered throughout the file and aggregated in Manual steps when setting up a new machine. Normally, running a markdown source block does nothing in Emacs. Hence, I just let it return the output, which inserts the noweb-ref blocks.
(defun org-babel-execute:markdown (body params) "Just return BODY unchanged, allowing noweb expansion." body)
5.4.2. Nix Mode
This adds a nix mode to Emacs. This has become increasingly useful since I have added lsp-mode in org-src blocks, because since that time, I am now able to actually make use of major modes while I theoretically stay in org-mode.
It supports all functions that I normally need. Note that getting completions for flake inputs is a bit finnicky and I am not quite fond of it yet.
(use-package nix-mode :after lsp-mode :ensure t :hook (nix-mode . lsp-deferred) ;; So that envrc mode will work :custom (lsp-disabled-clients '((nix-mode . nix-nil))) ;; Disable nil so that nixd will be used as lsp-server :config (setq lsp-nix-nixd-server-path "nixd" lsp-nix-nixd-formatting-command [ "nixpkgs-fmt" ] lsp-nix-nixd-nixpkgs-expr "import (builtins.getFlake \"/home/swarsel/.dotfiles\").inputs.nixpkgs { }" lsp-nix-nixd-nixos-options-expr "(builtins.getFlake \"/home/swarsel/.dotfiles\").nixosConfigurations.pyramid.options" lsp-nix-nixd-home-manager-options-expr "(builtins.getFlake \"/home/swarsel/.dotfiles\").nixosConfigurations.pyramid.options.home-manager.users.type.getSubOptions []" )) (use-package nix-ts-mode :after lsp-mode :mode "\\.nix\\'" "\\.nix\\.enc\\'" :ensure t :hook (nix-ts-mode . lsp-deferred) ;; So that envrc mode will work :custom (lsp-disabled-clients '((nix-ts-mode . nix-nil))) ;; Disable nil so that nixd will be used as lsp-server :config (setq lsp-nix-nixd-server-path "nixd" lsp-nix-nixd-formatting-command [ "nixpkgs-fmt" ] lsp-nix-nixd-nixpkgs-expr "import (builtins.getFlake \"/home/swarsel/.dotfiles\").inputs.nixpkgs { }" lsp-nix-nixd-nixos-options-expr "(builtins.getFlake \"/home/swarsel/.dotfiles\").nixosConfigurations.pyramid.options" lsp-nix-nixd-home-manager-options-expr "(builtins.getFlake \"/home/swarsel/.dotfiles\").nixosConfigurations.pyramid.options.home-manager.users.type.getSubOptions []" )) (add-to-list 'auto-mode-alist '("\\.nix\\.enc\\'" . nix-mode)) (add-to-list 'auto-mode-alist '("\\.nix\\.enc\\'" . nix-ts-mode)) (with-eval-after-load 'lsp-mode (lsp-register-client (make-lsp-client :new-connection (lsp-stdio-connection "nixd") :major-modes '(nix-mode nix-ts-mode) :priority 0 :server-id 'nixd)))
5.4.3. HCL Mode
This adds support for Hashicorp Configuration Language. Used at work, it is mostly a Terraform Mode that does not support autoformatting upon save. It still is nice :)
(use-package hcl-mode :mode "\\.hcl\\'" :config (setq hcl-indent-level 2))
5.4.4. Jenkinsfile/Groovy
This adds support for Groovy, which I specifically need to work with Jenkinsfiles. Similar to [BROKEN LINK: 7aa9803f-b419-40fa-aafc-4bb934c8f687], it just provides some nice functions.
(use-package groovy-mode) (use-package jenkinsfile-mode :mode "Jenkinsfile")
5.4.5. Ansible
This is supposed to provide auto-completion when turned on. Of course I cannot globally turn this on since it would run in any .yaml file then, but even when manually started, it seems to do nothing. This would be nice at work.
(use-package ansible)
5.4.6. Dockerfile
This adds support for Dockerfiles in a similar way to [BROKEN LINK: ebd53be9-c38a-4a0f-a7b4-eee30a0074fc].
(use-package dockerfile-mode :mode "Dockerfile")
5.4.7. Terraform Mode
This adds support for Terraform configuration files. This is basically the same as the [BROKEN LINK: 7aa9803f-b419-40fa-aafc-4bb934c8f687] mode as the languages are very similar.
(use-package terraform-mode :mode "\\.tf\\'" :config (setq terraform-indent-level 2) (setq terraform-format-on-save t)) (add-hook 'terraform-mode-hook #'outline-minor-mode)
5.4.8. nix formatting
Adds functions for formatting nix code. I make huge use of this using the chords C-<Space> o b (org-babel-mark-block) and then C-<Space> o n (nixpkgs-fmt-region). This is what I use to keep my nix org-src-blocks formatted. However, using [BROKEN LINK: a67adf2f-20ce-49d6-ba6b-0341ca3d9972], the resulting tangled files will be formatted in any case.
Note that for files that are not managed using this file (which there should normally not be many of), we can still use nix fmt for running treefmt for formatting and checks.
(use-package nixpkgs-fmt)
5.4.9. shfmt
Adds functions for formatting shellscripts. Similarly to [BROKEN LINK: 460a47fd-cddc-4080-9eba-6724fc63606e]m I use this using the chords C-<Space> o b (org-babel-mark-block) and then C-<Space> o s (shfmt-region). This is what I use to keep shell script blocks formatted in this file. This is also handled by treefmt, but still, I want this file to stay organized as well.
(use-package shfmt :config (setq shfmt-command "shfmt") (setq shfmt-arguments '("-i" "4" "-s" "-sr")))
5.4.10. Markdown Mode
5.4.10.1. Mode
Adds a mode for markdown, specifically MultiMarkdown, which allows me to render LaTeX and other nice things.
(setq markdown-command "pandoc") (use-package markdown-mode :ensure t :mode ("README\\.md\\'" . gfm-mode) :init (setq markdown-command "multimarkdown") :bind (:map markdown-mode-map ("C-c C-e" . markdown-do)))
5.4.10.2. LaTeX in Markdown
Allows me to render LaTeX just where I write it. I do not need this as much anymore, but during my studies this was very valuable to me.
(add-hook 'markdown-mode-hook
(lambda ()
(local-set-key (kbd "C-c C-x C-l") 'org-latex-preview)
(local-set-key (kbd "C-c C-x C-u") 'markdown-toggle-url-hiding)
))
5.4.11. elfeed
This adds elfeed, a neat RSS reader for Emacs. I use this as a client for FreshRSS. While I read most of my feeds on my phone (using Capy Reader), it is still good to have an Emacs-native reader as well. Some time ago I was still running a separate Emacs instance on my server: [BROKEN LINK: 0e07e2fb-adc4-4fd8-9b54-0a59338a471e]. This instance would then sync the read feeds to other instances. This was very brittle however and is only left as a historical note.
(use-package elfeed) (use-package elfeed-goodies) (elfeed-goodies/setup) (setq elfeed-db-directory "~/.elfeed/db/") (use-package elfeed-protocol :after elfeed) (elfeed-protocol-enable) (setq elfeed-use-curl t) (setq elfeed-set-timeout 36000) (setq elfeed-protocol-enabled-protocols '(fever)) (setq elfeed-protocol-fever-update-unread-only t) (setq elfeed-protocol-fever-fetch-category-as-tag t) (let ((domain (getenv "SWARSEL_RSS_DOMAIN"))) (setq elfeed-protocol-feeds `((,(concat "fever+https://Swarsel@" domain) :api-url ,(concat "https://" domain "/api/fever.php") :password-file "~/.emacs.d/.fever")))) (define-key elfeed-show-mode-map (kbd ";") 'visual-fill-column-mode) (define-key elfeed-show-mode-map (kbd "j") 'elfeed-goodies/split-show-next) (define-key elfeed-show-mode-map (kbd "k") 'elfeed-goodies/split-show-prev) (define-key elfeed-search-mode-map (kbd "j") 'next-line) (define-key elfeed-search-mode-map (kbd "k") 'previous-line) (define-key elfeed-show-mode-map (kbd "S-SPC") 'scroll-down-command)
5.4.12. Ripgrep
This is the ripgrep package for Emacs.
(use-package rg)
5.4.13. Tree-sitter
Tree-sitter is a parsing library integrated into Emacs to provide better syntax highlighting and code analysis. It generates concrete syntax trees for source code, enabling more accurate and efficient text processing. Emacs' tree-sitter integration enhances language support, offering features like incremental parsing and precise syntax-aware editing. This improves the development experience by providing robust and dynamic syntax features, making it easier for me to navigate and manipulate code.
In order to update the language grammars, run the next command below. NOTE: since we now load epkgs.treesit-grammars.with-all-grammars in Home-manager: Emacs, we actually never run this anymore. I leave it here however for a potential future reader. For safety, I still instruct treesit to install missing grammars on the fly.
(mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist))
;; (use-package emacs ;; :ensure nil ;; :init ;; (setq treesit-language-source-alist ;; '((bash . ("https://github.com/tree-sitter/tree-sitter-bash")) ;; (c . ("https://github.com/tree-sitter/tree-sitter-c")) ;; (cmake . ("https://github.com/uyha/tree-sitter-cmake")) ;; (cpp . ("https://github.com/tree-sitter/tree-sitter-cpp")) ;; (css . ("https://github.com/tree-sitter/tree-sitter-css")) ;; (elisp . ("https://github.com/Wilfred/tree-sitter-elisp")) ;; (go . ("https://github.com/tree-sitter/tree-sitter-go")) ;; (html . ("https://github.com/tree-sitter/tree-sitter-html")) ;; (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript")) ;; (json . ("https://github.com/tree-sitter/tree-sitter-json")) ;; (julia . ("https://github.com/tree-sitter/tree-sitter-julia")) ;; (latex . ("https://github.com/latex-lsp/tree-sitter-latex")) ;; (make . ("https://github.com/alemuller/tree-sitter-make")) ;; (markdown . ("https://github.com/ikatyang/tree-sitter-markdown")) ;; (nix . ("https://github.com/nix-community/tree-sitter-nix")) ;; (R . ("https://github.com/r-lib/tree-sitter-r")) ;; (python . ("https://github.com/tree-sitter/tree-sitter-python")) ;; (typescript . ("https://github.com/tree-sitter/tree-sitter-typescript" "typescript/src" "typescript")) ;; (rust . ("https://github.com/tree-sitter/tree-sitter-rust")) ;; (sql . ("https://github.com/m-novikov/tree-sitter-sql")) ;; (toml . ("https://github.com/tree-sitter/tree-sitter-toml")) ;; (tsx . ("https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src")) ;; (yaml . ("https://github.com/ikatyang/tree-sitter-yaml")))) ;; ) (use-package treesit-auto :custom (setq treesit-auto-install t) :config (treesit-auto-add-to-auto-mode-alist 'all) (global-treesit-auto-mode))
5.4.14. direnv (envrc)
In emacs, there are two packages for managing dev environments - emacs-direnv (direnv) and envrc. Direnv uses the global Emacs environment whereas envrc is buffer-local. I do not really care about this difference. What is more important to me is that emacs should not block upon handling a bigger flake.nix while setting up the dev environment. This seems to be better handled by envrc.
;; (use-package direnv ;; :custom (direnv-always-show-summary nil) ;; :config (direnv-mode)) (use-package envrc :hook (after-init . envrc-global-mode))
5.4.15. avy
avy provides the ability to search for any character on the screen (not only in the current buffer!) - I enjoy this utility a lot and use it possibly even more often than the native vim commands.
(use-package avy :bind (("M-o" . avy-goto-char-timer)) :config (setq avy-all-windows 'all-frames))
5.4.16. devdocs
devdocs is a very nice package that provides documentation from https:devdocs.io. This is very useful since e.g. pyright provides only a very bad documentation and I do not want to leave Emacs all the time just to read documentation.
To install a documentation, use the devdocs=install command and select the appropriate version. devdocs-update-all can be used to download and reinstall all installed documents if a newer version is available. Check documentation with devdocs-lookup (C-SPC h d).
(use-package devdocs) (add-hook 'python-mode-hook (lambda () (setq-local devdocs-current-docs '("python~3.12" "numpy~1.23" "matplotlib~3.7" "pandas~1")))) (add-hook 'python-ts-mode-hook (lambda () (setq-local devdocs-current-docs '("python~3.12" "numpy~1.23" "matplotlib~3.7" "pandas~1")))) (add-hook 'c-mode-hook (lambda () (setq-local devdocs-current-docs '("c")))) (add-hook 'c-ts-mode-hook (lambda () (setq-local devdocs-current-docs '("c")))) (add-hook 'c++-mode-hook (lambda () (setq-local devdocs-current-docs '("cpp")))) (add-hook 'c++-ts-mode-hook (lambda () (setq-local devdocs-current-docs '("cpp")))) ; (devdocs-update-all)
5.4.17. Projectile
projectile is useful for keeping track of your git projects within Emacs. I mostly use it to quickly switch between projects.
(use-package projectile :diminish projectile-mode :config (projectile-mode) :custom ((projectile-completion-system 'auto)) ;; integrate ivy into completion system :bind-keymap ("C-c p" . projectile-command-map) ; all projectile commands under this :init ;; NOTE: Set this to the folder where you keep your Git repos! (when (file-directory-p swarsel-work-projects-directory) (when (file-directory-p swarsel-private-projects-directory) (setq projectile-project-search-path (list swarsel-work-projects-directory swarsel-private-projects-directory)))) (setq projectile-switch-project-action #'magit-status))
5.4.18. Magit
magit is the best git utility I have ever used - it has a beautiful interface and is very verbose. Here I mostly just setup the list of repositories that I want to expost to magit.
Also, Emacs needs a little extra love to accept my Yubikey for git commits etc. We set that here: [BROKEN LINK: 59df9a4c-2a1f-466b-abe2-fbb8524cd0ed].
(use-package magit :config (setq magit-repository-directories `((,swarsel-work-projects-directory . 3) (,swarsel-private-projects-directory . 3) ("~/.dotfiles/" . 0))) :custom (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)) ; stay in the same window
5.4.19. Yubikey support
The following settings are needed to make sure emacs works for magit commits and pushes. It is not a beautiful solution since commiting uses pinentry-emacs and pushing uses pinentry-gtk2, but it works for now at least. This works especially well since I have switched from pinentry-gtk3 to pinentry-waypromt.
;; yubikey support for pushing commits ;; commiting is enabled through nixos gpg-agent config (use-package pinentry) (pinentry-start) (setq epg-pinentry-mode 'loopback) (setenv "SSH_AUTH_SOCK" (string-chop-newline (shell-command-to-string "gpgconf --list-dirs agent-ssh-socket")))
5.4.20. Forge
NOTE: Make sure to configure a GitHub token before using this package!
- https://magit.vc/manual/forge/Token-Creation.html#Token-Creation
- https://magit.vc/manual/ghub/Getting-Started.html#Getting-Started
- https://magit.vc/manual/ghub/Storing-a-Token.html
https://www.emacswiki.org/emacs/GnuPG
(1) in practice: github -<> settings -<> developer option -<> create classic token with repo; user; read:org permissions (2)machine api.github.com login USERNAMEforge password 012345abcdef…
The above is handled by [BROKEN LINK: ebb558ed-883a-486f-a6f5-8b283eb735a3] and only here as a historical note. Forge lets me interact with non-core git objects like issues and pull requests from within emacs.
(use-package forge :after magit)
5.4.21. git-timemachine
This is just a nice utility to browse different versions of a file of a git project within Emacs.
(use-package git-timemachine :hook (git-time-machine-mode . evil-normalize-keymaps) :init (setq git-timemachine-show-minibuffer-details t))
5.4.22. Delimiters (brackets): rainbow-delimiters, highlight-parentheses
- rainbow-delimiters colors all delimiters, also ones not in current selection
- paren highlights the current delimiter selection especially bold
- highlight-parentheses boldly highlights all delimiters in current selection
I am not completely sure on electric-pair-mode yet, sometimes it is very helpful, sometimes it annoys me to no end.
(use-package rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode)) (use-package highlight-parentheses :config (setq highlight-parentheses-colors '("black" "white" "black" "black" "black" "black" "black")) (setq highlight-parentheses-background-colors '("magenta" "blue" "cyan" "green" "yellow" "orange" "red")) (global-highlight-parentheses-mode t)) ;; (electric-pair-mode 1) ;; (setq electric-pair-preserve-balance t) ;; (setq electric-pair-skip-self nil) ;; (setq electric-pair-delete-adjacent-pairs t) ;; don't skip newline when auto-pairing parenthesis ;; (setq electric-pair-skip-whitespace-chars '(9 32)) ;; in org-mode buffers, do not pair < and > in order not to interfere with org-tempo ;; (add-hook 'org-mode-hook (lambda () ;; (setq-local electric-pair-inhibit-predicate ;; `(lambda (c) ;; (if (char-equal c ?<) t (,electric-pair-inhibit-predicate c))))))
5.4.23. rainbow-mode
Complimentary to the delimiters-packages above, this package sets the background color of the delimiters, which makes it easier to see at a glance where we are in a delimiter-tree.
(use-package rainbow-mode :config (rainbow-mode))
5.4.24. Corfu
This is the company equivalent to the vertico gang.
I dislike the standard behaviour that makes the cursor move into the completion framework on presses of <up> and <down>.
Nerd icons is originally enabled here: Icons
Navigation functions defined here: corfu: Do not interrupt navigation
(use-package corfu :init (global-corfu-mode) (corfu-history-mode) (corfu-popupinfo-mode) ; Popup completion info :custom (corfu-auto t) (corfu-auto-prefix 3) (corfu-auto-delay 1) (corfu-cycle t) (corfu-quit-no-match 'separator) (corfu-separator ?\s) ;; (corfu-quit-no-match t) (corfu-popupinfo-max-height 70) (corfu-popupinfo-delay '(0.5 . 0.2)) ;; (corfu-preview-current 'insert) ; insert previewed candidate (corfu-preselect 'prompt) (corfu-on-exact-match nil) ; Don't auto expand tempel snippets ;; Optionally use TAB for cycling, default is `corfu-complete'. :bind (:map corfu-map ("M-SPC" . corfu-insert-separator) ("<return>" . swarsel/corfu-normal-return) ;; ("C-<return>" . swarsel/corfu-complete) ("S-<up>" . corfu-popupinfo-scroll-down) ("S-<down>" . corfu-popupinfo-scroll-up) ("C-<up>" . corfu-previous) ("C-<down>" . corfu-next) ("<insert-state> <up>" . swarsel/corfu-quit-and-up) ("<insert-state> <down>" . swarsel/corfu-quit-and-down)) ) (use-package nerd-icons-corfu) (add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter) (setq nerd-icons-corfu-mapping '((array :style "cod" :icon "symbol_array" :face font-lock-type-face) (boolean :style "cod" :icon "symbol_boolean" :face font-lock-builtin-face) ;; ... (t :style "cod" :icon "code" :face font-lock-warning-face)))
5.4.25. cape
cape adds even more completion capabilities by adding a lot of completion logic that is exposed as separate functions. I tried out adding these to the completion-at-points-functions alist, but I felt like it cluttered my suggestions too much. Hence I now just call the respective functions when I need them. For this I setup the C-z keybinding in General evil.
I leave the commented out alist extensions here in case I want to try them out at some point in the future.
(use-package cape :bind ("C-z p" . completion-at-point) ;; capf ("C-z t" . complete-tag) ;; etags ("C-z d" . cape-dabbrev) ;; or dabbrev-completion ("C-z h" . cape-history) ("C-z f" . cape-file) ("C-z k" . cape-keyword) ("C-z s" . cape-elisp-symbol) ("C-z e" . cape-elisp-block) ("C-z a" . cape-abbrev) ("C-z l" . cape-line) ("C-z w" . cape-dict) ("C-z :" . cape-emoji) ("C-z \\" . cape-tex) ("C-z _" . cape-tex) ("C-z ^" . cape-tex) ("C-z &" . cape-sgml) ("C-z r" . cape-rfc1345) )
5.4.26. rust
This sets up rustic-mode with tree-sitter support - there is still one issue to iron out with automatic adding of dependency crates, but everything else works fine now.
;;(use-package rustic ;; :init ;; (setq rust-mode-treesitter-derive t) ;; :config ;; (define-key rust-ts-mode-map (kbd "C-c C-c C-r") 'rustic-cargo-run) ;; (define-key rust-ts-mode-map (kbd "C-c C-c C-b") 'rustic-cargo-build) ;; (define-key rust-ts-mode-map (kbd "C-c C-c C-k") 'rustic-cargo-check) ;; (define-key rust-ts-mode-map (kbd "C-c C-c d") 'rustic-cargo-doc) ;; (define-key rust-ts-mode-map (kbd "C-c C-c a") 'rustic-cargo-add) ;; (setq rustic-format-on-save t) ;; (setq rustic-lsp-client 'eglot) ;; :mode ("\\.rs" . rustic-mode))
5.4.27. Tramp
Tramp allows for SSH access of files over Emacs. I have no ideas what the options here mean, but this is a recommended configuration that I found (sadly I lost the link). I need to research more what these options really do.
(use-package tramp ;; :ensure nil :init (setq vc-ignore-dir-regexp (format "\\(%s\\)\\|\\(%s\\)" vc-ignore-dir-regexp tramp-file-name-regexp)) (setq tramp-default-method "ssh") (setq tramp-auto-save-directory (expand-file-name "tramp-auto-save" user-emacs-directory)) (setq tramp-persistency-file-name (expand-file-name "tramp-connection-history" user-emacs-directory)) (setq password-cache-expiry nil) (setq tramp-use-ssh-controlmaster-options nil) (setq remote-file-name-inhibit-cache nil) :config (customize-set-variable 'tramp-ssh-controlmaster-options (concat "-o ControlPath=/tmp/ssh-tramp-%%r@%%h:%%p " "-o ControlMaster=auto -o ControlPersist=yes")) ) (setq vterm-tramp-shells '(("ssh" "'sh'")))
5.4.28. diff-hl
This is a simple highlighting utility that uses the margin to visually show the differences since the last git commit.
(use-package diff-hl :hook ((prog-mode org-mode) . diff-hl-mode) :init (diff-hl-flydiff-mode) (diff-hl-margin-mode) (diff-hl-show-hunk-mouse-mode))
5.4.29. Commenting
This package allows for swift commenting out and in of code snippets. For some reason, it is a bit broken in my config, as it sometimes comments out too much, sometimes too little, and sometimes it splits lines during commenting. Also, in org-mode when inside a src-block, it often times jumps to the top of the block.
Still, this is avery convenient package.
(use-package evil-nerd-commenter :bind ("M-/" . evilnc-comment-or-uncomment-lines))
5.4.30. eglot
Up comes the section of lsp clients for Emacs. For a longer time, I thought that I had to choose one only, and after having started with lsp-mode I had tried out lsp-booster and then went to eglot. My requirements are as follow:
Must have:
- mostly unintrusive, non-blocking
- fast (configurable) completion
- xref (or similar)
Nice to have:
- Debugger
- Multi-lsp support (running two lsp's on a single project)
- Native Emacs support
eglot fills most items on the first list except for the non-blocking issue initially. It blocks sometimes on bigger projects as well as when entering directories using (nix-)direnv and the lsp is not yet loaded. The first issue is solved by using eglot-booster, which increases the parsing speed by what feels like a huge margin (but I never ran any actual tests). The second issue is solved with eglot-sync-connect, which avoids blocking the interface while the server is starting.
A blocking issue can still occur while entering a direnv that has a longer evaluation/build time. That issue can only be fixed by using Mic92's emacs-direnv fork, which calls direnv asynchronously, which in turn avoids the blocking. I am not using this on a daily basis however, since my environments are normally cached anyways and most of them (except for the LaTeX one) are not blocking for long enough for this to be worth it. However, I am considering spinning up my own fork of this at some point.
(use-package eglot :hook ((python-mode python-ts-mode c-mode c-ts-mode c++-mode c++-ts-mode go-mode go-ts-mode ;;rust-ts-mode ;;rustic-mode tex-mode LaTeX-mode ) . (lambda () (progn (eglot-ensure) (add-hook 'before-save-hook 'eglot-format nil 'local)))) :custom (eldoc-echo-area-use-multiline-p nil) (completion-category-defaults nil) (fset #'jsonrpc--log-event #'ignore) (eglot-events-buffer-size 0) (eglot-sync-connect nil) (eglot-connect-timeout nil) (eglot-autoshutdown t) (eglot-send-changes-idle-time 3) (flymake-no-changes-timeout 5) :bind (:map eglot-mode-map ("M-(" . flymake-goto-next-error) ("C-c ," . eglot-code-actions))) (use-package eglot-booster :ensure nil :after eglot :config (eglot-booster-mode)) (defalias 'start-lsp-server #'eglot)
5.4.31. lsp-mode & company
company is now disabled since it seems that corfu runs just fine with lsp-mode and I prefer it. Also I set the auto-guess-root option to true in order to stop excessive nag when editing within org-src blocks.
(use-package lsp-mode :init ;; set prefix for lsp-command-keymap (few alternatives - "C-l", "C-c l") (setq lsp-keymap-prefix "C-c l") (setq lsp-auto-guess-root "t") :commands lsp) ;; (use-package company)
5.4.32. lsp-mode in org-src blocks
This incredible function allows to start a sub-pane in a org-file while in a source-block that spins up a lsp-server. In practise that allows me to use a nix lsp when editing complex blocks in my config. The only bother is that we have to add the modes where it should run manually to org-babel-lang-list, but that is a small price to pay for the usefulness that it brings.
;; thanks to https://tecosaur.github.io/emacs-config/config.html#lsp-support-src (cl-defmacro lsp-org-babel-enable (lang) "Support LANG in org source code block." (setq centaur-lsp 'lsp-mode) (cl-check-type lang string) (let* ((edit-pre (intern (format "org-babel-edit-prep:%s" lang))) (intern-pre (intern (format "lsp--%s" (symbol-name edit-pre))))) `(progn (defun ,intern-pre (info) (let ((file-name (->> info caddr (alist-get :file)))) (unless file-name (setq file-name (make-temp-file "babel-lsp-"))) (setq buffer-file-name file-name) (lsp-deferred))) (put ',intern-pre 'function-documentation (format "Enable lsp-mode in the buffer of org source block (%s)." (upcase ,lang))) (if (fboundp ',edit-pre) (advice-add ',edit-pre :after ',intern-pre) (progn (defun ,edit-pre (info) (,intern-pre info)) (put ',edit-pre 'function-documentation (format "Prepare local buffer environment for org source block (%s)." (upcase ,lang)))))))) (defvar org-babel-lang-list '( "nix" "nix-ts" "go" "python" "ipython" "bash" "sh" )) (dolist (lang org-babel-lang-list) (eval `(lsp-org-babel-enable ,lang)))
5.4.33. lsp-bridge
This is another lsp-implementation for Emacs using multi-threading, so this should be the least blocking one. Still, in general I prefer eglot.
(use-package lsp-bridge :ensure nil)
5.4.34. sideline-flymake
This brings back warnings and errors on the sideline for eglot; a feature that I have been missing from lsp-mode for a while.
(use-package sideline-flymake :hook (flymake-mode . sideline-mode) :init (setq sideline-flymake-display-mode 'point) ; 'point to show errors only on point ; 'line to show errors on the current line (setq sideline-backends-right '(sideline-flymake)))
5.4.35. Prevent breaking of hardlinks
This setting ensures that hard links are preserved during the backup process, which is useful for maintaining the integrity of files that are linked in multiple locations.
(setq backup-by-copying-when-linked t)
5.4.36. Dirvish
Dirvish is an improvement upon the dired-framework and has more features like file preview etc. Sadly it has an incompatibility with openwith which is why I have disabled that package.
(use-package dirvish :init (dirvish-override-dired-mode) :config (dirvish-peek-mode) (dirvish-side-follow-mode) ;; (setq dirvish-open-with-programs ;; (append dirvish-open-with-programs '( ;; (("xlsx" "docx" "doc" "odt" "ods") "libreoffice" "%f") ;; (("jpg" "jpeg" "png") "imv" "%f") ;; (("pdf") "sioyek" "%f") ;; (("xopp") "xournalpp" "%f")))) :custom (delete-by-moving-to-trash t) (dired-listing-switches "-l --almost-all --human-readable --group-directories-first --no-group") (dirvish-attributes '(vc-state subtree-state nerd-icons collapse file-time file-size)) (dirvish-quick-access-entries '(("h" "~/" "Home") ("c" "~/.dotfiles/" "Config") ("d" "~/Downloads/" "Downloads") ("D" "~/Documents/" "Documents") ("p" "~/Documents/GitHub/" "Projects") ("/" "/" "Root"))) :bind (("<DUMMY-i> d" . 'dirvish) ("C-=" . 'dirvish-side) :map dirvish-mode-map ("h" . dired-up-directory) ("<left>" . dired-up-directory) ("l" . dired-find-file) ("<right>" . dired-find-file) ("j" . evil-next-visual-line) ("k" . evil-previous-visual-line) ("a" . dirvish-quick-access) ("f" . dirvish-file-info-menu) ("z" . dirvish-history-last) ("J" . dirvish-history-jump) ("y" . dirvish-yank-menu) ("/" . dirvish-narrow) ("TAB" . dirvish-subtree-toggle) ("M-f" . dirvish-history-go-forward) ("M-b" . dirvish-history-go-backward) ("M-l" . dirvish-ls-switches-menu) ("M-m" . dirvish-mark-menu) ("M-t" . dirvish-layout-toggle) ("M-s" . dirvish-setup-menu) ("M-e" . dirvish-emerge-menu) ("M-j" . dirvish-fd-jump)))
5.4.37. undo-tree
Base emacs undo logic is very useful, but not easy to understand for me. I prefer undo-tree, which makes switching between branches easier and also allows quickly switching back to a much older state using the visualizer.
Evil needs to be told to use this mode, see (evil-set-undo-system 'undo-tree) in Evil/General.
By default, I am not using undo-tree-mode in every buffer. This might change in the future, but for now this is fine. It can be enabled manually should the need arise.
While we are at it, we are also setting up a persistent undo-file for every file that we are working with.
(use-package undo-tree :init (global-undo-tree-mode) :bind (:map undo-tree-visualizer-mode-map ("h" . undo-tree-visualize-switch-branch-left) ("l" . undo-tree-visualize-switch-branch-left) ("j" . undo-tree-visualize-redo) ("k" . undo-tree-visualize-undo)) :config (setq undo-tree-history-directory-alist '(("." . "~/.emacs.d/undo"))))
5.4.38. Hydra
Hydra allows for the writing of macro-style functions. I have not yet looked into this all too much, but it seems to be a potent feature.
(use-package hydra)
5.4.38.1. Text scaling
I only wrote this in order to try out hydra; rarely do I really need this. However, it can be useful for Presentations. It simply scales the text size.
;; change the text size of the current buffer (defhydra hydra-text-scale (:timeout 4) "scale text" ("j" text-scale-increase "in") ("k" text-scale-decrease "out") ("f" nil "finished" :exit t))
5.4.39. Email
5.4.39.1. mu4e
In this section we are setting up mu4e, a mail client for emacs using mu with mbsync as backend. The mail accounts themselves are setup in the NixOS configuration, so we only need to add Emacs specific settings here.
The hook functions are defined here: mu4e functions
(use-package mu4e :ensure nil ;; :load-path "/usr/share/emacs/site-lisp/mu4e/" ;;:defer 20 ; Wait until 20 seconds after startup :config ;; This is set to 't' to avoid mail syncing issues when using mbsync (setq send-mail-function 'sendmail-send-it) (setq mu4e-change-filenames-when-moving t) (setq mu4e-mu-binary (executable-find "mu")) (setq mu4e-hide-index-messages t) ;; this is so that messages that target multiple addresses still are individually shown in the unreads ;; this is needed because otherwise after closing the view there will still be an unread message (setq mu4e-search-skip-duplicates nil) (setq mu4e-update-interval 60) (setq mu4e-get-mail-command "mbsync -a") (setq mu4e-maildir "~/Mail") ;; enable inline images (setq mu4e-view-show-images t) ;; use imagemagick, if available (when (fboundp 'imagemagick-register-types) (imagemagick-register-types)) (setq mu4e-drafts-folder "/Drafts") (setq mu4e-sent-folder "/Sent Mail") (setq mu4e-refile-folder "/All Mail") (setq mu4e-trash-folder "/Trash") (setq mu4e-maildir-shortcuts '((:maildir "/leon/Inbox" :key ?1) (:maildir "/nautilus/Inbox" :key ?2) (:maildir "/mrswarsel/Inbox" :key ?3) (:maildir "/work/Inbox" :key ?4) (:maildir "/Sent Mail" :key ?s) (:maildir "/Trash" :key ?t) (:maildir "/Drafts" :key ?d) (:maildir "/All Mail" :key ?a))) (setq user-mail-address (getenv "SWARSEL_MAIL4") user-full-name (getenv "SWARSEL_FULLNAME")) ;; this does the equivalent of (setq mu4e-user-mail-address-list '(address1@about.com address2@about.com [...]))) (setq mu4e-user-mail-address-list (mapcar #'intern (split-string (or (getenv "SWARSEL_MAIL_ALL") "") "[ ,]+" t))) ) (add-hook 'mu4e-compose-mode-hook #'swarsel/mu4e-send-from-correct-address) (add-hook 'mu4e-compose-post-hook #'swarsel/mu4e-restore-default)
5.4.39.2. mu4e-alert
This adds the simple utility of sending desktop notifications whenever a new mail is received. I am using libnotify because I want to use this with notify-send.
(use-package mu4e-alert :config (mu4e-alert-enable-notifications) (mu4e-alert-set-default-style 'libnotify) (setq mu4e-alert-interesting-mail-query (concat "(maildir:/leon/Inbox AND date:today..now" " OR maildir:/work/Inbox AND date:today..now)" " AND flag:unread")) (alert-add-rule :category "mu4e-alert" :predicate (lambda (_) (string-match-p "^mu4e-" (symbol-name major-mode))) :continue t) (add-hook 'after-init-hook #'mu4e-alert-enable-notifications) ) (mu4e t)
5.4.39.3. Work: Signing Mails (S/MIME, smime)
Used to automatically sign messages sent from my work email address using S/MIME certificate.
(let ((work (getenv "SWARSEL_MAIL_WORK"))) (when (and work (not (string-empty-p work))) (setq swarsel-smime-cert-path "~/.Certificates/$SWARSEL_MAIL_WORK.pem") (setq swarsel-smime-cert-path (substitute-env-vars swarsel-smime-cert-path)) (setq mml-secure-prefer-scheme 'smime) (setq mml-secure-smime-sign-with-sender t) (add-hook 'mu4e-compose-mode-hook (lambda () (when (and (boundp 'user-mail-address) (stringp user-mail-address) (string-equal user-mail-address (getenv "SWARSEL_MAIL_WORK"))) (mml-secure-message-sign-smime)))) (setq smime-keys `((,(getenv "SWARSEL_MAIL_WORK") ,swarsel-smime-cert-path ("~/Certificates/harica-root.pem" "~/Certificates/harica-intermediate.pem" )))) ))
5.4.40. Calendar
This provides a beautiful calender to emacs.
(use-package org-caldav :init ;; set org-caldav-sync-initalization (setq swarsel-caldav-synced 0) ;; (setq org-caldav-url "https://cal.example.org/swarsel/calendar") ;; (setq org-caldav-calendars ;; '((:calendar-id "personal" ;; :inbox "~/Calendars/leon_cal.org"))) ;; (setq org-caldav-files '("~/Calendars/leon_cal.org")) ;; (setq org-caldav-backup-file "~/org-caldav/org-caldav-backup.org") ;; (setq org-caldav-save-directory "~/org-caldav/") :config (setq org-icalendar-alarm-time 1) ;; This makes sure to-do items as a category can show up on the calendar (setq org-icalendar-include-todo t) ;; This ensures all org "deadlines" show up, and show up as due dates (setq org-icalendar-use-deadline '(event-if-todo event-if-not-todo todo-due)) ;; This ensures "scheduled" org items show up, and show up as start times (setq org-icalendar-use-scheduled '(todo-start event-if-todo event-if-not-todo)) ) (use-package calfw :ensure nil :bind ("C-c A" . swarsel/open-calendar) :init (use-package calfw-cal :ensure nil) (use-package calfw-org :ensure nil) (use-package calfw-ical :ensure nil) :config (bind-key "g" 'cfw:refresh-calendar-buffer cfw:calendar-mode-map) (bind-key "q" 'evil-quit cfw:details-mode-map) ;; dont change the order of days in this one, as it will break weekend markings (setq calendar-day-name-array ["Sunday" "Monday" "Tuesday" "Wednesday" "Thursday" "Friday" "Saturday"]) ;; First day of the week (setq calendar-week-start-day 1) ; 0:Sunday, 1:Monday ;; (custom-set-faces ;; '(cfw:face-title ((t (:foreground "#f0dfaf" :weight bold :height 65)))) ;; ) ) (defun swarsel/open-calendar () (interactive) ;; (unless (eq swarsel-caldav-synced 1) (org-caldav-sync) (setq swarsel-caldav-synced 1)) ;; (select-frame (make-frame '((name . "calendar")))) ; makes a new frame and selects it ;; (set-face-attribute 'default (selected-frame) :height 65) ; reduces the font size of the new frame (cfw:open-calendar-buffer :contents-sources (list (cfw:org-create-source "Blue") ; orgmode source (cfw:ical-create-source (getenv "SWARSEL_CAL1NAME") (getenv "SWARSEL_CAL1") "Cyan") (cfw:ical-create-source (getenv "SWARSEL_CAL2NAME") (getenv "SWARSEL_CAL2") "Green") (cfw:ical-create-source (getenv "SWARSEL_CAL3NAME") (getenv "SWARSEL_CAL3") "Magenta") )))
5.4.41. Dashboard: emacs startup screen
This sets up the dashboard, which is really quite useless. But, it looks cool and makes me happy whenever I start an emacsclient without a file name as argument :)
(use-package dashboard :ensure t :config (dashboard-setup-startup-hook) ;; (setq initial-buffer-choice (lambda () (get-buffer-create "*dashboard*"))) (let ((files-domain (getenv "SWARSEL_FILES_DOMAIN")) (music-domain (getenv "SWARSEL_MUSIC_DOMAIN")) (insta-domain (getenv "SWARSEL_INSTA_DOMAIN")) (sport-domain (getenv "SWARSEL_SPORT_DOMAIN")) (swarsel-domain (getenv "SWARSEL_DOMAIN")) ) (setq dashboard-display-icons-p t ;; display icons on both GUI and terminal dashboard-icon-type 'nerd-icons ;; use `nerd-icons' package dashboard-set-file-icons t dashboard-items '((recents . 5) (projects . 5) (agenda . 5)) dashboard-set-footer nil dashboard-banner-logo-title "Welcome to SwarsEmacs!" dashboard-image-banner-max-height 300 dashboard-startup-banner "~/.dotfiles/files/wallpaper/swarsel.png" dashboard-projects-backend 'projectile dashboard-projects-switch-function 'magit-status dashboard-set-navigator t dashboard-startupify-list '(dashboard-insert-banner dashboard-insert-newline dashboard-insert-banner-title dashboard-insert-newline dashboard-insert-navigator dashboard-insert-newline dashboard-insert-init-info dashboard-insert-items ) dashboard-navigator-buttons `(;; line1 ((,"" "SwarselSocial" "Browse Swarsele" (lambda (&rest _) (browse-url ,insta-domain))) (,"" "SwarselSound" "Browse SwarselSound" (lambda (&rest _) (browse-url ,(concat "https://" music-domain))) ) (,"" "SwarselSwarsel" "Browse Swarsel" (lambda (&rest _) (browse-url "https://github.com/Swarsel")) ) (,"" "SwarselStash" "Browse SwarselStash" (lambda (&rest _) (browse-url ,(concat "https://" files-domain))) ) (,"" "SwarselSport" "Browse SwarselSports" (lambda (&rest _) (browse-url ,sport-domain))) ) ( (,"" ,swarsel-domain ,(concat "Browse " main-domain) (lambda (&rest _) (browse-url ,(concat "https://" swarsel-domain)))) ) )))) (add-to-list 'recentf-exclude "\\Archive\\.org\\'") (add-to-list 'recentf-exclude "\\Tasks\\.org\\'")
5.4.42. vterm
(use-package vterm :ensure t)
5.4.43. multiple cursors
(use-package multiple-cursors)
5.4.44. Less logging
(setq mu4e--log-max-size 1000) (setq message-log-max 30) (setq comint-buffer-maximum-size 50) (add-hook 'comint-output-filter-functions 'comint-truncate-buffer)
5.4.45. Popup frames
(defun prot-window-delete-popup-frame (&rest _) "Kill selected selected frame if it has parameter `prot-window-popup-frame'. Use this function via a hook." (when (frame-parameter nil 'prot-window-popup-frame) (delete-frame))) (defmacro prot-window-define-with-popup-frame (command) "Define interactive function which calls COMMAND in a new frame. Make the new frame have the `prot-window-popup-frame' parameter." `(defun ,(intern (format "prot-window-popup-%s" command)) () ,(format "Run `%s' in a popup frame with `prot-window-popup-frame' parameter. Also see `prot-window-delete-popup-frame'." command) (interactive) (let ((frame (make-frame '((prot-window-popup-frame . t) (title . "Emacs Popup Frame"))))) (unwind-protect (progn (select-frame frame) (switch-to-buffer " prot-window-hidden-buffer-for-popup-frame") (condition-case nil (call-interactively ',command) ((quit error user-error) (delete-frame frame)))) (dolist (fr (frame-list)) (when (string= (frame-parameter fr 'name) "Emacs Popup Anchor") (delete-frame fr))))))) (declare-function org-capture "org-capture" (&optional goto keys)) (defvar org-capture-after-finalize-hook) ;;;###autoload (autoload 'prot-window-popup-org-capture "prot-window") (prot-window-define-with-popup-frame org-capture) (add-hook 'org-capture-after-finalize-hook #'prot-window-delete-popup-frame) (declare-function mu4e "mu4e" (&optional goto keys)) ;;;###autoload (autoload 'prot-window-popup-mu4e "prot-window") (prot-window-define-with-popup-frame mu4e) (advice-add 'mu4e-quit :after #'prot-window-delete-popup-frame) (declare-function swarsel/open-calendar "swarsel/open-calendar" (&optional goto keys)) ;;;###autoload (autoload 'prot-window-popup-swarsel/open-calendar "prot-window") (prot-window-define-with-popup-frame swarsel/open-calendar) (advice-add 'bury-buffer :after #'prot-window-delete-popup-frame) (declare-function org-agenda "org-agenda" (&optional goto keys)) ;;;###autoload (autoload 'prot-window-popup-org-agenda "prot-window") (prot-window-define-with-popup-frame org-agenda)
6. Appendix A: Noweb-Ref blocks
This sections is no longer used really. An introduction can be found in Structure of this file under the historical note. The little noweb-ref blocks that I still use are found in Hosts and Services.
6.1. General steps when setting up a new machine
These general steps are needed when setting up a new machine and do not fit into another block well:
- setup yubikey (automatic yubikey enrollment is not yet supported by `disko`): - `systemd-cryptenroll --fido2-device=auto /dev/<device, e.g. 'nvme0n1p2'>`
6.2. Current patches and fixes
These are current deviations from the standard settings that I take while some things are broken upstream
- 20260101:
- nixos:
- firezone-gateway does not work in newer versions with firezone-server (currently working version v1.4.8)
- 20251102:
- flake:
- emacs-overlay:
- : version pinned because emacsclient is currently broken on latest
- niri-flake:
- currently not using the sugared version of screenshot-[,window], as it is currently broken
- home-manager:
- emacs-tramp:
- using stable version in extraPackages (broken in unstable)
- :ensure nil in emacs tramp settings to use package in extraPackages
- emacs-calfwL
- pinned to version not in nixpkgs (is in latest emacs-overlay, but that is broken)
- vesktop:
- running stable version (broken in unstable)
- batgrep:
- running stable version (broken in unstable)
- swayosd:
- pinned to version not in nixpkgs (fixes https://github.com/ErikReider/SwayOSD/issues/175)
6.3. HTML Export: Darkmode toggle
This adds a simple darkmode toggle to the HTML version of this document.
<script src='https://cdn.jsdelivr.net/npm/darkmode-js@1.5.7/lib/darkmode-js.min.js'></script> <script> function addDarkmodeWidget() { new Darkmode({label:'[☀︎]'}).showWidget(); } window.addEventListener('load', addDarkmodeWidget); </script>
6.4. HTML Export: Docs QoL
This adds the following functionalities to the HTML version of this document:
- Section pinning with persistent pins
- Copy section links to clipboard button
- Searching in table of contents
- Skip to next section button
(function() { function ready(fn) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', fn); } else { fn(); } } ready(function initPinned() { const STORAGE_KEY = 'org-pinned-items-v2'; let pinnedPanel = document.getElementById('pinned-panel'); if (!pinnedPanel) { pinnedPanel = document.createElement('aside'); pinnedPanel.id = 'pinned-panel'; pinnedPanel.innerHTML = ` <div id='pinned-panel-header'> <h2>Pinned</h2> <button id='toggle-pinned-btn' type='button' title='Hide pinned panel'>✕</button> </div> <button id='clear-all-pins-btn' type='button'>Clear All</button> <ul id='pinned-list'></ul> `; document.body.appendChild(pinnedPanel); } let showBtn = document.getElementById('show-pinned-btn'); if (!showBtn) { showBtn = document.createElement('button'); showBtn.id = 'show-pinned-btn'; showBtn.type = 'button'; showBtn.textContent = 'Pinned'; document.body.appendChild(showBtn); } const content = document.getElementById('content'); const pinnedList = document.getElementById('pinned-list'); const toggleBtn = document.getElementById('toggle-pinned-btn'); const clearAllBtn = document.getElementById('clear-all-pins-btn'); const toc = document.getElementById('table-of-contents'); const body = document.body; if (!content || !pinnedList || !toggleBtn || !clearAllBtn || !toc) return; function injectSearch() { // Check if already injected if (document.getElementById('toc-search-input')) return; const searchContainer = document.createElement('div'); searchContainer.id = 'toc-search-container'; const searchInput = document.createElement('input'); searchInput.id = 'toc-search-input'; searchInput.type = 'text'; searchInput.placeholder = 'Search TOC...'; searchInput.autocomplete = 'off'; const clearBtn = document.createElement('button'); clearBtn.id = 'toc-search-clear'; clearBtn.type = 'button'; clearBtn.textContent = 'Clear'; searchContainer.appendChild(searchInput); searchContainer.appendChild(clearBtn); toc.insertBefore(searchContainer, toc.firstChild); function filterTOC(term) { const allLinks = toc.querySelectorAll('a'); allLinks.forEach(link => { const li = link.closest('li'); if (!li) return; const text = link.textContent.toLowerCase(); const matches = text.includes(term); if (matches) { li.classList.remove('hidden-by-search'); let parent = li.parentElement; while (parent && parent !== toc) { if (parent.tagName === 'UL') { parent.style.display = ''; } if (parent.tagName === 'LI') { parent.classList.remove('hidden-by-search'); } parent = parent.parentElement; } } else { li.classList.add('hidden-by-search'); } }); if (term === '') { const allLis = toc.querySelectorAll('li'); allLis.forEach(li => li.classList.remove('hidden-by-search')); } } searchInput.addEventListener('input', function(e) { const term = e.target.value.toLowerCase(); filterTOC(term); }); clearBtn.addEventListener('click', function() { searchInput.value = ''; filterTOC(''); searchInput.focus(); }); } injectSearch(); function addHeadingLinks() { const headers = content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9'); headers.forEach(header => { const id = header.getAttribute('id'); if (!id) return; if (header.querySelector('.heading-link')) return; const link = document.createElement('a'); link.className = 'heading-link'; link.href = '#' + id; link.textContent = '#'; link.title = 'Copy link to this heading'; const pinBtn = header.querySelector('.toc-pin-btn'); if (pinBtn) { header.insertBefore(link, pinBtn); } else { header.appendChild(link); } link.addEventListener('click', function(e) { e.preventDefault(); const url = window.location.origin + window.location.pathname + '#' + id; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(url) .then(() => { const originalText = link.textContent; link.textContent = '✓'; setTimeout(() => { link.textContent = originalText; }, 1000); }) .catch(err => { console.warn('Failed to copy to clipboard', err); window.location.hash = id; }); } else { window.location.hash = id; } }); }); } addHeadingLinks(); function addNextHeadingButtons() { const headers = Array.from(content.querySelectorAll('h1, h2, h3, h4, h5, h6, h7, h8, h9')); headers.forEach((header, index) => { // Skip if button already exists if (header.querySelector('.heading-next')) return; // Find next heading const nextHeader = headers[index + 1]; if (!nextHeader) return; // No next heading const nextId = nextHeader.getAttribute('id'); if (!nextId) return; const nextBtn = document.createElement('button'); nextBtn.className = 'heading-next'; nextBtn.type = 'button'; nextBtn.textContent = '↓'; nextBtn.title = 'Jump to next heading'; // Insert after the heading link, before the pin button const headingLink = header.querySelector('.heading-link'); const pinBtn = header.querySelector('.toc-pin-btn'); if (pinBtn) { header.insertBefore(nextBtn, pinBtn); } else if (headingLink) { headingLink.after(nextBtn); } else { header.appendChild(nextBtn); } nextBtn.addEventListener('click', function(e) { e.preventDefault(); nextHeader.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Update URL hash history.pushState(null, null, '#' + nextId); }); }); } addNextHeadingButtons(); let mobileTocBtn = document.getElementById('mobile-toc-toggle'); if (!mobileTocBtn) { mobileTocBtn = document.createElement('button'); mobileTocBtn.id = 'mobile-toc-toggle'; mobileTocBtn.type = 'button'; mobileTocBtn.textContent = 'TOC'; document.body.appendChild(mobileTocBtn); } let mobilePinnedBtn = document.getElementById('mobile-pinned-toggle'); if (!mobilePinnedBtn) { mobilePinnedBtn = document.createElement('button'); mobilePinnedBtn.id = 'mobile-pinned-toggle'; mobilePinnedBtn.type = 'button'; mobilePinnedBtn.textContent = 'Pinned'; document.body.appendChild(mobilePinnedBtn); } function anyMobilePanelOpen() { return toc.classList.contains('mobile-visible') || pinnedPanel.classList.contains('mobile-visible'); } function updateBodyMobilePanelState() { if (anyMobilePanelOpen()) body.classList.add('mobile-panel-open'); else body.classList.remove('mobile-panel-open'); } document.addEventListener('click', function(e) { if (window.innerWidth > 1000) return; if (!anyMobilePanelOpen()) return; const clickedInsideToc = toc.contains(e.target); const clickedInsidePinned = pinnedPanel.contains(e.target); const clickedTocBtn = mobileTocBtn.contains(e.target); const clickedPinnedBtn = mobilePinnedBtn.contains(e.target); const clickedInteractive = e.target.tagName === 'A' || e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.closest('a') || e.target.closest('button'); if (!clickedInsideToc && !clickedInsidePinned && !clickedTocBtn && !clickedPinnedBtn && !clickedInteractive) { toc.classList.remove('mobile-visible'); pinnedPanel.classList.remove('mobile-visible'); updateBodyMobilePanelState(); } }); mobileTocBtn.addEventListener('click', function() { const isOpen = toc.classList.toggle('mobile-visible'); if (isOpen) { pinnedPanel.classList.remove('mobile-visible'); } updateBodyMobilePanelState(); }); mobilePinnedBtn.addEventListener('click', function() { const isOpen = pinnedPanel.classList.toggle('mobile-visible'); if (isOpen) { toc.classList.remove('mobile-visible'); } updateBodyMobilePanelState(); }); const pinnedItems = new Map(); let initiallyPinnedHrefs = new Set(); function loadFromStorage() { try { const raw = window.localStorage && localStorage.getItem(STORAGE_KEY); if (!raw) return; const arr = JSON.parse(raw); if (!Array.isArray(arr)) return; initiallyPinnedHrefs = new Set(arr); } catch (e) { console.warn('Pinned: failed to load from localStorage', e); } } function saveToStorage() { try { if (!window.localStorage) return; const arr = []; pinnedItems.forEach((entry, href) => { if (entry.li) arr.push(href); }); localStorage.setItem(STORAGE_KEY, JSON.stringify(arr)); } catch (e) { console.warn('Pinned: failed to save to localStorage', e); } } function sortPinnedList() { const items = Array.from(pinnedList.children) .map(li => { const link = li.querySelector('a'); return { li: li, text: link ? link.textContent.trim() .toLowerCase() : '' }; }); items.sort((a, b) => a.text.localeCompare(b.text)); items.forEach(item => pinnedList.appendChild(item.li)); } function hidePinnedPanel() { pinnedPanel.classList.add('hidden'); content.classList.add('pinned-hidden'); showBtn.classList.add('visible'); } function showPinnedPanel() { pinnedPanel.classList.remove('hidden'); content.classList.remove('pinned-hidden'); showBtn.classList.remove('visible'); } toggleBtn.addEventListener('click', hidePinnedPanel); showBtn.addEventListener('click', showPinnedPanel); clearAllBtn.addEventListener('click', function() { if (pinnedItems.size === 0) return; const confirmed = confirm('Are you sure you want to clear all pinned items?'); if (!confirmed) return; pinnedItems.forEach((entry, href) => { if (entry.li && entry.li.parentElement) { entry.li.parentElement.removeChild(entry.li); } entry.li = null; entry.btns.forEach(b => b.textContent = '[pin]'); }); saveToStorage(); }); function attachPinBehavior(pinBtn, href, text) { if (!href) return; if (!pinnedItems.has(href)) { pinnedItems.set(href, { li: null, btns: new Set(), text: text }); } const entry = pinnedItems.get(href); entry.btns.add(pinBtn); pinBtn.textContent = entry.li ? '[unpin]' : '[pin]'; pinBtn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); const current = pinnedItems.get(href); if (!current) return; if (current.li) { if (current.li.parentElement) { current.li.parentElement.removeChild(current.li); } current.li = null; current.btns.forEach(b => b.textContent = '[pin]'); saveToStorage(); } else { const li = document.createElement('li'); const a = document.createElement('a'); a.href = href; a.textContent = current.text; const removeBtn = document.createElement('button'); removeBtn.className = 'pin-remove'; removeBtn.type = 'button'; removeBtn.textContent = '✕'; removeBtn.addEventListener('click', () => { const cur = pinnedItems.get(href); if (!cur) return; if (cur.li && cur.li.parentElement) { cur.li.parentElement.removeChild(cur.li); } cur.li = null; cur.btns.forEach(b => b.textContent = '[pin]'); saveToStorage(); }); li.appendChild(a); li.appendChild(removeBtn); pinnedList.appendChild(li); current.li = li; current.btns.forEach(b => b.textContent = '[unpin]'); sortPinnedList(); saveToStorage(); } }); } loadFromStorage(); const tocLinks = document.querySelectorAll('#text-table-of-contents a'); tocLinks.forEach(link => { if (link.parentElement && link.parentElement.classList.contains('toc-entry')) { return; } const li = link.closest('li'); if (!li) return; const wrapper = document.createElement('span'); wrapper.className = 'toc-entry'; li.insertBefore(wrapper, link); wrapper.appendChild(link); const pinBtn = document.createElement('button'); pinBtn.className = 'toc-pin-btn'; pinBtn.type = 'button'; pinBtn.textContent = '[pin]'; wrapper.appendChild(pinBtn); const href = link.getAttribute('href'); const text = link.textContent.trim(); attachPinBehavior(pinBtn, href, text); }); const headers = content.querySelectorAll('h2, h3, h4, h5, h6, h7, h8, h9'); headers.forEach(header => { const id = header.getAttribute('id'); if (!id) return; if (header.querySelector('.toc-pin-btn')) return; const href = '#' + id; const text = header.textContent.trim(); const pinBtn = document.createElement('button'); pinBtn.className = 'toc-pin-btn'; pinBtn.type = 'button'; pinBtn.textContent = '[pin]'; pinBtn.style.marginLeft = '0.8rem'; pinBtn.style.fontSize = '0.75em'; header.appendChild(pinBtn); attachPinBehavior(pinBtn, href, text); }); initiallyPinnedHrefs.forEach(href => { const entry = pinnedItems.get(href); if (!entry) return; const li = document.createElement('li'); const a = document.createElement('a'); a.href = href; a.textContent = entry.text; const removeBtn = document.createElement('button'); removeBtn.className = 'pin-remove'; removeBtn.type = 'button'; removeBtn.textContent = '✕'; removeBtn.addEventListener('click', () => { const cur = pinnedItems.get(href); if (!cur) return; if (cur.li && cur.li.parentElement) { cur.li.parentElement.removeChild(cur.li); } cur.li = null; cur.btns.forEach(b => b.textContent = '[pin]'); saveToStorage(); }); li.appendChild(a); li.appendChild(removeBtn); pinnedList.appendChild(li); entry.li = li; entry.btns.forEach(b => b.textContent = '[unpin]'); }); sortPinnedList(); saveToStorage(); }); })();
7. Appendix B: Supplementary Files
This section now holds some of the configuration files that cannot be defined directly within NixOS configuration. These files are usually symlinked using home.file.
7.1. Server Emacs config
On my server, I use a reduced, self-contained emacs configuration that only serves as an elfeed sync server. This is currently unused, however, I am keeping this in here for now as a reference. The big problem here was the bidirectional syncing using bjm/elfeed-updater. As I am using this both on a laptop client (using elfeed) as well as on a mobile phone (using elfeed-cljsrn over elfeed-web), I set up a Syncthing service to take care of the feeds as well as the db state. However, I could only either achieve changes propagating properly from the laptop to the server or from the phone to the server. Both would not work. This current state represents the state where from-laptop changes would propagate. To allow from-phone changes, change (elfeed-db-load) in bjm/elfeed-updater to (elfeed-db-save).
(require 'package) (package-initialize nil) (setq package-enable-at-startup nil) (add-to-list 'package-archives '("org" . "http://orgmode.org/elpa/") t) (add-to-list 'package-archives '("melpa" . "https://melpa.org/packages/") t) (package-initialize) (let ((default-directory "~/.emacs.d/elpa/")) (normal-top-level-add-subdirs-to-load-path)) (unless (package-installed-p 'use-package) (package-refresh-contents) (package-install 'use-package)) (require 'use-package) (use-package elfeed :ensure t :bind (:map elfeed-search-mode-map ("q" . bjm/elfeed-save-db-and-bury))) (require 'elfeed) (use-package elfeed-org :ensure t :config (elfeed-org) (setq rmh-elfeed-org-files (list "/var/lib/syncthing/.elfeed/elfeed.org"))) (use-package elfeed-goodies :ensure t) (elfeed-goodies/setup) (use-package elfeed-web :ensure t) (global-set-key (kbd "C-x w") 'bjm/elfeed-load-db-and-open) (define-key elfeed-show-mode-map (kbd "j") 'elfeed-goodies/split-show-next) (define-key elfeed-show-mode-map (kbd "k") 'elfeed-goodies/split-show-prev) (define-key elfeed-search-mode-map (kbd "j") 'next-line) (define-key elfeed-search-mode-map (kbd "k") 'previous-line) (define-key elfeed-show-mode-map (kbd "S-SPC") 'scroll-down-command) (defun bjm/elfeed-save-db-and-bury () "Wrapper to save the elfeed db to disk before burying buffer" (interactive) (elfeed-db-save) (quit-window)) (defun bjm/elfeed-load-db-and-open () "Wrapper to load the elfeed db from disk before opening" (interactive) (elfeed-db-load) (elfeed) (elfeed-search-update--force) (elfeed-update)) (defun bjm/elfeed-updater () "Wrapper to load the elfeed db from disk before opening" (interactive) (elfeed-db-load)) (run-with-timer 0 (* 1 60) 'bjm/elfeed-updater) (setq httpd-port 9812) (setq httpd-host "0.0.0.0") (setq httpd-root "/root/.emacs.d/elpa/elfeed-web-20240729.1741/") (setq elfeed-db-directory "/var/lib/syncthing/.elfeed/db/") (httpd-start) (elfeed-web-start)
7.2. tridactylrc
This is the configuration file for tridactyl, which provides keyboard-driven navigation in firefox. Pay attention to the warnings in this file; depending on your browsing behaviour, you might expose yourself to some vulnerabilities by copying this configuration.
The command command can be supplied with a -p flag that will take a single argmuent which is exposed as JS_ARG. I use this in a function that switches to an open tab if it exists and otherwise creates it.
sanitise tridactyllocal tridactylsync
colourscheme swarsel
" General Settings
set update.lastchecktime 1720629386560
set update.lastnaggedversion 1.24.1
set update.nag true
set update.nagwait 7
set update.checkintervalsecs 86400
set configversion 2.0
set searchurls.no https://search.nixos.org/options?query=
set searchurls.np https://search.nixos.org/packages?query=
set searchurls.hm https://home-manager-options.extranix.com/?query=
set searchurls.@c https://vbc.atlassian.net/wiki/search?text=
set searchurls.@j https://vbc.atlassian.net/issues/?jql=textfields%20~%20%22%s*%22&wildcardFlag=true
set completions.Tab.statusstylepretty true
set hintfiltermode vimperator-reflow
set hintnames numeric
unbind --mode=hint <Space>
" Binds
bind <C-m> buffer #
bind gd tabdetach
bind gD composite tabduplicate; tabdetach
bind d composite tabprev; tabclose #
bind D tabclose
bind c hint -J
bindurl ^http(s)?://www\.google\.com c hint -Jc [class="LC20lb MBeuO DKV0Md"],[class="YmvwI"],[class="YyVfkd"],[class="fl"]
bindurl ^http(s)?://news\.ycombinator\.com c hint -Jc [class="titleline"],[class="age"]
bindurl ^http(s)?://lobste\.rs c hint -Jc [class="u-url"],[class="comments_label"]
bindurl ^http(s)?://reddit\.com c hint -Jc [class="title may-blank loggedin"],[class="bylink comments may-blank"]
bindurl ^http(s)?://github\.com c hint -Jc [class="Link--primary"],[class="AppHeader-button Button--secondary Button--medium Button p-0 color-fg-muted"],[class="UnderlineNav-item no-wrap js-responsive-underlinenav-item js-selected-navigation-item"],[class="prc-ActionList-ItemLabel-TmBhn"],[class="PRIVATE_TreeView-item-content-text prc-TreeView-TreeViewItemContentText-smZM-"]
bindurl ^http(s)?://vbc\.atlassian\.net\/wiki c hint -Jc [class="_1reo15vq _18m915vq _1bto1l2s _kqswh2mm _o5721q9c _syaz1fxt"],[class="_11c81ixg _1reo15vq _18m915vq _18s81b66 _kqswh2mm _k48p1wq8 _o5721q9c _1bto1l2s _u5f31b66"],[class="_1r04ze3t _kqswstnw"],[class="css-a61etj"],[class="jira-macro-table-underline-pdfexport"]
bindurl ^http(s)?://www\.google\.com gi composite focusinput -l ; text.end_of_line
" Work
command tab_or_tabopen jsb -p (async () => {let tabs = await browser.tabs.query({}); let tab = tabs.find(t => t.url.includes(JS_ARG)); if (tab) {browser.tabs.update(tab.id, { active: true });} else {tri.excmds.tabopen(JS_ARG);}})()
command tab_or_tabopen_local jsb -p (async () => {const currentWindow = await browser.windows.getCurrent(); const tabs = await browser.tabs.query({ windowId: currentWindow.id }); const tab = tabs.find(t => t.url.includes(JS_ARG)); if (tab) {browser.tabs.update(tab.id, { active: true });} else {tri.excmds.tabopen(JS_ARG);}})()
bind gwa tab_or_tabopen_local apic-impimba-1.m.imp.ac.at
bind gwA tab_or_tabopen_local artifactory.imp.ac.at
bind gwb tab_or_tabopen_local bitbucket.vbc.ac.at
bind gwc tab_or_tabopen_local vbc.atlassian.net/wiki
bind gwd tab_or_tabopen_local datadomain-impimba-2.imp.ac.at
bind gwe tab_or_tabopen_local exivity.vbc.ac.at
bind gwg tab_or_tabopen_local github.com
bind gwG tab_or_tabopen_local goc.egi.eu
bind gwh tab_or_tabopen_local jupyterhub.vbc.ac.at
bind gwj tab_or_tabopen_local jenkins.vbc.ac.at
bind gwJ tab_or_tabopen_local test-jenkins.vbc.ac.at
bind gwl tab_or_tabopen_local lucid.app
bind gwm tab_or_tabopen_local monitoring.vbc.ac.at/grafana
bind gwM tab_or_tabopen_local monitoring.vbc.ac.at/prometheus
bind gwn tab_or_tabopen_local netbox.vbc.ac.at
bind gwN tab_or_tabopen_local nap.imp.ac.at
bind gwo tab_or_tabopen_local outlook.office.com
bind gws tab_or_tabopen_local satellite.vbc.ac.at
bind gwt tab_or_tabopen_local tower.vbc.ac.at
bind gwv tab_or_tabopen_local vc-impimba-1.m.imp.ac.at/ui
bind gwx tab_or_tabopen_local xclarity.vbc.ac.at
unbind --mode=normal gh
bind ghp tab_or_tabopen_local https://github.com/pulls
bind ghi tab_or_tabopen_local https://github.com/issues/assigned?q=is%3Aissue%20state%3Aopen%20archived%3Afalse%20(assignee%3A%40me%20OR%20author%3A%40me)%20sort%3Aupdated-desc
bind ghv tab_or_tabopen_local github.com/orgs/vbc-it/repositories
bind ghc tab_or_tabopen_local github.com/orgs/CLIP-HPC/repositories
bind ghd tab_or_tabopen_local github.com/Swarsel/.dotfiles
bind ghni tab_or_tabopen_local github.com/NixOS/nixpkgs/issues
bind ghnp tab_or_tabopen_local github.com/NixOS/nixpkgs/pulls
unbind --mode=normal gp
bind gprn tab_or_tabopen_local www.reddit.com/r/NixOS/
bind gpd tab_or_tabopen_local discourse.nixos.org/
bind gpp tab_or_tabopen_local parkour.wien/categories
" Search in page
set findcase smart
bind / fillcmdline find
bind ? fillcmdline find -?
bind n findnext 1
bind N findnext -1
bind j scrollline 4
bind k scrollline -4
" WARNING: This file defines and runs a command called fixamo_quiet. If you
" also have a malicious addon that operates on `<all_urls>` installed this
" will allow it to steal your firefox account credentials!
"
" With those credentials, an attacker can read anything in your sync account,
" publish addons to the AMO, etc, etc.
"
" Without this command a malicious addon can steal credentials from any site
" that you visit that is not in the restrictedDomains list.
"
" You should comment out the fixamo lines unless you are entirely sure that
" they are what you want.
command fixamo_quiet jsb tri.excmds.setpref("privacy.resistFingerprinting.block_mozAddonManager", "true").then(tri.excmds.setpref("extensions.webextensions.restrictedDomains", '""'))
command fixamo js tri.excmds.setpref("privacy.resistFingerprinting.block_mozAddonManager", "true").then(tri.excmds.setpref("extensions.webextensions.restrictedDomains", '""').then(tri.excmds.fillcmdline_tmp(3000, "Permissions added to user.js. Please restart Firefox to make them take affect.")))
fixamo_quiet
set allowautofocus false
" The following modification allows Tridactyl to function on more pages, e.g. raw GitHub pages.
" You may not wish to run this. Mozilla strongly feels that you shouldn't.
" Read https://wiki.mozilla.org/Security/CSP#Goals for more information.
"
" Equivalent to `set csp clobber` before it was removed.
" This weakens your defences against cross-site-scripting attacks
" and other types of code-injection by reducing the strictness
" of Content Security Policy on all sites in a couple of ways.
"
" We remove the sandbox directive
" https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
" which allows our iframe (and anyone else's) to run on any website.
"
" We weaken the style-src directive
" https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src
" to allow us to theme our elements.
" This exposes you to 'cross site styling' attacks
jsb browser.webRequest.onHeadersReceived.addListener(tri.request.clobberCSP,{urls:["<all_urls>"],types:["main_frame"]},["blocking","responseHeaders"])
" default is 300ms
set hintdelay 500
" Some pages like github break on the tridactyl quick search. have this as a fallback
unbind <C-f>
" Do not let websites steal sitefocus
set allowautofocus false
" whitelist sites
" :seturl [URL regex for sites with text editors you use] allowautofocus true
" stronger blacklist for specific sites
seturl vbc.atlassian.net preventautofocusjackhammer true
" Subconfig Settings
seturl www.google.com followpagepatterns.next Next
seturl www.google.com followpagepatterns.prev Previous
" Autocmds
autocmd DocStart undefined mode ignore
autocmd DocStart pokerogue.net mode ignore
autocmd DocStart typelit.io mode ignore
autocmd DocStart vc-impimba-1.m.imp.ac.at/ui/webconsole mode ignore
" For syntax highlighting see https://github.com/tridactyl/vim-tridactyl
" vim: set filetype=tridactyl
7.3. tridactyl theme
:root {
--base00: #1D252C;
--base01: #171D23;
--base02: #5EC4FF;
--base03: #566C7D;
--base04: #5EC4FF;
--base05: #A0B3C5;
--base06: #C06ECE;
--base07: #A0B3C5;
--base08: #D95468;
--base09: #FFA880;
--base0A: #5EC4FF;
--base0B: #8BD49C;
--base0C: #008B94;
--base0D: #5EC4FF;
--base0E: #C06ECE;
--base0F: #5EC4FF;
--tridactyl-def-fg: var(--base02);
--tridactyl-cmdl-bg: var(--base00);
--tridactyl-cmdl-fg: var(--base0C);
--tridactyl-font-family: "San Francisco", sans-serif;
--tridactyl-cmdl-font-size: 1.5rem;
--tridactyl-cmdl-line-height: 1.5;
--tridactyl-cmplt-option-height: 1.4em;
--tridactyl-cmplt-font-size: var(--tridactyl-small-font-size);
--tridactyl-cmplt-border-top: unset;
--tridactyl-status-font-size: 9px;
--tridactyl-status-font-family: "Fira Code", monospace;
--tridactyl-status-border: 1px var(--tridactyl-fg) solid;
--tridactyl-header-font-size: var(--tridactyl-small-font-size);
--tridactyl-header-font-weight: 200;
--tridactyl-header-border-bottom: unset;
--tridactyl-hintspan-font-size: var(--tridactyl-font-size);
--tridactyl-hint-active-fg: none;
}
:root #command-line-holder {
order: 1;
border: 2px solid var(--tridactyl-cmdl-fg);
color: var(--tridactyl-cmdl-bg);
}
:root #tridactyl-input {
width: 90%;
padding: 1rem;
color: var(--tridactyl-def-fg);
}
:root #completions table {
font-size: 0.8rem;
font-weight: 200;
border-spacing: 0;
table-layout: fixed;
padding: 1rem;
padding-top: 0;
}
:root #completions > div {
max-height: calc(20 * var(--tridactyl-cmplt-option-height));
min-height: calc(10 * var(--tridactyl-cmplt-option-height));
}
/* COMPLETIONS */
:root #completions {
font-weight: 200;
order: 2;
color: var(--tridactyl-def-fg);
background: var(--tridactyl-cmdl-bg);
}
/* Olie doesn't know how CSS inheritance works */
:root #completions .HistoryCompletionSource {
max-height: unset;
min-height: unset;
}
:root #completions .HistoryCompletionSource table {
width: 100%;
font-size: 9pt;
border-spacing: 0;
table-layout: fixed;
}
/* redundancy 2: redundancy 2: more redundancy */
:root #completions .BmarkCompletionSource {
max-height: unset;
min-height: unset;
}
:root #completions table tr { white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:root #completions .url {
background: var(--tridactyl-cmdl-bg);
}
:root #completions .focused {
background: #44391F;
}
:root #completions .focused .url {
background: #44391F;
}
:root #completions .BufferCompletionSource table {
width: unset;
font-size: unset;
border-spacing: unset;
table-layout: unset;
}
:root #completions table tr {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:root #completions .sectionHeader {
background: unset;
padding: 1rem !important;
padding-left: unset;
padding-bottom: 0.2rem;
}
:root #cmdline_iframe {
position: fixed !important;
bottom: unset;
top: 25% !important;
left: 10% !important;
z-index: 2147483647 !important;
width: 80% !important;
box-shadow: rgba(0, 0, 0, 0.5) 0px 0px 15px !important;
}
:root .TridactylStatusIndicator {
position: fixed !important;
bottom: 0 !important;
font-weight: 200 !important;
padding: 0.8ex !important;
}
/* #Shydactyl-normal { */
/* border-color: green !important; */
/* } */
/* #Shydactyl-insert { */
/* border-color: yellow !important; */
/* } */
7.4. Waybar style.css
This is the stylesheet used by waybar.
@define-color foreground #fdf6e3; @define-color background #1a1a1a; @define-color background-alt #292b2e; @define-color foreground-warning #268bd2; @define-color background-warning @background; @define-color foreground-error red; @define-color background-error @background; @define-color foreground-critical gold; @define-color background-critical blue; * { border: none; border-radius: 0; font-family: "FiraCode Nerd Font Propo", "Font Awesome 5 Free"; font-size: 14px; min-height: 0; margin: -1px 0px; } window#waybar { background: transparent; color: @foreground; transition-duration: .5s; } window#waybar.hidden { opacity: 0.2; } #mpris { padding: 0 10px; background-color: transparent; color: #1DB954; font-family: Monospace; font-size: 12px; } #custom-right-arrow-dark, #custom-left-arrow-dark { color: @background; background: @background-alt; font-size: 24px; } #window { font-size: 12px; padding: 0 20px; } #mode { background: @background-critical; color: @foreground-critical; padding: 0 3px; } #privacy, #custom-configwarn { color: black; padding: 0 3px; animation-name: configblink; animation-duration: 0.5s; animation-timing-function: linear; animation-iteration-count: infinite; animation-direction: alternate; } #custom-nix-updates { color: white; padding: 0 3px; } #custom-outer-right-arrow-dark, #custom-outer-left-arrow-dark { color: @background; font-size: 24px; } #custom-outer-left-arrow-dark, #custom-left-arrow-dark, #custom-left-arrow-light { margin: 0 -1px; } #custom-right-arrow-light, #custom-left-arrow-light { color: @background-alt; background: @background; font-size: 24px; } #workspaces, #clock.1, #clock.2, #clock.3, #pulseaudio, #memory, #cpu, #temperature, #custom-scratchpad-indicator, #power-profiles-daemon, #idle_inhibitor, #backlight-slider, #mpris, #tray { background: @background; } #network, #custom-vpn, #clock.2, #battery, #cpu, #custom-pseudobat, #disk { background: @background-alt; } #workspaces button { padding: 0 2px; color: #fdf6e3; } #workspaces button.focused { color: @foreground-warning; } #workspaces button:hover { background: @foreground; color: @background; border: @foreground; padding: 0 2px; box-shadow: inherit; text-shadow: inherit; } #workspaces button.urgent { color: @background-critical; background: @foreground-critical; } #custom-vpn, #network { color: #cc99c9; } #temperature, #power-profiles-daemon { color: #9ec1cf; } #disk { /*color: #b58900;*/ color: #9ee09e; } #custom-scratchpad-indicator { color: #ffffff; } #disk.warning { color: @foreground-error; background-color: @background-error; } #disk.critical, #temperature.critical { color: @foreground-critical; background-color: @background-critical; animation-name: blink; animation-duration: 0.5s; animation-timing-function: linear; animation-iteration-count: infinite; animation-direction: alternate; } #pulseaudio.muted { color: @foreground-error; } #memory { /*color: #2aa198;*/ color: #fdfd97; } #cpu { /*color: #6c71c4;*/ color: #feb144; } #pulseaudio { /*color: #268bd2;*/ color: #ff6663; } #battery, #custom-pseudobat { color: cyan; } #battery.discharging { color: #859900; } @keyframes blink { to { color: @foreground-error; background-color: @background-error; } } @keyframes configblink { to { color: @foreground-error; background-color: transparent; } } #battery.critical:not(.charging) { color: @foreground-critical; background-color: @background-critical; animation-name: blink; animation-duration: 0.5s; animation-timing-function: linear; animation-iteration-count: infinite; animation-direction: alternate; } #backlight-slider slider { min-height: 0px; min-width: 0px; opacity: 0; background-image: none; border: none; box-shadow: none; } #backlight-slider trough { min-height: 5px; min-width: 80px; border-radius: 5px; background-color: black; } #backlight-slider highlight { min-width: 0px; border-radius: 5px; background-color: grey; } #clock.1, #clock.2, #clock.3 { font-family: Monospace; } #clock, #pulseaudio, #memory, #cpu, #tray, #temperature, #power-profiles-daemon, #network, #custom-vpn, #mpris, #battery, #custom-scratchpad-indicator, #custom-pseudobat, #disk { padding: 0 3px; }
7.5. Doc Page style.css
This is the stylesheet used by the HTML version of this document. Mostly this holds logic for hiding/showing the pin icons and resizing the page dynamically.
html, body { margin: 0; padding: 0; background-color: #1d252c; color: #b7c5d3; font-family: "Inter", "Fira Sans", system-ui, sans-serif; line-height: 1.6; overflow-x: hidden; } body { display: flex; } #table-of-contents { position: fixed; top: 0; left: 0; width: 280px; height: 100vh; overflow-y: auto; padding: 1.2rem 1rem; background-color: #232b32; border-right: 1px solid #2f3b45; font-size: 0.9rem; display: flex; flex-direction: column; } #table-of-contents h2 { display: none; } #toc-search-container { margin-bottom: 1rem; position: sticky; top: 0; background-color: #232b32; z-index: 10; padding-bottom: 0.5rem; display: flex; align-items: center; gap: 0.5rem; } #toc-search-input { flex: 1; padding: 0.5rem; background-color: #1d252c; border: 1px solid #2f3b45; color: #b7c5d3; border-radius: 4px; font-size: 0.9rem; box-sizing: border-box; } #toc-search-input:focus { outline: none; border-color: #5ec4ff; } #toc-search-clear { padding: 0.5rem 0.7rem; background-color: #2f3b45; border: 1px solid #3a4a56; color: #b7c5d3; cursor: pointer; font-size: 0.85rem; border-radius: 4px; transition: background-color 0.2s, color 0.2s; white-space: nowrap; } #toc-search-clear:hover { background-color: #3a4a56; color: #ff6b6b; } .hidden-by-search { display: none !important; } #text-table-of-contents { flex: 1; } #text-table-of-contents ul { list-style: none; padding-left: 0; } #text-table-of-contents li { margin: 0.2rem 0; position: relative; } .toc-entry { display: inline-flex; align-items: center; } #text-table-of-contents a { color: #b7c5d3; text-decoration: none; } #text-table-of-contents a:hover { color: #5ec4ff; } #text-table-of-contents ul ul { padding-left: 1rem; border-left: 1px solid #2f3b45; } #content { margin-left: 300px; margin-right: 320px; padding: 2rem 3rem; max-width: 1200px; width: calc(100vw - 620px); box-sizing: border-box; transition: margin 0.3s ease, padding 0.3s ease, width 0.3s ease, max-width 0.3s ease; } #content.pinned-hidden { margin-right: 0; width: calc(100vw - 300px); } h1, h2, h3, h4, h5, h6, h7, h8, h9 { color: #70e1e8; font-weight: 500; margin-top: 2.2rem; } h1 { font-size: 2rem; } h2 { font-size: 1.6rem; } h3 { font-size: 1.3rem; } h4 { font-size: 1.2rem; } h5 { font-size: 1.1rem; } h6 { font-size: 1.0rem; } h7 { font-size: 0.95rem; } h8 { font-size: 0.925rem; } h9 { font-size: 0.9rem; } a { color: #5ec4ff; } a:hover { text-decoration: underline; } pre, code { font-family: "Fira Code", monospace; background-color: #232b32; color: #b7c5d3; } pre { padding: 1rem; overflow-x: auto; border: 1px solid #2f3b45; border-radius: 4px; max-width: 100%; box-sizing: border-box; } code { padding: 0.15rem 0.3rem; border-radius: 3px; } table { border-collapse: collapse; max-width: 100%; } th, td { border: 1px solid #2f3b45; padding: 0.5rem 0.8rem; } th { background-color: #232b32; color: #70e1e8; } blockquote { border-left: 3px solid #5ec4ff; margin-left: 0; padding-left: 1rem; color: #718ca1; } #pinned-panel { position: fixed; top: 0; right: 0; width: 280px; height: 100vh; overflow-y: auto; padding: 1.2rem 1rem; padding-bottom: 5rem; background-color: #232b32; border-left: 1px solid #2f3b45; font-size: 0.9rem; box-sizing: border-box; transition: transform 0.3s ease; } #pinned-panel.hidden { transform: translateX(100%); } #pinned-panel-header { display: flex; justify-content: space-between; align-items: center; } #pinned-panel h2 { margin: 0; font-size: 1rem; color: #70e1e8; font-weight: 500; } #toggle-pinned-btn { background: none; border: none; color: #718ca1; cursor: pointer; font-size: 1.2rem; padding: 0; line-height: 1; } #toggle-pinned-btn:hover { color: #5ec4ff; } #pinned-list { list-style: none; padding-left: 0; } #pinned-list li { margin: 0.5rem 0; display: flex; justify-content: space-between; align-items: center; } #pinned-list a { color: #b7c5d3; text-decoration: none; flex: 1; } #pinned-list a:hover { color: #5ec4ff; } .pin-remove { background: none; border: none; color: #718ca1; cursor: pointer; font-size: 0.9rem; padding: 0 0.3rem; } .pin-remove:hover { color: #ff6b6b; } .toc-pin-btn { opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; cursor: pointer; margin-left: 0.4rem; font-size: 0.85rem; color: #718ca1; background: none; border: none; padding: 0; } .toc-pin-btn:hover { color: #5ec4ff; } #text-table-of-contents .toc-entry:hover .toc-pin-btn { opacity: 1; visibility: visible; } #show-pinned-btn { position: fixed; top: 4.5rem; right: 1rem; background-color: #232b32; border: 1px solid #2f3b45; color: #b7c5d3; cursor: pointer; padding: 0.5rem 0.8rem; font-size: 0.9rem; border-radius: 4px; display: none; z-index: 1000; } #show-pinned-btn:hover { background-color: #2f3b45; color: #5ec4ff; } #show-pinned-btn.visible { display: block; } #mobile-toc-toggle, #mobile-pinned-toggle { position: fixed; top: 1rem; z-index: 1100; padding: 0.5rem 0.8rem; border-radius: 4px; border: 1px solid #2f3b45; background-color: #232b32; color: #b7c5d3; font-size: 0.8rem; cursor: pointer; display: none; } #mobile-toc-toggle { left: 1rem; } #mobile-pinned-toggle { right: 1rem; } #mobile-toc-toggle:hover, #mobile-pinned-toggle:hover { background-color: #2f3b45; color: #5ec4ff; } .heading-next { opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; margin-left: 0.5rem; color: #718ca1; text-decoration: none; font-size: 0.8em; vertical-align: middle; cursor: pointer; background: none; border: none; padding: 0; } .heading-next:hover { color: #5ec4ff; text-decoration: none; } h1:hover .heading-next, h2:hover .heading-next, h3:hover .heading-next, h4:hover .heading-next, h5:hover .heading-next, h6:hover .heading-next, h7:hover .heading-next, h8:hover .heading-next, h9:hover .heading-next { opacity: 1; visibility: visible; } @media (max-width: 1600px) { #content { max-width: 100%; } } @media (max-width: 1300px) { #mobile-pinned-toggle { display: block; } #pinned-panel { display: block !important; position: fixed; top: 0; right: 0; width: 260px; max-width: 80vw; height: 100vh; overflow-y: auto; transform: translateX(100%); transition: transform 0.25s ease; z-index: 1000; } #pinned-panel.mobile-visible { transform: translateX(0); } #show-pinned-btn { display: none !important; } #content, #content.pinned-hidden { margin-right: 0; width: calc(100vw - 300px); max-width: 100%; padding: 1.8rem 2.2rem; } } @media (max-width: 1000px) { #mobile-toc-toggle, #mobile-pinned-toggle { display: block; } #table-of-contents { display: block; position: fixed; top: 0; left: 0; width: 260px; max-width: 80vw; height: 100vh; overflow-y: auto; transform: translateX(-100%); transition: transform 0.25s ease; z-index: 1000; } #table-of-contents.mobile-visible { transform: translateX(0); } #pinned-panel { display: block !important; position: fixed; top: 0; right: 0; width: 260px; max-width: 80vw; height: 100vh; overflow-y: auto; transform: translateX(100%); transition: transform 0.25s ease; z-index: 1000; } #pinned-panel.mobile-visible { transform: translateX(0); } #content, #content.pinned-hidden { margin-left: 0; margin-right: 0; width: 100vw; max-width: 100%; padding: 1.5rem 1.25rem; } #toc-search-container { top: 3rem; margin-top: 2.5rem; } } .heading-link { opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; margin-left: 0.5rem; color: #718ca1; text-decoration: none; font-size: 0.8em; vertical-align: middle; } .heading-link:hover { color: #5ec4ff; text-decoration: none; } h1:hover .heading-link, h2:hover .heading-link, h3:hover .heading-link, h4:hover .heading-link, h5:hover .heading-link, h6:hover .heading-link, h7:hover .heading-link, h8:hover .heading-link, h9:hover .heading-link { opacity: 1; visibility: visible; } @media (max-width: 700px) { #content { padding: 1.2rem 1rem; } } .darkmode-layer, .darkmode-toggle { z-index: 500; } html, body { overflow-x: hidden; } h1 .toc-pin-btn, h2 .toc-pin-btn, h3 .toc-pin-btn, h4 .toc-pin-btn, h5 .toc-pin-btn, h6 .toc-pin-btn, h7 .toc-pin-btn, h8 .toc-pin-btn, h9 .toc-pin-btn { opacity: 0; visibility: hidden; transition: opacity 0.2s, visibility 0.2s; } h1:hover .toc-pin-btn, h2:hover .toc-pin-btn, h3:hover .toc-pin-btn, h4:hover .toc-pin-btn, h5:hover .toc-pin-btn, h6:hover .toc-pin-btn, h7:hover .toc-pin-btn, h8:hover .toc-pin-btn, h9:hover .toc-pin-btn { opacity: 1; visibility: visible; } #clear-all-pins-btn { width: 100%; margin: 0.5rem 0; padding: 0.4rem 0.6rem; background-color: #2f3b45; border: 1px solid #3a4a56; color: #b7c5d3; cursor: pointer; font-size: 0.85rem; border-radius: 4px; transition: background-color 0.2s, color 0.2s; } #clear-all-pins-btn:hover { background-color: #3a4a56; color: #ff6b6b; } @media (max-width: 1000px) { body.mobile-panel-open { overflow: hidden; } }
7.6. justfile
This file defines a few workflows that I often need to run when working on my configuration. This works similar to make, but is geared towards general tasks and as such requires no extra handling (as long as there are no dependencies involved) or .PHONY recipes.
(In the org-src block I still call it a Makefile in order to get syntax highlighting)
default: @just --list check: nix flake check --keep-going check-trace: nix flake check --show-trace update: nix flake update iso CONFIG="live-iso": rm -rf result nix build --print-out-paths .#live-iso iso-install DRIVE: iso sudo dd if=$(eza --sort changed result/iso/*.iso | tail -n1) of={{DRIVE}} bs=4M status=progress oflag=sync dd DRIVE ISO: sudo dd if=$(eza --sort changed {{ISO}} | tail -n1) of={{DRIVE}} bs=4M status=progress oflag=sync sync USER HOST: rsync -rltv --filter=':- .gitignore' -e "ssh -l {{USER}}" . {{USER}}@{{HOST}}:.dotfiles/ secrets USER HOST: rsync -rltv -e "ssh -l {{USER}}" /var/tmp/nix-import-encrypted/1000/ {{USER}}@{{HOST}}:/var/tmp/nix-import-encrypted/0 bootstrap DEST CONFIG ARCH="x86_64-linux" NODISKODEPS="": nix develop .#deploy --command zsh -c "swarsel-bootstrap {{NODISKODEPS}} -n {{CONFIG}} -d {{DEST}} -a {{ARCH}}"
7.7. aspell.conf
dict-dir /run/current-system/sw/lib/aspell
7.8. nix-plugins.patch
diff --git a/extra-builtins.cc b/extra-builtins.cc index 3a0f90e..bb10f8b 100644 --- a/extra-builtins.cc +++ b/extra-builtins.cc @@ -1,9 +1,9 @@ -#include <config.h> -#include <primops.hh> -#include <globals.hh> -#include <config-global.hh> -#include <eval-settings.hh> -#include <common-eval-args.hh> -#include <filtering-source-accessor.hh> +#include <nix/cmd/common-eval-args.hh> +#include <nix/expr/eval-settings.hh> +#include <nix/expr/primops.hh> +#include <nix/fetchers/filtering-source-accessor.hh> +#include <nix/store/globals.hh> +#include <nix/util/configuration.hh> +#include <nix/util/config-global.hh> #include "nix-plugins-config.h"
7.9. Zellij layout swarsel.kdl.nix
{ config, pkgs }:
let
inherit (config.lib.stylix) colors;
in
''
layout {
swap_tiled_layout name="vertical" {
tab max_panes=5 {
pane split_direction="vertical" {
pane
pane { children; }
}
}
tab max_panes=8 {
pane split_direction="vertical" {
pane { children; }
pane { pane; pane; pane; pane; }
}
}
tab max_panes=12 {
pane split_direction="vertical" {
pane { children; }
pane { pane; pane; pane; pane; }
pane { pane; pane; pane; pane; }
}
}
}
swap_tiled_layout name="horizontal" {
tab max_panes=5 {
pane
pane
}
tab max_panes=8 {
pane {
pane split_direction="vertical" { children; }
pane split_direction="vertical" { pane; pane; pane; pane; }
}
}
tab max_panes=12 {
pane {
pane split_direction="vertical" { children; }
pane split_direction="vertical" { pane; pane; pane; pane; }
pane split_direction="vertical" { pane; pane; pane; pane; }
}
}
}
swap_tiled_layout name="stacked" {
tab min_panes=5 {
pane split_direction="vertical" {
pane
pane stacked=true { children; }
}
}
}
swap_floating_layout name="staggered" {
floating_panes
}
swap_floating_layout name="enlarged" {
floating_panes max_panes=10 {
pane { x "5%"; y 1; width "90%"; height "90%"; }
pane { x "5%"; y 2; width "90%"; height "90%"; }
pane { x "5%"; y 3; width "90%"; height "90%"; }
pane { x "5%"; y 4; width "90%"; height "90%"; }
pane { x "5%"; y 5; width "90%"; height "90%"; }
pane { x "5%"; y 6; width "90%"; height "90%"; }
pane { x "5%"; y 7; width "90%"; height "90%"; }
pane { x "5%"; y 8; width "90%"; height "90%"; }
pane { x "5%"; y 9; width "90%"; height "90%"; }
pane focus=true { x 10; y 10; width "90%"; height "90%"; }
}
}
swap_floating_layout name="spread" {
floating_panes max_panes=1 {
pane {y "50%"; x "50%"; }
}
floating_panes max_panes=2 {
pane { x "1%"; y "25%"; width "45%"; }
pane { x "50%"; y "25%"; width "45%"; }
}
floating_panes max_panes=3 {
pane focus=true { y "55%"; width "45%"; height "45%"; }
pane { x "1%"; y "1%"; width "45%"; }
pane { x "50%"; y "1%"; width "45%"; }
}
floating_panes max_panes=4 {
pane { x "1%"; y "55%"; width "45%"; height "45%"; }
pane focus=true { x "50%"; y "55%"; width "45%"; height "45%"; }
pane { x "1%"; y "1%"; width "45%"; height "45%"; }
pane { x "50%"; y "1%"; width "45%"; height "45%"; }
}
}
default_tab_template {
children
pane size=1 borderless=true {
plugin location="file://${pkgs.zjstatus}/bin/zjstatus.wasm" {
format_left "{mode}#[bg=#${colors.base00}] {tabs}"
format_center ""
format_right "#[bg=#${colors.base00},fg=#${colors.base02}]#[bg=#${colors.base02},fg=#${colors.base01},bold] #[bg=#${colors.base02},fg=#${colors.base01},bold] {session} #[bg=#${colors.base02},fg=#${colors.base01},bold]"
format_space ""
format_hide_on_overlength "false"
format_precedence "lcr"
border_enabled "false"
border_char "─"
border_format "#[fg=#6C7086]{char}"
border_position "top"
mode_normal "#[bg=#${colors.base0B},fg=#${colors.base01},bold] NORMAL#[bg=#${colors.base01},fg=#${colors.base0B}]█"
mode_locked "#[bg=#${colors.base04},fg=#${colors.base01},bold] LOCKED #[bg=#${colors.base01},fg=#${colors.base04}]█"
mode_resize "#[bg=#${colors.base08},fg=#${colors.base01},bold] RESIZE#[bg=#${colors.base01},fg=#${colors.base08}]█"
mode_pane "#[bg=#${colors.base0D},fg=#${colors.base01},bold] PANE#[bg=#${colors.base01},fg=#${colors.base0D}]█"
mode_tab "#[bg=#${colors.base07},fg=#${colors.base01},bold] TAB#[bg=#${colors.base01},fg=#${colors.base07}]█"
mode_scroll "#[bg=#${colors.base0A},fg=#${colors.base01},bold] SCROLL#[bg=#${colors.base01},fg=#${colors.base0A}]█"
mode_enter_search "#[bg=#${colors.base0D},fg=#${colors.base01},bold] ENT-SEARCH#[bg=#${colors.base01},fg=#${colors.base0D}]█"
mode_search "#[bg=#${colors.base0D},fg=#${colors.base01},bold] SEARCHARCH#[bg=#${colors.base01},fg=#${colors.base0D}]█"
mode_rename_tab "#[bg=#${colors.base07},fg=#${colors.base01},bold] RENAME-TAB#[bg=#${colors.base01},fg=#${colors.base07}]█"
mode_rename_pane "#[bg=#${colors.base0D},fg=#${colors.base01},bold] RENAME-PANE#[bg=#${colors.base01},fg=#${colors.base0D}]█"
mode_session "#[bg=#${colors.base0E},fg=#${colors.base01},bold] SESSION#[bg=#${colors.base01},fg=#${colors.base0E}]█"
mode_move "#[bg=#${colors.base0F},fg=#${colors.base01},bold] MOVE#[bg=#${colors.base01},fg=#${colors.base0F}]█"
mode_prompt "#[bg=#${colors.base0D},fg=#${colors.base01},bold] PROMPT#[bg=#${colors.base01},fg=#${colors.base0D}]█"
mode_tmux "#[bg=#${colors.base09},fg=#${colors.base01},bold] TMUX#[bg=#${colors.base01},fg=#${colors.base09}]█"
// formatting for inactive tabs
tab_normal "#[bg=#${colors.base01},fg=#${colors.base0C}]█#[bg=#${colors.base0C},fg=#${colors.base01},bold]{index} #[bg=#${colors.base01},fg=#${colors.base0C},bold] {name}{floating_indicator}#[bg=#${colors.base01},fg=#${colors.base01},bold]█"
tab_normal_fullscreen "#[bg=#${colors.base01},fg=#${colors.base0C}]█#[bg=#${colors.base0C},fg=#${colors.base01},bold]{index} #[bg=#${colors.base01},fg=#${colors.base0C},bold] {name}{fullscreen_indicator}#[bg=#${colors.base01},fg=#${colors.base01},bold]█"
tab_normal_sync "#[bg=#${colors.base01},fg=#${colors.base0C}]█#[bg=#${colors.base0C},fg=#${colors.base01},bold]{index} #[bg=#${colors.base01},fg=#${colors.base0C},bold] {name}{sync_indicator}#[bg=#${colors.base01},fg=#${colors.base01},bold]█"
// formatting for the current active tab
tab_active "#[bg=#${colors.base01},fg=#${colors.base09}]█#[bg=#${colors.base09},fg=#${colors.base01},bold]{index} #[bg=#${colors.base01},fg=#${colors.base09},bold] {name}{floating_indicator}#[bg=#${colors.base01},fg=#${colors.base01},bold]█"
tab_active_fullscreen "#[bg=#${colors.base01},fg=#${colors.base09}]█#[bg=#${colors.base09},fg=#${colors.base01},bold]{index} #[bg=#${colors.base01},fg=#${colors.base09},bold] {name}{fullscreen_indicator}#[bg=#${colors.base01},fg=#${colors.base01},bold]█"
tab_active_sync "#[bg=#${colors.base01},fg=#${colors.base09}]█#[bg=#${colors.base09},fg=#${colors.base01},bold]{index} #[bg=#${colors.base01},fg=#${colors.base09},bold] {name}{sync_indicator}#[bg=#${colors.base01},fg=#${colors.base01},bold]█"
// separator between the tabs
tab_separator "#[bg=#${colors.base00}] "
// indicators
tab_sync_indicator " "
tab_fullscreen_indicator " "
tab_floating_indicator " "
command_git_branch_command "git rev-parse --abbrev-ref HEAD"
command_git_branch_format "#[fg=blue] {stdout} "
command_git_branch_interval "10"
command_git_branch_rendermode "static"
datetime "#[fg=#6C7086,bold] {format} "
datetime_format "%A, %d %b %Y %H:%M"
datetime_timezone "Europe/Vienna"
}
}
}
}
''
7.10. Zellij config.kdl.nix
{ config }:
''
pane_frames false
simplified_ui false
default_shell "zsh"
copy_on_select true
on_force_close "detach"
show_startup_tips false
default_layout "default"
layout_dir "${config.home.homeDirectory}/.config/zellij/layouts"
theme_dir "${config.home.homeDirectory}/.config/zellij/themes"
keybinds clear-defaults=true {
locked {
bind "Ctrl g" { SwitchToMode "normal"; }
}
pane {
bind "left" { MoveFocus "left"; }
bind "down" { MoveFocus "down"; }
bind "up" { MoveFocus "up"; }
bind "right" { MoveFocus "right"; }
bind "c" { SwitchToMode "renamepane"; PaneNameInput 0; }
bind "d" { NewPane "down"; SwitchToMode "normal"; }
bind "e" { TogglePaneEmbedOrFloating; SwitchToMode "normal"; }
bind "f" { ToggleFocusFullscreen; SwitchToMode "normal"; }
bind "h" { MoveFocus "left"; }
bind "j" { MoveFocus "down"; }
bind "k" { MoveFocus "up"; }
bind "l" { MoveFocus "right"; }
bind "n" { NewPane; SwitchToMode "normal"; }
bind "p" { SwitchFocus; }
bind "Ctrl p" { SwitchToMode "normal"; }
bind "r" { NewPane "right"; SwitchToMode "normal"; }
bind "w" { ToggleFloatingPanes; SwitchToMode "normal"; }
bind "z" { TogglePaneFrames; SwitchToMode "normal"; }
}
tab {
bind "left" { GoToPreviousTab; }
bind "down" { GoToNextTab; }
bind "up" { GoToPreviousTab; }
bind "right" { GoToNextTab; }
bind "1" { GoToTab 1; SwitchToMode "normal"; }
bind "2" { GoToTab 2; SwitchToMode "normal"; }
bind "3" { GoToTab 3; SwitchToMode "normal"; }
bind "4" { GoToTab 4; SwitchToMode "normal"; }
bind "5" { GoToTab 5; SwitchToMode "normal"; }
bind "6" { GoToTab 6; SwitchToMode "normal"; }
bind "7" { GoToTab 7; SwitchToMode "normal"; }
bind "8" { GoToTab 8; SwitchToMode "normal"; }
bind "9" { GoToTab 9; SwitchToMode "normal"; }
bind "[" { BreakPaneLeft; SwitchToMode "normal"; }
bind "]" { BreakPaneRight; SwitchToMode "normal"; }
bind "b" { BreakPane; SwitchToMode "normal"; }
bind "h" { GoToPreviousTab; }
bind "j" { GoToNextTab; }
bind "k" { GoToPreviousTab; }
bind "l" { GoToNextTab; }
bind "n" { NewTab; SwitchToMode "normal"; }
bind "r" { SwitchToMode "renametab"; TabNameInput 0; }
bind "s" { ToggleActiveSyncTab; SwitchToMode "normal"; }
bind "Ctrl t" { SwitchToMode "normal"; }
bind "x" { CloseTab; SwitchToMode "normal"; }
bind "tab" { ToggleTab; }
}
resize {
bind "left" { Resize "Increase left"; }
bind "down" { Resize "Increase down"; }
bind "up" { Resize "Increase up"; }
bind "right" { Resize "Increase right"; }
bind "+" { Resize "Increase"; }
bind "-" { Resize "Decrease"; }
bind "=" { Resize "Increase"; }
bind "H" { Resize "Decrease left"; }
bind "J" { Resize "Decrease down"; }
bind "K" { Resize "Decrease up"; }
bind "L" { Resize "Decrease right"; }
bind "h" { Resize "Increase left"; }
bind "j" { Resize "Increase down"; }
bind "k" { Resize "Increase up"; }
bind "l" { Resize "Increase right"; }
bind "Ctrl n" { SwitchToMode "normal"; }
}
move {
bind "left" { MovePane "left"; }
bind "down" { MovePane "down"; }
bind "up" { MovePane "up"; }
bind "right" { MovePane "right"; }
bind "h" { MovePane "left"; }
bind "Ctrl h" { SwitchToMode "normal"; }
bind "j" { MovePane "down"; }
bind "k" { MovePane "up"; }
bind "l" { MovePane "right"; }
bind "n" { MovePane; }
bind "p" { MovePaneBackwards; }
bind "tab" { MovePane; }
}
scroll {
bind "e" { EditScrollback; SwitchToMode "normal"; }
bind "s" { SwitchToMode "entersearch"; SearchInput 0; }
}
search {
bind "c" { SearchToggleOption "CaseSensitivity"; }
bind "n" { Search "down"; }
bind "o" { SearchToggleOption "WholeWord"; }
bind "p" { Search "up"; }
bind "w" { SearchToggleOption "Wrap"; }
}
session {
bind "c" {
LaunchOrFocusPlugin "configuration" {
floating true
move_to_focused_tab true
}
SwitchToMode "normal"
}
bind "Ctrl o" { SwitchToMode "normal"; }
bind "p" {
LaunchOrFocusPlugin "plugin-manager" {
floating true
move_to_focused_tab true
}
SwitchToMode "normal"
}
bind "w" {
LaunchOrFocusPlugin "session-manager" {
floating true
move_to_focused_tab true
}
SwitchToMode "normal"
}
}
shared_except "locked" {
bind "Alt left" { MoveFocusOrTab "left"; }
bind "Alt down" { MoveFocus "down"; }
bind "Alt up" { MoveFocus "up"; }
bind "Alt right" { MoveFocusOrTab "right"; }
bind "Alt +" { Resize "Increase"; }
bind "Alt -" { Resize "Decrease"; }
bind "Alt =" { Resize "Increase"; }
bind "Alt [" { PreviousSwapLayout; }
bind "Alt ]" { NextSwapLayout; }
bind "Alt f" { ToggleFloatingPanes; }
bind "Ctrl g" { SwitchToMode "locked"; }
bind "Alt h" { MoveFocusOrTab "left"; }
bind "Alt i" { MoveTab "left"; }
bind "Alt j" { MoveFocus "down"; }
bind "Alt k" { MoveFocus "up"; }
bind "Alt l" { MoveFocusOrTab "right"; }
bind "Alt n" { NewPane; }
bind "Alt o" { MoveTab "right"; }
bind "Ctrl q" { Quit; }
}
shared_except "locked" "move" {
bind "Ctrl h" { SwitchToMode "move"; }
}
shared_except "locked" "session" {
bind "Ctrl o" { SwitchToMode "session"; }
}
shared_except "locked" "scroll" "search" "tmux" {
bind "Ctrl b" { SwitchToMode "tmux"; }
}
shared_except "locked" "scroll" "search" {
bind "Ctrl s" { SwitchToMode "scroll"; }
}
shared_except "locked" "tab" {
bind "Ctrl t" { SwitchToMode "tab"; }
}
shared_except "locked" "pane" {
bind "Ctrl p" { SwitchToMode "pane"; }
}
shared_except "locked" "resize" {
bind "Ctrl n" { SwitchToMode "resize"; }
}
shared_except "normal" "locked" "entersearch" {
bind "enter" { SwitchToMode "normal"; }
}
shared_except "normal" "locked" "entersearch" "renametab" "renamepane" {
bind "esc" { SwitchToMode "normal"; }
}
shared_among "pane" "tmux" {
bind "x" { CloseFocus; SwitchToMode "normal"; }
}
shared_among "scroll" "search" {
bind "PageDown" { PageScrollDown; }
bind "PageUp" { PageScrollUp; }
bind "left" { PageScrollUp; }
bind "down" { ScrollDown; }
bind "up" { ScrollUp; }
bind "right" { PageScrollDown; }
bind "Ctrl b" { PageScrollUp; }
bind "Ctrl c" { ScrollToBottom; SwitchToMode "normal"; }
bind "d" { HalfPageScrollDown; }
bind "Ctrl f" { PageScrollDown; }
bind "h" { PageScrollUp; }
bind "j" { ScrollDown; }
bind "k" { ScrollUp; }
bind "l" { PageScrollDown; }
bind "Ctrl s" { SwitchToMode "normal"; }
bind "u" { HalfPageScrollUp; }
}
entersearch {
bind "Ctrl c" { SwitchToMode "scroll"; }
bind "esc" { SwitchToMode "scroll"; }
bind "enter" { SwitchToMode "search"; }
}
renametab {
bind "esc" { UndoRenameTab; SwitchToMode "tab"; }
}
shared_among "renametab" "renamepane" {
bind "Ctrl c" { SwitchToMode "normal"; }
}
renamepane {
bind "esc" { UndoRenamePane; SwitchToMode "pane"; }
}
shared_among "session" "tmux" {
bind "d" { Detach; }
}
tmux {
bind "left" { MoveFocus "left"; SwitchToMode "normal"; }
bind "down" { MoveFocus "down"; SwitchToMode "normal"; }
bind "up" { MoveFocus "up"; SwitchToMode "normal"; }
bind "right" { MoveFocus "right"; SwitchToMode "normal"; }
bind "space" { NextSwapLayout; }
bind "\"" { NewPane "down"; SwitchToMode "normal"; }
bind "%" { NewPane "right"; SwitchToMode "normal"; }
bind "," { SwitchToMode "renametab"; }
bind "[" { SwitchToMode "scroll"; }
bind "Ctrl b" { Write 2; SwitchToMode "normal"; }
bind "c" { NewTab; SwitchToMode "normal"; }
bind "h" { MoveFocus "left"; SwitchToMode "normal"; }
bind "j" { MoveFocus "down"; SwitchToMode "normal"; }
bind "k" { MoveFocus "up"; SwitchToMode "normal"; }
bind "l" { MoveFocus "right"; SwitchToMode "normal"; }
bind "n" { GoToNextTab; SwitchToMode "normal"; }
bind "o" { FocusNextPane; }
bind "p" { GoToPreviousTab; SwitchToMode "normal"; }
bind "z" { ToggleFocusFullscreen; SwitchToMode "normal"; }
}
}
// Plugin aliases - can be used to change the implementation of Zellij
// changing these requires a restart to take effect
plugins {
compact-bar location="zellij:compact-bar"
configuration location="zellij:configuration"
filepicker location="zellij:strider" {
cwd "/"
}
plugin-manager location="zellij:plugin-manager"
session-manager location="zellij:session-manager"
status-bar location="zellij:status-bar"
strider location="zellij:strider"
tab-bar location="zellij:tab-bar"
welcome-screen location="zellij:session-manager" {
welcome_screen true
}
}
// Plugins to load in the background when a new session starts
// eg. "file:/path/to/my-plugin.wasm"
// eg. "https://example.com/my-plugin.wasm"
load_plugins {
}
// Provide a command to execute when copying text. The text will be piped to
// the stdin of the program to perform the copy. This can be used with
// terminal emulators which do not support the OSC 52 ANSI control sequence
// that will be used by default if this option is not set.
// Examples:
//
// copy_command "xclip -selection clipboard" // x11
// copy_command "wl-copy" // wayland
// copy_command "pbcopy" // osx
//
// copy_command "pbcopy"
// Choose the destination for copied text
// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard.
// Does not apply when using copy_command.
// Options:
// - system (default)
// - primary
//
// copy_clipboard "primary"
// Path to the default editor to use to edit pane scrollbuffer
// Default: $EDITOR or $VISUAL
// scrollback_editor "/usr/bin/vim"
// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible
// Options:
// - true (default)
// - false
//
// auto_layout false
// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected
// Options:
// - true (default)
// - false
//
// session_serialization false
// Whether pane viewports are serialized along with the session, default is false
// Options:
// - true
// - false (default)
//
// serialize_pane_viewport false
// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0
// defaults to the scrollback size. If this number is higher than the scrollback size, it will
// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true.
//
// scrollback_lines_to_serialize 10000
// Enable or disable the rendering of styled and colored underlines (undercurl).
// May need to be disabled for certain unsupported terminals
// (Requires restart)
// Default: true
//
// styled_underlines false
// How often in seconds sessions are serialized
//
// serialization_interval 10000
// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know
// metadata info on this session)
// (Requires restart)
// Default: false
//
// disable_session_metadata false
// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it)
// (Requires restart)
// Default: true (if the host terminal supports it)
//
// support_kitty_keyboard_protocol false
''
7.11. Vieb config
" Options set adblocker=update set adblockernotifications=all set cache=clearonquit set noclearcookiesonquit set nocleardownloadsoncompleted set nocleardownloadsonquit set clearhistoryinterval=none set noclearlocalstorageonquit set noclosablepinnedtabs set commandhist=persistuseronly set containerkeeponreopen set containernewtab=s:usecurrent set containershowname=automatic set containersplitpage=s:usecurrent set containerstartuppage=main set countlimit=100 " set nodarkreader set darkreaderbg=#181a1b set darkreaderbrightness=100 set darkreadercontrast=100 set darkreaderfg=#e8e6e3 set darkreadergrayscale=0 set darkreadermode=dark set darkreadersepia=0 set darkreadertextstroke=0 set devtoolsposition=split set dialogalert=notifyblock set dialogconfirm=notifyallow set dialogprompt=notifyblock set downloadmethod=automatic set downloadpath= set encodeurlcopy=nospaces set encodeurlext=nospaces set explorehist=persist set externalcommand= set favicons=session set followchars=numbers set followfallbackaction=filter set followlabelposition=outsiderighttop set follownewtabswitch set guifontsize=14 set guifullscreennavbar=oninput set guifullscreentabbar=onupdate set guihidetimeout=5000 set guinavbar=oninput set guiscrollbar=onscroll set guitabbar=onupdate set historyperpage=100 set ignorecase set incsearch set inputfocusalignment=rememberend set keeprecentlyclosed set lang=en set loadingindicator=line set mapsuggest=9000000000000000 set mapsuggestposition=topright set markposition=newtab set markpositionshifted=default set maxmapdepth=10 set menupage=elementasneeded set menusuggest=both set menuvieb=both set mintabwidth=28 set mouse=all set mousedisabledbehavior=nothing set nomousefocus set mousenewtabswitch set mousevisualmode=onswitch set nativenotification=never set nativetheme=dark set newtaburl= set notificationduration=6000 set notificationforpermissions=silent set notificationforsystemcommands=errors set notificationlimitsmall=3 set notificationposition=bottomright set pdfbehavior=download set permissioncamera=ask set permissioncertificateerror=allow set permissionclipboardread=allow set permissionclipboardwrite=allow set permissionclosepage=allow set permissiondisplaycapture=ask set permissionfullscreen=allow set permissiongeolocation=block set permissionhid=block set permissionidledetection=block set permissionmediadevices=allowfull set permissionmicrophone=ask set permissionmidi=ask set permissionmidisysex=ask set permissionnotifications=ask set permissionopenexternal=ask set permissionpersistentstorage=ask set permissionpointerlock=block set permissionscreenwakelock=block set permissionsensors=block set permissionserial=block set permissionunknown=ask set permissionusb=allow set permissionwindowmanagement=ask set pointerposlocalid=domain set pointerpostype=casing set noquitonlasttabclose set redirecttohttp set noreloadtaboncrash set replacespecial=special set replacestartup=never set requesttimeout=20000 set restoretabs=all set scrollposlocalid=domain set scrollpostype=casing set searchemptyscope=global set searchpointeralignment=left set searchscope=global set shell= set showcmd set smartcase set spell set nosplitbelow set nosplitright set nosponsorblock set suggestbouncedelay=100 set suggestcommands=9000000000000000 set suggesttopsites=10 set suspendbackgroundtab set suspendonrestore=regular set suspendtimeout=0 set tabclosefocus=left set tabcycle set tabnewposition=right set tabopenmuted=never set taboverflow=scroll set tabreopenmuted=remember set tabreopenposition=right set timeout set timeoutlen=2000 set translateapi=auto set translatekey= set translatelang=en-us set translateurl=https://api-free.deepl.com/v2/ " set nouserscript " set nouserstyle set useragent=%firefox set vimcommand=gvim set windowfullscreen=restore set windowmaximize=restore set windowposition=restore set windowsize=restore set windowtitle="%app - %title" " Commands colorscheme default " Mappings nmap o <toExploreMode> nmap t <:tabnew><toExploreMode> nmap b <toCommandMode>buffer<Space> nmap g0 <:buffer 0> nmap g^ <:buffer 0> nmap g$ <:buffer -1> nmap <C-m> <:buffer #> nmap J <previousTab> nmap K <nextTab> nmap >> <moveTabForward> nmap <lt><lt> <moveTabBackward> nmap O <toExploreMode><End> nmap gx0 <:lclose> nmap gx$ <:rclose> nmap x <stopLoadingPage> nunmap [ nunmap ] nmap [[ <previousPage> nmap ]] <nextPage> nmap [c <decreasePageNumber> nmap ]c <increasePageNumber> nmap zz <zoomReset> nmap zi <zoomIn> nmap zo <zoomOut> nmap zm 5<zoomIn> nmap zr 5<zoomOut> nmap zM 5<zoomIn> nmap zR 5<zoomOut> nmap zI 7<zoomIn> nmap zO 7<zoomOut> " Search set searchengine=https://kagi.com/search?q=%s set searchwords+=no~https://search.nixos.org/options?query=%s set searchwords+=np~https://search.nixos.org/packages?query=%s set searchwords+=hm~https://home-manager-options.extranix.com/?query=%s " Viebrc generated by Vieb " vim: ft=vim
7.12. swayidle
timeout 300 'swaylock -f --screenshots --clock --effect-blur 7x5 --effect-vignette 0.5:0.5 --fade-in 0.2' timeout 600 'swaymsg "output * dpms off"' resume 'swaymsg "output * dpms on"' before-sleep 'swaylock -f --screenshots --clock --effect-blur 7x5 --effect-vignette 0.5:0.5 --fade-in 0.2'
7.13. stylix color scheme
# scheme: "better-contrast" author: "Swarsel" author: "Swarsel" base00: "1d252c" #1d252c" base01: "171d23" #171d23" base02: "206a86" #206a86" base03: "003a66" #003a66" base04: "5ec4ff" #5ec4ff" base05: "a0b3c5" #a0b3c5" base06: "7ed4ff" #7ed4ff" base07: "8ef4ff" #8ef4ff" base08: "d95468" #d95468" base09: "ffa880" #ffa880" base0A: "6ed4ff" #6ed4ff" base0B: "8bd49c" #8bd49c" base0C: "008b94" #008b94" base0D: "5ec4ff" #5ec4ff" base0E: "c06ece" #c06ece" base0F: "3c0044" #3c0044" # base00: "1D252C" # #1d252c passt # base01: "171D23" # #171d23 # base02: "5EC4FF" # #5EC4FF # base03: "566C7D" # #566C7D passt # base04: "5EC4FF" # #5EC4FF passt # base05: "A0B3C5" # #A0B3C5 passt # base06: "C06ECE" # #C06ECE passt # base07: "A0B3C5" # #A0B3C5 passt # base08: "D95468" # #D95468 passt # base09: "FFA880" # #ffA880 passt # base0A: "5EC4FF" # #5EC4FF # base0B: "8BD49C" # #8BD49C # base0C: "008B94" # #008B94 passt # base0D: "5EC4FF" # #5EC4FF passt # base0E: "C06ECE" # #C06ECE passt # base0F: "5EC4FF" # #5EC4FF passt # base00 - Default Background # base01 - Lighter Background (Used for status bars) # base02 - Selection Background # base03 - Comments, Invisibles, Line Highlighting # base04 - Dark Foreground (Used for status bars) # base05 - Default Foreground, Caret, Delimiters, Operators # base06 - Light Foreground (Not often used) # base07 - Light Background (Not often used) # base08 - Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted # base09 - Integers, Boolean, Constants, XML Attributes, Markup Link Url # base0A - Classes, Markup Bold, Search Text Background # base0B - Strings, Inherited Class, Markup Code, Diff Inserted # base0C - Support, Regular Expressions, Escape Characters, Markup Quotes # base0D - Functions, Methods, Attribute IDs, Headings # base0E - Keywords, Storage, Selector, Markup Italic, Diff Changed # base0F - Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
7.14. .gitmessage
The double source block is intended here to circumvent a org-babel convenience where the first n empty lines of each source block are not taken into the final file. For the .gitmessage I want an empty newline to type into, so this is what I use to achieve that.
# max. 50 chars is here: # # <type>[optional scope]: <description> # types: feat, fix, build, chore, ci, docs, style, refactor, perf, test # ! indicates a breaking change. # Body: wrap at 72 chars is here: # # Include at least one empty line before co-authored. Format: # Co-authored-by: name <user@users.noreply.github.com>
7.15. userChrome.css
/* Source file https://github.com/MrOtherGuy/firefox-csshacks/tree/master/chrome/autohide_toolbox.css made available under Mozilla Public License v. 2.0 See the above repository for updates as well as full license text. */ /* Hide the whole toolbar area unless urlbar is focused or cursor is over the toolbar * Dimensions on non-Win10 OS probably needs to be adjusted. */ :root{ --uc-autohide-toolbox-delay: 200ms; /* Wait 0.1s before hiding toolbars */ --uc-toolbox-rotation: 82deg; /* This may need to be lower on mac - like 75 or so */ --base00: #1D252C; --base01: #171D23; --base02: #5EC4FF; --base03: #566C7D; --base04: #5EC4FF; --base05: #A0B3C5; --base06: #C06ECE; --base07: #A0B3C5; --base08: #D95468; --base09: #FFA880; --base0A: #5EC4FF; --base0B: #8BD49C; --base0C: #008B94; --base0D: #5EC4FF; --base0E: #C06ECE; --base0F: #5EC4FF; } :root[sizemode="maximized"]{ --uc-toolbox-rotation: 88.5deg; } @media (-moz-platform: windows){ :root:not([lwtheme]) #navigator-toolbox{ background-color: -moz-dialog !important; } } :root[sizemode="fullscreen"], :root[sizemode="fullscreen"] #navigator-toolbox{ margin-top: 0 !important; } #navigator-toolbox{ --browser-area-z-index-toolbox: 3; position: fixed !important; background-color: var(--lwt-accent-color,black) !important; transition: transform 82ms linear, opacity 82ms linear !important; transition-delay: var(--uc-autohide-toolbox-delay) !important; transform-origin: top; transform: rotateX(var(--uc-toolbox-rotation)); opacity: 0; line-height: 0; z-index: 1; pointer-events: none; width: 100vw; } :root[sessionrestored] #urlbar[popover]{ pointer-events: none; opacity: 0; transition: transform 82ms linear var(--uc-autohide-toolbox-delay), opacity 0ms calc(var(--uc-autohide-toolbox-delay) + 82ms); transform-origin: 0px calc(0px - var(--tab-min-height) - var(--tab-block-margin) * 2); transform: rotateX(89.9deg); } :root[window-modal-open] #urlbar[popover], #mainPopupSet:has(> [panelopen]:not(#ask-chat-shortcuts,#selection-shortcut-action-panel,#chat-shortcuts-options-panel,#tab-preview-panel), > #tab-group-editor > [panelopen]) ~ toolbox #urlbar[popover], /* swarsel: removed :hover from below line */ #navigator-toolbox:is(:focus-within,[movingtab]) #urlbar[popover], #urlbar-container > #urlbar[popover]:is([focused],[open]){ pointer-events: auto; opacity: 1; transition-delay: 33ms; transform: rotateX(0deg); } :root[window-modal-open] #navigator-toolbox, #mainPopupSet:has(> [panelopen]:not(#ask-chat-shortcuts,#selection-shortcut-action-panel,#chat-shortcuts-options-panel,#tab-preview-panel), > #tab-group-editor > [panelopen]) ~ toolbox, #navigator-toolbox:has(#urlbar:is([open],[focus-within])), /* swarsel: removed :hover from below line */ #navigator-toolbox:is(:focus-within,[movingtab]){ transition-delay: 33ms !important; transform: rotateX(0); opacity: 1; } /* This makes things like OS menubar/taskbar show the toolbox when hovered in maximized windows. * Unfortunately it also means that other OS native surfaces (such as context menu on macos) * and other always-on-top applications will trigger toolbox to show up. */ @media -moz-pref("userchrome.autohide-toolbox.unhide-by-native-ui.enabled"){ :root[sizemode="maximized"]:not(:hover){ #navigator-toolbox:not(:-moz-window-inactive), #urlbar[popover]:not(:-moz-window-inactive){ transition-delay: 33ms !important; transform: rotateX(0); opacity: 1; } } } #navigator-toolbox > *{ line-height: normal; pointer-events: auto } /* Don't apply transform before window has been fully created */ :root:not([sessionrestored]) #navigator-toolbox{ transform:none !important } :root[customizing] #navigator-toolbox{ position: relative !important; transform: none !important; opacity: 1 !important; } #navigator-toolbox[inFullscreen] > #PersonalToolbar, #PersonalToolbar:is([collapsed=""],[collapsed="true"]){ display: none } /* This is a bit hacky fix for an issue that will make urlbar zero pixels tall after you enter customize mode */ #urlbar[breakout][breakout-extend] > .urlbar-input-container{ padding-block: calc(min(4px,(var(--urlbar-container-height) - var(--urlbar-height)) / 2) + var(--urlbar-container-padding)) !important; } /* Uncomment the following for compatibility with tabs_on_bottom.css - this isn't well tested though */ /* #navigator-toolbox{ flex-direction: column; display: flex; } */
7.16. Default Flake Template
{
description = "General purpose Flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
systems.url = "github:nix-systems/default";
};
outputs =
{ nixpkgs
, systems
, ...
}:
let
forEachSystem = nixpkgs.lib.genAttrs (import systems);
pkgsFor = forEachSystem (system: import nixpkgs { inherit system; });
in
{
formatter = forEachSystem (system: pkgsFor.${system}.nixpkgs-fmt);
devShells = forEachSystem (system: {
default = pkgsFor.${system}.mkShell {
packages = with pkgsFor.${system}; [
];
};
});
};
}
7.17. C++ Flake Template
# heavily inspired by https://github.com/nulladmin1/nix-flake-templates/blob/main/cpp-cmake/flake.nix
{
description = "C++ Flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
systems.url = "github:nix-systems/default";
};
outputs =
{ self
, nixpkgs
, systems
, ...
}:
let
forEachSystem = nixpkgs.lib.genAttrs (import systems);
pkgsFor = forEachSystem (system: import nixpkgs { inherit system; });
pname = "name";
in
{
formatter = forEachSystem (system: pkgsFor.${system}.nixpgks-fmt);
devShells = forEachSystem (system: {
default = pkgsFor.${system}.mkShell {
packages = with pkgsFor.${system}; [
libllvm
cmake
gtest
cppcheck
valgrind
doxygen
clang-tools
# cudatoolkit
];
};
});
packages = forEachSystem (system: {
default = pkgsFor.${system}.stdenv.mkDerivation {
inherit pname;
version = "0.1.0";
src = ./.;
nativeBuildInputs = with pkgsFor.${system}; [
cmake
];
buildInputs = with pkgsFor.${system}; [
gtest
];
};
});
apps = forEachSystem (system: {
default = {
type = "app";
program = "${self.packages.${system}.default}/bin/${pname}";
};
});
};
}
7.18. Go Flake Template
# heavily inspired by https://github.com/nulladmin1/nix-flake-templates/blob/main/go-nix/flake.nix
{
description = "Go Flake";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
systems.url = "github:nix-systems/default";
};
outputs =
{ self
, nixpkgs
, systems
, ...
}:
let
forEachSystem = nixpkgs.lib.genAttrs (import systems);
pkgsFor = forEachSystem (system: import nixpkgs { inherit system; });
pname = "name";
in
{
formatter = forEachSystem (system: pkgsFor.${system}.nixpkgs-fmt);
devShells = forEachSystem (system: {
default = pkgsFor.${system}.mkShell {
packages = with pkgsFor.${system}; [
go
gopls
go-tools
gotools
];
};
});
packages = forEachSystem (system: {
default = pkgsFor.${system}.buildGoModule {
inherit pname;
version = "0.1.0";
src = ./.;
vendorHash = null;
};
});
apps = forEachSystem (system: {
default = {
type = "app";
program = "${self.packages.${system}.default}/bin/${pname}";
};
});
};
}
7.19. LaTeX Flake Template
# This template is based on https://github.com/Leixb/latex-template/tree/master
{
description = "LaTeX Flake";
inputs.flake-utils.url = "github:numtide/flake-utils";
outputs = { self, nixpkgs, flake-utils }:
{
lib.latexmk = import ./build-document.nix;
} // flake-utils.lib.eachDefaultSystem (system:
let
pname = "document";
pkgs = import nixpkgs { inherit system; };
latex-packages = with pkgs; [
(texlive.combine {
inherit (texlive)
scheme-medium
framed
titlesec
cleveref
multirow
wrapfig
tabu
threeparttable
threeparttablex
makecell
environ
biblatex
biber
fvextra
upquote
catchfile
xstring
csquotes
minted
dejavu
comment
footmisc
xltabular
ltablex
;
})
which
python39Packages.pygments
];
dev-packages = with pkgs; [
texlab
zathura
wmctrl
];
in
rec {
devShell = pkgs.mkShell {
buildInputs = [ latex-packages dev-packages ];
};
formatter = pkgs.nixpkgs-fmt;
packages = flake-utils.lib.flattenTree {
default = import ./build-document.nix {
inherit pkgs;
name = pname;
texlive = latex-packages;
shellEscape = true;
minted = true;
SOURCE_DATE_EPOCH = toString self.lastModified;
};
};
apps.default = flake-utils.lib.mkApp { drv = "${pkgs.texlivePackages.latexmk}"; exePath = "/bin/latexmk"; };
}
);
}
7.20. Python Flake Template
# based on https://github.com/pyproject-nix/uv2nix/tree/master/templates/hello-world
{
description = "Python flake using uv2nix";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
pyproject-nix = {
url = "github:pyproject-nix/pyproject.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
uv2nix = {
url = "github:pyproject-nix/uv2nix";
inputs = {
pyproject-nix.follows = "pyproject-nix";
nixpkgs.follows = "nixpkgs";
};
};
pyproject-build-systems = {
url = "github:pyproject-nix/build-system-pkgs";
inputs = {
pyproject-nix.follows = "pyproject-nix";
uv2nix.follows = "uv2nix";
nixpkgs.follows = "nixpkgs";
};
};
};
outputs =
{ nixpkgs
, uv2nix
, pyproject-nix
, pyproject-build-systems
, ...
}:
let
inherit (nixpkgs) lib;
pname = "name";
forAllSystems = lib.genAttrs lib.systems.flakeExposed;
# Load a uv workspace from a workspace root.
# Uv2nix treats all uv projects as workspace projects.
workspace = uv2nix.lib.workspace.loadWorkspace { workspaceRoot = ./.; };
overlay = workspace.mkPyprojectOverlay {
# Prefer prebuilt binary wheels as a package source.
# Sdists are less likely to "just work" because of the metadata missing from uv.lock.
# Binary wheels are more likely to, but may still require overrides for library dependencies.
sourcePreference = "wheel"; # or sourcePreference = "sdist";
# Optionally customise PEP 508 environment
# environ = {
# platform_release = "5.10.65";
# };
};
pythonSets = forAllSystems
(system:
let
inherit (pkgs) stdenv;
pkgs = nixpkgs.legacyPackages.${system};
pyprojectOverrides = final: prev: {
# Implement build fixups here.
${pname} = prev.${pname}.overrideAttrs (old: {
passthru = old.passthru // {
# Put all tests in the passthru.tests attribute set.
# Nixpkgs also uses the passthru.tests mechanism for ofborg test discovery.
#
# For usage with Flakes we will refer to the passthru.tests attributes to construct the flake checks attribute set.
tests =
let
virtualenv = final.mkVirtualEnv "${pname}-pytest-env" {
${pname} = [ "test" ];
};
in
(old.tests or { })
// {
pytest = stdenv.mkDerivation {
name = "${final.${pname}.name}-pytest";
inherit (final.${pname}) src;
nativeBuildInputs = [
virtualenv
];
dontConfigure = true;
# Because this package is running tests, and not actually building the main package
# the build phase is running the tests.
#
# We also output a HTML coverage report, which is used as the build output.
buildPhase = ''
runHook preBuild
pytest --cov tests --cov-report html
runHook postBuild
'';
# Install the HTML coverage report into the build output.
#
# If you wanted to install multiple test output formats such as TAP outputs
# you could make this derivation a multiple-output derivation.
#
# See https://nixos.org/manual/nixpkgs/stable/#chap-multiple-output for more information on multiple outputs.
installPhase = ''
runHook preInstall
mv htmlcov $out
runHook postInstall
'';
};
};
};
});
};
baseSet = pkgs.callPackage pyproject-nix.build.packages {
python = pkgs.python312;
};
in
baseSet.overrideScope
(
lib.composeManyExtensions [
pyproject-build-systems.overlays.default
overlay
pyprojectOverrides
]
));
in
{
packages = forAllSystems (system:
let
pythonSet = pythonSets.${system};
in
{ default = pythonSet.mkVirtualEnv "${pname}-env" workspace.deps.default; });
devShells = forAllSystems
(system:
let
pythonSet = pythonSets.${system};
pkgs = nixpkgs.legacyPackages.${system};
in
{
default =
let
# Create an overlay enabling editable mode for all local dependencies.
editableOverlay = workspace.mkEditablePyprojectOverlay {
# Use environment variable
root = "$REPO_ROOT";
# Optional: Only enable editable for these packages
# members = [ "hello-world" ];
};
# Override previous set with our overrideable overlay.
editablePythonSet = pythonSet.overrideScope editableOverlay;
virtualenv = editablePythonSet.mkVirtualEnv "${pname}-dev-env" {
${pname} = [ "dev" ];
};
in
pkgs.mkShell {
packages = [
virtualenv
pkgs.uv
];
shellHook = ''
# Undo dependency propagation by nixpkgs.
unset PYTHONPATH
# Don't create venv using uv
export UV_NO_SYNC=1
# Prevent uv from downloading managed Python's
export UV_PYTHON_DOWNLOADS=never
# Get repository root using git. This is expanded at runtime by the editable `.pth` machinery.
export REPO_ROOT=$(git rev-parse --show-toplevel)
'';
};
});
checks = forAllSystems (
system:
let
pythonSet = pythonSets.${system};
in
{
inherit (pythonSet.${pname}.passthru.tests) pytest;
}
);
formatter = forAllSystems (system: nixpkgs.legacyPackages.${system}.nixpkgs-fmt);
};
}
7.21. Rust Flake Template
# heavily inspired by https://github.com/nulladmin1/nix-flake-templates/blob/main/rust-fenix-naersk/flake.nix
{
description = "Rust Flake using Fenix and Naersk";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
systems.url = "github:nix-systems/default";
naersk.url = "github:nix-community/naersk";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{ self
, nixpkgs
, naersk
, fenix
, systems
, ...
}:
let
forEachSystem = nixpkgs.lib.genAttrs (import systems);
pkgsFor = forEachSystem (system:
import nixpkgs {
inherit system;
overlays = [
fenix.overlays.default
];
});
rust-toolchain = forEachSystem (system: pkgsFor.${system}.fenix.stable);
in
{
formatter = forEachSystem (system: pkgsFor.${system}.nixpkgs-fmt);
devShells = forEachSystem (system: {
default = pkgsFor.${system}.mkShell {
packages = with rust-toolchain.${system}; [
cargo
rustc
clippy
rustfmt
rust-analyzer
];
env = {
RUST_BACKTRACE = "full";
};
RUST_SRC_PATH = "${rust-toolchain.${system}.rust-src}/lib/rustlib/src/rust/library";
};
});
packages = forEachSystem (system: {
default =
(pkgsFor.${system}.callPackage naersk {
inherit (rust-toolchain.${system}) cargo rustc;
}).buildPackage {
src = ./.;
};
});
apps = forEachSystem (system: {
default = {
type = "app";
program = "${self.packages.${system}.default}/bin/rust";
};
});
};
}
7.22. GitHub Readme
Here lies defined the readme for GitHub and Forgejo:
[](https://actions-badge.atrox.dev/Swarsel/.dotfiles/goto?ref=main)
###### Disclaimer
You probably do not want to use this setup verbatim. This is made to fit my specific use cases, and I do not guarantee best practises everywhere. Changes are made on a daily basis.
That being said, there is a lot of general configuration that you *probably* can use without changes; if you only want to use this repository as a starting point for your own configuration, you should be fine. See below for more information. Also, if you see something that can be done more efficiently or better in general, please let me know! :)
# \~SwarselSystems\~
<p align="center">
<img width="49%" title="Tiling" alt="swarselsystems_preview1" src="https://github.com/user-attachments/assets/f6021ab9-6289-497d-8747-28f5d526b75a" />
<img width="49%" title="Waybar" alt="swarselsystems_preview2" src="https://github.com/user-attachments/assets/1160d9f7-710c-4046-8fcf-476bb4a0be84" />
</p>
## Overview
- [Literate configuration](https://swarsel.github.io/.dotfiles/) defining my entire infrastructure, including Emacs
- Configuration based on flakes for personal hosts as well as servers on:
- [NixOS](https://github.com/NixOS/nixpkgs)
- [home-manager](https://github.com/nix-community/home-manager) only (no full NixOS) with support from [nixGL](https://github.com/nix-community/nixGL)
- [nix-darwin](https://github.com/LnL7/nix-darwin)
- [nix-on-droid](https://github.com/nix-community/nix-on-droid)
- Streamlined configuration and deployment pipeline:
- Framework for [packages](https://github.com/Swarsel/.dotfiles/blob/main/nix/packages.nix), [overlays](https://github.com/Swarsel/.dotfiles/blob/main/nix/overlays.nix), [modules](https://github.com/Swarsel/.dotfiles/tree/main/modules), and [library functions](https://github.com/Swarsel/.dotfiles/blob/main/nix/lib.nix)
- Dynamically generated config:
- host configurations
- dns records
- network setup (+ wireguard mesh on systemd-networkd)
- Remote Builders for [x86_64,aarch64]-linux running in hydra, feeding a private nix binary cache
- Bootstrapping:
- Limited local installer (no secrets handling) with a supported demo build
- Fully autonomous remote deployment using [nixos-anywhere](https://github.com/nix-community/nixos-anywhere) and [disko](https://github.com/nix-community/disko) (with secrets handling)
- Improved nix tooling
- Support for advanced features:
- Secrets handling using [sops-nix](https://github.com/Mic92/sops-nix) (pls no pwn ❤️)
- Management of personally identifiable information using [nix-plugins](https://github.com/shlevy/nix-plugins)
- Full Yubikey support
- LUKS-encryption with support for remote disk unlock over SSH
- Secure boot using [Lanzaboote](https://github.com/nix-community/lanzaboote)
- BTRFS-based [Impermanence](https://github.com/nix-community/impermanence)
- Configuration shared between configurations (configuration for one nixosConfiguration can be defined in another nixosConfiguration)
- Global attributes shared between all configurations to reduce attribute redeclaration
- [Config library](https://github.com/Swarsel/.dotfiles/blob/9acfc5f93457ec14773cc0616cab616917cc8af5/modules/shared/config-lib.nix#L4) for defining config-based functions for generating service information
- Reduced friction between full NixOS- and home-manager-only deployments regarding secrets handling and config sharing
## Documentation
The full documentation can be found here:
[SwarselSystems literate configuration](https://swarsel.github.io/.dotfiles/)
I went to great lengths in order to document the full design process of my infrastructure properly; the above document strives to serve as an introductory lecture to nix / NixOS while at the same time explaining the config in general.
If you only came here for my Emacs configuration, the relevant files are here:
- [early-init.el](../files/emacs/early-init.el)
- [init.el](../files/emacs/init.el)
### Getting started
#### Demo configuration
<details>
<summary>Click here for instructions on how to install the demo system</summary>
If you just want to see if this configuration is for you, run this command on any system that has `nix` installed:
``` shell
nix run --experimental-features 'nix-command flakes' github:Swarsel/.dotfiles#swarsel-rebuild -- -u <YOUR_USERNAME>
```
This will activate the `hotel` configuration on your system, which is a de-facto mirror of my main configuration with secret-based settings removed.
Please keep in mind that this limited installer will make local changes to the cloned repository in order to be able to install it (otherwise the builder would fail at fetching my private secrets repository). As such, this should only be used to evaluate the system - if you want to use it longterm, you will need to create a fork and make some changes.
</details>
### Deployment
<details>
<summary>Click here for deployment instructions</summary>
The deployment process for this configuration is mostly automated, there are only a few steps that are needed to be done manually. You can choose between a remote deployment strategy that is also able to deploy new age keys for sops for you and a local installer that will only install the system without any secret handling.
#### Remote deployment (recommended if you have at least one running system)
0) Fork this repo, and write your own host config at `hosts/nixos/<YOUR_ARCHITECTURE>/<YOUR_CONFIG_NAME>/default.nix` (you can use one of the other configurations as a template. Also see https://github.com/Swarsel/.dotfiles/tree/main/modules for a list of all additional options). At the very least, you should replace the `secrets/` directory with your own secrets and replace the SSH public keys with your own ones (otherwise I will come visit you!🔓❤️). I personally recommend to use the literate configuration and `org-babel-tangle-file` in Emacs, but you can also simply edit the separate `.nix` files.
1) Have a system with `nix` available booted (this does not need to be installed, i.e. you can use a NixOS installer image; a custom minimal installer ISO can be built by running `just iso` in the root of this repo)
2) Make sure that your Yubikey is plugged in or that you have your SSH key available (and configured)
3) Run `swarsel-bootstrap -n <CONFIGURATION_NAME> -d <TARGET_IP>` on your existing system.
- Alternatively (if you run this on a system that is not yet running this configuration), you can also run `nix run --experimental-features 'nix-command flakes' github:Swarsel/.dotfiles -- -n <CONFIGURATION_NAME> -d <TARGET_IP>` (this runs the same program as the command above).
4) Follow the installers instructions:
- you will have to choose a disk encryption password (if you want that feature)
- you will have to confirm once that the target system has rebooted
- you will have to enter the root password once during the final system install
5) That should be it! The installer will take care of setting up disks, secrets, and the rest of the hardware configuration! You will still have to sign in manually to some webservices etc.
#### Local deployment (recommended for setting up the first system)
1) Boot the latest install ISO from this repository on an UEFI system.
2) Run `swarsel-install -n <CONFIGURATION_NAME>`
3) Reboot
Alternatively, to install this from any NixOS live ISO, run `nix run --experimental-features 'nix-command flakes' github:Swarsel/.dotfiles#install -- -n <CONFIGURATION_NAME>` at step 2.
</details>
## Infrastructure
<details>
<summary>Click here for a summary of my infrastructure</summary>
<img width="3854" height="7060" alt="topology" src="https://github.com/user-attachments/assets/fc557fdb-b779-4530-ae19-93ff019a2b29" />
### Programs
| Topic | Program |
|---------------|-----------------------------------------------------------------------------------------------------------------------------|
|🐚 **Shell** | [zsh](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/zsh.nix) |
|🚪 **DM** | [greetd](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/common/login.nix) |
|🪟 **WM** | [SwayFX](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/sway.nix) |
|⛩️ **Bar** | [Waybar](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/waybar.nix) |
|✒️ **Editor** | [Emacs](https://github.com/Swarsel/.dotfiles/tree/main/files/emacs/init.el) |
|🖥️ **Terminal**| [Kitty](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/kitty.nix) |
|🚀 **Launcher**| [Fuzzel](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/fuzzel.nix) |
|🚨 **Alerts** | [Mako](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/mako.nix) |
|🌐 **Browser** | [Firefox](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/zsh.nix) |
|🎨 **Theme** | [City-Lights (managed by stylix)](https://github.com/Swarsel/.dotfiles/tree/main/modules/home/common/sharedsetup.nix) |
### Services
| Topic | Program |
|------------------------------|----------------------------------------------------------------------------------------------------------------|
|📖 **Books** | [Kavita](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/kavita.nix) |
|📼 **Videos** | [Jellyfin](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/jellyfin.nix) |
|🎵 **Music** | [Navidrome](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/navidrome.nix) + [Spotifyd](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/spotifyd.nix) + [MPD](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/mpd.nix) |
|🗨️ **Messaging** | [Matrix](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/matrix.nix) |
|📁 **Filesharing** | [Nectcloud](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/nextcloud.nix) |
|🎞️ **Photos** | [Immich](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/immich.nix) |
|📄 **Documents** | [Paperless](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/paperless.nix) |
|🔄 **File Sync** | [Syncthing](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/syncthing.nix) |
|💾 **Backups** | [Restic](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/restic.nix) |
|👁️ **Monitoring** | [Grafana](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/monitoring.nix) |
|🍴 **RSS** | [FreshRss](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/freshrss.nix) |
|🌳 **Git** | [Forgejo](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/forgejo.nix) |
|⚓ **Anki Sync** | [Anki Sync Server](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/ankisync.nix) |
|🪪 **SSO** | [Kanidm](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/kanidm.nix) + [oauth2-proxy](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/oauth2-proxy.nix) |
|💸 **Finance** | [Firefly-III](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/firefly-iii.nix) |
|🃏 **Collections** | [Koillection](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/koillection.nix) |
|🗃️ **Shell History** | [Atuin](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/atuin.nix) |
|📅 **CalDav/CardDav** | [Radicale](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/radicale.nix) |
|↔️ **P2P Filesharing** | [Croc](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/croc.nix) |
|✂️ **Paste Tool** | [Microbin](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/microbin.nix) |
|📸 **Image Sharing** | [Slink](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/slink.nix) |
|🔗 **Link Shortener** | [Shlink](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/shlink.nix) |
|⛏️ **Minecraft** | [Minecraft](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/minecraft.nix) |
|☁️ **S3** | [Garage](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/garage.nix) |
|🕸️ **Nix Binary Cache** | [Attic](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/attic.nix) |
|🐙 **Nix Build farm** | [Attic](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/hydra.nix) |
|🔑 **Cert-based SSH** | [OPKSSH](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/opkssh.nix) |
|🔨 **Home Asset Management**| [Homebox](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/homebox.nix) |
|👀 **DNS Records** | [NSD](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/nsd.nix) |
|✉️ **Mail** | [simple-nixos-mailserver](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/mailserver.nix) |
|🚇 **VPN Access** | [Firezone](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/firezone.nix) |
|🛡️ **Local DNS Resolver** | [AdGuard Home](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/adguardhome.nix) |
|🛎️ **DHCP** | [Kea](https://github.com/Swarsel/.dotfiles/tree/main/modules/nixos/server/kea.nix) |
### Hosts
| Name | Hardware | Use |
|---------------------|-----------------------------------------------------|-----------------------------------------------------------------|
|💻 **pyramid** | Framework Laptop 16, AMD 7940HS, RX 7700S, 64GB RAM | Work laptop |
|💻 **bakery** | Lenovo Ideapad 720S-13IKB | Personal laptop |
|💻 **machpizza** | MacBook Pro 2016 | MacOS reference and build sandbox |
|🏠 **treehouse** | NVIDIA DGX Spark | AI Workstation, remote builder, hm-only-reference |
|🖥️ **summers** | ASUS Z10PA-D8, 2* Intel Xeon E5-2650 v4, 128GB RAM | Homeserver (microvms), remote builder, data storage |
|🖥️ **winters** | ASRock J4105-ITX, 32GB RAM | Homeserver (IoT server in spe) |
|🖥️ **hintbooth** | HUNSN RM02, 8GB RAM | Router, DNS Resolver, home NGINX endpoint |
|☁️ **stoicclub** | Cloud Server: 1 vCPUs, 8GB RAM | Authoritative DNS server |
|☁️ **liliputsteps** | Cloud Server: 1 vCPUs, 8GB RAM | SSH bastion |
|☁️ **twothreetunnel**| Cloud Server: 2 vCPUs, 8GB RAM | Service proxy |
|☁️ **eagleland** | Cloud Server: 2 vCPUs, 8GB RAM | Mailserver |
|☁️ **moonside** | Cloud Server: 4 vCPUs, 24GB RAM | Game servers, syncthing + other lightweight services |
|☁️ **belchsfactory** | Cloud Server: 4 vCPUs, 24GB RAM | Hydra builder and nix binary cache |
|🪟 **chaostheater** | Asus Z97-A, i7-4790k, GTX970, 32GB RAM | Home Game Streaming Server (Windows/AtlasOS, not nix-managed) |
|📱 **magicant** | Samsung Galaxy Z Flip 6 | Phone |
|💿 **drugstore** | - | NixOS-installer ISO for bootstrapping new hosts |
|💿 **brickroad** | - | Kexec tarball for bootstrapping low-memory machines |
|❔ **hotel** | - | Demo config for checking out this configuration |
|❔ **toto** | - | Helper configuration for testing purposes |
</details>
## General Nix tips & useful links
<details>
<summary>Click here for a summary of nix tips & links</summary>
- Below is a small list of tips that should be helpful if you are new to the nix ecosystem:
- Temporarily install any package using `nix shell nixpkgs#<PACKAGE_NAME>` - this can be e.g. useful if you accidentally removed home-manager from your packages on a non-NixOS machine.
- if you need multiple packages, you can do `nix shell nixpkgs#{<pkg1>,<pkg2>,<pkg3>}`.
- you can set `nix.registry` to add more flakes to your registry. I use this to add a `n` shorthand to `nixpkgs`, which allows me to do `nix shell n#{<pkg1>,<pkg2>,<pkg3>}`.
- Alternatively, use [comma](https://github.com/nix-community/comma)
- More info on `nix [...]` commands: https://nixos.org/manual/nix/stable/command-ref/new-cli/nix
- some examples:
- `nix flake update <input-name>` lets you update a specific input only.
- `nix repl <your flake path>` gives quick insight into your written configuration.
- `nix eval <your flake path>#<config attribute>` quickly returns an attribute in your written configuration
- `nix fmt` formats your flake using the formatter specified under `formatter` in your `flake.nix`
- When you are trying to setup a new configuration part, [GitHub code search](https://github.com/search?q=language%3ANix&type=code) can really help you to find a working configuration. Just filter for `.nix` files and the options you are trying to set up.
- getting packages at a different version than your target (or not packaged at all) can be done in most cases easily with fetchFromGithub (https://ryantm.github.io/nixpkgs/builders/fetchers/)
- you can easily install old revisions of packages using https://lazamar.co.uk/nix-versions/. You can conveniently spawn a shell with a chosen package available using `vershell <NIXPKGS_REVISION> <PACKAGE>`. Just make sure to pick a revision that has flakes enabled, otherwise you will need the legacy way of spawning the shell (see the link for more info)
- when developing modules in a dev branch of another flake, you can use `--override-input` to temporarily use the local directory as the flake source.
- including `nixosConfig ? config` in your module arguments is a smart way of enabling a module to pull in config from NixOS or home-manager config, no matter if it is a NixOS system or not.
- you can have a quick cli evaluation for nix commands with e.g. `nixpgks.lib` available using `nix-instantiate --strict --eval --expr "let lib = import <nixpkgs/lib>; in <expression>"`.
- if you are looking for a specific library, `nix-locate` makes it easy to look for them.
- to look at the dependencies pulled in by a tool, use `nix-tree`
- to find out which derivation uses another derivation, use `nix store --query --referrers <derivation>`
- to get a neat overview of your config changes in recent generations, use `nix profile diff-closures --profile /nix/var/nix/profiles/system`
- to get instead the changes since the last boot, use `nix profile diff-closures /run/*-system`
- if you just need the generation numbers, use `sudo nix-env --list-generations --profile /nix/var/nix/profiles/system`
- to then switch to another generation, you can use `sudo nix-env --switch-generation <generation number> -p /nix/var/nix/profiles/system` followed by `sudo /nix/var/nix/profiles/system/bin/switch-to-configuration switch`
- These links are your best friends:
- The nix documentation: https://nix.dev/
- The nixpkgs reference manual: https://nixos.org/manual/nixpkgs/unstable/#buildpythonapplication-function
- the [nixpkgs repository](https://github.com/NixOS/nixpkgs) - especially useful to look at the various READMEs that are in various places in the repository (find using GitHub code search) as well as the [issues](https://github.com/Swarsel/.dotfiles/issues) and [PRs](https://github.com/Swarsel/.dotfiles/pulls) pages
- and the [nixpkgs Pull Request Tracker](https://nixpk.gs/pr-tracker.html)
- The NixOS manual: https://nixos.org/manual/nixos/stable/
- The NixOS package search: https://search.nixos.org/packages
- and the nix package version search: https://lazamar.co.uk/nix-versions/
- The NixOS option search https://search.nixos.org/options
- [mipmip](https://github.com/mipmip)'s home-manager option search: https://mipmip.github.io/home-manager-option-search/
- [Alan Pearce](https://alanpearce.eu/)'s nix-darwin search: https://searchix.alanpearce.eu/options/darwin/search (which supports all of the other versions as well :o)
- For the above, you can use the CLI tool [manix](https://github.com/mlvzk/manix)
- Nix function search: https://noogle.dev/
- Search for nix-community options: https://search.nüschtos.de/
- But that is not all:
- Some nix resources
- A tour of Nix: https://nixcloud.io/tour/
- The Nix One Pager: https://github.com/tazjin/nix-1p
- another one page introduction: https://learnxinyminutes.com/nix/
- a very short introduction to Nix features: https://zaynetro.com/explainix
- introductory nix article: https://medium.com/@MrJamesFisher/nix-by-example-a0063a1a4c55
- and another one: https://web.archive.org/web/20210121042658/https://ebzzry.io/en/nix/#nix
- How to learn nix: https://ianthehenry.com/posts/how-to-learn-nix/
- the Nix Cookbook: https://github.com/functionalops/nix-cookbook?tab=readme-ov-file
- and the Nix Pills: https://nixos.org/guides/nix-pills/
- Some resources on flakes
- Why to use flakes and introduction to flakes: https://www.tweag.io/blog/2020-05-25-flakes/
- The [NixOS & Flakes Book](https://nixos-and-flakes.thiscute.world/)
- and [Wombat's book](https://mhwombat.codeberg.page/nix-book/)
- or the [Zero to Nix series](https://zero-to-nix.com/)
- Practical nix flakes article: https://serokell.io/blog/practical-nix-flakes
- A bit on Overlays:
- Overview on overlays: [Mastering Nixpkgs overlays article](https://nixcademy.com/posts/mastering-nixpkgs-overlays-techniques-and-best-practice/)
- Some examples on best practises: [Do's and Don'ts of overlays](https://flyingcircus.io/news/detailsansicht/nixos-the-dos-and-donts-of-nixpkgs-overlays)
- Blog article about overrides: https://bobvanderlinden.me/customizing-packages-in-nix/#using-modified-packages
- Also useful is the [official NixOS Wiki](https://wiki.nixos.org/wiki/NixOS_Wiki)
- there is also the [unofficial NixOS Wiki](https://nixos.wiki/) that tends to be a bit outdated, use with care
- Some resources for specific nix tools:
- Flake output reference: https://nixos-and-flakes.thiscute.world/other-usage-of-flakes/outputs
- You can find public repositories with modules at https://nur.nix-community.org/ (you should check what you are installing however):
- I like to use this for rycee's firefox extensions: https://nur.nix-community.org/repos/rycee/
- List of nerdfonts: https://github.com/NixOS/nixpkgs/blob/nixos-unstable/pkgs/data/fonts/nerd-fonts/manifests/fonts.json
- Stylix configuration options: https://danth.github.io/stylix/
- nix-on-droid options: https://nix-community.github.io/nix-on-droid/nix-on-droid-options.html#sec-options
- Very useful tools that are mostly not directly used in configuration but instead called on need:
- Convert non-NixOS machines to NixOS using [nixos-infect](https://github.com/elitak/nixos-infect)
- Create various installation media with [nixos-generators](https://github.com/nix-community/nixos-generators)
- Remotely deploy NixOS using [nixos-anywhere](https://github.com/nix-community/nixos-anywhere)
- And a few links that are not directly nix-related, but may still serve you well:
- List of pre-commit-hooks: https://devenv.sh/reference/options/#pre-commithooks
- Waybar configuration: https://github.com/Alexays/Waybar/wiki
</details>
## Attributions, Acknowledgements, Inspirations, etc.
These are in random order (also known as 'the order in which I discovered them'). I would like to express my gratitude to:
<details>
<summary>The great people who have contributed code for the nix-community, with special mentions for (this list is unfairly incomplete)</summary>
- [guibou](https://github.com/guibou/)
- [rycee](https://github.com/rycee)
- [adisbladis](https://github.com/adisbladis)
- [Mic92](https://github.com/Mic92/sops-nix)
- [lassulus](https://github.com/lassulus)
- [danth](https://github.com/danth/)
- [LnL7](https://github.com/LnL7)
- [t184256](https://github.com/t184256)
- [bennofs](https://github.com/bennofs)
- [Pandapip1](https://github.com/Pandapip1)
- [zowoq](https://github.com/zowoq)
- [numtide](https://github.com/numtide)
- [hsjobeki](https://github.com/hsjobeki)
- [blitz](https://github.com/blitz)
- [RaitoBezarius](https://github.com/RaitoBezarius)
- [nikstur](https://github.com/nikstur)
- [talyz](https://github.com/talyz)
- [infinisil](https://github.com/infinisil)
- [zhaofengli](https://github.com/zhaofengli)
- [Artturin](https://github.com/Artturin)
- [oddlama](https://github.com/oddlama)
</details>
<details>
<summary>The people who have inspired me with their configurations (sadly also highly incomplete)</summary>
- [theSuess](https://github.com/theSuess) with their [home-manager](https://code.kulupu.party/thesuess/home-manager)
- [hlissner](https://github.com/hlissner) with their [dotfiles](https://github.com/hlissner/dotfiles)
- [drduh](https://github.com/drduh/YubiKey-Guide) with their [YubiKey-Guide](https://github.com/drduh/YubiKey-Guide)
- [AntonHakansson](https://github.com/AntonHakansson) with their [nixos-config](https://github.com/AntonHakansson/nixos-config?tab=readme-ov-file)
- [Guekka](https://github.com/Guekka/) with their [blog](https://guekka.github.io/)
- [NotAShelf](https://github.com/NotAShelf) with their [nyx](https://github.com/NotAShelf/nyx)
- [Misterio77](https://github.com/Misterio77) with their [nix-config](https://github.com/Misterio77/nix-config)
- [0xdade](https://github.com/0xdade) with their [blog](https://0xda.de/blog/)
- [EmergentMind](https://github.com/EmergentMind) with their [nix-config](https://github.com/EmergentMind/nix-config)
- [librephoenix](https://github.com/librephoenix) with their [nixos-config](https://github.com/librephoenix/nixos-config)
- [Xe](https://github.com/Xe) with their [blog](https://xeiaso.net/blog/)
- [oddlama](https://github.com/oddlama) with their [nix-config](https:/github.com/oddlama/nix-config)
</details>
If you feel that I forgot to pay you tribute for code that I used in this repository, please shoot me a message and I will fix it :)
7.23. GitHub Workflow: Build-and-deploy
This file defines the workflow that I use to build the HTML version of this document. It will first tangle this file, which will generate the style.css that is going to be used by the site before exporting the file to html, which will generate the index.html. Finally we build the site.
name: Build and Deploy
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Emacs
run: |
sudo apt-get update
sudo apt-get install -y emacs-nox elpa-htmlize
- name: Prepare site images
run: |
mkdir -p site/files/topology
cp files/topology/topology_small.png site/files/topology/topology_small.png
- name: Tangle files & export to HTML
run: |
emacs --batch \
--eval "(require 'org)" \
--eval "(setq org-confirm-babel-evaluate nil
org-html-validation-link nil
org-export-headline-levels 6
org-export-with-broken-links 'mark)" \
--visit=SwarselSystems.org \
--funcall org-babel-tangle \
--funcall org-html-export-to-html
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'site'
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
7.24. GitHub Workflow: Flake check
This is a crude workflow that performs some rudimentary checks on the repository. As long as Devshell (checks/pre-commit-hooks) are used, this should never fail.
name: Flake check
on:
pull_request:
push:
branches: [main]
jobs:
build:
name: Check flake
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check Nix flake inputs
uses: DeterminateSystems/flake-checker-action@v4
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v3
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Check for dead code in .nix files
run: git ls-files '*.nix' | nix run nixpkgs#deadnix
- name: Check for lints in .nix files
run: nix run nixpkgs#statix -- check
- name: Check formatting in .nix files
run: git ls-files '*.nix' | xargs nix run nixpkgs#nixpkgs-fmt -- --check
7.25. Private topology flake
This flake is automatically loaded as an override when building the repo topology using the alias build-topology defined in zsh.
{
description = "Flake that sets topologyPrivate to true for building topology.";
outputs = _: { topologyPrivate = true; };
}
7.26. Public topology flake
This flake is loaded per default as an input to not obfuscate values that could be exposed to nix-topology.
{
description = "Flake that sets topologyPrivate to false for general purpose.";
outputs = _: { topologyPrivate = false; };
}
8. Appendix C: Explanations to nix functions and operators
This sections explains commonly used functions in nix (both builtins as well as in the nixpkgs library).
8.1. Concepts
8.1.1. The '//' operator
The // operator in nix is used to merge attribute sets; if an attribute exists in both sets, then the latter set takes precedence:
swarsel-instantiate '{ a = 1; b = 2; } // { b = 3; c = 4; }'
{ a = 1; b = 3; c = 4; }
Also, this will not merge nested sets:
swarsel-instantiate '{ a = { a1 = 1; a2 = 2; }; b = { b1 = 1; b2 = 2; }; } // { b = { b1 = 3; b3 = 4;}; c = 4; }'
{ a = { a1 = 1; a2 = 2; }; b = { b1 = 3; b3 = 4; }; c = 4; }
It only took the latter b set, even though the b2 value was not contested. This functionality is provided by nixpkgs.lib.recursiveUpdate.
8.2. Builtin functions
The following functions are only base functionality of nix, and not exported to nixpkgs. When using them we need to make sure that the nix version in use is matching the expected function.
8.2.1. builtins.readDir
builtins.readDir reads the name of items of a directory as attributes and their type as values.
swarsel-instantiate 'builtins.readDir ./hosts/nixos'
{ bakery = "directory"; hotel= "directory"; milkywell = "directory"; moonside = "directory"; pyramid = "directory"; toto = "directory"; winters = "directory"; }
8.3. Builtin functions exported to nixpkgs
These functions exist in nix, but are exported to nixpkgs to make sure they use a nix version that works in the used nixpgks. Most of the times I should prefer to use the nixpkgs version of these.
8.3.1. nixpkgs.lib.listToAttrs
builtins.listToAttrs converts a list of name-value pairs into an attribute set.
swarsel-instantiate 'builtins.listToAttrs [{ name = "foo"; value = 1; } { name = "bar"; value = 2; }]'
{ bar = 2; foo = 1; }
8.3.2. nixpkgs.lib.attrNames
nixpkgs.lib.attrNames returns the list of attribute names from an attribute set.
swarsel-instantiate 'lib.attrNames { a = 1; b = 2; c = 3; }'
[ "a" "b" "c" ]
8.3.3. nixpkgs.lib.map
nixpkgs.lib.map takes a function and applies the elements of a list upon them.
swarsel-instantiate 'lib.map (x: x + 1) [1 2 3]'
[ 2 3 4 ]
swarsel-instantiate 'lib.map (x: { result = x + 1; }) [1 2 3]'
[ { result = 2; } { result = 3; } { result = 4; } ]
8.3.4. nixpkgs.lib.filter
nixpkgs.lib.filter takes a list as input and only keeps elements that fulfill a given function:
swarsel-instantiate 'lib.filter (x: x != "a") (lib.attrNames { a = 1; b = 2; c = 3; })'
[ "b" "c" ]
8.3.5. nixpkgs.lib.concatLists
nixpkgs.lib.concatLists, as the name suggests, concatenates lists, while keeping the original order of items:
swarsel-instantiate 'lib.concatLists [ [ 1 2 ] [ 3 4 ] [ 5 ] ]'
[ 1 2 3 4 5 ]
8.3.6. nixpkgs.lib.genList
nixpkgs.lib.genList generates a list from a function:
swarsel-instantiate 'lib.genList (i: if i < 16 then 1 else 0) 32'
[ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ]
8.3.7. nixpkgs.lib.concatStringsSep
nixpkgs.lib.concatStringsSep concatenates a list of strings with a defined separator:
swarsel-instantiate 'lib.concatStringsSep "." ["services" "nginx"]'
services.nginx
8.3.8. nixpkgs.lib.split
nixpkgs.lib.split creates a list containing both the list of regex matches as well as the non-matched strings of a regex pattern:
swarsel-instantiate 'lib.split "\\." "sub.about.com"'
[ "sub" [ ] "about" [ ] "com" ]
Another example with regex matches:
swarsel-instantiate 'lib.split "(s)." "swarsel"'
[ "" [ "s" ] "ar" [ "s" ] "l" ]
The result list always begins with non-matching patterns, so if the first character is in a match, the first element will be "".
8.3.9. nixpkgs.lib.tail
nixpkgs.lib.tail returns a list without its first item:
swarsel-instantiate 'lib.tail [ "sub" "about" "com" ]'
[ "about" "com" ]
8.3.10. nixpkgs.lib.head
nixpkgs.lib.tail returns the first item of a list:
swarsel-instantiate 'lib.head [ "sub" "about" "com" ]'
sub
8.3.11. nixpkgs.lib.isString
nixpkgs.lib.isString returns a bool based on if the input evaluates to a string:
swarsel-instantiate 'lib.isString [ "1" ]' swarsel-instantiate 'lib.isString "1"'
| false |
| true |
8.3.12. nixpkgs.lib.length
nixpkgs.lib.length returns the length of a list:
swarsel-instantiate 'lib.length [ "1" [] ]'
2
8.3.13. nixpkgs.lib.stringLength
nixpkgs.lib.stringLength returns the byte length of a string:
swarsel-instantiate 'lib.stringLength "<3"' swarsel-instantiate 'lib.stringLength "❤"' swarsel-instantiate 'lib.stringLength "❤️"'
| 2 |
| 3 |
| 6 |
8.3.14. nixpkgs.lib.subString
nixpkgs.lib.substring returns the substring within the specified range (excluding the end index):
swarsel-instantiate 'lib.substring 0 3 "about"'
abo
8.4. Functions in nixpgks
These functions only exist in nixpkgs and cannot be used in generic nix code.
8.4.1. nixpkgs.lib.recursiveUpdate
If you want to merge nested attribute sets, use nixpkgs.lib.recursiveUpdate instead of The '//' operator:
swarsel-instantiate 'lib.recursiveUpdate { a = { a1 = 1; a2 = 2; }; b = { b1 = 1; b2 = 2; }; } { b = { b1 = 3; b3 = 4;}; c = 4; }'
{ a = { a1 = 1; a2 = 2; }; b = { b1 = 3; b2 = 2; b3 = 4; }; c = 4; }
8.4.2. nixpkgs.lib.genAttrs
nixpkgs.lib.genAttrs is used to generate an attribute set (a dictionary-like structure) from a list of keys and a function that computes the values for those keys.
swarsel-instantiate 'lib.genAttrs ["a" "b" "c"] (x: "${x}-value")'
{ a = "a-value"; b = "b-value"; c = "c-value"; }
8.4.3. nixpkgs.lib.mkOverride
nixpkgs.lib.mkOverride sets the priority of an expression.
If two expression are defined twice accross the configuration, the evaluator does not know which one should take precedence; this will lead to an error. By default, all option definitions are given priority 100. Lower values take precedence over lower values. For reference, here are some commonly used values:
- nixpkgs.lib.mkForce sets the priority to 50, which is a very low value, meaning it will be prioritised in nearly all cases
- nixpkgs.lib.mkDefault sets the priority to 1000, which is a quite high value that will almost never be used if the same attribute is defined elswheer.
8.4.4. nixpkgs.lib.mkForce
An alias for (nixpkgs.lib.mkOverride 50).
8.4.5. nixpkgs.lib.mkDefault
An alias for (nixpkgs.lib.mkOverride 1000).
8.4.6. nixpkgs.lib.mapAttrsToList
nixpkgs.lib.mapAttrsToList converts an attribute set into a list by applying a given function to each name-value pair:
swarsel-instantiate 'lib.mapAttrsToList (name: value: "${name} = ${value}") { a = "1"; b = "2"; }'
[ "a = 1" "b = 2" ]
8.4.7. nixpkgs.lib.flip
nixpkgs.lib.flipreverses the argument order passed to an expression (in semantics, that is lib.flip f a b == f b a). This is useful when an expression has one big argument and one small one; in that case it is convenient to have the shorter expression at the start of the function. Take this example of a function that simply prints its attributes as a list:
swarsel-instantiate 'lib.mapAttrsToList (name: value: "${name} = ${value}") {a = "1"; b = "2";}'
[ "a = 1" "b = 2" ]
It prints the attributs unchanged, as is to be expected. If, however, we call the function using nixpkgs.lib.flip:
swarsel-instantiate 'lib.mapAttrsToList (lib.flip (name: value: "${name} = ${value}")) {a = "1"; b = "2";}'
[ "1 = a" "2 = b" ]
8.4.8. nixpkgs.lib.mkMerge
nixpgks.lib.mkMergeis used to deeply merge values.
swarsel-instantiate ' lib.mkMerge [ { a = [ 1 ]; } { a = [ 3 ]; } ] '
{ _type = "merge"; contents = [ { a = [ 1 ]; } { a = [ 3 ]; } ]; }
8.4.9. nixpkgs.lib.foldl
nixpkgs.lib.foldl reduces an attribute set by repeatedly calling a function on an attribute set from the left.
swarsel-instantiate 'lib.foldl (acc: x: "(" + acc + " + " + x + ")") "0" ["1" "2" "3"]'
(((0 + 1) + 2) + 3)
similarly, there exists an version that starts from the right.
swarsel-instantiate 'lib.foldr (acc: x: "(" + acc + " + " + x + ")") "0" ["1" "2" "3"]'
(1 + (2 + (3 + 0)))
8.4.10. nixpkgs.lib.toInt
nixpkgs.lib.toInt casts a value to an int. Strips spaces:
swarsel-instantiate 'lib.toInt " 3"'
3
Note that the string cannot be preceded by 0 - lib.toInt "01" would yield => error: Ambiguity in interpretation of 00024 between octal and zero padded integer.
