.dotfiles/SwarselSystems.org
2024-12-15 14:59:58 +01:00

417 KiB
Raw Blame History

SwarselSystems: NixOS + Emacs Configuration

This file has {{{count-words}}} words spanning {{{count-lines}}} lines and was last revised on {{{revision-date}}}.

In order to have working links and macros when viewing this file online, you might want to switch to the html version.

Introduction (no code)

This literate configuration file holds the entirety of all configuration files for both NixOS as well as home-manager across all machines that I currently use. It also holds an extensive Emacs configuration.

I used to have two separate files (Emacs.org and Nixos.org) because the NixOS setting for installing Emacs packages used to break if it found UTF-8 characters in .el files but not in .org files. Hence I used to pass Emacs.org to that function rather than init.el. This seems to be fixed now however and I was finally able to consolidate both files into one.

This configuration is part of a NixOS system that is (nearly) fully declarative and can be found here:

This literate configuration 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 configuration 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, the literate configuration approach 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 Emacs and NixOS as I know it can be a struggle in the beginning.

This file is structured as follows:

  • 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 my rough mentality
  • Noweb-Ref blocks This section hold code that can be templated at other parts of the configuration. This is mostly used for the NixOS side of the configuration where I define my host systems that usually have a lot in common.
  • flake.nix This block holds everything related to the heart of the nix side of the configuration - the flake.nix file.
  • 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.
  • Emacs This section defines my Emacs configuration. For a while, I considered to use rycee's emacs-init module (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 .nix and .el files 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 to emacs-init then I would be indeed to some level confined to the nix ecosystem with my Emacs configuration, as I would no longer have a valid .org file to manage it with, instead generating an init.el directly 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 generated init.el file for package installs. 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: {{{revision-date}}})
  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) - instead, it generates two more files:

  • early-init.el This file handle startup optimization and sets up the basic frame that I will be working in.
  • init.el This 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)

The .html version of this page can be generated 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 (I hope!). Lastly, I add this javascript bit to the file in order to have a darkmode toggle when exporting to html:

  (concat
   "<script src=\"https://cdn.jsdelivr.net/npm/darkmode-js@1.5.7/lib/darkmode-js.min.js\"></script>\n"
   "<script>\n"
   "function addDarkmodeWidget() {\n"
   "new Darkmode().showWidget();\n"
  "}\n"
  "window.addEventListener('load', addDarkmodeWidget);\n"
   "</script>")

The rest of this file will now contain actual code that is used in the configuration.

Noweb-Ref blocks

These blocks are used in several places throughout the configurations, but not on all machines necessarily. For example, the theming section needs 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.

This serves only to reduce code duplication in this file. The tangled files experience no size reduction, since noweb-ref only substitutes these blocks in.

Theme (stylix)

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 firefox). The difference here is, however, 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).

  enable = true;
  base16Scheme = "${self}/wallpaper/swarsel.yaml";
  # base16Scheme = "${pkgs.base16-schemes}/share/themes/shapeshifter.yaml";
  polarity = "dark";
  opacity.popups = 0.5;
  cursor = {
    package = pkgs.capitaine-cursors;
    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-emoji;
      name = "Noto Color Emoji";
    };
  };

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. The noweb-approach here makes this a little bit less painful.

These blocks are later inserted here: flake.nix template. Adding new flake inputs is very easy, you just add them to Inputs & Inputs@Outputs first by name in the first source-block, and then the path in the second source-block. Any variables to be set for the host configuration are done in let, and the specific setup is done in either nixosConfigurations (for NixOS systems), homeConfigurations (for home-manager systems), or nixOnDroidConfigurations (for Nix on Android) and darwinConfigurations (for Darwin systems, also known as Macs). There also used to be a nixos-generators section that used to define a Proxmox LXC image when I was still using Proxmox as my main server. An example of the repository at that time would be acc0ad6 main Add several NixOS hosts on Proxmox and Oracle Cloud.

flake.nix template

This sections puts together the flake.nix file from the Noweb-Ref blocks section. This tangles the flake.nix file; This block only needs to be touched when updating the general structure of the flake. For everything else, see the respective noweb-ref block.

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, options, and more. 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.

In 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 most flakes this is fully sufficient, as they do not need to be called often and it saves me maintainance effort with this file.

      {
        description = "SwarseFlake - Nix Flake for all SwarselSystems";

        nixConfig = {
          extra-substituters = [
            "https://nix-community.cachix.org"
            "https://cache.ngi0.nixos.org/"
          ];

          extra-trusted-public-keys = [
            "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
            "cache.ngi0.nixos.org-1:KqH5CBLNSyX184S9BKZJo1LxrxJ9ltnY2uAs5c/f1MA="
          ];
        };

        inputs = {
          <<flakeinputs>>
        };

        outputs =
          inputs@{ self
          , nixpkgs
          , home-manager
          , nix-darwin
          , systems
          , ...
          }:
          let
            <<flakelet>>
          in
          {
            <<flakeoutputgeneral>>

            nixosConfigurations =
              <<flakenixosconf>>

            homeConfigurations = {
               <<flakehomeconf>>
            };

            darwinConfigurations =
              <<flakedarwinconf>>

            nixOnDroidConfigurations = {
              <<flakedroidconf>>
            };

          };
      }

Pre-commit-hooks (Checks)

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)

  { self, inputs, pkgs, system, ... }:
  {
    pre-commit-check = inputs.pre-commit-hooks.lib.${system}.run {
      src = "${self}";
      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;

        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";
        };

      };
    };
  }

Inputs

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.
  • home-manager This handles user-level configuration and mostly provides dotfiles that are generated and symlinked to ~/.config/.
  • 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. But I do not fully understand what it does. All I know is that I usually have to use this on non-NIxoS systems.
  • 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.
  • nix-alien This is supposed to allow me to run unpatched libraries directly without a need for ELF patching or resorting to steam-run. However, I have not yet gotten this to work.
  • 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-index that is updated weekly. This allows for declarative management, without needing to run the nix-index command 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.
  nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";

  nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-24.05";

  systems.url = "github:nix-systems/default-linux";

  # user-level configuration
  home-manager = {
    url = "github:nix-community/home-manager";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  # overlay to access bleeding edge emacs
  emacs-overlay = {
    url = "github:nix-community/emacs-overlay";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  # nix user repository
  # i use this mainly to not have to build all firefox extensions
  # myself as well as for the emacs-init package (tbd)
  nur.url = "github:nix-community/NUR";

  # provides GL to non-NixOS hosts
  nixgl.url = "github:guibou/nixGL";

  # manages all theming using Home-Manager
  stylix.url = "github:danth/stylix";

  # nix secrets management
  sops-nix.url = "github:Mic92/sops-nix";

  # enable secure boot on NixOS
  lanzaboote.url = "github:nix-community/lanzaboote";

  # nix for android
  nix-on-droid = {
    url = "github:nix-community/nix-on-droid/release-24.05";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  # generate NixOS images
  nixos-generators = {
    url = "github:nix-community/nixos-generators";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  # hardware quirks on nix
  nixos-hardware = {
    url = "github:NixOS/nixos-hardware/master";
  };

  # dynamic library loading
  nix-alien = {
    url = "github:thiagokokada/nix-alien";
  };

  # automatic nintendo switch payload injection
  nswitch-rcm-nix = {
    url = "github:Swarsel/nswitch-rcm-nix";
  };

  # weekly updated nix-index database
  nix-index-database = {
    url = "github:nix-community/nix-index-database";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  disko = {
     url =  "github:nix-community/disko";
     inputs.nixpkgs.follows = "nixpkgs";
  };

  impermanence.url = "github:nix-community/impermanence";

  zjstatus = {
    url = "github:dj95/zjstatus";
  };

  fw-fanctrl = {
    url = "github:TamtamHero/fw-fanctrl/packaging/nix";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  nix-darwin = {
    url = "github:lnl7/nix-darwin";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  pre-commit-hooks = {
    url = "github:cachix/git-hooks.nix";
    inputs.nixpkgs.follows = "nixpkgs";
  };

  nix-secrets = {
    url = "git+ssh://git@github.com/Swarsel/nix-secrets.git?ref=main&shallow=1";
    flake = false;
    inputs = { };
  };

let

Here I define a few variables that I need for my system specifications. First and foremost, pkgs, which gets passed the emacs-overlay, nur, and nixgl modules to it. With this, I can grab all these packages by referencing pkgs.<name> instead of having to put e.g. nixgl.auto.nixGLDefault.

I also define some common module lists that I can simply load depending on the fundamental system (NixOS vs. non-NixOS) - nixModules, homeModules, and mixedModules.

The interesting part is in the start:

  • first, I define pkgsFor. This function reads all available systems from nixpkgs and generates pkgs for them.
  • next, forEachSystem is a function that can be called to declare an output for each such defined system.
  • forAllSystems is a crude function that I use for expressions that depend on system, as the prior two attributes already consumed it at that stage. This is only really used to generate the checks in their own file.
  • mkFullHost is a function that takes a hostname as well as a boolean whether it is NixOS or not, and returns a matching nixosSystem or darwinSystem. This function is only used for systems that can use both NixOS and home-manager options (darwin still counts here as it can use some NixOS options).
  • mkFullHostConfigs is the function that dynamically creates all definded hosts. The hosts are defined by placing a directory in hosts/ under either the nixos/ or darwin/ directory. These directories are being read by readHosts and delivered to this funtion in the later call in nixosConfigurations or darwinConfigurations.
  inherit (self) outputs;
  lib = nixpkgs.lib // home-manager.lib;

  pkgsFor = lib.genAttrs (import systems) (
    system:
    import nixpkgs {
      inherit system;
      config.allowUnfree = true;
    }
  );
  forEachSystem = f: lib.genAttrs (import systems) (system: f pkgsFor.${system});
  forAllSystems = lib.genAttrs [
    "x86_64-linux"
    "aarch64-linux"
    "x86_64-darwin"
    "aarch64-darwin"
  ];
  mkFullHost = host: isNixos: {
    ${host} =
      let
        func = if isNixos then lib.nixosSystem else inputs.nix-darwin.lib.darwinSystem;
        systemFunc = func;
      in
      systemFunc {
        specialArgs = { inherit inputs outputs self; };
        modules = [ ./hosts/${if isNixos then "nixos" else "darwin"}/${host} ];
      };
  };
  mkFullHostConfigs = hosts: isNixos: lib.foldl (acc: set: acc // set) { } (lib.map (host: mkFullHost host isNixos) hosts);
  readHosts = folder: lib.attrNames (builtins.readDir ./hosts/${folder});

  # NixOS modules that can only be used on NixOS systems
  nixModules = [
    inputs.stylix.nixosModules.stylix
    inputs.lanzaboote.nixosModules.lanzaboote
    inputs.disko.nixosModules.disko
    # inputs.impermanence.nixosModules.impermanence
    inputs.sops-nix.nixosModules.sops
    inputs.nswitch-rcm-nix.nixosModules.nswitch-rcm
    ./profiles/common/nixos
  ];

  # Home-Manager modules wanted on non-NixOS systems
  homeModules = [
    inputs.stylix.homeManagerModules.stylix
  ];

  # Home-Manager modules wanted on both NixOS and non-NixOS systems
  mixedModules = [
    inputs.sops-nix.homeManagerModules.sops
    inputs.nix-index-database.hmModules.nix-index
    ./profiles/common/home
  ];

  # For adding things to _module.args (making arguments available globally)
  # moduleArgs = [
  #   {
  #     _module.args = { inherit self; };
  #   }
  # ];

General (outputs)

In this section I am creating some attributes that define general concepts of my configuration:

  • nixosModules imports 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.
  • homeManagerModules imports 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.
  • packages holds packages that I am building myself. These are mostly shell scripts, but also a few others such as AppImages and firefox addons.
  • devShells provides a development shell that can be used as a bootstrap for new installs using nix develop while inside the flake directory. It received an overhaul in 0a6cf0e feat: add checks to devShell, since when it is handled using forAllSystems and now including pre-commit-hook checks.
  • formatter provides the formatter that is to be used on .nix files. It can be called by using nix fmt.
  • check provides the pre-commit-hook checks that I have explained in Pre-commit-hooks (Checks).
  • overlays imports a few community overlays (such as the emacs-overlay) and also three overlays of my own:

    1. additions holds derivations that I am adding myself to nixpkgs - i.e. this is where the packages defined in /pkgs get added to nixpkgs.
    2. modifications holds 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.
    3. nixpkgs-stable holds the newest version of stable nixpkgs. I only use this on packages that seem broken on unstable, which are not many.
    4. zjstatus holds some options for zellij, but I have stopped using it since I prefer tmux.

    They are defined in Overlays (additions, overrides, nixpkgs-stable). 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.

  inherit lib;
  inherit mixedModules;
  inherit nixModules;

  nixosModules = import ./modules/nixos;
  homeManagerModules = import ./modules/home;

  packages = forEachSystem (pkgs: import ./pkgs { inherit pkgs; });
  devShells = forAllSystems (
    system:
    let
      pkgs = nixpkgs.legacyPackages.${system};
      checks = self.checks.${system};
    in
      {
        default = pkgs.mkShell {
          NIX_CONFIG = "experimental-features = nix-command flakes";
          inherit (checks.pre-commit-check) shellHook;
          buildInputs = checks.pre-commit-check.enabledPackages;
          nativeBuildInputs = [
            pkgs.nix
            pkgs.home-manager
            pkgs.git
            pkgs.just
            pkgs.age
            pkgs.ssh-to-age
            pkgs.sops
          ];
        };
      });

  formatter = forEachSystem (pkgs: pkgs.nixpkgs-fmt);
  checks = forAllSystems (
        system:
        let
          pkgs = nixpkgs.legacyPackages.${system};
        in
          import ./checks { inherit self inputs system pkgs; }
  );
  overlays = import ./overlays { inherit inputs; };

nixosConfigurations

This section used to be much longer, since I performed all of my imports right here in the past. Since then, I have however refactored and now my important hosts can be defined in little space. Once I have fully transitioned my server to NixOS too this section will become even smaller once more.

Note: The preceding nixosConfigurations is found in flake.nix template. Also, the method of generating the hosts was changed in commit 3a272b1 feat!: dynamically create hosts, and the deprecated system definitions removed in 7457109 main chore: remove deprecated static host config. See those commits for a state with a simpler config.

  mkFullHostConfigs (readHosts "nixos") true;

darwinConfigurations

And this defines darwin systems (MacOS), which I only have one of, that serves as a template mostly.

Note: The preceding darwinConfigurations is found in flake.nix template. Also, the method of generating the hosts was changed in commit 3a272b1 feat!: dynamically create hosts, and the deprecated system definitions removed in 7457109 main chore: remove deprecated static host config. See those commits for a state with a simpler config.

  mkFullHostConfigs (readHosts "darwin") false;

homeConfigurations

In contrast, this defines home-manager systems, which I only have one of, that serves as a template mostly.

  "swarsel@home-manager" = inputs.home-manager.lib.homeManagerConfiguration {
   pkgs = pkgsFor.x86_64-linux;
   extraSpecialArgs = { inherit inputs outputs; };
    modules = homeModules ++ mixedModules ++ [
      ./hosts/home-manager
    ];
  };

nixOnDroidConfigurations

Nix on Android also demands an own flake output, which is provided here.

  magicant = inputs.nix-on-droid.lib.nixOnDroidConfiguration {
   pkgs = pkgsFor.aarch64-linux;
    modules = [
      ./hosts/magicant
    ];
  };

System

System specific configuration

This section mainly exists house different `configuration.nix` files for system level configurations of NixOS systems as well as `home.nix` for user level configurations on all systems.

Physical hosts

live (ISO)
  { self, inputs, config, pkgs, lib, modulesPath, ... }:
  let
    pubKeys = lib.filesystem.listFilesRecursive "${self}/secrets/keys/ssh";
  in
  {

    imports = [

    inputs.lanzaboote.nixosModules.lanzaboote
    inputs.disko.nixosModules.disko
    inputs.impermanence.nixosModules.impermanence
    inputs.sops-nix.nixosModules.sops
    "${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix"
    "${modulesPath}/installer/cd-dvd/channel.nix"

    "${self}/profiles/iso//minimal.nix"

    ];


    isoImage = {
      makeEfiBootable = true;
      makeUsbBootable = true;
      squashfsCompression = "zstd -Xcompression-level 3";
    };

    nixpkgs = {
      hostPlatform = lib.mkDefault "x86_64-linux";
      config.allowUnfree = true;
    };

    services.getty.autologinUser = lib.mkForce "swarsel";

    users = {
      groups.swarsel = {};
      users = {
        swarsel = {
          name = "swarsel";
          group = "swarsel";
          isNormalUser = true;
          shell = pkgs.zsh;
          password = "setup"; # this is overwritten after install
          openssh.authorizedKeys.keys = lib.lists.forEach pubKeys (key: builtins.readFile key);
        };
        root = {
          shell = pkgs.zsh;
          password = lib.mkForce config.users.users.swarsel.password; # this is overwritten after install
          openssh.authorizedKeys.keys = config.users.users.swarsel.openssh.authorizedKeys.keys;
        };
      };
    };

    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";

    networking = {
      hostName = "live";
      wireless.enable = false;
    };

  }
Home-manager only (non-NixOS)

This is the "reference implementation" of a setup that runs without NixOS, only relying on home-manager. I try to test this every now and then and keep it supported. However, manual steps are needed to get the system to work fully, depending on what distribution you are running on.

  { self, inputs, outputs, config, ... }:
  {

    imports = builtins.attrValues outputs.homeManagerModules;

    nixpkgs = {
      overlays = [ outputs.overlays.default ];
      config = {
        allowUnfree = true;
      };
    };

    services.xcape = {
      enable = true;
      mapExpression = {
        Control_L = "Escape";
      };
    };

    programs.zsh.initExtra = "
    export GPG_TTY=\"$(tty)\"
    export SSH_AUTH_SOCK=$(gpgconf --list-dirs agent-ssh-socket)
    gpgconf --launch gpg-agent
          ";

    swarselsystems = {
      isLaptop = true;
      isNixos = false;
      wallpaper = self + /wallpaper/surfacewp.png;
      temperatureHwmon = {
        isAbsolutePath = true;
        path = "/sys/devices/platform/thinkpad_hwmon/hwmon/";
        input-filename = "temp1_input";
      };
      monitors = {
        main = {
          name = "California Institute of Technology 0x1407 Unknown";
          mode = "1920x1080"; # TEMPLATE
          scale = "1";
          position = "2560,0";
          workspace = "2:二";
          output = "eDP-1";
        };
      };
      inputs = {
        "1:1:AT_Translated_Set_2_keyboard" = {
          xkb_layout = "us";
          xkb_options = "grp:win_space_toggle";
          xkb_variant = "altgr-intl";
        };
      };
      keybindings = { };
    };

  }
nbl-imba-2 (Framework Laptop 16)

My work machine. Built for more security, this is the gold standard of my configurations at the moment.

  { self, inputs, outputs, config, pkgs, lib, ... }:
  let
    profilesPath = "${self}/profiles";
  in
  {

    imports = outputs.nixModules ++ [
      inputs.nixos-hardware.nixosModules.framework-16-7040-amd
      inputs.fw-fanctrl.nixosModules.default

      ./hardware-configuration.nix
      ./disk-config.nix

      "${profilesPath}/optional/nixos/virtualbox.nix"
      # "${profilesPath}/optional/nixos/vmware.nix"
      "${profilesPath}/optional/nixos/autologin.nix"
      "${profilesPath}/optional/nixos/nswitch-rcm.nix"
      "${profilesPath}/optional/nixos/gaming.nix"
      "${profilesPath}/optional/nixos/work.nix"

      inputs.home-manager.nixosModules.home-manager
      {
        home-manager.users.swarsel.imports =  outputs.mixedModules ++ [
          "${profilesPath}/optional/home/gaming.nix"
          "${profilesPath}/optional/home/work.nix"
        ] ++ (builtins.attrValues outputs.homeManagerModules);
      }
    ] ++ (builtins.attrValues outputs.nixosModules);


    nixpkgs = {
      overlays = [ outputs.overlays.default ];
      config = {
        allowUnfree = true;
      };
    };

    networking.networkmanager.wifi.scanRandMacAddress = false;

    boot = {
      loader.systemd-boot.enable = lib.mkForce false;
      loader.efi.canTouchEfiVariables = true;
      lanzaboote = {
        enable = true;
        pkiBundle = "/etc/secureboot";
      };
      supportedFilesystems = [ "btrfs" ];
      kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
      kernelParams = [
        "resume_offset=533760"
      ];
      resumeDevice = "/dev/disk/by-label/nixos";
    };

     hardware = {
       amdgpu = {
         opencl.enable = true;
         amdvlk = {
           enable = true;
           support32Bit.enable = true;
         };
       };
     };

    programs.fw-fanctrl.enable = true;

    networking = {
      hostName = "nbl-imba-2";
      fqdn = "nbl-imba-2.imp.univie.ac.at";
      firewall.enable = true;
    };


    services = {
      fwupd.enable = true;
      udev.extraRules = ''
        ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0bda", ATTR{idProduct}=="8156", ATTR{power/autosuspend}="20"
      '';
    };

    swarselsystems = {
      wallpaper = self + /wallpaper/lenovowp.png;
      hasBluetooth = true;
      hasFingerprint = true;
      impermanence = false;
      isBtrfs = true;
    };

    home-manager.users.swarsel.swarselsystems = {
      isLaptop = true;
      isNixos = true;
      isBtrfs = true;
      # temperatureHwmon = {
      #   isAbsolutePath = true;
      #   path = "/sys/devices/platform/thinkpad_hwmon/hwmon/";
      #   input-filename = "temp1_input";
      # };
      #  ------   -----
      # | DP-4 | |eDP-1|
      #  ------   -----
      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_WAYLAND=1 anki"; }
        { command = "OBSIDIAN_USE_WAYLAND=1 obsidian"; }
        { command = "nm-applet"; }
        { command = "teams-for-linux"; }
        { command = "1password"; }
        { command = "feishin"; }
      ];
      sharescreen = "eDP-2";
      lowResolution = "1280x800";
      highResolution = "2560x1600";
      monitors = {
        main = {
          name = "BOE 0x0BC9 Unknown";
          mode = "2560x1600"; # TEMPLATE
          scale = "1";
          position = "2560,0";
          workspace = "15:L";
          output = "eDP-2";
        };
        homedesktop = {
          name = "Philips Consumer Electronics Company PHL BDM3270 AU11806002320";
          mode = "2560x1440";
          scale = "1";
          position = "0,0";
          workspace = "1:一";
          output = "DP-11";
        };
        work_back_middle = {
          name = "LG Electronics LG Ultra HD 0x000305A6";
          mode = "2560x1440";
          scale = "1";
          position = "5120,0";
          workspace = "1:一";
          output = "DP-10";
        };
        work_front_left = {
          name = "LG Electronics LG Ultra HD 0x0007AB45";
          mode = "3840x2160";
          scale = "1";
          position = "5120,0";
          workspace = "1:一";
          output = "DP-7";
        };
        work_back_right = {
          name = "HP Inc. HP Z32 CN41212T55";
          mode = "3840x2160";
          scale = "1";
          position = "5120,0";
          workspace = "1:一";
          output = "DP-3";
        };
        work_middle_middle_main = {
          name = "HP Inc. HP 732pk CNC4080YL5";
          mode = "3840x2160";
          scale = "1";
          position = "-1280,0";
          workspace = "11:M";
          output = "DP-8";
        };
        work_middle_middle_side = {
          name = "Hewlett Packard HP Z24i CN44250RDT";
          mode = "1920x1200";
          transform = "270";
          scale = "1";
          position = "-2480,0";
          workspace = "12:S";
          output = "DP-9";
        };
        work_seminary = {
          name = "Applied Creative Technology Transmitter QUATTRO201811";
          mode = "1280x720";
          scale = "1";
          position = "10000,10000"; # i.e. this screen is inaccessible by moving the mouse
          workspace = "12:S";
          output = "DP-4";
        };
      };
      inputs = {
        "12972:18:Framework_Laptop_16_Keyboard_Module_-_ANSI_Keyboard" = {
          xkb_layout = "us";
          xkb_variant = "altgr-intl";
        };
        "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";
        };
        "1133:50504:Logitech_USB_Receiver" = {
          xkb_layout = "us";
          xkb_variant = "altgr-intl";
        };
        "1133:45944:MX_KEYS_S" = {
          xkb_layout = "us";
          xkb_variant = "altgr-intl";
        };
      };
      keybindings = {
        "Mod4+Ctrl+Shift+p" = "exec screenshare";
      };
      shellAliases = {
        ans2-15_3-9 = ". ~/.venvs/ansible39_2_15_0/bin/activate";
        ans3-9 = ". ~/.venvs/ansible39/bin/activate";
        ans = ". ~/.venvs/ansible/bin/activate";
        ans2-15 = ". ~/.venvs/ansible2.15.0/bin/activate";
      };
    };
  }
Winters (Server)
  { self, inputs, outputs, config, ... }:
  let
    profilesPath = "${self}/profiles";
  in
  {

    imports = [
      inputs.sops-nix.nixosModules.sops

      ./hardware-configuration.nix

      "${profilesPath}/optional/nixos/autologin.nix"
      "${profilesPath}/server/nixos"

      inputs.home-manager.nixosModules.home-manager
      {
        home-manager.users.swarsel.imports = [
      "${profilesPath}/server/home"
        ] ++ (builtins.attrValues outputs.homeManagerModules);
      }

    ] ++ (builtins.attrValues outputs.nixosModules);


    nixpkgs = {
      overlays = [ outputs.overlays.default ];
      config = {
        allowUnfree = true;
      };
    };

    boot = {
      loader.systemd-boot.enable = true;
      loader.efi.canTouchEfiVariables = true;
    };

    networking = {
      hostName = "winters";
      hostId = "b7778a4a";
      firewall.enable = true;
      enableIPv6 = false;
      firewall.allowedTCPPorts = [ 80 443 ];
    };


    swarselsystems = {
      hasBluetooth = false;
      hasFingerprint = false;
      impermanence = false;
      isBtrfs = false;
      flakePath = "/home/swarsel/.dotfiles";
      server = {
        enable = true;
        kavita = true;
        navidrome = true;
        jellyfin = true;
        spotifyd = true;
        mpd = false;
        matrix = true;
        nextcloud = true;
        immich = true;
        paperless = true;
        transmission = true;
        syncthing = true;
        monitoring = true;
        jenkins = false;
        emacs = true;
      };
    };

  }
nbm-imba-166 (MacBook Pro)
  { self, inputs, outputs, ... }:
  let
    profilesPath = "${self}/profiles";
  in
  {
    imports = [
      "${profilesPath}/darwin/common/nixos"

      inputs.home-manager.darwinModules.home-manager
      {
        home-manager.users."leon.schwarzaeugl".imports = [
          "${profilesPath}/darwin/common/home"
        ] ++ (builtins.attrValues outputs.homeManagerModules);
      }
    ] ++ (builtins.attrValues outputs.nixosModules);


    # Auto upgrade nix package and the daemon service.
    services.nix-daemon.enable = true;
    services.karabiner-elements.enable = true;

    home-manager.users."leon.schwarzaeugl".home = {
      username = lib.mkForce "leon.schwarzaeugl";
      swarselsystems = {
        isDarwin = true;
        isLaptop = true;
        isNixos = false;
        isBtrfs = false;
      };
    };


  }
Magicant (Phone)
  { pkgs, ... }: {
    environment = {
      packages = with pkgs; [
        vim
        git
        openssh
        # toybox
        dig
        man
        gnupg
      ];

      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
    '';
  }

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 it is still functional for now. 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 (mostly sync for medium-important data) and one other machine that I left for now as a reference.

Sync (OCI)
NixOS
  { self, config, inputs, pkgs, ... }:

  {
    imports = [

       inputs.sops-nix.nixosModules.sops
      ./hardware-configuration.nix
    ];

    environment.systemPackages = with pkgs; [
      git
      gnupg
      ssh-to-age
    ];

    services.xserver.xkb = {
      layout = "us";
      variant = "altgr-intl";
    };

    nix.settings.experimental-features = [ "nix-command" "flakes" ];

    sops = {
      age.sshKeyPaths = [ "/etc/ssh/sops" ];
      defaultSopsFile = "/root/.dotfiles/secrets/sync/secrets.yaml";
      validateSopsFiles = false;
      secrets.swarsel = { owner = "root"; };
      secrets.dnstokenfull = { owner = "acme"; };
      templates."certs.secret".content = ''
        CF_DNS_API_TOKEN=${config.sops.placeholder.dnstokenfull}
      '';
    };

    security.acme = {
      acceptTerms = true;
      preliminarySelfsigned = false;
      defaults.email = "mrswarsel@gmail.com";
      defaults.dnsProvider = "cloudflare";
      defaults.environmentFile = "${config.sops.templates."certs.secret".path}";
    };

    services.nginx = {
      enable = true;
      recommendedProxySettings = true;
      recommendedTlsSettings = true;
      recommendedOptimisation = true;
      recommendedGzipSettings = true;
      virtualHosts = {

        "synki.swarsel.win" = {
          enableACME = true;
          forceSSL = true;
          acmeRoot = null;
          locations = {
            "/" = {
              proxyPass = "http://localhost:27701";
              extraConfig = ''
                client_max_body_size 0;
              '';
            };
          };
        };

        "sync.swarsel.win" = {
          enableACME = true;
          forceSSL = true;
          acmeRoot = null;
          locations = {
            "/" = {
              proxyPass = "http://localhost:8384/";
              extraConfig = ''
                client_max_body_size 0;
              '';
            };
          };
        };

        "swagit.swarsel.win" = {
          enableACME = true;
          forceSSL = true;
          acmeRoot = null;
          locations = {
            "/" = {
              proxyPass = "http://localhost:3000";
              extraConfig = ''
                client_max_body_size 0;
              '';
            };
          };
        };
      };
    };

    boot.tmp.cleanOnBoot = true;
    zramSwap.enable = false;
    networking = {
      hostName = "sync";
      enableIPv6 = false;
      domain = "subnet03112148.vcn03112148.oraclevcn.com";
      firewall.extraCommands = ''
        iptables -I INPUT -m state --state NEW -p tcp --dport 80 -j ACCEPT
        iptables -I INPUT -m state --state NEW -p tcp --dport 443 -j ACCEPT
        iptables -I INPUT -m state --state NEW -p tcp --dport 27701 -j ACCEPT
        iptables -I INPUT -m state --state NEW -p tcp --dport 8384 -j ACCEPT
        iptables -I INPUT -m state --state NEW -p tcp --dport 3000 -j ACCEPT
        iptables -I INPUT -m state --state NEW -p tcp --dport 22000 -j ACCEPT
        iptables -I INPUT -m state --state NEW -p udp --dport 22000 -j ACCEPT
        iptables -I INPUT -m state --state NEW -p udp --dport 21027 -j ACCEPT
      '';
    };
    services.openssh = {
      enable = true;
      # settings.PermitRootLogin = "yes";
    };
    users.users.root.openssh.authorizedKeys.keyFiles = [
      "${self}/secrets/keys/ssh/nbl-imba-2.pub"
    ];

    system.stateVersion = "23.11"; # TEMPLATE - but probably no need to change

    environment.shellAliases = {
      nswitch = "cd ~/.dotfiles; git pull; nixos-rebuild --flake .#$(hostname) switch; cd -;";
    };

    boot.loader.grub.device = "nodev";

    services.anki-sync-server = {
      enable = true;
      port = 27701;
      address = "0.0.0.0";
      openFirewall = true;
      users = [
        {
          username = "Swarsel";
          passwordFile = config.sops.secrets.swarsel.path;
        }
      ];
    };

    services.syncthing = {
      enable = true;
      guiAddress = "0.0.0.0:8384";
      openDefaultPorts = true;
    };

    services.forgejo = {
      enable = true;
      settings = {
        DEFAULT = {
          APP_NAME = "~SwaGit~";
        };
        server = {
          PROTOCOL = "http";
          HTTP_PORT = 3000;
          HTTP_ADDR = "0.0.0.0";
          DOMAIN = "swagit.swarsel.win";
          ROOT_URL = "https://swagit.swarsel.win";
        };
        service = {
          DISABLE_REGISTRATION = true;
          SHOW_REGISTRATION_BUTTON = false;
        };
      };
    };

  }

Additions and modifications

In this section I define packages that I manually want to nixpkgs. 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 overlays:

  1. additions These are for the aforementioned added packages
  2. modification These are for packages that are on nixpkgs, but do not fit my usecase, meaning I need to perform modifications on them.
  3. nixpkgs-stable This is simply a mirror of the most recent stable branch of nixpkgs. Useful for packages that are broken on nixpkgs, but do not need to be on bleeding edge anyways.

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.

  { pkgs, ... }:
  let
    packageNames = [
      "pass-fuzzel"
      "cura5"
      "hm-specialisation"
      "cdw"
      "cdb"
      "bak"
      "timer"
      "e"
      "swarselcheck"
      "waybarupdate"
      "opacitytoggle"
      "fs-diff"
      "update-checker"
      "github-notifications"
      "screenshare"
      "bootstrap"
      "t2ts"
      "ts2t"
    ];
    mkPackages = names: builtins.listToAttrs (map (name: {
      inherit name;
      value = pkgs.callPackage ./${name} { };
    }) names);
    in
    mkPackages packageNames
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"
  { writeShellApplication, libnotify, pass, fuzzel, wtype }:

  writeShellApplication {
    name = "pass-fuzzel";
    runtimeInputs = [ libnotify (pass.withExtensions (exts: [ exts.pass-otp ])) fuzzel wtype ];
    text = builtins.readFile ../../scripts/pass-fuzzel.sh;
  }
cura5

The version of cura used to be quite outdated in nixpkgs. I am fetching a newer AppImage here and use that instead.

  { 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[@]}"
  ''
hm-specialisation

This script allows for quick git home-manager specialisation switching.

    { writeShellApplication, fzf, findutils, home-manager }:

    writeShellApplication {
      name = "hm-specialisation";
      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
      '';
    }
cdw

This script allows for quick git worktree switching.

  { writeShellApplication, fzf }:

  writeShellApplication {
    name = "cdw";
    runtimeInputs = [ fzf ];
    text = ''
      cd "$(git worktree list | fzf | awk '{print $1}')"
    '';
  }
cdb

This script allows for quick git branch switching.

  { writeShellApplication, fzf }:

  writeShellApplication {
    name = "cdb";
    runtimeInputs = [ fzf ];
    text = ''
      git checkout "$(git branch --list | grep -v "^\*" | fzf | awk '{print $1}')"
    '';
  }
bak

This script lets me quickly backup files by appending .bak to the filename.

  { writeShellApplication }:

  writeShellApplication {
    name = "bak";
    text = ''
      cp "$1"{,.bak}
    '';
  }
timer

This app starts a configuratble timer and uses TTS to say something once the timer runs out.

  { writeShellApplication, speechd }:

  writeShellApplication {
    name = "timer";
    runtimeInputs = [ speechd ];
    text = ''
      sleep "$1"; while true; do spd-say "$2"; sleep 0.5; done;
    '';
  }
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
  { writeShellApplication, emacs30-pgtk, sway, jq }:

  writeShellApplication {
    name = "e";
    runtimeInputs = [ emacs30-pgtk sway jq ];
    text = builtins.readFile ../../scripts/e.sh;
  }
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 --top-level --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 $?
  }
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 -T 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 -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
  { writeShellApplication, kitty, element-desktop-wayland, vesktop, spotify-player, jq }:

  writeShellApplication {
    name = "swarselcheck";
    runtimeInputs = [ kitty element-desktop-wayland vesktop spotify-player jq ];
    text = builtins.readFile ../../scripts/swarselcheck.sh;
  }
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="$HOME"/Documents/GitHub/CSE_TUWIEN/.git --work-tree="$HOME"/Documents/GitHub/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"
  { writeShellApplication, git }:

  writeShellApplication {
    name = "waybarupdate";
    runtimeInputs = [ git ];
    text = builtins.readFile ../../scripts/waybarupdate.sh;
  }
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
  { writeShellApplication, sway }:

  writeShellApplication {
    name = "opacitytoggle";
    runtimeInputs = [ sway ];
    text = builtins.readFile ../../scripts/opacitytoggle.sh;
  }
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
  { writeShellApplication }:

  writeShellApplication {
    name = "fs-diff";
    text = builtins.readFile ../../scripts/fs-diff.sh;
  }
update-checker

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.

  updates="$({ cd /home/swarsel/.dotfiles && nix flake lock --update-input nixpkgs && nix build .#nixosConfigurations."$(eval hostname)".config.system.build.toplevel && nvd diff /run/current-system ./result | grep -c '\[U'; } || true)"

  alt="has-updates"
  if [[ $updates -eq 0 ]]; then
      alt="updated"
  fi

  tooltip="System updated"
  if [[ $updates != 0 ]]; then
      tooltip=$(cd ~/.dotfiles && nvd diff /run/current-system ./result | grep -e '\[U' | awk '{ for (i=3; i<NF; i++) printf $i " "; if (NF >= 3) print $NF; }' ORS='\\n')
      echo "{ \"text\":\"$updates\", \"alt\":\"$alt\", \"tooltip\":\"$tooltip\" }"
  else
      echo "{ \"text\":\"\", \"alt\":\"$alt\", \"tooltip\":\"\" }"
  fi
  { writeShellApplication, nvd }:

  writeShellApplication {
    name = "update-checker";
    runtimeInputs = [ nvd ];
    text = builtins.readFile ../../scripts/update-checker.sh;
  }
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.

  { writeShellApplication, jq }:

  writeShellApplication {
    name = "github-notifications";
    runtimeInputs = [ jq ];
    text = ''
      count=$(curl -u Swarsel:"$(cat /run/user/1000/secrets/github_notif)" https://api.github.com/notifications | jq '. | length')

      if [[ "$count" != "0" ]]; then
          echo "{\"text\":\"$count\"}"
      fi
    '';
  }
screenshare
  SHARESCREEN="$(nix eval --raw ~/.dotfiles#nixosConfigurations."$(hostname)".config.home-manager.users."$(whoami)".swarselsystems.sharescreen)"

  touch /tmp/screenshare.state
  STATE=$(< /tmp/screenshare.state)

  if [[ $STATE != "1" ]]; then
      wl-mirror "$SHARESCREEN" &
      sleep 0.1
      swaymsg output "$SHARESCREEN" mode "$SWARSEL_LO_RES"
      echo 1 > /tmp/screenshare.state
      swaymsg '[app_id=at.yrlf.wl_mirror] move to workspace 12:S'
      swaymsg '[app_id=at.yrlf.wl_mirror] fullscreen'
  else
      swaymsg output "$SHARESCREEN" mode "$SWARSEL_HI_RES"
      echo 0 > /tmp/screenshare.state
      swaymsg '[app_id=at.yrlf.wl_mirror] kill'
  fi
  { writeShellApplication, sway }:

  writeShellApplication {
    name = "screenshare";
    runtimeInputs = [ sway ];
    text = builtins.readFile ../../scripts/screenshare.sh;
  }
bootstrap

This program sets up a new NixOS host.

  # highly inspired by https://github.com/EmergentMind/nix-config/blob/dev/scripts/bootstrap-nixos.sh
  set -eo pipefail

  target_hostname=""
  target_destination=""
  target_user="swarsel"
  ssh_port="22"
  temp=$(mktemp -d)

  function help_and_exit() {
      echo
      echo "Remotely installs NixOS on a target machine using this nix-config."
      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 "                                          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 "  --impermanence                          Use this flag if the target machine has impermanence enabled. WARNING: Assumes /persist path."
      echo "  --debug                                 Enable debug mode."
      echo "  -h | --help                             Print this help."
      exit 0
  }

  function cleanup() {
      rm -rf "$temp"
  }
  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
  }

  while [[ $# -gt 0 ]]; do
      case "$1" in
      -n)
          shift
          target_hostname=$1
          ;;
      -d)
          shift
          target_destination=$1
          ;;
      -u)
          shift
          target_user=$1
          ;;
      --port)
          shift
          ssh_port=$1
          ;;
      --temp-override)
          shift
          temp=$1
          ;;
      --debug)
          set -x
          ;;
      -h | --help) help_and_exit ;;
      ,*)
          echo "Invalid option detected."
          help_and_exit
          ;;
      esac
      shift
  done

  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"

  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 "Generating hardware-config.nix for $target_hostname and adding it to the nix-config."
  $ssh_root_cmd "nixos-generate-config --no-filesystems --root /mnt"
  mkdir profiles/"$target_hostname"
  $scp_cmd root@"$target_destination":/mnt/etc/nixos/hardware-configuration.nix "${git_root}"/profiles/"$target_hostname"/hardware-configuration.nix
  { writeShellApplication, openssh }:

  writeShellApplication {
    name = "bootstrap";
    runtimeInputs = [ openssh ];
    text = builtins.readFile ../../scripts/bootstrap.sh;
  }
t2ts

This script allows for quick git branch switching.

    { writeShellApplication }:

    writeShellApplication {
      name = "t2ts";
      runtimeInputs = [ ];
      text = ''
        date -d"$1" +%s
      '';
    }
ts2t

This script allows for quick git branch switching.

  { writeShellApplication }:

  writeShellApplication {
    name = "ts2t";
    runtimeInputs = [ ];
    text = ''
      date -d @"$1" 2>/dev/null || date -r "$1"
    '';
  }

Overlays (additions, overrides, nixpkgs-stable)

This file now holds all of the "nixpkgs-changes" that I am using across the configurations. Most notable here are the modifications, where I am editing derivations according to my needs.

  { inputs, ... }:

  let
    additions = final: _prev: import ../pkgs { pkgs = final; };
    modifications = _: _prev: {
      vesktop = _prev.vesktop.override {
        withSystemVencord = true;
      };

      firefox = _prev.firefox.override {
        nativeMessagingHosts = [
          _prev.tridactyl-native
          _prev.browserpass
          _prev.plasma5Packages.plasma-browser-integration
        ];
      };

      # prismlauncher = _prev.prismlauncher.override {
      #   glfw = _prev.glfw-wayland-minecraft;
      # };

      # #river = prev.river.overrideAttrs (oldAttrs: rec {
      #   pname = "river";
      #   version = "git";
      #   src = prev.fetchFromGitHub {
      #     owner = "riverwm";
      #     repo = pname;
      #     rev = "c16628c7f57c51d50f2d10a96c265fb0afaddb02";
      #     hash = "sha256-E3Xtv7JeCmafiNmpuS5VuLgh1TDAbibPtMo6A9Pz6EQ=";
      #     fetchSubmodules = true;
      #   };
      # });
    };

    nixpkgs-stable = final: _prev: {
      stable = import inputs.nixpkgs-stable {
        inherit (final) system;
        config.allowUnfree = true;
      };
    };

    zjstatus = _: _prev: {
      zjstatus = inputs.zjstatus.packages.${_prev.system}.default;
    };

  in
  {
    default =
      final: prev:

      (additions final prev)
      // (modifications final prev)
      // (nixpkgs-stable final prev)
      // (zjstatus final prev)
      // (inputs.nur.overlays.default final prev)
      // (inputs.emacs-overlay.overlay final prev)
      // (inputs.nixgl.overlay final prev);

  }

Modules

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.

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

let
  moduleNames = [
    "wallpaper"
    "hardware"
    "setup"
    "impermanence"
    "filesystem"
    "input"
  ];

  mkImports = names: builtins.listToAttrs (map (name: {
    inherit name;
    value = import ./${name}.nix;
  }) names);

in
  mkImports moduleNames
Wallpaper

This lets me set the wallpaper that I want to use. Duplicated with home-manager options because mixing system and user level configuration is not a good idea.

  { lib, ... }:

  {
    options.swarselsystems.wallpaper = lib.mkOption {
      type = lib.types.path;
      default = "";
    };
  }
Hardware

This lets me set some basic flags about the hardware of the configured systems.

  { lib, ... }:

  {
    options.swarselsystems.hasBluetooth = lib.mkEnableOption "bluetooth availability";
    options.swarselsystems.hasFingerprint = lib.mkEnableOption "fingerprint sensor availability";
    options.swarselsystems.trackpoint.isAvailable = lib.mkEnableOption "trackpoint availability";
    options.swarselsystems.trackpoint.device = lib.mkOption {
      type = lib.types.str;
      default = "";
    };
  }
Setup

I usually use mutableUsers = false in my NixOS configuration. However, on a new system where sops-keys have not been deployed, this would immediately lock me out of the system. Hence this flag can be used until sops-keys are created.

  { lib, ... }:
  let
    inherit (lib) mkOption types;
  in

  {
    options.swarselsystems.flakePath = mkOption {
      type = types.str;
      default = "";
    };
    options.swarselsystems.initialSetup = lib.mkEnableOption "initial setup (no sops keys available)";
    options.swarselsystems.server.enable = lib.mkEnableOption "is a server machine";
    options.swarselsystems.server.kavita = lib.mkEnableOption "enable kavita on server";
    options.swarselsystems.server.jellyfin = lib.mkEnableOption "enable jellyfin on server";
    options.swarselsystems.server.navidrome = lib.mkEnableOption "enable navidrome on server";
    options.swarselsystems.server.spotifyd = lib.mkEnableOption "enable spotifyd on server";
    options.swarselsystems.server.mpd = lib.mkEnableOption "enable mpd on server";
    options.swarselsystems.server.matrix = lib.mkEnableOption "enable matrix on server";
    options.swarselsystems.server.nextcloud = lib.mkEnableOption "enable nextcloud on server";
    options.swarselsystems.server.immich = lib.mkEnableOption "enable immich on server";
    options.swarselsystems.server.paperless = lib.mkEnableOption "enable paperless on server";
    options.swarselsystems.server.transmission = lib.mkEnableOption "enable transmission and friends on server";
    options.swarselsystems.server.syncthing = lib.mkEnableOption "enable syncthing on server";
    options.swarselsystems.server.restic = lib.mkEnableOption "enable restic backups on server";
    options.swarselsystems.server.monitoring = lib.mkEnableOption "enable monitoring on server";
    options.swarselsystems.server.jenkins = lib.mkEnableOption "enable jenkins on server";
    options.swarselsystems.server.emacs = lib.mkEnableOption "enable emacs server on server";
  }
Input
  { lib, ... }:
  let
    inherit (lib) mkOption types;
  in
  {
    options.swarselsystems.shellAliases = mkOption {
      type = types.attrsOf types.str;
      default = { };
    };
  }
Impermanence

Option to enable impermanence configurations. This could also be done via optional imports, but impermanence is a "big enough" change to warrant a line in the machine default.nix.

  { lib, ... }:

  {
    options.swarselsystems.impermanence = lib.mkEnableOption "use impermanence on this system";
  }
Filesystem

This lets me quickly set flags for "special" file systems. These options mostly function in conjunction with other settings (for example, the isBtrfs function is mostly used for impermanence configuration).

  { lib, ... }:

  {
    options.swarselsystems.isBtrfs = lib.mkEnableOption "use btrfs filesystem";
  }
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.

  let
    moduleNames = [
      "laptop"
      "hardware"
      "monitors"
      "input"
      "nixos"
      "darwin"
      "waybar"
      "startup"
      "wallpaper"
      "filesystem"
      "firefox"
    ];

    mkImports = names: builtins.listToAttrs (map (name: {
      inherit name;
      value = import ./${name}.nix;
    }) names);

  in
    mkImports moduleNames
Laptop

Laptops are not always plugged in, so they should show a battery icon in Waybar. Also, most laptops have a touchpad which usually needs to be configured.

  { lib, config, ... }:
  {
    options.swarselsystems.isLaptop = lib.mkEnableOption "laptop host";
    config.swarselsystems.touchpad = lib.mkIf config.swarselsystems.isLaptop {
      "type:touchpad" = {
        dwt = "enabled";
        tap = "enabled";
        natural_scroll = "enabled";
        middle_emulation = "enabled";
      };
    };
    config.swarselsystems.waybarModules = lib.mkIf config.swarselsystems.isLaptop [
      "custom/outer-left-arrow-dark"
      "mpris"
      "custom/left-arrow-light"
      "network"
      "custom/vpn"
      "custom/left-arrow-dark"
      "pulseaudio"
      "custom/left-arrow-light"
      "battery"
      "custom/left-arrow-dark"
      "group/hardware"
      "custom/left-arrow-light"
      "clock#2"
      "custom/left-arrow-dark"
      "clock#1"
    ];
  }
Hardware

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

  { lib, ... }:

  {
    options.swarselsystems.cpuCount = lib.mkOption {
      type = lib.types.int;
      default = 8;
    };
    options.swarselsystems.temperatureHwmon.isAbsolutePath = lib.mkEnableOption "absolute temperature path";
    options.swarselsystems.temperatureHwmon.path = lib.mkOption {
      type = lib.types.str;
      default = "";
    };
    options.swarselsystems.temperatureHwmon.input-filename = lib.mkOption {
      type = lib.types.str;
      default = "";
    };
  }
Waybar

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 most part of this configuration is done here: Waybar

  { lib, config, ... }:

  let
    generateIcons = n: lib.concatStringsSep " " (builtins.map (x: "{icon" + toString x + "}") (lib.range 0 (n - 1)));
  in
  {
    options.swarselsystems.cpuString = lib.mkOption {
      type = lib.types.str;
      default = generateIcons config.swarselsystems.cpuCount;
      description = "The generated icons string for use by Waybar.";
      internal = true;
    };
    options.swarselsystems.waybarModules = lib.mkOption {
      type = lib.types.listOf lib.types.str;
      default = [
        "custom/outer-left-arrow-dark"
        "mpris"
        "custom/left-arrow-light"
        "network"
        "custom/vpn"
        "custom/left-arrow-dark"
        "pulseaudio"
        "custom/left-arrow-light"
        "custom/pseudobat"
        "battery"
        "custom/left-arrow-dark"
        "group/hardware"
        "custom/left-arrow-light"
        "clock#2"
        "custom/left-arrow-dark"
        "clock#1"
      ];
    };
  }
Monitors

This allows me to define my monitors in the machine's default.nix.

  { lib, ... }:
  let
    inherit (lib) mkOption types;
  in
  {
    options.swarselsystems.monitors = mkOption {
      type = types.attrsOf (types.attrsOf types.str);
      default = { };
    };
    options.swarselsystems.sharescreen = mkOption {
      type = types.str;
      default = "";
    };
    options.swarselsystems.lowResolution = mkOption {
      type = types.str;
      default = "";
    };
    options.swarselsystems.highResolution = mkOption {
      type = types.str;
      default = "";
    };
  }
Input

This allows me to configure input options. Here, I am globally defining my split keyboards. Then, I am joining some attribute sets so that they can be easier used in the rest of the configurations.

  { lib, config, ... }:
  let
    inherit (lib) mkOption types;
  in
  {
    options.swarselsystems.inputs = mkOption {
      type = types.attrsOf (types.attrsOf types.str);
      default = { };
    };
    options.swarselsystems.kyria = mkOption {
      type = types.attrsOf (types.attrsOf 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";
        };
      };
    };
    options.swarselsystems.touchpad = mkOption {
      type = types.attrsOf (types.attrsOf types.str);
      default = { };
    };
    options.swarselsystems.standardinputs = mkOption {
      type = types.attrsOf (types.attrsOf types.str);
      default = lib.recursiveUpdate (lib.recursiveUpdate config.swarselsystems.touchpad config.swarselsystems.kyria) config.swarselsystems.inputs;
      internal = true;
    };
    options.swarselsystems.keybindings = mkOption {
      type = types.attrsOf types.str;
      default = { };
    };
    options.swarselsystems.shellAliases = mkOption {
      type = types.attrsOf types.str;
      default = { };
    };
  }
Nixos

These are some extra options that will be used if the machine also runs NixOS. For example, non-NixOS hosts need nixGL prepended to most graphic commands, and swayfx works less nicely on these machines.

  { lib, config, ... }:
  {
    options.swarselsystems.isNixos = lib.mkEnableOption "nixos host";
    config.swarselsystems.startup = lib.mkIf (!config.swarselsystems.isNixos) [
      {
        command = "sleep 60 && nixGL nextcloud --background";
      }
      { command = "sleep 60 && nixGL vesktop --start-minimized -enable-features=UseOzonePlatform -ozone-platform=wayland"; }
      { command = "sleep 60 && nixGL syncthingtray --wait"; }
      { command = "sleep 60 && ANKI_WAYLAND=1 nixGL anki"; }
      { command = "nm-applet --indicator"; }
      { command = "sleep 60 && OBSIDIAN_USE_WAYLAND=1 nixGL obsidian -enable-features=UseOzonePlatform -ozone-platform=wayland"; }
      { command = "sleep 60 && element-desktop --hidden  -enable-features=UseOzonePlatform -ozone-platform=wayland --disable-gpu-driver-bug-workarounds"; }
    ];
    options.swarselsystems.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.swarselsystems.swayfxConfig = lib.mkIf (!config.swarselsystems.isNixos) " ";
  }
darwin
{ lib, ... }:
{
  options.swarselsystems.isDarwin = lib.mkEnableOption "darwin host";

}
System startup

This defines programs I want to have starting when I start the system

Part of the startup is also defined in Sway. The distinction is as follows. As this configuration also needs to work on systems that are running only home manager, I probably need to run nixGL or something similar on those systems to get these graphic apps to display properly. In this section we only define such graphical programs, in the other location we only put shell applications and such.

These other apps currently include:

  • spotifytui
  • kitty

Do not that syncthingtray is also not mentioned here. It is installed as a home manager package that automatically starts at system start.

{ lib, ... }:
let
  inherit (lib) mkOption types;
in
{

  options.swarselsystems.startup = mkOption {
    type = types.listOf (types.attrsOf 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_WAYLAND=1 anki"; }
      { command = "OBSIDIAN_USE_WAYLAND=1 obsidian"; }
      { command = "nm-applet"; }
      { command = "feishin"; }
    ];
  };
}
Wallpaper

Again, I set the wallpaper here for stylix.

  { lib, ... }:

  {
    options.swarselsystems.wallpaper = lib.mkOption {
      type = lib.types.path;
      default = "";
    };
  }
Filesystem

Another duplicated option for the filesystem.

  { lib, ... }:

  {
    options.swarselsystems.isBtrfs = lib.mkEnableOption "use btrfs filesystem";
  }
firefox

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

{ lib, pkgs, ... }:
  let
    lock-false = {
      Value = false;
      Status = "locked";
    };
    lock-true = {
      Value = true;
      Status = "locked";
    };
  in
{
  options.swarselsystems.firefox = lib.mkOption {
    type = lib.types.attrs;
    default = {
      isDefault = false;
      userChrome = builtins.readFile ../../programs/firefox/chrome/userChrome.css;
      extensions = 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
        (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" = lock-true;
          "toolkit.legacyUserProfileCustomizations.stylesheets" = lock-true;
          "browser.search.suggest.enabled" = lock-false;
          "browser.search.suggest.enabled.private" = lock-false;
          "browser.urlbar.suggest.searches" = lock-false;
          "browser.urlbar.showSearchSuggestionsFirst" = lock-false;
          "browser.topsites.contile.enabled" = lock-false;
          "browser.newtabpage.activity-stream.feeds.section.topstories" = lock-false;
          "browser.newtabpage.activity-stream.feeds.snippets" = lock-false;
          "browser.newtabpage.activity-stream.section.highlights.includePocket" = lock-false;
          "browser.newtabpage.activity-stream.section.highlights.includeBookmarks" = lock-false;
          "browser.newtabpage.activity-stream.section.highlights.includeDownloads" = lock-false;
          "browser.newtabpage.activity-stream.section.highlights.includeVisited" = lock-false;
          "browser.newtabpage.activity-stream.showSponsored" = lock-false;
          "browser.newtabpage.activity-stream.system.showSponsored" = lock-false;
          "browser.newtabpage.activity-stream.showSponsoredTopSites" = lock-false;
        };

      search = {
        default = "Kagi";
        privateDefault = "Kagi";
        engines = {
          "Kagi" = {
            urls = [{
              template = "https://kagi.com/search";
              params = [
                { name = "q"; value = "{searchTerms}"; }
              ];
            }];
            iconUpdateURL = "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}";
            }];
            iconUpdateURL = "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" ];
          };

          "Google".metaData.alias = "@g";
        };
        force = true; # this is required because otherwise the search.json.mozlz4 symlink gets replaced on every firefox restart
      };
    };
  };


}

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.

Common

These are system-level settings specific to NixOS machines. All settings that are required on all machines go here.

Imports, non-server settings

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.

  _:
  {
    imports = [
      ./settings.nix
      ./home-manager.nix
      ./xserver.nix
      ./users.nix
      ./env.nix
      ./stylix.nix
      ./polkit.nix
      ./gc.nix
      ./store.nix
      ./systemd.nix
      ./network.nix
      ./time.nix
      ./hardware.nix
      ./pipewire.nix
      ./sops.nix
      ./packages.nix
      ./programs.nix
      ./zsh.nix
      ./syncthing.nix
      ./blueman.nix
      ./networkdevices.nix
      ./gvfs.nix
      ./interceptiontools.nix
      ./hardwarecompatibility.nix
      ./login.nix
      ./stylix.nix
      ./power-profiles-daemon.nix
      # ./impermanence.nix
      ./nvd-rebuild.nix
      ./nix-ld.nix
      ./gnome-keyring.nix
      ./sway.nix
      ./xdg-portal.nix
      # ./yubikey-touch-detector.nix
      # ./safeeyes.nix
      ./distrobox.nix
      ./lid.nix
    ];

    nixpkgs.config.permittedInsecurePackages = [
      "jitsi-meet-1.0.8043"
      "electron-29.4.6"
    ];

  }
General NixOS settings (enable home-manager module, stateVersion)

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, inputs, ... }:
  {
    nix =
      let
        flakeInputs = lib.filterAttrs (_: lib.isType "flake") inputs;
      in
      {
        settings = {
          experimental-features = [
            "nix-command"
            "flakes"
            "ca-derivations"
            "pipe-operators"
          ];
          trusted-users = [ "swarsel" ];
          flake-registry = "";
          warn-dirty = false;
        };
        channel.enable = false;
        registry = lib.mapAttrs (_: flake: { inherit flake; }) flakeInputs;
        nixPath = lib.mapAttrsToList (n: _: "${n}=flake:${n}") flakeInputs;
      };


    system.stateVersion = lib.mkDefault "23.05";

  }
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.

  { pkgs, ... }:
  {
    environment.systemPackages = with pkgs; [
      # yubikey packages
      gnupg
      yubikey-personalization
      yubikey-personalization-gui
      yubico-pam
      yubioath-flutter
      yubikey-manager
      yubikey-manager-qt
      yubikey-touch-detector
      yubico-piv-tool
      cfssl
      pcsctools
      pcscliteWithPolkit.out

      # ledger packages
      ledger-live-desktop

      # pinentry
      dbus
      swaylock-effects
      syncthingtray-minimal

      # secure boot
      sbctl

      nix-index

      # better make for general tasks
      just

      # keyboards
      qmk
      vial
      via

      # theme related
      adwaita-icon-theme

      # kde-connect
      xdg-desktop-portal
      xdg-desktop-portal-wlr

      # bluetooth
      bluez

      # lsp-related -------------------------------
      # nix
      # latex
      texlab
      ghostscript_headless
      # wireguard
      wireguard-tools
      # rust
      rust-analyzer
      clippy
      rustfmt
      # go
      go
      gopls
      # nix
      nixd
      # zig
      zig
      zls
      # cpp
      clang-tools
      # + cuda
      cudatoolkit
      # ansible
      ansible-lint
      ansible-language-server
      molecule
      #lsp-bridge / python
      gcc
      gdb
      (python3.withPackages (ps: with ps; [ jupyter ipython pyqt5 epc orjson sexpdata six setuptools paramiko numpy pandas scipy matplotlib requests debugpy flake8 gnureadline python-lsp-server ]))
      # (python3.withPackages(ps: with ps; [ jupyter ipython pyqt5 numpy pandas scipy matplotlib requests debugpy flake8 gnureadline python-lsp-server]))
      # --------------------------------------------

      (stdenv.mkDerivation {
        name = "oama";

        src = pkgs.fetchurl {
          name = "oama";
          url = "https://github.com/pdobsan/oama/releases/download/0.13.1/oama-0.13.1-Linux-x86_64-static.tgz";
          sha256 = "sha256-OTdCObVfnMPhgZxVtZqehgUXtKT1iyqozdkPIV+i3Gc=";
        };

        phases = [
          "unpackPhase"
        ];

        unpackPhase = ''
          mkdir -p $out/bin
          tar xvf $src -C $out/
          mv $out/oama-0.13.1-Linux-x86_64-static/oama $out/bin/
        '';

      })

    ];
  }
Setup home-manager

First, we enable the use of home-manager as a NixoS modul.

  { inputs, ... }:
  {
    home-manager = {
      useGlobalPkgs = true;
      useUserPackages = true;
      extraSpecialArgs = inputs; # used mainly for inputs.self
    };
  }
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 extremly comfortable to use

  _:
  {
    services.xserver = {
      xkb = {
        layout = "us";
        variant = "altgr-intl";
      };
    };
  }
User setup, Make users non-mutable

This ensures that all user-configuration happens here in the config file.

  { pkgs, config, lib, ... }:
  {
    sops.secrets.swarseluser = { neededForUsers = true; };

    users = {
      mutableUsers = lib.mkIf (!config.swarselsystems.initialSetup) false;
      users.swarsel = {
        isNormalUser = true;
        description = "Leon S";
        hashedPasswordFile = lib.mkIf (!config.swarselsystems.initialSetup) config.sops.secrets.swarseluser.path;
        extraGroups = [ "networkmanager" "syncthing" "docker" "wheel" "lp" "audio" "video" "vboxusers" "libvirtd" "scanner" ];
        packages = with pkgs; [ ];
      };
    };
  }
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, pkgs, ... }:
  {
    environment = {
      wordlist.enable = true;
      sessionVariables = {
        NIXOS_OZONE_WL = "1";
        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
        ]);
      };
    };
    # gstreamer plugins for nautilus (used for file metadata)
  }
Security

Needed for control over system-wide privileges etc.

  _:
  {

    security.pam.services = {
      login.u2fAuth = true;
      sudo.u2fAuth = true;
      swaylock.u2fAuth = true;
      swaylock.fprintAuth = false;
    };
    security.polkit.enable = true;


  }
Enable automatic garbage collection

The nix store fills up over time, until /boot/efi is filled. This snippet cleans it automatically on a weekly basis.

  _:
  {
    nix.gc = {
      automatic = true;
      randomizedDelaySec = "14m";
      dates = "weekly";
      options = "--delete-older-than 10d";
    };
  }
Enable automatic store optimisation

This enables hardlinking identical files in the nix store, to save on disk space. I have read this incurs a significant I/O overhead, I need to keep an eye on this.

  _:
  {
    nix.optimise = {
      automatic = true;
      dates = [ "weekly" ];
    };
  }
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.

  _:
  {
    # systemd
    systemd.extraConfig = ''
      DefaultTimeoutStartSec=60s
      DefaultTimeoutStopSec=15s
    '';
  }
Hardware settings

Enable OpenGL, Sound, Bluetooth and various drivers.

  { pkgs, config, lib, ... }:
  {

    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;


      pulseaudio = {
        enable = lib.mkIf (!config.services.pipewire.enable) true;
        package = pkgs.pulseaudioFull;
      };

      enableAllFirmware = 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;
  }
Pipewire

Pipewire handles communication on Wayland. This enables several sound tools as well as screen sharing in combinaton with xdg-desktop-portal-wlr.

  _: {
    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;
      };
    };
  }
Common network settings

Here I only enable networkmanager. Most of the 'real' network config is done in System specific configuration.

  { lib, config, ... }:
  {
    networking = {
      nftables.enable = lib.mkDefault true;
      enableIPv6 = lib.mkDefault true;
      firewall = {
        checkReversePath = lib.mkDefault false;
        enable = lib.mkDefault true;
        allowedUDPPorts = [ 51820 ]; # 51820: wireguard
        allowedTCPPortRanges = [
          { from = 1714; to = 1764; } # kde-connect
        ];
        allowedUDPPortRanges = [
          { from = 1714; to = 1764; } # kde-connect
        ];
      };

      networkmanager = {
        enable = true;
        ensureProfiles = {
          environmentFiles = [
            "${config.sops.templates."network-manager.env".path}"
          ];
          profiles = {
            "Ernest Routerford" = {
              connection = {
                id = "Ernest Routerford";
                permissions = "";
                type = "wifi";
              };
              ipv4 = {
                dns-search = "";
                method = "auto";
              };
              ipv6 = {
                addr-gen-mode = "stable-privacy";
                dns-search = "";
                method = "auto";
              };
              wifi = {
                mac-address-blacklist = "";
                mode = "infrastructure";
                ssid = "Ernest Routerford";
              };
              wifi-security = {
                auth-alg = "open";
                key-mgmt = "wpa-psk";
                psk = "$ERNEST";
              };
            };

            LAN-Party = {
              connection = {
                autoconnect = "false";
                id = "LAN-Party";
                type = "ethernet";
              };
              ethernet = {
                auto-negotiate = "true";
                cloned-mac-address = "preserve";
                mac-address = "90:2E:16:D0:A1:87";
              };
              ipv4 = { method = "shared"; };
              ipv6 = {
                addr-gen-mode = "stable-privacy";
                method = "auto";
              };
              proxy = { };
            };

            eduroam = {
              "802-1x" = {
                eap = "ttls;";
                identity = "$EDUID";
                password = "$EDUPASS";
                phase2-auth = "mschapv2";
              };
              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 = { };
            };

            HH40V_39F5 = {
              connection = {
                id = "HH40V_39F5";
                type = "wifi";
              };
              ipv4 = { method = "auto"; };
              ipv6 = {
                addr-gen-mode = "stable-privacy";
                method = "auto";
              };
              proxy = { };
              wifi = {
                band = "bg";
                mode = "infrastructure";
                ssid = "HH40V_39F5";
              };
              wifi-security = {
                key-mgmt = "wpa-psk";
                psk = "$FRAUNS";
              };
            };

            magicant = {
              connection = {
                id = "magicant";
                type = "wifi";
              };
              ipv4 = { method = "auto"; };
              ipv6 = {
                addr-gen-mode = "default";
                method = "auto";
              };
              proxy = { };
              wifi = {
                mode = "infrastructure";
                ssid = "magicant";
              };
              wifi-security = {
                auth-alg = "open";
                key-mgmt = "wpa-psk";
                psk = "$HANDYHOTSPOT";
              };
            };

            wireguardvpn = {
              connection = {
                id = "HomeVPN";
                type = "wireguard";
                autoconnect = "false";
                interface-name = "wg1";
              };
              wireguard = { private-key = "$WIREGUARDPRIV"; };
              "wireguard-peer.$WIREGUARDPUB" = {
                endpoint = "$WIREGUARDENDPOINT";
                allowed-ips = "0.0.0.0/0";
              };
              ipv4 = {
                method = "ignore";
                address1 = "192.168.3.3/32";
              };
              ipv6 = {
                addr-gen-mode = "stable-privacy";
                method = "ignore";
              };
              proxy = { };
            };

            "sweden-aes-128-cbc-udp-dns" = {
              connection = {
                autoconnect = "false";
                id = "PIA Sweden";
                type = "vpn";
              };
              ipv4 = { method = "auto"; };
              ipv6 = {
                addr-gen-mode = "stable-privacy";
                method = "auto";
              };
              proxy = { };
              vpn = {
                auth = "sha1";
                ca =
                  "${config.users.users.swarsel.home}/.dotfiles/secrets/certs/sweden-aes-128-cbc-udp-dns-ca.pem";
                challenge-response-flags = "2";
                cipher = "aes-128-cbc";
                compress = "yes";
                connection-type = "password";
                crl-verify-file = "${config.users.users.swarsel.home}/.dotfiles/secrets/certs/sweden-aes-128-cbc-udp-dns-crl-verify.pem";
                dev = "tun";
                password-flags = "0";
                remote = "sweden.privacy.network:1198";
                remote-cert-tls = "server";
                reneg-seconds = "0";
                service-type = "org.freedesktop.NetworkManager.openvpn";
                username = "$VPNUSER";
              };
              vpn-secrets = { password = "$VPNPASS"; };
            };

            Hotspot = {
              connection = {
                autoconnect = "false";
                id = "Hotspot";
                type = "wifi";
              };
              ipv4 = { method = "shared"; };
              ipv6 = {
                addr-gen-mode = "default";
                method = "ignore";
              };
              proxy = { };
              wifi = {
                mode = "ap";
                ssid = "Hotspot-swarsel";
              };
              wifi-security = {
                group = "ccmp;";
                key-mgmt = "wpa-psk";
                pairwise = "ccmp;";
                proto = "rsn;";
                psk = "$HOTSPOT";
              };
            };

          };
        };
      };
    };

    systemd.services.NetworkManager-ensure-profiles.after = [ "NetworkManager.service" ];
  }
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.

  _:
  {
    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";
      };
    };
  }
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/keys/NAME.pub
  • update entry for sops.age.sshKeyPaths
  { config, lib, ... }:
  let
    mkIfElse = p: yes: no: lib.mkMerge [
      (lib.mkIf p yes)
      (lib.mkIf (!p) no)
    ];
  in
  {
    sops = {

      age.sshKeyPaths = mkIfElse config.swarselsystems.isBtrfs [ "/persist/.ssh/sops" ] [ "${config.users.users.swarsel.home}/.ssh/sops" ];
      defaultSopsFile = mkIfElse config.swarselsystems.isBtrfs "/persist/.dotfiles/secrets/general/secrets.yaml" "${config.users.users.swarsel.home}/.dotfiles/secrets/general/secrets.yaml";

      validateSopsFiles = false;

      secrets = {
        ernest = { };
        frauns = { };
        hotspot = { };
        eduid = { };
        edupass = { };
        handyhotspot = { };
        vpnuser = { };
        vpnpass = { };
        wireguardpriv = { };
        wireguardpub = { };
        wireguardendpoint = { };
        stashuser = { };
        stashpass = { };
        githubforgeuser = { };
        githubforgepass = { };
        gitlabforgeuser = { };
        gitlabforgepass = { };
      };
      templates = {
        "network-manager.env".content = ''
          ERNEST=${config.sops.placeholder.ernest}
          FRAUNS=${config.sops.placeholder.frauns}
          HOTSPOT=${config.sops.placeholder.hotspot}
          EDUID=${config.sops.placeholder.eduid}
          EDUPASS=${config.sops.placeholder.edupass}
          HANDYHOTSPOT=${config.sops.placeholder.handyhotspot}
          VPNUSER=${config.sops.placeholder.vpnuser}
          VPNPASS=${config.sops.placeholder.vpnpass}
          WIREGUARDPRIV=${config.sops.placeholder.wireguardpriv}
          WIREGUARDPUB=${config.sops.placeholder.wireguardpub}
          WIREGUARDENDPOINT=${config.sops.placeholder.wireguardendpoint}
        '';
        ".authinfo" = {
          owner = "swarsel";
          path = "${config.users.users.swarsel.home}/.emacs.d/.authinfo";
          content = ''
            machine stash.swarsel.win:443 port https login ${config.sops.placeholder.stashuser} password ${config.sops.placeholder.stashpass}
            machine gitlab.com/api/v4 login ${config.sops.placeholder.githubforgeuser} password ${config.sops.placeholder.githubforgepass}
            machine api.github.com login ${config.sops.placeholder.gitlabforgeuser} password ${config.sops.placeholder.gitlabforgepass}
          '';
        };
      };
    };
  }
Theme (stylix)

By default, stylix wants to style GRUB as well. However, I think that looks horrible. theme is defined in Theme (stylix).

  { self, pkgs, home-manager, config, ... }:
  {
    stylix = {
      <<theme>>
      targets.grub.enable = false; # the styling makes grub more ugly
      image = config.swarselsystems.wallpaper;
    };
    home-manager.users.swarsel = {
      stylix = {
        targets = {
          emacs.enable = false;
          waybar.enable = false;
        };
      };
    };
  }
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.

  _:
  {
    programs = {
      dconf.enable = true;
      evince.enable = true;
      kdeconnect.enable = true;
    };
  }
zsh

Do not touch this.

  { pkgs, ... }:
  {
    programs.zsh.enable = true;
    users.defaultUserShell = pkgs.zsh;
    environment.shells = with pkgs; [ zsh ];
    environment.pathsToLink = [ "/share/zsh" ];
  }
syncthing
  _:
  {
    services.syncthing = {
      enable = true;
      user = "swarsel";
      dataDir = "/home/swarsel";
      configDir = "/home/swarsel/.config/syncthing";
      openDefaultPorts = true;
      settings = {
        devices = {
          "magicant" = {
            id = "VMWGEE2-4HDS2QO-KNQOVGN-LXLX6LA-666E4EK-ZBRYRRO-XFEX6FB-6E3XLQO";
          };
          "sync (@oracle)" = {
            id = "ETW6TST-NPK7MKZ-M4LXMHA-QUPQHDT-VTSHH5X-CR5EIN2-YU7E55F-MGT7DQB";
          };
          "winters" = {
            id = "O7RWDMD-AEAHPP7-7TAVLKZ-BSWNBTU-2VA44MS-EYGUNBB-SLHKB3C-ZSLMOAA";
          };
        };
        folders = {
          "Default Folder" = {
            path = "/home/swarsel/Sync";
            devices = [ "sync (@oracle)" "magicant" "winters" ];
            id = "default";
          };
          "Obsidian" = {
            path = "/home/swarsel/Nextcloud/Obsidian";
            devices = [ "sync (@oracle)" "magicant" "winters" ];
            id = "yjvni-9eaa7";
          };
          "Org" = {
            path = "/home/swarsel/Nextcloud/Org";
            devices = [ "sync (@oracle)" "magicant" "winters" ];
            id = "a7xnl-zjj3d";
          };
          "Vpn" = {
            path = "/home/swarsel/Vpn";
            devices = [ "sync (@oracle)" "magicant" "winters" ];
            id = "hgp9s-fyq3p";
          };
          ".elfeed" = {
            path = "/home/swarsel/.elfeed";
            devices = [ "sync (@oracle)" "magicant" "winters" ];
            id = "h7xbs-fs9v1";
          };
        };
      };
    };
  }
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.

blueman

Enables the blueman service including the nice system tray icon.

  _:
  {
    services.blueman.enable = true;
    services.hardware.bolt.enable = true;
  }
Network devices

In this section we enable compatibility with several network devices I have at home, mainly printers and scanners.

Scanners

This allows me to use my big scanner/printer's scanning function over the network.

  { pkgs, ... }:
  {
    # enable scanners over network
    hardware.sane = {
      enable = true;
      extraBackends = [ pkgs.sane-airscan ];
    };
Printers

This allows me to use my big scanner/printer's printing function over the network. Most of the settings are driver related.

  # 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
    '';
  };
Avahi (device discovery)

Avahi is the service used for the network discovery.

  services.avahi = {
    enable = true;
    nssmdns4 = true;
    openFirewall = true;
  };
  }
enable GVfs

This is being set to allow myself to use all functions of nautilus in NixOS

  _:
  {
    services.gvfs.enable = true;
  }
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.

  { pkgs, ... }:
  {
    # 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]
        '';
    };
  }
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.

  _:
  {
    services.power-profiles-daemon.enable = true;
  }
Hardware compatibility settings (Yubikey, Ledger, Keyboards) - udev rules

It makes sense to house these settings in their own section, since they are all needed really. 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.

pcscd is needed to use the smartcard mode (CCID) of the Yubikey.

The exception is the system packages, since that cannot be defined twice in the same file (common.nix). The comment is left in as a remider for that.

Also, this is a good place to setup the udev rules.

  { pkgs, ... }:
  {
    programs.ssh.startAgent = false;

    services.pcscd.enable = true;

    hardware.ledger.enable = true;

    services.udev.packages = with pkgs; [
      yubikey-personalization
      ledger-udev-rules
      qmk-udev-rules
      vial
      via
    ];
  }
System Login

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

  { pkgs, ... }:
  {
    services.greetd = {
      enable = true;
      settings = {
        initial_session.command = "sway";
        # initial_session.user ="swarsel";
        default_session.command = ''
          ${pkgs.greetd.tuigreet}/bin/tuigreet \
            --time \
            --asterisks \
            --user-menu \
            --cmd sway
        '';
      };
    };

    environment.etc."greetd/environments".text = ''
      sway
    '';
  }
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.

  { pkgs, ... }:
  {
    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
        gnome2.pango
        gtk2
        gtk3
        icu
        libGL
        libappindicator-gtk2
        libappindicator-gtk3
        libcaca
        libcanberra
        libcap
        libdbusmenu-gtk2
        libdrm
        libelf
        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
        mesa
        nspr
        nss
        openssl
        pango
        pipewire
        pixman
        speex
        stdenv.cc.cc
        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
      ];
    };
  }
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, ... }:
  {

    security.sudo.extraConfig = lib.mkIf config.swarselsystems.impermanence ''
      # 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.initrd.systemd.enable = true;

    boot.initrd.systemd.services.rollback = lib.mkIf config.swarselsystems.impermanence {
      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 = [ "systemd-cryptsetup@enc.service" ];
      # 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 subvol=/ /dev/mapper/cryptroot /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 config.swarselsystems.impermanence {
      hideMounts = true;
      directories =
        [
          "/.cache/nix"
          "/srv"
          "/etc/nixos"
          "/etc/nix"
          "/home/swarsel/.dotfiles"
          "/etc/NetworkManager/system-connections"
          "/etc/secureboot"
          "/var/db/sudo"
          "/var/cache"
          "/var/lib"
        ];

      files = [
        # ssh stuff
        /*
          "/etc/ssh/ssh_host_ed25519_key"
          "/etc/ssh/ssh_host_ed25519_key.pub"
          "/etc/ssh/ssh_host_rsa_key"
          "/etc/ssh/ssh_host_rsa_key.pub"
          ,*/
      ];
    };

  }
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.

  { pkgs, ... }:
  {
    system.activationScripts.diff = {
      supportsDryActivation = true;
      text = ''
        ${pkgs.nvd}/bin/nvd --nix-bin-dir=${pkgs.nix}/bin diff \
             /run/current-system "$systemConfig"
      '';
    };
  }
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.

  _:
  {
    services.gnome.gnome-keyring = {
      enable = true;
    };

    programs.seahorse.enable = true;
  }
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.

  { pkgs, ... }:
  {

    programs.sway = {
      enable = true;
      package = pkgs.swayfx;
      wrapperFeatures = {
        base = true;
        gtk = true;
      };

      extraSessionCommands = ''
        export XDG_SESSION_DESKTOP=sway
        export SDL_VIDEODRIVER=wayland
        export QT_QPA_PLATFORM=wayland-egl
        export QT_WAYLAND_DISABLE_WINDOWDECORATION=1
        export MOZ_ENABLE_WAYLAND=1
        export MOZ_DISABLE_RDD_SANDBOX=1
      '';
    };

  }
xdg-portal

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.

  { pkgs, ... }:
  {

    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";
      };
    };

  }
safeeyes

A friend of mine used this service and I used to make fun of him. But I have to admit this is actually a nice program. It forces you to look away from the screen from time to time, reducing eye strain.

  _:
  {
    services.safeeyes.enable = true;
  }
Podmam (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.

  { pkgs, ... }:
  {
    environment.systemPackages = with pkgs; [
      distrobox
      boxbuddy
    ];

    virtualisation.podman = {
      enable = true;
    };

  }
Handle lid switch correctly

This turns off the display when the lid is closed.

  _:
  {
    services.logind = {
      lidSwitch = "suspend";
      lidSwitchDocked = "ignore";
    };
    services.acpid = {
      enable = true;
      lidEventCommands =
        ''
          export PATH=$PATH:/run/current-system/sw/bin
          export WAYLAND_DISPLAY=wayland-1
          export XDG_RUNTIME_DIR=/run/user/1000
          export SWAYSOCK=$(ls /run/user/1000/sway-ipc.* | head -n 1)

          LID_STATE=$(cat /proc/acpi/button/lid/*/state | grep -q closed && echo "closed" || echo "open")
          DOCKED=$(swaymsg -t get_outputs | grep -q 'HDMI\|DP' && echo "docked" || echo "undocked")

          if [ "$LID_STATE" == "closed" ] && [ "$DOCKED" == "docked" ]; then
              swaymsg output eDP-2 disable
          else
              swaymsg output eDP-2 enable
          fi
        '';
    };
  }

Server

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.

  { self, ... }:
  let
    profilesPath = "${self}/profiles";
  in
  {
    imports = [
    "${profilesPath}/common/nixos/settings.nix"
    "${profilesPath}/common/nixos/home-manager.nix"
    "${profilesPath}/common/nixos/xserver.nix"
    "${profilesPath}/common/nixos/gc.nix"
    "${profilesPath}/common/nixos/store.nix"
    "${profilesPath}/common/nixos/time.nix"
    "${profilesPath}/common/nixos/pipewire.nix"
    "${profilesPath}/common/nixos/users.nix"
    "${profilesPath}/common/nixos/nix-ld.nix"
    ./settings.nix
    ./packages.nix
    ./sops.nix
    ./ssh.nix
    ./nfs.nix
    ./nginx.nix
    ./kavita.nix
    ./jellyfin.nix
    ./navidrome.nix
    ./spotifyd.nix
    ./mpd.nix
    ./matrix.nix
    ./nextcloud.nix
    ./immich.nix
    ./paperless.nix
    ./transmission.nix
    ./syncthing.nix
    ./restic.nix
    ./monitoring.nix
    ./jenkins.nix
    ./emacs.nix
    ];
  }
General NixOS Server settings
  { lib, config, ... }:
  {
    environment.shellAliases = lib.recursiveUpdate
      {
        npswitch = "cd ${config.swarselsystems.flakePath}; git pull; sudo nixos-rebuild --flake .#$(hostname) switch --impure; cd -;";
        nswitch = "cd ${config.swarselsystems.flakePath}; sudo nixos-rebuild --flake .#$(hostname) switch --impure; cd -;";
      }
      config.swarselsystems.shellAliases;

    nixpkgs.config.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"
    ];

  }
System Packages
  { pkgs, ... }:
  {
    environment.systemPackages = with pkgs; [
      gnupg
      nix-index
      ssh-to-age
      git
      emacs
    ];
  }
sops
  { config, ... }:
  {
    sops = {
      age.sshKeyPaths = [ "/etc/ssh/sops" ];
      defaultSopsFile = "${config.swarselsystems.flakePath}/secrets/server/winters/secrets.yaml";
      validateSopsFiles = false;
    };

  }
nfs/samba (smb)
  { pkgs, ... }:
  {
    services = {
      # add a user with sudo smbpasswd -a <user>
      samba = {
        package = pkgs.samba4Full;
        # 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" = "Swarsel";
        };
      };


      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;
      };
    };
  }
NGINX
  { pkgs, config, ... }:
  {
    environment.systemPackages = with pkgs; [
      lego
    ];

    # users.users.acme = {};

    sops = {
      # secrets.dnstokenfull = { owner = "acme"; };
      secrets.dnstokenfull = { };
      templates."certs.secret".content = ''
        CF_DNS_API_TOKEN=${config.sops.placeholder.dnstokenfull}
      '';
    };

    security.acme = {
      acceptTerms = true;
      preliminarySelfsigned = false;
      defaults.email = "mrswarsel@gmail.com";
      defaults.dnsProvider = "cloudflare";
      defaults.environmentFile = "${config.sops.templates."certs.secret".path}";
    };

    services.nginx = {
      enable = true;
      statusPage = true;
      recommendedProxySettings = true;
      recommendedTlsSettings = true;
      recommendedOptimisation = true;
      recommendedGzipSettings = true;
      # virtualHosts are defined in the respective sections
    };

  }
ssh
  { self, ... }:
  {
    services.openssh = {
      enable = true;
    };
    users.users.swarsel.openssh.authorizedKeys.keyFiles = [
      (self + /secrets/keys/ssh/nbl-imba-2.pub)
      (self + /secrets/keys/ssh/magicant.pub)
    ];
    users.users.root.openssh.authorizedKeys.keyFiles = [
      (self + /secrets/keys/ssh/nbl-imba-2.pub)
      (self + /secrets/keys/ssh/magicant.pub)
    ];

  }
kavita
  { pkgs, lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.kavita {
      environment.systemPackages = with pkgs; [
        calibre
      ];


      users.users.jellyfin = {
        extraGroups = [ "users" ];
      };

      sops.secrets.kavita = { owner = "kavita"; };

      networking.firewall.allowedTCPPorts = [ 8080 ];

      services.kavita = {
        enable = true;
        user = "kavita";
        settings.Port = 8080;
        tokenKeyFile = config.sops.secrets.kavita.path;
      };

      services.nginx = {
        virtualHosts = {
          "scroll.swarsel.win" = {
            enableACME = true;
            forceSSL = true;
            acmeRoot = null;
            locations = {
              "/" = {
                proxyPass = "http://localhost:8080";
                extraConfig = ''
                  client_max_body_size 0;
                '';
              };
            };
          };
        };
      };
    };
  }
jellyfin
{ pkgs, lib, config, ... }:
{
  config = lib.mkIf config.swarselsystems.server.jellyfin {
    users.users.jellyfin = {
      extraGroups = [ "video" "render" "users" ];
    };
    nixpkgs.config.packageOverrides = pkgs: {
      vaapiIntel = pkgs.vaapiIntel.override { enableHybridCodec = true; };
    };
    hardware.graphics = {
      enable = true;
      extraPackages = with pkgs; [
        intel-media-driver # LIBVA_DRIVER_NAME=iHD
        vaapiIntel # LIBVA_DRIVER_NAME=i965 (older but works better for Firefox/Chromium)
        vaapiVdpau
        libvdpau-va-gl
      ];
    };
    services.jellyfin = {
      enable = true;
      user = "jellyfin";
      openFirewall = true; # this works only for the default ports
    };

    services.nginx = {
      virtualHosts = {
        "screen.swarsel.win" = {
          enableACME = true;
          forceSSL = true;
          acmeRoot = null;
          locations = {
            "/" = {
              proxyPass = "http://localhost:8096";
              extraConfig = ''
                client_max_body_size 0;
              '';
            };
          };
        };
      };
    };
  };

}
navidrome
  { pkgs, lib, inputs, config, ... }:
  let
    secretsDirectory = builtins.toString inputs.nix-secrets;
  in
  {
    config = lib.mkIf config.swarselsystems.server.navidrome {
      environment.systemPackages = with pkgs; [
        pciutils
        alsa-utils
        mpv
      ];

      users = {
        groups = {
          navidrome = {
            gid = 61593;
          };
        };

        users = {
          navidrome = {
            isSystemUser = true;
            uid = 61593;
            group = "navidrome";
            extraGroups = [ "audio" "utmp" "users" "pipewire" ];
          };
        };
      };


      hardware = {
        # opengl.enable = true;
        enableAllFirmware = true;
      };

      networking.firewall.allowedTCPPorts = [ 4040 ];

      services.navidrome = {
        enable = true;
        openFirewall = true;
        settings = {
          LogLevel = "error";
          Address = "127.0.0.1";
          Port = 4040;
          MusicFolder = "/Vault/Eternor/Musik";
          EnableSharing = true;
          EnableTranscodingConfig = true;
          Scanner.GroupAlbumReleases = true;
          ScanSchedule = "@every 24h";
          MPVPath = "${pkgs.mpv}/bin/mpv";
          MPVCommandTemplate = "mpv --audio-device=%d --no-audio-display --pause %f";
          Jukebox = {
            Enabled = true;
            Default = "pch";
            Devices = [
              [ "pch" "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.ApiKey = builtins.readFile "${secretsDirectory}/navidrome/lastfm-secret";
          LastFM.Secret = builtins.readFile "${secretsDirectory}/navidrome/lastfm-key";
          Spotify.ID = builtins.readFile "${secretsDirectory}/navidrome/spotify-id";
          Spotify.Secret = builtins.readFile "${secretsDirectory}/navidrome/spotify-secret";
          UILoginBackgroundUrl = "https://i.imgur.com/OMLxi7l.png";
          UIWelcomeMessage = "~SwarselSound~";
        };
      };

      services.nginx = {
        virtualHosts = {
          "sound.swarsel.win" = {
            enableACME = true;
            forceSSL = true;
            acmeRoot = null;
            locations = {
              "/" = {
                proxyPass = "http://localhost:4040";
                proxyWebsockets = true;
                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;
                '';
              };
            };
          };
        };
      };
    };


  }
spotifyd
  { lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.spotifyd {
      users.groups.spotifyd = {
        gid = 65136;
      };

      users.users.spotifyd = {
        isSystemUser = true;
        uid = 65136;
        group = "spotifyd";
        extraGroups = [ "audio" "utmp" "pipewire" ];
      };

      networking.firewall.allowedTCPPorts = [ 1025 ];

      services.pipewire.systemWide = true;

      services.spotifyd = {
        enable = true;
        settings = {
          global = {
            dbus_type = "session";
            use_mpris = false;
            device = "sysdefault:CARD=PCH";
            device_name = "SwarselSpot";
            mixer = "alsa";
            zeroconf_port = 1025;
          };
        };
      };
    };

  }
mpd
{ pkgs, lib, config, ... }:
{
  config = lib.mkIf config.swarselsystems.server.mpd {
    users = {
      groups = {
        mpd = { };
      };

      users = {
        mpd = {
          isSystemUser = true;
          group = "mpd";
          extraGroups = [ "audio" "utmp" ];
        };
      };
    };

    sops = {
      secrets.mpdpass = { owner = "mpd"; };
    };

    environment.systemPackages = with pkgs; [
      pciutils
      alsa-utils
      mpv
    ];

    services.mpd = {
      enable = true;
      musicDirectory = "/media";
      user = "mpd";
      group = "mpd";
      network = {
        port = 3254;
        listenAddress = "any";
      };
      credentials = [
        {
          passwordFile = config.sops.secrets.mpdpass.path;
          permissions = [
            "read"
            "add"
            "control"
            "admin"
          ];
        }
      ];
    };
  };

}
matrix
  { config, lib, pkgs, sops, ... }:
  let
    matrixDomain = "swatrix.swarsel.win";
    baseUrl = "https://${matrixDomain}";
    clientConfig."m.homeserver".base_url = baseUrl;
    serverConfig."m.server" = "${matrixDomain}:443";
    mkWellKnown = data: ''
      default_type application/json;
      add_header Access-Control-Allow-Origin *;
      return 200 '${builtins.toJSON data}';
    '';
  in
  {

    config = lib.mkIf config.swarselsystems.server.matrix {
      environment.systemPackages = with pkgs; [
        matrix-synapse
        lottieconverter
        ffmpeg
      ];

      sops = {
        secrets = {
          matrixsharedsecret = { owner = "matrix-synapse"; };
          mautrixtelegram_as = { owner = "matrix-synapse"; };
          mautrixtelegram_hs = { owner = "matrix-synapse"; };
          mautrixtelegram_api_id = { owner = "matrix-synapse"; };
          mautrixtelegram_api_hash = { owner = "matrix-synapse"; };
        };
        templates = {
          "matrix_user_register.sh".content = ''
            register_new_matrix_user -k ${config.sops.placeholder.matrixsharedsecret} http://localhost:8008
          '';
          matrixshared = {
            owner = "matrix-synapse";
            content = ''
              registration_shared_secret: ${config.sops.placeholder.matrixsharedsecret}
            '';
          };
          mautrixtelegram = {
            owner = "matrix-synapse";
            content = ''
              MAUTRIX_TELEGRAM_APPSERVICE_AS_TOKEN=${config.sops.placeholder.mautrixtelegram_as}
              MAUTRIX_TELEGRAM_APPSERVICE_HS_TOKEN=${config.sops.placeholder.mautrixtelegram_hs}
              MAUTRIX_TELEGRAM_TELEGRAM_API_ID=${config.sops.placeholder.mautrixtelegram_api_id}
              MAUTRIX_TELEGRAM_TELEGRAM_API_HASH=${config.sops.placeholder.mautrixtelegram_api_hash}
            '';
          };
        };
      };

      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";
        '';
      };

      services.matrix-synapse = {
        enable = true;
        settings = {
          app_service_config_files = [
            "/var/lib/matrix-synapse/telegram-registration.yaml"
            "/var/lib/matrix-synapse/whatsapp-registration.yaml"
            "/var/lib/matrix-synapse/signal-registration.yaml"
            "/var/lib/matrix-synapse/doublepuppet.yaml"
          ];
          server_name = matrixDomain;
          public_baseurl = "https://${matrixDomain}";
          listeners = [
            {
              port = 8008;
              bind_addresses = [
                "127.0.0.1"
                "::1"
              ];
              type = "http";
              tls = false;
              x_forwarded = true;
              resources = [
                {
                  names = [ "client" "federation" ];
                  compress = true;
                }
              ];
            }
          ];
        };
        extraConfigFiles = [
          config.sops.templates.matrixshared.path
        ];
      };

      services.mautrix-telegram = {
        enable = true;
        environmentFile = config.sops.templates.mautrixtelegram.path;
        settings = {
          homeserver = {
            address = "http://localhost:8008";
            domain = matrixDomain;
          };
          appservice = {
            address = "http://localhost:29317";
            hostname = "localhost";
            port = "29317";
            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:${matrixDomain}" = "admin";
            };
            animated_sticker = {
              target = "gif";
              args = {
                width = 256;
                height = 256;
                fps = 30; # only for webm
                background = "020202"; # only for gif, transparency not supported
              };
            };
          };
        };
      };
      systemd.services.mautrix-telegram.path = with pkgs; [
        lottieconverter # for animated stickers conversion, unfree package
        ffmpeg # if converting animated stickers to webm (very slow!)
      ];

      services.mautrix-whatsapp = {
        enable = true;
        registerToSynapse = false;
        settings = {
          homeserver = {
            address = "http://localhost:8008";
            domain = matrixDomain;
          };
          appservice = {
            address = "http://localhost:29318";
            hostname = "127.0.0.1";
            port = 29318;
            database = {
              type = "postgres";
              uri = "postgresql:///mautrix-whatsapp?host=/run/postgresql";
            };
          };
          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 = {
              matrixDomain = "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 = {
              "*" = "relaybot";
              "@swarsel:${matrixDomain}" = "admin";
            };
          };
        };
      };

      services.mautrix-signal = {
        enable = true;
        registerToSynapse = false;
        settings = {
          homeserver = {
            address = "http://localhost:8008";
            domain = matrixDomain;
          };
          appservice = {

            address = "http://localhost:29328";
            hostname = "127.0.0.1";
            port = 29328;
            database = {
              type = "postgres";
              uri = "postgresql:///mautrix-signal?host=/run/postgresql";
            };
          };
          bridge = {
            displayname_template = "{{or .ContactName .ProfileName .PhoneNumber}} (Signal)";
            login_shared_secret_map = {
              matrixDomain = "as_token:doublepuppet";
            };
            caption_in_message = true;
            permissions = {
              "*" = "relay";
              "@swarsel:${matrixDomain}" = "admin";
            };
          };
        };
      };

      # restart the bridges daily. this is done for the signal bridge mainly which stops carrying
      # messages out after a while.

      systemd.timers."restart-bridges" = {
        wantedBy = [ "timers.target" ];
        timerConfig = {
          OnBootSec = "1d";
          OnUnitActiveSec = "1d";
          Unit = "restart-bridges.service";
        };
      };

      systemd.services."restart-bridges" = {
        script = ''
          systemctl restart mautrix-whatsapp.service
          systemctl restart mautrix-signal.service
          systemctl restart mautrix-telegram.service
        '';
        serviceConfig = {
          Type = "oneshot";
          User = "root";
        };
      };

      services.nginx = {
        virtualHosts = {
          "swatrix.swarsel.win" = {
            enableACME = true;
            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;
              }
            ];
            locations = {
              "~ ^(/_matrix|/_synapse/client)" = {
                # proxyPass = "http://localhost:8008";
                proxyPass = "http://localhost:8008";
                extraConfig = ''
                  client_max_body_size 0;
                '';
              };
              "= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig;
              "= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig;
            };
          };
        };
      };
    };


  }
nextcloud
  { pkgs, lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.nextcloud {

      sops.secrets.nextcloudadminpass = {
        owner = "nextcloud";
        group = "nextcloud";
        mode = "0440";
      };

      services.nextcloud = {
        enable = true;
        package = pkgs.nextcloud30;
        hostName = "stash.swarsel.win";
        home = "/Vault/apps/nextcloud";
        datadir = "/Vault/data/nextcloud";
        https = true;
        configureRedis = true;
        maxUploadSize = "4G";
        extraApps = {
          inherit (pkgs.nextcloud30Packages.apps) mail calendar contacts cospend phonetrack polls tasks;
        };
        config = {
          adminuser = "admin";
          adminpassFile = config.sops.secrets.nextcloudadminpass.path;
        };
      };


      services.nginx = {
        virtualHosts = {
          "stash.swarsel.win" = {
            enableACME = true;
            forceSSL = true;
            acmeRoot = null;
            # config is automatically added by nixos nextcloud config.
            # hence, only provide certificate
          };
        };
      };
    };

  }
immich
  { lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.immich {

      users.users.immich = {
        extraGroups = [ "video" "render" "users" ];
      };

      # sops.secrets.nextcloudadminpass = { owner = "nextcloud"; };

      services.immich = {
        enable = true;
        port = 3001;
        openFirewall = true;
        mediaLocation = "/Vault/Eternor/Immich";
        environment.IMMICH_MACHINE_LEARNING_URL = lib.mkForce "http://localhost:3003";
      };


      services.nginx = {
        virtualHosts = {
          "shots.swarsel.win" = {
            enableACME = true;
            forceSSL = true;
            acmeRoot = null;
            locations = {
              "/" = {
                proxyPass = "http://localhost:3001";
                extraConfig = ''
                  client_max_body_size    0;

                  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;
                '';
              };
            };
          };
        };
      };

    };

  }
paperless
  { lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.paperless {

      users.users.paperless = {
        extraGroups = [ "users" ];
      };


      sops.secrets.paperless_admin = { owner = "paperless"; };

      services.paperless = {
        enable = true;
        mediaDir = "/Vault/Eternor/Paperless";
        dataDir = "/Vault/data/paperless";
        user = "paperless";
        port = 28981;
        passwordFile = config.sops.secrets.paperless_admin.path;
        address = "127.0.0.1";
        settings = {
          PAPERLESS_OCR_LANGUAGE = "deu+eng";
          PAPERLESS_URL = "https://scan.swarsel.win";
          PAPERLESS_OCR_USER_ARGS = builtins.toJSON {
            optimize = 1;
            invalidate_digital_signatures = true;
            pdfa_image_compression = "lossless";
          };
        };
      };

      services.nginx = {
        virtualHosts = {
          "scan.swarsel.win" = {
            enableACME = true;
            forceSSL = true;
            acmeRoot = null;
            locations = {
              "/" = {
                proxyPass = "http://localhost:28981";
                extraConfig = ''
                  client_max_body_size    0;
                '';
              };
            };
          };
        };
      };
    };

  }
transmission
  { pkgs, lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.transmission {

      # this user/group section is probably unneeded
      users = {
        groups = {
          dockeruser = {
            gid = 1155;
          };
          radarr = { };
          readarr = { };
          sonarr = { };
          lidarr = { };
          prowlarr = { };
        };
        users = {
          dockeruser = {
            isSystemUser = true;
            uid = 1155;
            group = "docker";
            extraGroups = [ "users" ];
          };
          radarr = {
            isSystemUser = true;
            group = "radarr";
            extraGroups = [ "users" ];
          };
          readarr = {
            isSystemUser = true;
            group = "readarr";
            extraGroups = [ "users" ];
          };
          sonarr = {
            isSystemUser = true;
            group = "sonarr";
            extraGroups = [ "users" ];
          };
          lidarr = {
            isSystemUser = true;
            group = "lidarr";
            extraGroups = [ "users" ];
          };
          prowlarr = {
            isSystemUser = true;
            group = "prowlarr";
            extraGroups = [ "users" ];
          };
        };
      };

      virtualisation.docker.enable = true;
      environment.systemPackages = with pkgs; [
        docker
      ];

      services = {
        radarr = {
          enable = true;
          openFirewall = true;
          dataDir = "/Vault/apps/radarr";
        };
        readarr = {
          enable = true;
          openFirewall = true;
          dataDir = "/Vault/apps/readarr";
        };
        sonarr = {
          enable = true;
          openFirewall = true;
          dataDir = "/Vault/apps/sonarr";
        };
        lidarr = {
          enable = true;
          openFirewall = true;
          dataDir = "/Vault/apps/lidarr";
        };
        prowlarr = {
          enable = true;
          openFirewall = true;
        };

        nginx = {
          virtualHosts = {
            "store.swarsel.win" = {
              enableACME = false;
              forceSSL = false;
              acmeRoot = null;
              locations = {
                "/" = {
                  proxyPass = "http://localhost:9091";
                  extraConfig = ''
                    client_max_body_size    0;
                  '';
                };
                "/radarr" = {
                  proxyPass = "http://localhost:7878";
                  extraConfig = ''
                    client_max_body_size    0;
                  '';
                };
                "/readarr" = {
                  proxyPass = "http://localhost:8787";
                  extraConfig = ''
                    client_max_body_size    0;
                  '';
                };
                "/sonarr" = {
                  proxyPass = "http://localhost:8989";
                  extraConfig = ''
                    client_max_body_size    0;
                  '';
                };
                "/lidarr" = {
                  proxyPass = "http://localhost:8686";
                  extraConfig = ''
                    client_max_body_size    0;
                  '';
                };
                "/prowlarr" = {
                  proxyPass = "http://localhost:9696";
                  extraConfig = ''
                    client_max_body_size    0;
                  '';
                };
              };
            };
          };
        };
      };
    };
  }
syncthing
  { lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.syncthing {

      users.users.syncthing = {
        extraGroups = [ "users" ];
        group = "syncthing";
        isSystemUser = true;
      };

      users.groups.syncthing = {};

      services.syncthing = {
        enable = true;
        user = "swarsel";
        dataDir = "/Vault/data/syncthing";
        configDir = "/Vault/apps/syncthing";
        guiAddress = "0.0.0.0:8384";
        openDefaultPorts = true;
        relay.enable = false;
        settings = {
          urAccepted = -1;
          devices = {
            "magicant" = {
              id = "VMWGEE2-4HDS2QO-KNQOVGN-LXLX6LA-666E4EK-ZBRYRRO-XFEX6FB-6E3XLQO";
            };
            "sync (@oracle)" = {
              id = "ETW6TST-NPK7MKZ-M4LXMHA-QUPQHDT-VTSHH5X-CR5EIN2-YU7E55F-MGT7DQB";
            };
            "nbl-imba-2" = {
              id = "YAPV4BV-I26WPTN-SIP32MV-SQP5TBZ-3CHMTCI-Z3D6EP2-MNDQGLP-53FT3AB";
            };
          };
          folders = {
            "Default Folder" = {
              path = "/Vault/data/syncthing/Sync";
              type = "receiveonly";
              versioning = null;
              devices = [ "sync (@oracle)" "magicant" "nbl-imba-2" ];
              id = "default";
            };
            "Obsidian" = {
              path = "/Vault/data/syncthing/Obsidian";
              type = "receiveonly";
              versioning = {
                type = "simple";
                params.keep = "5";
              };
              devices = [ "sync (@oracle)" "magicant" "nbl-imba-2" ];
              id = "yjvni-9eaa7";
            };
            "Org" = {
              path = "/Vault/data/syncthing/Org";
              type = "receiveonly";
              versioning = {
                type = "simple";
                params.keep = "5";
              };
              devices = [ "sync (@oracle)" "magicant" "nbl-imba-2" ];
              id = "a7xnl-zjj3d";
            };
            "Vpn" = {
              path = "/Vault/data/syncthing/Vpn";
              type = "receiveonly";
              versioning = {
                type = "simple";
                params.keep = "5";
              };
              devices = [ "sync (@oracle)" "magicant" "nbl-imba-2" ];
              id = "hgp9s-fyq3p";
            };
            "Documents" = {
              path = "/Vault/data/syncthing/Documents";
              type = "receiveonly";
              versioning = {
                type = "simple";
                params.keep = "5";
              };
              devices = [ "magicant" "nbl-imba-2" ];
              id = "hgr3d-pfu3w";
            };
            ".elfeed" = {
              path = "/Vault/data/syncthing/.elfeed";
              devices = [ "sync (@oracle)" "magicant" "nbl-imba-2" ];
              id = "h7xbs-fs9v1";
            };
          };
        };
      };

      services.nginx = {
        virtualHosts = {
          "storync.swarsel.win" = {
            enableACME = true;
            forceSSL = true;
            acmeRoot = null;
            locations = {
              "/" = {
                proxyPass = "http://localhost:8384";
                extraConfig = ''
                  client_max_body_size 0;
                '';
              };
            };
          };
        };
      };
    };

  }
restic
  { lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.restic {

      # TODO

    };
  }
monitoring
  { self, lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.monitoring {

      sops.secrets = {
        grafanaadminpass = {
          owner = "grafana";
        };
        prometheusadminpass = {
          owner = "grafana";
        };
      };

      users.users.nextcloud-exporter = {
        extraGroups = [ "nextcloud" ];
      };

      users.users.grafana = {
        extraGroups = [ "users" ];
      };

      services.grafana = {
        enable = true;
        dataDir = "/Vault/data/grafana";
        provision = {
          enable = true;
          datasources.settings = {
            datasources = [
              {
                name = "prometheus";
                type = "prometheus";
                url = "https://status.swarsel.win/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/prometheusadminpass}";
                };
              }
            ];
          };
        };

        settings = {
          security.admin_password = "$__file{/run/secrets/grafanaadminpass}";
          server = {
            http_port = 3000;
            http_addr = "127.0.0.1";
            protocol = "http";
            domain = "status.swarsel.win";
          };
        };
      };

      services.prometheus = {
        enable = true;
        webExternalUrl = "https://status.swarsel.win/prometheus";
        port = 9090;
        listenAddress = "127.0.0.1";
        globalConfig = {
          scrape_interval = "10s";
        };
        webConfigFile = self + /programs/server/prometheus/web.config;
        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.swarselsystems.server.nextcloud {
            enable = true;
            port = 9205;
            url = "https://stash.swarsel.win/ocs/v2.php/apps/serverinfo/api/v1/info";
            username = "admin";
            passwordFile = config.sops.secrets.nextcloudadminpass.path;
          };
        };
      };


      services.nginx = {
        virtualHosts = {
          "status.swarsel.win" = {
            enableACME = true;
            forceSSL = true;
            acmeRoot = null;
            locations = {
              "/" = {
                proxyPass = "http://localhost:3000";
                extraConfig = ''
                  client_max_body_size 0;
                '';
              };
              "/prometheus" = {
                proxyPass = "http://localhost:9090";
                extraConfig = ''
                  client_max_body_size 0;
                '';
              };
            };
          };
        };
      };
    };

  }
Jenkins
  { pkgs, lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.jenkins {

      services.jenkins = {
        enable = true;
        withCLI = true;
        port = 8088;
        packages = [ pkgs.stdenv pkgs.git pkgs.jdk17 config.programs.ssh.package pkgs.nix ];
        listenAddress = "127.0.0.1";
        home = "/Vault/apps/jenkins";
      };



      services.nginx = {
        virtualHosts = {
          "servant.swarsel.win" = {
            enableACME = true;
            forceSSL = true;
            acmeRoot = null;
            locations = {
              "/" = {
                proxyPass = "http://localhost:8088";
                extraConfig = ''
                  client_max_body_size 0;
                '';
              };
            };
          };
        };
      };
    };

  }
Emacs elfeed (RSS Server)
  { lib, config, ... }:
  {
    config = lib.mkIf config.swarselsystems.server.emacs {

      networking.firewall.allowedTCPPorts = [ 9812 ];

      services.emacs = {
        enable = true;

        install = true;
        startWithGraphical = false;
      };

      services.nginx = {
        virtualHosts = {
          "signpost.swarsel.win" = {
            enableACME = false;
            forceSSL = false;
            acmeRoot = null;
            locations = {
              "/" = {
                proxyPass = "http://localhost:9812";
                extraConfig = ''
                  client_max_body_size 0;
                '';
              };
            };
          };
        };
      };
    };

  }

Darwin

Imports

This section sets up all the imports that are used in the home-manager section.

  { self, ... }:
  let
    profilesPath = "${self}/profiles";
  in
  {
    imports = [
      "${profilesPath}/nixos/home-manager.nix"
    ];

    nix.settings.experimental-features = "nix-command flakes";
    nixpkgs = {
      hostPlatform = "x86_64-darwin";
      overlays = [ outputs.overlays.default ];
      config = {
        allowUnfree = true;
      };
    };

    system.stateVersion = 4;
  }

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-rcm is 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.
gaming

This opens a few gaming ports and installs the steam configuration suite for gaming. There are more options in Gaming (home-manager side).

{ pkgs, ... }:
{
  specialisation = {
    gaming.configuration = {
      networking = {
        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
          ];
        };
      };

      programs.steam = {
        enable = true;
        extraCompatPackages = [
          pkgs.proton-ge-bin
        ];
      };

      hardware.xone.enable = true;

      environment.systemPackages = [
        pkgs.linuxKernel.packages.linux_6_12.xone
      ];
    };
  };

}
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, pkgs, ... }:
  {

    specialisation = {
      VBox.configuration = {
        virtualisation.virtualbox = {
          host = {
            enable = true;
            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.linuxPackages;
      };
    };

  }
VmWare

This sets the VirtualBox configuration. Guest should not be enabled if not direly needed, it will make rebuilds unbearably slow.

  _:
  {

    virtualisation.vmware.host.enable = true;
    virtualisation.vmware.guest.enable = true;
  }
Auto-login

Auto login for the initial session.

  _:
  {
    services = {
      getty.autologinUser = "swarsel";
      greetd.settings.initial_session.user = "swarsel";
    };
  }
nswitch-rcm

This smashes Atmosphere 1.3.2 on the switch, which is what I am currenty using.

  { pkgs, ... }:
  {
    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=";
      };
    };
  }
work

Options that I need specifically at work. There are more options at Work (home-manager side).

  { self, pkgs, config, ... }:
  let
    owner = "swarsel";
    sopsFile = self + /secrets/work/secrets.yaml;
  in
  {
    sops = {
      secrets = {
        clad = {
          inherit owner sopsFile;
        };
        dcad = {
          inherit owner sopsFile;
        };
        wsad = {
          inherit owner sopsFile;
        };
        imbad = {
          inherit owner sopsFile;
        };
      };
    };

    # boot.initrd.luks.yubikeySupport = true;
    programs = {
      zsh.shellInit = ''
        export CLAD="$(cat ${config.sops.secrets.clad.path})"
        export DCAD="$(cat ${config.sops.secrets.dcad.path})"
        export GOVC_PASSWORD="$(cat ${config.sops.secrets.dcad.path})"
        export WSAD="$(cat ${config.sops.secrets.wsad.path})"
        export IMBAD="$(cat ${config.sops.secrets.imbad.path})"
        export DCUSER="dc_adm_schwarzaeugl@IMP.UNIVIE.AC.AT"
        export GOVC_USERNAME="dc_adm_schwarzaeugl@IMP.UNIVIE.AC.AT"
        export PACKER_SSH_EXTRA_ARGS='"--scp-extra-args","'-O'"'
      '';

      browserpass.enable = true;
      _1password.enable = true;
      _1password-gui = {
        enable = true;
        polkitPolicyOwners = [ "swarsel" ];
      };
    };

    networking.firewall.trustedInterfaces = [ "virbr0" ];

    virtualisation = {
      docker.enable = 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; [
      # (python39.withPackages (ps: with ps; [
      # cryptography
      # ]))
      #   docker
      python39
      qemu
      packer
      gnumake
      libisoburn
      govc
      terraform

      # vm
      virt-manager
      virt-viewer
      virtiofsd
      spice
      spice-gtk
      spice-protocol
      win-virtio
      win-spice
    ];


    services = {
      spice-vdagentd.enable = true;
      openssh = {
        enable = true;
        extraConfig = ''
          '';
      };

      syncthing = {
        settings = {
          "winters" = {
            id = "O7RWDMD-AEAHPP7-7TAVLKZ-BSWNBTU-2VA44MS-EYGUNBB-SLHKB3C-ZSLMOAA";
          };
          folders = {
            "Documents" = {
              path = "/home/swarsel/Documents";
              devices = [ "magicant" "winters" ];
              id = "hgr3d-pfu3w";
            };
          };
        };
      };
    };

    # cgroups v1 is required for centos7 dockers
    specialisation = {
      cgroup_v1.configuration = {
        boot.kernelParams = [
          "SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1"
          "systemd.unified_cgroup_hierarchy=0"
        ];
      };
    };

  }
Minimal Install

These options are really only to be used on the iso image in order to run nixos-anywhere.

  { lib, pkgs, ... }:
  {

    nix.settings = {
      experimental-features = [ "nix-command" "flakes" ];
      warn-dirty = false;
    };

    boot = {
      # initrd.systemd.enable = true;
      kernelPackages = pkgs.linuxPackages_latest;
      supportedFilesystems = lib.mkForce [ "brtfs" "vfat" ];
      loader = {
        efi.canTouchEfiVariables = true;
        systemd-boot = {
          enable = true;
          configurationLimit = lib.mkDefault 5;
          consoleMode = lib.mkDefault "max";
        };
      };
    };

    services = {
      qemuGuest.enable = true;
      openssh = {
        enable = true;
        ports = lib.mkDefault [ 22 ];
        settings.PermitRootLogin = "yes";
        authorizedKeysFiles = lib.mkForce [
          "/etc/ssh/authorized_keys.d/%u"
        ];
      };
    };

    security.pam = {
      sshAgentAuth.enable = true;
      services = {
        sudo.u2fAuth = true;
      };
    };

    environment.systemPackages = with pkgs; [
      curl
      rsync
      ssh-to-age
      sops
      vim
      just
    ];

    programs = {
      git.enable = true;
      zsh.enable = lib.mkDefault true;
    };

    fileSystems."/boot".options = [ "umask=0077" ];

    networking.networkmanager.enable = true;


  }

Home-manager

The general structure is the same as in the NixOS section.

Common

Imports

This section sets up all the imports that are used in the home-manager section.

  _:
  {
    imports =  [
      ./settings.nix
      ./packages.nix
      ./custom-packages.nix
      ./sops.nix
      ./ssh.nix
      ./stylix.nix
      ./desktop.nix
      ./symlink.nix
      ./env.nix
      ./programs.nix
      ./nix-index.nix
      ./password-store.nix
      ./direnv.nix
      ./eza.nix
      ./git.nix
      ./fuzzel.nix
      ./starship.nix
      ./kitty.nix
      ./zsh.nix
      ./mail.nix
      ./emacs.nix
      ./waybar.nix
      ./firefox.nix
      ./gnome-keyring.nix
      ./kdeconnect.nix
      ./mako.nix
      ./sway.nix
      ./gpg-agent.nix
      ./gammastep.nix
      # ./safeeyes.nix
      ./yubikey-touch-detector.nix
      ./zellij.nix
      ./tmux.nix
    ];
  }
General home-manager-settings

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.

  { lib, config, pkgs, ... }:
  {
    nix = {
      package = lib.mkDefault pkgs.nix;
      settings = {
        experimental-features = [
          "nix-command"
          "flakes"
          "ca-derivations"
          "pipe-operators"
        ];
      };
    };

    programs.home-manager.enable = lib.mkIf (!config.swarselsystems.isNixos) true;

    home = {
      username = lib.mkDefault "swarsel";
      homeDirectory = lib.mkDefault "/home/${config.home.username}";
      stateVersion = lib.mkDefault "23.05";
      keyboard.layout = "us";
      sessionVariables = {
        FLAKE = "$HOME/.dotfiles";
      };
    };
  }
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

Packaged

This holds packages that I can use as provided, or with small modifications (as in the texlive package that needs special configuration).

  { pkgs, ... }:

  {
    home.packages = with pkgs; [

      # audio stuff
      spek # spectrum analyzer
      losslessaudiochecker
      ffmpeg_7-full
      flac
      mediainfo
      picard-tools
      audacity
      sox
      feishin

      # printing
      cups
      simple-scan

      # dict
      (aspellWithDicts (dicts: with dicts; [ de en en-computers en-science ]))

      # utilities
      util-linux
      nmap
      lsof
      nvd
      nh
      nix-output-monitor
      hyprpicker # color picker
      findutils

      # nix
      alejandra
      nixpkgs-fmt
      deadnix
      statix
      nix-tree

      # shellscripts
      shfmt

      # local file sharing
      wormhole-rs

      # b2 backup @backblaze
      restic

      # "big" programs
      gimp
      inkscape
      zoom-us
      # nomacs
      libreoffice-qt
      xournalpp
      obsidian
      spotify
      vesktop # discord client
      nextcloud-client
      spotify-player
      element-desktop-wayland
      nicotine-plus
      stable.transmission
      mktorrent
      hexchat
      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-clipboard
      wl-mirror

      # screenshotting tools
      grim
      slurp

      # the following packages are used (in some way) by waybar
      playerctl
      pavucontrol
      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-emoji
      font-awesome_5
      noto-fonts
      noto-fonts-cjk-sans
    ];
  }
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.

  { pkgs, ... }:

  {
    home.packages = with pkgs; [
      pass-fuzzel
      cura5
      cdw
      cdb
      bak
      timer
      e
      swarselcheck
      waybarupdate
      opacitytoggle
      fs-diff
      update-checker
      github-notifications
      screenshare
      hm-specialisation
      t2ts
      ts2t

      (pkgs.writeScriptBin "project" ''
        #! ${pkgs.bash}/bin/bash
        if [ "$1" == "rust" ]; then
        cp ~/.dotfiles/templates/rust_flake.nix ./flake.nix
        cp ~/.dotfiles/templates/toolchain.toml .
        elif [ "$1" == "cpp" ]; then
        cp ~/.dotfiles/templates/cpp_flake.nix ./flake.nix
        elif [ "$1" == "python" ]; then
        cp ~/.dotfiles/templates/py_flake.nix ./flake.nix
        elif [ "$1" == "cuda" ]; then
        cp ~/.dotfiles/templates/cu_flake.nix ./flake.nix
        elif [ "$1" == "other" ]; then
        cp ~/.dotfiles/templates/other_flake.nix ./flake.nix
        elif [ "$1" == "latex" ]; then
          if [ "$2" == "" ]; then
          echo "No filename specified, usage: 'project latex <NAME>'"
          exit 0
          fi
        cp ~/.dotfiles/templates/tex_standard.tex ./"$2".tex
        exit 0
        else
        echo "No valid argument given. Valid arguments are rust cpp python, cuda"
        exit 0
        fi
        echo "use flake" >> .envrc
        direnv allow
      '')






    ];
  }
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/keys/NAME.pub
  • update entry for sops.age.sshKeyPaths Since we are using the home-manager implementation here, we need to specify the runtime path.
  { config, lib, ... }:
  let
    mkIfElse = p: yes: no: lib.mkMerge [
      (lib.mkIf p yes)
      (lib.mkIf (!p) no)
    ];
  in
  {
    sops = {
      age.sshKeyPaths = [ "${config.home.homeDirectory}/.ssh/sops" ];
      defaultSopsFile = mkIfElse config.swarselsystems.isBtrfs "/persist/.dotfiles/secrets/general/secrets.yaml" "${config.home.homeDirectory}/.dotfiles/secrets/general/secrets.yaml";

      validateSopsFiles = false;
      secrets = {
        mrswarsel = { path = "/run/user/1000/secrets/mrswarsel"; };
        nautilus = { path = "/run/user/1000/secrets/nautilus"; };
        leon = { path = "/run/user/1000/secrets/leon"; };
        swarselmail = { path = "/run/user/1000/secrets/swarselmail"; };
        github_notif = { path = "/run/user/1000/secrets/github_notif"; };
        # caldav = { path = "${config.home.homeDirectory}/.emacs.d/.caldav"; };
      };
    };
  }
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.

  _:
  {
    programs.ssh = {
      enable = true;
      forwardAgent = true;
      extraConfig = ''
        SetEnv TERM=xterm-256color
      '';
      matchBlocks = {
        # Local machines
        "pfsense" = {
          hostname = "192.168.1.1";
          user = "root";
        };
        "winters" = {
          hostname = "192.168.1.2";
          user = "swarsel";
        };
        "minecraft" = {
          hostname = "130.61.119.129";
          user = "opc";
        };
        "sync" = {
          hostname = "193.122.53.173";
          user = "root"; #this is a oracle vm server but needs root due to nixos-infect
        };
        "songdiver" = {
          hostname = "89.168.100.65";
          user = "ubuntu";
        };
        "pkv" = {
          hostname = "46.232.248.161";
          user = "root";
        };
        "efficient" = {
          hostname = "g0.complang.tuwien.ac.at";
          user = "ep01427399";
        };
      };
    };
  }
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 Theme (stylix).

  { self, lib, config, pkgs, ... }:
  {
    stylix = lib.mkIf (!config.swarselsystems.isNixos) {
      <<theme>>
      image = config.swarselsystems.wallpaper;
      targets = {
        emacs.enable = false;
        waybar.enable = false;
      };
    };
  }
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.

  _:
  {
    xdg.desktopEntries = {

      cura = {
        name = "Ultimaker Cura";
        genericName = "Cura";
        exec = "cura";
        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.mimeApps = {

      enable = true;
      defaultApplications = {
        "x-scheme-handler/http" = [ "firefox.desktop" ];
        "x-scheme-handler/https" = [ "firefox.desktop" ];
        "x-scheme-handler/chrome" = [ "firefox.desktop" ];
        "text/plain" = [ "emacsclient.desktop" ];
        "text/csv" = [ "emacsclient.desktop" ];
        "text/html" = [ "firefox.desktop" ];
        "application/x-extension-htm" = [ "firefox.desktop" ];
        "application/x-extension-html" = [ "firefox.desktop" ];
        "application/x-extension-shtml" = [ "firefox.desktop" ];
        "application/xhtml+xml" = [ "firefox.desktop" ];
        "application/x-extension-xhtml" = [ "firefox.desktop" ];
        "application/x-extension-xht" = [ "firefox.desktop" ];
        "image/png" = [ "imv.desktop" ];
        "image/jpeg" = [ "imv.desktop" ];
        "image/gif" = [ "imv.desktop" ];
        "image/svg" = [ "imv.desktop" ];
        "image/webp" = [ "firefox.desktop" ];
        "image/vnd.adobe.photoshop" = [ "gimp.desktop" ];
        "image/vnd.dxf" = [ "org.inkscape.Inkscape.desktop" ];
        "audio/flac" = [ "mpv.desktop" ];
        "audio/mp3" = [ "mpv.desktop" ];
        "audio/ogg" = [ "mpv.desktop" ];
        "audio/wav" = [ "mpv.desktop" ];
        "video/mp4" = [ "umpv.desktop" ];
        "video/mkv" = [ "umpv.desktop" ];
        "video/flv" = [ "umpv.desktop" ];
        "video/3gp" = [ "umpv.desktop" ];
        "application/pdf" = [ "org.gnome.Evince.desktop" ];
        "application/metalink+xml" = [ "emacsclient.desktop" ];
        "application/sql" = [ "emacsclient.desktop" ];
        "application/vnd.ms-powerpoint" = [ "impress.desktop" ];
        "application/msword" = [ "writer.desktop" ];
        "application/vnd.ms-excel" = [ "calc.desktop" ];
      };
      associations = {
        added = {
          "application/x-zerosize" = [ "emacsclient.desktop" ];
        };
      };
    };
  }
Linking 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, ... }:
    {
      home.file = {
        "init.el" = {
          source = self + /programs/emacs/init.el;
          target = ".emacs.d/init.el";
        };
        "early-init.el" = {
          source = self + /programs/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 + /programs/config/.aspell.conf;
          target = ".aspell.conf";
        };
        ".gitmessage" = {
          source = self + /programs/git/.gitmessage;
          target = ".gitmessage";
        };
        "swayidle/config" = {
          source = self + /programs/swayidle/config;
          target = ".config/swayidle/config";
        };
      };

Also, we link some files to the users XDG configuration home: Also in firefox `about:config > toolkit.legacyUserProfileCustomizations.stylesheets` to true.

    xdg.configFile = {
      "tridactyl/tridactylrc".source = self + /programs/firefox/tridactyl/tridactylrc;
      "tridactyl/themes/base16-codeschool.css".source = self + /programs/firefox/tridactyl/themes/base16-codeschool.css;
    };
  }
Sourcing environment variables

Sets environment variables. Here I am only setting the EDITOR variable, most variables are set in the Sway section.

  { config, ... }:
  {
    home.sessionVariables = {
      EDITOR = "e -w";
      SWARSEL_LO_RES = config.swarselsystems.lowResolution;
      SWARSEL_HI_RES = config.swarselsystems.highResolution;
    };
  }
General Programs: bottom, imv, sioyek, bat, carapace, wlogout, swayr, yt-dlp, mpv, jq, nix-index, ripgrep, pandoc, fzf

This section is for programs that require no further configuration. zsh Integration is enabled by default for these.

  _:
  {
    programs = {
      bottom.enable = true;
      imv.enable = true;
      sioyek.enable = true;
      bat.enable = true;
      carapace.enable = true;
      wlogout.enable = true;
      swayr.enable = true;
      yt-dlp.enable = true;
      mpv.enable = true;
      jq.enable = true;
      ripgrep.enable = true;
      pandoc.enable = true;
      fzf.enable = true;
      zoxide.enable = true;
    };
  }
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.

  { self, pkgs, ... }:
  {
    programs.nix-index =
      let
        commandNotFound = pkgs.runCommandLocal "command-not-found.sh" { } ''
          mkdir -p $out/etc/profile.d
          substitute ${self + /scripts/command-not-found.sh}        \
            $out/etc/profile.d/command-not-found.sh                 \
            --replace @nix-locate@ ${pkgs.nix-index}/bin/nix-locate \
            --replace @tput@ ${pkgs.ncurses}/bin/tput
        '';
      in

      {
        enable = true;
        package = pkgs.symlinkJoin {
          name = "nix-index";
          paths = [ commandNotFound ];
        };
      };
  }
password-store

Enables password store with the pass-otp extension which allows me to store and generate one-time-passwords.

  { pkgs, ... }:
  {
    programs.password-store = {
      enable = true;
      settings = {
        PASSWORD_STORE_DIR = "$HOME/.local/share/password-store";
      };
      package = pkgs.pass.withExtensions (exts: [ exts.pass-otp ]);
    };
  }
direnv

Enables direnv, which I use for nearly all of my nix dev flakes.

  _:
  {
    programs.direnv = {
      enable = true;
      nix-direnv.enable = true;
    };
  }
eza

Eza provides me with a better ls command and some other useful aliases.

  _:
  {
    programs.eza = {
      enable = true;
      icons = "auto";
      git = true;
      extraOptions = [
        "-l"
        "--group-directories-first"
      ];
    };
  }
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, ... }:
  {
    programs.git = {
      enable = true;
      aliases = {
        a = "add";
        c = "commit";
        cl = "clone";
        co = "checkout";
        b = "branch";
        i = "init";
        m = "merge";
        s = "status";
        r = "restore";
        p = "pull";
        pp = "push";
      };
      signing = {
        key = "0x76FD3810215AE097";
        signByDefault = true;
      };
      userEmail = lib.mkDefault "leon.schwarzaeugl@gmail.com";
      userName = "Leon Schwarzäugl";
      difftastic.enable = true;
      lfs.enable = true;
      includes = [
        {
          contents = {
            github = {
              user = "Swarsel";
            };
            commit = {
              template = "~/.gitmessage";
            };
          };
        }
      ];
    };
  }
Fuzzel

Here I only need to set basic layout options - the rest is being managed by stylix.

  _:
  {
    programs.fuzzel = {
      enable = true;
      settings = {
        main = {
          layer = "overlay";
          lines = "10";
          width = "40";
        };
        border.radius = "0";
      };
    };
  }
Starship

Starship makes my zsh look cooler! I have symbols for most programming languages and toolchains, also I build my own powerline.

  _:
  {
    programs.starship = {
      enable = true;
      enableZshIntegration = true;
      settings = {
        add_newline = false;
        format = "$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)";
        };

        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 = "󰆥 ";
        nix_shell.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 = " ";
      };
    };
  }
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.

  _:
  {
    programs.kitty = {
      enable = true;
      keybindings = { };
      settings = {
        scrollback_lines = 10000;
        enable_audio_bell = false;
        notify_on_cmd_finish = "always 20";
      };
    };
  }
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.

{ config, pkgs, lib, ... }:
{
  programs.zsh = {
    enable = true;
    shellAliases = lib.recursiveUpdate
      {
        hg = "history | grep";
        hmswitch = "cd ~/.dotfiles; home-manager --flake .#$(whoami)@$(hostname) switch; cd -;";
        nswitch = "cd ~/.dotfiles; sudo nixos-rebuild --flake .#$(hostname) switch; cd -;";
        nswitch-stay = "cd ~/.dotfiles; git restore flake.lock; sudo nixos-rebuild --flake .#$(hostname) switch; cd -;";
        edithome = "e -w ~/.dotfiles/SwarselSystems.org";
        magit = "emacsclient -nc -e \"(magit-status)\"";
        config = "git --git-dir=$HOME/.cfg/ --work-tree=$HOME";
        g = "git";
        c = "git --git-dir=$HOME/.dotfiles/.git --work-tree=$HOME/.dotfiles/";
        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;";
        cd = "z";
        cdr = "cd \"$( (find /home/swarsel/Documents/GitHub -maxdepth 1 && echo /home/swarsel/.dotfiles) | fzf )\"";
        nix-ldd = "LD_LIBRARY_PATH=$NIX_LD_LIBRARY_PATH ldd";
        fs-diff = "sudo mount -o subvol=/ /dev/mapper/cryptroot /mnt ; fs-diff";
        lt = "ls -lath";
        oldshell = "nix shell github:nixos/nixpkgs/\"$1\" \"$2\"";
      }
      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;
      path = "$HOME/.histfile";
      save = 10000;
      size = 10000;
    };
    historySubstringSearch.enable = true;
    plugins = [
      {
        name = "fzf-tab";
        src = pkgs.zsh-fzf-tab;
      }
    ];
    initExtra = ''
      bindkey "^[[1;5D" backward-word
      bindkey "^[[1;5C" forward-word

      my-backward-delete-word() {
          # Copy the global WORDCHARS variable to a local variable. That way any
          # modifications are scoped to this function only
          local WORDCHARS=$WORDCHARS
          # Use bash string manipulation to remove `:` so our delete will stop at it
          WORDCHARS="''${WORDCHARS//:}"
          # Use bash string manipulation to remove `/` so our delete will stop at it
          WORDCHARS="''${WORDCHARS//\/}"
          # Use bash string manipulation to remove `.` so our delete will stop at it
          WORDCHARS="''${WORDCHARS//.}"
          # zle <widget-name> will run an existing widget.
          zle backward-delete-word
      }
      zle -N my-backward-delete-word
      bindkey '^H' my-backward-delete-word

      # This will be our `ctrl+alt+w` command
      my-backward-delete-whole-word() {
          # Copy the global WORDCHARS variable to a local variable. That way any
          # modifications are scoped to this function only
          local WORDCHARS=$WORDCHARS
          # Use bash string manipulation to add `:` to WORDCHARS if it's not present
          # already.
          [[ ! $WORDCHARS == *":"* ]] && WORDCHARS="$WORDCHARS"":"
          # zle <widget-name> will run that widget.
          zle backward-delete-word
      }
      # `zle -N` will create a new widget that we can use on the command line
      zle -N my-backward-delete-whole-word
      # bind this new widget to `ctrl+alt+w`
      bindkey '^W' my-backward-delete-whole-word

      vterm_printf() {
                      if [ -n "$TMUX" ] && ([ "''${TERM%%-*}" = "tmux" ] || [ "''${TERM%%-*}" = "screen" ]); then
                        # Tell tmux to pass the escape sequences through
                        printf "\ePtmux;\e\e]%s\007\e\\" "$1"
                      elif [ "''${TERM%%-*}" = "screen" ]; then
                        # GNU screen (screen, screen-256color, screen-256color-bce)
                        printf "\eP\e]%s\007\e\\" "$1"
                      else
                        printf "\e]%s\e\\" "$1"
                      fi
                               }
      vterm_prompt_end() {
            vterm_printf "51;A$(whoami)@$(hostname):$(pwd)"
      }
      setopt PROMPT_SUBST
      PROMPT=$PROMPT'%{$(vterm_prompt_end)%}'

      vterm_cmd() {
          local vterm_elisp
          vterm_elisp=""
          while [ $# -gt 0 ]; do
              vterm_elisp="$vterm_elisp""$(printf '"%s" ' "$(printf "%s" "$1" | sed -e 's|\\|\\\\|g' -e 's|"|\\"|g')")"
              shift
          done
          vterm_printf "51;E$vterm_elisp"
      }

    '';
  };
}
zellij
  _:
  # { pkgs, config, ... }:
  # let
  # inherit (config.lib.stylix) colors;
  #   sesh = pkgs.writeScriptBin "sesh" ''
  #     #! /usr/bin/env sh

  #     # Taken from https://github.com/zellij-org/zellij/issues/884#issuecomment-1851136980
  #     # select a directory using zoxide
  #     ZOXIDE_RESULT=$(zoxide query --interactive)
  #     # checks whether a directory has been selected
  #     if [[ -z "$ZOXIDE_RESULT" ]]; then
  #     	# if there was no directory, select returns without executing
  #     	exit 0
  #     fi
  #     # extracts the directory name from the absolute path
  #     SESSION_TITLE=$(echo "$ZOXIDE_RESULT" | sed 's#.*/##')

  #     # get the list of sessions
  #     SESSION_LIST=$(zellij list-sessions -n | awk '{print $1}')

  #     # checks if SESSION_TITLE is in the session list
  #     if echo "$SESSION_LIST" | grep -q "^$SESSION_TITLE$"; then
  #     	# if so, attach to existing session
  #     	zellij attach "$SESSION_TITLE"
  #     else
  #     	# if not, create a new session
  #     	echo "Creating new session $SESSION_TITLE and CD $ZOXIDE_RESULT"
  #     	cd $ZOXIDE_RESULT
  #     	zellij attach -c "$SESSION_TITLE"
  #     fi
  #   '';

  # in
  {
    programs.zellij = {
      enable = true;
    };
    home.packages = [
      # pkgs.tmate
      # sesh
    ];
    #   xdg.configFile."zellij/config.kdl".source = ../../../programs/zellij/config.kdl;
    #   xdg.configFile."zellij/layouts/default.kdl".text = ''
    #     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 {
    #             pane size=2 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.base0D}]#[bg=#${colors.base0D},fg=#${colors.base01},bold] #[bg=#${colors.base02},fg=#${colors.base05},bold] {session} #[bg=#${colors.base03},fg=#${colors.base05},bold]"
    #                     format_space  ""
    #                     format_hide_on_overlength "true"
    #                     format_precedence "crl"

    #                     border_enabled  "false"
    #                     border_char     "─"
    #                     border_format   "#[fg=#6C7086]{char}"
    #                     border_position "top"

    #                     mode_normal        "#[bg=#${colors.base0B},fg=#${colors.base02},bold] NORMAL#[bg=#${colors.base03},fg=#${colors.base0B}]█"
    #                     mode_locked        "#[bg=#${colors.base04},fg=#${colors.base02},bold] LOCKED #[bg=#${colors.base03},fg=#${colors.base04}]█"
    #                     mode_resize        "#[bg=#${colors.base08},fg=#${colors.base02},bold] RESIZE#[bg=#${colors.base03},fg=#${colors.base08}]█"
    #                     mode_pane          "#[bg=#${colors.base0D},fg=#${colors.base02},bold] PANE#[bg=#${colors.base03},fg=#${colors.base0D}]█"
    #                     mode_tab           "#[bg=#${colors.base07},fg=#${colors.base02},bold] TAB#[bg=#${colors.base03},fg=#${colors.base07}]█"
    #                     mode_scroll        "#[bg=#${colors.base0A},fg=#${colors.base02},bold] SCROLL#[bg=#${colors.base03},fg=#${colors.base0A}]█"
    #                     mode_enter_search  "#[bg=#${colors.base0D},fg=#${colors.base02},bold] ENT-SEARCH#[bg=#${colors.base03},fg=#${colors.base0D}]█"
    #                     mode_search        "#[bg=#${colors.base0D},fg=#${colors.base02},bold] SEARCHARCH#[bg=#${colors.base03},fg=#${colors.base0D}]█"
    #                     mode_rename_tab    "#[bg=#${colors.base07},fg=#${colors.base02},bold] RENAME-TAB#[bg=#${colors.base03},fg=#${colors.base07}]█"
    #                     mode_rename_pane   "#[bg=#${colors.base0D},fg=#${colors.base02},bold] RENAME-PANE#[bg=#${colors.base03},fg=#${colors.base0D}]█"
    #                     mode_session       "#[bg=#${colors.base0E},fg=#${colors.base02},bold] SESSION#[bg=#${colors.base03},fg=#${colors.base0E}]█"
    #                     mode_move          "#[bg=#${colors.base0F},fg=#${colors.base02},bold] MOVE#[bg=#${colors.base03},fg=#${colors.base0F}]█"
    #                     mode_prompt        "#[bg=#${colors.base0D},fg=#${colors.base02},bold] PROMPT#[bg=#${colors.base03},fg=#${colors.base0D}]█"
    #                     mode_tmux          "#[bg=#${colors.base09},fg=#${colors.base02},bold] TMUX#[bg=#${colors.base03},fg=#${colors.base09}]█"

    #                     // formatting for inactive tabs
    #                     tab_normal              "#[bg=#${colors.base03},fg=#${colors.base0D}]█#[bg=#${colors.base0D},fg=#${colors.base02},bold]{index} #[bg=#${colors.base02},fg=#${colors.base05},bold] {name}{floating_indicator}#[bg=#${colors.base03},fg=#${colors.base02},bold]█"
    #                     tab_normal_fullscreen   "#[bg=#${colors.base03},fg=#${colors.base0D}]█#[bg=#${colors.base0D},fg=#${colors.base02},bold]{index} #[bg=#${colors.base02},fg=#${colors.base05},bold] {name}{fullscreen_indicator}#[bg=#${colors.base03},fg=#${colors.base02},bold]█"
    #                     tab_normal_sync         "#[bg=#${colors.base03},fg=#${colors.base0D}]█#[bg=#${colors.base0D},fg=#${colors.base02},bold]{index} #[bg=#${colors.base02},fg=#${colors.base05},bold] {name}{sync_indicator}#[bg=#${colors.base03},fg=#${colors.base02},bold]█"

    #                     // formatting for the current active tab
    #                     tab_active              "#[bg=#${colors.base03},fg=#${colors.base09}]█#[bg=#${colors.base09},fg=#${colors.base02},bold]{index} #[bg=#${colors.base02},fg=#${colors.base05},bold] {name}{floating_indicator}#[bg=#${colors.base03},fg=#${colors.base02},bold]█"
    #                     tab_active_fullscreen   "#[bg=#${colors.base03},fg=#${colors.base09}]█#[bg=#${colors.base09},fg=#${colors.base02},bold]{index} #[bg=#${colors.base02},fg=#${colors.base05},bold] {name}{fullscreen_indicator}#[bg=#${colors.base03},fg=#${colors.base02},bold]█"
    #                     tab_active_sync         "#[bg=#${colors.base03},fg=#${colors.base09}]█#[bg=#${colors.base09},fg=#${colors.base02},bold]{index} #[bg=#${colors.base02},fg=#${colors.base05},bold] {name}{sync_indicator}#[bg=#${colors.base03},fg=#${colors.base02},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/London"
    #                 }
    #             }
    #             children
    #         }
    #     }
    #   '';


  }
tmux
    { 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
  {

    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

      '';
    };
  }
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.

  { config, ... }:
  {
    programs.mbsync = {
      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.msmtp = {
      enable = true;
    };

    programs.mu = {
      enable = true;
    };

    accounts.email = {
      maildirBasePath = "Mail";
      accounts.leon = {
        primary = true;
        address = "leon.schwarzaeugl@gmail.com";
        userName = "leon.schwarzaeugl@gmail.com";
        realName = "Leon Schwarzäugl";
        passwordCommand = "cat ${config.sops.secrets.leon.path}";
        # passwordCommand = "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.local/share/password-store/mail/mbsync/leon.schwarzaeugl@gmail.com.gpg";
        gpg = {
          key = "0x76FD3810215AE097";
          signByDefault = true;
        };
        imap.host = "imap.gmail.com";
        smtp.host = "smtp.gmail.com";
        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;
            };
          };
        };
      };

      accounts.swarsel = {
        address = "leon@swarsel.win";
        userName = "8227dc594dd515ce232eda1471cb9a19";
        realName = "Leon Schwarzäugl";
        passwordCommand = "cat ${config.sops.secrets.swarselmail.path}";
        smtp = {
          host = "in-v3.mailjet.com";
          port = 587;
          tls = {
            enable = true;
            useStartTls = true;
          };
        };
        mu.enable = false;
        msmtp = {
          enable = true;
        };
        mbsync = {
          enable = false;
        };
      };

      accounts.nautilus = {
        primary = false;
        address = "nautilus.dw@gmail.com";
        userName = "nautilus.dw@gmail.com";
        realName = "Nautilus";
        passwordCommand = "cat ${config.sops.secrets.nautilus.path}";
        # passwordCommand = "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.local/share/password-store/mail/mbsync/nautilus.dw@gmail.com.gpg";
        imap.host = "imap.gmail.com";
        smtp.host = "smtp.gmail.com";
        msmtp.enable = true;
        mu.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;
            };
          };
        };
      };
      accounts.mrswarsel = {
        primary = false;
        address = "mrswarsel@gmail.com";
        userName = "mrswarsel@gmail.com";
        realName = "Swarsel";
        # passwordCommand = "gpg --quiet --for-your-eyes-only --no-tty --decrypt ~/.local/share/password-store/mail/mbsync/mrswarsel@gmail.com.gpg";
        passwordCommand = "cat ${config.sops.secrets.mrswarsel.path}";
        imap.host = "imap.gmail.com";
        smtp.host = "smtp.gmail.com";
        msmtp.enable = true;
        mu.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;
            };
          };
        };
      };
    };
  }
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, pkgs, ... }:
  {
    # enable emacs overlay for bleeding edge features
    # also read init.el file and install use-package packages
    programs.emacs = {
      enable = true;
      package = pkgs.emacsWithPackagesFromUsePackage {
        config = self + /programs/emacs/init.el;
        package = pkgs.emacs-pgtk;
        alwaysEnsure = true;
        alwaysTangle = true;
        extraEmacsPackages = epkgs: [
          epkgs.mu4e
          epkgs.use-package
          # epkgs.lsp-bridge
          epkgs.doom-themes
          epkgs.vterm
          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 ];
          })
          (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";
    };
  }
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)

The rest of the related configuration is found here:

{ self, config, lib, ... }:
{
  programs.waybar = {

    enable = true;
    systemd = {
      enable = true;
      target = "sway-sessions.target";
    };
    settings = {
      mainBar = {
        layer = "top";
        position = "top";
        modules-left = [ "sway/workspaces" "custom/outer-right-arrow-dark" "sway/window" ];
        modules-center = [ "sway/mode" "privacy" "custom/github" "custom/configwarn" "custom/nix-updates" ];
        "sway/mode" = {
          format = "<span style=\"italic\" font-weight=\"bold\">{}</span>";
        };

        modules-right = config.swarselsystems.waybarModules;

        "custom/pseudobat" = lib.mkIf (!config.swarselsystems.isLaptop) {
          format = "";
          on-click-right = "wlogout -p layer-shell";
        };

        "custom/configwarn" = {
          exec = "waybarupdate";
          interval = 60;
        };

        "custom/scratchpad-indicator" = {
          interval = 3;
          exec = "swaymsg -t get_tree | jq 'recurse(.nodes[]) | first(select(.name==\"__i3_scratch\")) | .floating_nodes | length | select(. >= 1)'";
          format = "{} ";
          on-click = "swaymsg 'scratchpad show'";
          on-click-right = "swaymsg 'move scratchpad'";
        };

        "custom/github" = {
          format = "{}  ";
          return-type = "json";
          interval = 60;
          exec = "github-notifications";
          on-click = "xdg-open https://github.com/notifications";
        };

        # "custom/nix-updates" = {
        #   exec = "update-checker";
        #   on-click = "update-checker && notify-send 'The system has been updated'";
        #   interval = "once";
        #   tooltip = true;
        #   return-type = "json";
        #   format = "{} {icon}";
        #   format-icon = {
        #     "has-updates" = "";
        #     "updated" = " ";
        #   };
        # };

        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 = "pamixer -t";
          on-click-right = "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 = "kitty -o confirm_os_window_close=0 btm";

        };
        "custom/vpn" = {
          format = "()";
          exec = "echo '{\"class\": \"connected\"}'";
          exec-if = "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 + /programs/waybar/style.css);
  };
}
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.

  { self, pkgs, lib, ... }:
  let
    lock-false = {
      Value = false;
      Status = "locked";
    };
    lock-true = {
      Value = true;
      Status = "locked";
    };
  in
  {
    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; # Dont recommend extensions while the user is visiting web pages
          FeatureRecommendations = false; # Dont recommend browser features
          Locked = true; # Prevent the user from changing user messaging preferences
          MoreFromMozilla = false; # Dont show the “More from Mozilla” section in Preferences
          SkipOnboarding = true; # Dont show onboarding messages on the new tab page
          UrlbarInterventions = false; # Dont offer suggestions in the URL bar
          WhatsNew = false; # Remove the “Whats 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 = {
        id = 0;
        isDefault = true;
        userChrome = builtins.readFile (self + /programs/firefox/chrome/userChrome.css);
        extensions = 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
          single-file
          widegithub
          enhanced-github
          unpaywall
          don-t-fuck-with-paste
          plasma-integration
          (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" = lock-true;
          "toolkit.legacyUserProfileCustomizations.stylesheets" = lock-true;
          "browser.search.suggest.enabled" = lock-false;
          "browser.search.suggest.enabled.private" = lock-false;
          "browser.urlbar.suggest.searches" = lock-false;
          "browser.urlbar.showSearchSuggestionsFirst" = lock-false;
          "browser.topsites.contile.enabled" = lock-false;
          "browser.newtabpage.activity-stream.feeds.section.topstories" = lock-false;
          "browser.newtabpage.activity-stream.feeds.snippets" = lock-false;
          "browser.newtabpage.activity-stream.section.highlights.includePocket" = lock-false;
          "browser.newtabpage.activity-stream.section.highlights.includeBookmarks" = lock-false;
          "browser.newtabpage.activity-stream.section.highlights.includeDownloads" = lock-false;
          "browser.newtabpage.activity-stream.section.highlights.includeVisited" = lock-false;
          "browser.newtabpage.activity-stream.showSponsored" = lock-false;
          "browser.newtabpage.activity-stream.system.showSponsored" = lock-false;
          "browser.newtabpage.activity-stream.showSponsoredTopSites" = lock-false;
        };

        search = {
          default = "Kagi";
          privateDefault = "Kagi";
          engines = {
            "Kagi" = {
              urls = [{
                template = "https://kagi.com/search";
                params = [
                  { name = "q"; value = "{searchTerms}"; }
                ];
              }];
              iconUpdateURL = "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}";
              }];
              iconUpdateURL = "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" ];
            };

            "Google".metaData.alias = "@g";
          };
          force = true; # this is required because otherwise the search.json.mozlz4 symlink gets replaced on every firefox restart
        };
      };
    };
  }
Services

Services that can be defined through home-manager should be defined here.

gnome-keyring

Used for storing sessions in e.g. Nextcloud

  { lib, config, ... }:
  {
    services.gnome-keyring = lib.mkIf (!config.swarselsystems.isNixos) {
      enable = true;
    };
  }
KDE Connect

This enables phone/computer communication, including sending clipboard, files etc. Sadly on Wayland many of the features are broken (like remote control).

  _:
  {
    services.kdeconnect = {
      enable = true;
      indicator = true;
    };

  }
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.

  _:
  {
    services.mako = {
      enable = true;
      # backgroundColor = "#2e3440";
      # borderColor = "#88c0d0";
      borderRadius = 15;
      borderSize = 1;
      defaultTimeout = 5000;
      height = 150;
      icons = true;
      ignoreTimeout = true;
      layer = "overlay";
      maxIconSize = 64;
      sort = "-time";
      width = 300;
      # font = "monospace 10";
      extraConfig = ''
        [urgency=low]
        border-color=#cccccc
        [urgency=normal]
        border-color=#d08770
        [urgency=high]
        border-color=#bf616a
        default-timeout=3000
        [category=mpd]
        default-timeout=2000
        group-by=category
      '';
    };
  }
yubikey-touch-detector
  { pkgs, ... }:
  {
    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" ];
      };
    };
  }
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, ... }:
  let
    inherit (config.swarselsystems) monitors;
    eachMonitor = _name: monitor: {
      inherit (monitor) name;
      value = builtins.removeAttrs monitor [ "workspace" "name" "output" ];
    };
    eachOutput = _name: monitor: {
      inherit (monitor) name;
      value = builtins.removeAttrs monitor [ "mode" "name" "scale" "transform" "position" ];
    };
    workplaceSets = lib.mapAttrs' eachOutput monitors;
    workplaceOutputs = map (key: lib.getAttr key workplaceSets) (lib.attrNames workplaceSets);
  in
  {
    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;
      };
      wrapperFeatures.gtk = true;
      config = rec {
        modifier = "Mod4";
        terminal = "kitty";
        menu = "fuzzel";
        bars = [{ command = "waybar"; }];
        keybindings =
          let
            inherit (config.wayland.windowManager.sway.config) modifier;
          in
          lib.recursiveUpdate
            {
              "${modifier}+q" = "kill";
              "${modifier}+f" = "exec firefox";
              "${modifier}+Shift+f" = "exec swaymsg fullscreen";
              "${modifier}+Space" = "exec fuzzel";
              "${modifier}+Shift+Space" = "floating toggle";
              "${modifier}+e" = "exec emacsclient -nquc -a emacs -e \"(dashboard-open)\"";
              "${modifier}+Shift+m" = "exec emacsclient -nquc -a emacs -e \"(mu4e)\"";
              "${modifier}+Shift+c" = "exec emacsclient -nquc -a emacs -e \"(swarsel/open-calendar)\"";
              "${modifier}+m" = "exec swarselcheck -s";
              "${modifier}+x" = "exec swarselcheck -k";
              "${modifier}+d" = "exec swarselcheck -d";
              "${modifier}+w" = "exec swarselcheck -e";
              "${modifier}+Shift+t" = "exec opacitytoggle";
              "${modifier}+Shift+F12" = "move scratchpad";
              "${modifier}+F12" = "scratchpad show";
              "${modifier}+c" = "exec qalculate-gtk";
              "${modifier}+p" = "exec pass-fuzzel";
              "${modifier}+o" = "exec pass-fuzzel --otp";
              "${modifier}+Shift+p" = "exec pass-fuzzel --type";
              "${modifier}+Shift+o" = "exec pass-fuzzel --otp --type";
              "${modifier}+Ctrl+p" = "exec 1password --quick-acces";
              "${modifier}+Escape" = "mode $exit";
              "${modifier}+Shift+Escape" = "exec kitty -o confirm_os_window_close=0 btm";
              "${modifier}+h" = "exec hyprpicker | wl-copy";
              "${modifier}+s" = "exec grim -g \"$(slurp)\" -t png - | wl-copy -t image/png";
              "${modifier}+Shift+s" = "exec slurp | grim -g - Pictures/Screenshots/$(date +'screenshot_%Y-%m-%d-%H%M%S.png')";
              "${modifier}+1" = "workspace 1:一";
              "${modifier}+Shift+1" = "move container to workspace 1:一";
              "${modifier}+2" = "workspace 2:二";
              "${modifier}+Shift+2" = "move container to workspace 2:二";
              "${modifier}+3" = "workspace 3:三";
              "${modifier}+Shift+3" = "move container to workspace 3:三";
              "${modifier}+4" = "workspace 4:四";
              "${modifier}+Shift+4" = "move container to workspace 4:四";
              "${modifier}+5" = "workspace 5:五";
              "${modifier}+Shift+5" = "move container to workspace 5:五";
              "${modifier}+6" = "workspace 6:六";
              "${modifier}+Shift+6" = "move container to workspace 6:六";
              "${modifier}+7" = "workspace 7:七";
              "${modifier}+Shift+7" = "move container to workspace 7:七";
              "${modifier}+8" = "workspace 8:八";
              "${modifier}+Shift+8" = "move container to workspace 8:八";
              "${modifier}+9" = "workspace 9:九";
              "${modifier}+Shift+9" = "move container to workspace 9:九";
              "${modifier}+0" = "workspace 10:十";
              "${modifier}+Shift+0" = "move container to workspace 10:十";
              "${modifier}+Ctrl+m" = "workspace 11:M";
              "${modifier}+Ctrl+Shift+m" = "move container to workspace 11:M";
              "${modifier}+Ctrl+s" = "workspace 12:S";
              "${modifier}+Ctrl+Shift+s" = "move container to workspace 12:S";
              "${modifier}+Ctrl+e" = "workspace 13:E";
              "${modifier}+Ctrl+Shift+e" = "move container to workspace 13:E";
              "${modifier}+Ctrl+t" = "workspace 14:T";
              "${modifier}+Ctrl+Shift+t" = "move container to workspace 14:T";
              "${modifier}+Ctrl+l" = "workspace 15:L";
              "${modifier}+Ctrl+Shift+l" = "move container to workspace 15:L";
              "${modifier}+Ctrl+f" = "workspace 16:F";
              "${modifier}+Ctrl+Shift+f" = "move container to workspace 16:F";
              "${modifier}+Left" = "focus left";
              "${modifier}+Right" = "focus right";
              "${modifier}+Down" = "focus down";
              "${modifier}+Up" = "focus up";
              "${modifier}+Shift+Left" = "move left 40px";
              "${modifier}+Shift+Right" = "move right 40px";
              "${modifier}+Shift+Down" = "move down 40px";
              "${modifier}+Shift+Up" = "move up 40px";
              "${modifier}+Ctrl+Shift+c" = "reload";
              "${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}+r" = "mode resize";
              "${modifier}+Return" = "exec kitty";
              "${modifier}+Print" = "exec screenshare";
              # "XF86AudioRaiseVolume" = "exec pa 5%";
              "XF86AudioRaiseVolume" = "exec pamixer -i 5";
              # "XF86AudioLowerVolume" = "exec pactl set-sink-volume @DEFAULT_SINK@ -5%";
              "XF86AudioLowerVolume" = "exec pamixer -d 5";
              # "XF86AudioMute" = "exec pactl set-sink-mute @DEFAULT_SINK@ toggle";
              "XF86AudioMute" = "exec pamixer -t";
              "XF86MonBrightnessUp" = "exec brightnessctl set +5%";
              "XF86MonBrightnessDown" = "exec brightnessctl set 5%-";
              "XF86Display" = "exec wl-mirror eDP-1";
            }
            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 = lib.mapAttrs' eachMonitor monitors;
        input = config.swarselsystems.standardinputs;
        workspaceOutputAssign = workplaceOutputs;
        startup = config.swarselsystems.startup ++ [
          { command = "kitty -T kittyterm"; }
          { command = "sleep 60; kitty -T spotifytui -o confirm_os_window_close=0 spotify_player"; }
        ];
        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"; }
            { class = "1Password"; }
            { app_id = "com.nextcloud.desktopclient.nextcloud"; }
            { title = "(?:Open|Save) (?:File|Folder|As)"; }
            { title = "^Add$"; }
            { title = "^Picture-in-Picture$"; }
            { title = "Syncthing Tray"; }
            { 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 = "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.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 SDL_VIDEODRIVER=wayland
        export QT_QPA_PLATFORM=wayland
        export QT_WAYLAND_DISABLE_WINDOWDECORATION="1"
        export _JAVA_AWT_WM_NONREPARENTING=1
        export XDG_CURRENT_DESKTOP=sway
        export XDG_SESSION_DESKTOP=sway
        export QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox";
        export ANKI_WAYLAND=1;
        export OBSIDIAN_USE_WAYLAND=1;
      '';
      # 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\"
                    l exec \"swaylock --screenshots --clock --effect-blur 7x5 --effect-vignette 0.5:0.5 --fade-in 0.2 --daemonize && systemctl suspend \", 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


            ${swayfxSettings}

            ";
    };
  }
gpg-agent

Settinfs that are needed for the gpg-agent. Also we are enabling emacs support for unlocking my Yubikey here.

  { pkgs, ... }:
  {
    services.gpg-agent = {
      enable = true;
      enableSshSupport = true;
      enableExtraSocket = true;
      pinentryPackage = pkgs.pinentry.gtk2;
      defaultCacheTtl = 600;
      maxCacheTtl = 7200;
      extraConfig = ''
        allow-loopback-pinentry
        allow-emacs-pinentry
      '';
    };
  }
gammastep

This service changes the screen hue at night. I am not sure if that really does something, but I like the color anyways.

  _:
  {
    services.gammastep = {
      enable = true;
      provider = "manual";
      latitude = 48.210033;
      longitude = 16.363449;
    };
  }

Server

Imports

This section sets up all the imports that are used in the home-manager section.

  { self, ... }:
  let
    profilesPath = "${self}/profiles";
  in
  {
    imports =  [
      "${profilesPath}/common/home/settings.nix"
      ./symlink.nix
    ];
  }
Linking 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, ... }:
  {
    home.file = {
      "init.el" = {
        source = self + /programs/emacs/server.el;
        target = ".emacs.d/init.el";
      };
    };
  }

Darwin

Imports

This section sets up all the imports that are used in the home-manager section.

  { self, ... }:
  let
    profilesPath = "${self}/profiles";
  in
  {
    imports = [
      "${profilesPath}/common/home/settings.nix"
    ];
  }

Optional

Akin to the optional NixOS modules.

Gaming

The rest of the settings is at gaming.

  { pkgs, ... }:
  {
    # specialisation = {
    #   gaming.configuration = {
        home.packages = with pkgs; [
          stable.lutris
          wine
          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
        ];
    #   };
    # };
  }
Work

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.

  { config, pkgs, lib, ... }:
  {
    home.packages = with pkgs; [
      stable.teams-for-linux
      shellcheck
      dig
      docker
      postman
      rclone
      awscli2
      libguestfs-with-appliance
    ];

    programs = {
      git.userEmail = "leon.schwarzaeugl@imba.oeaw.ac.at";

      zsh = {
        cdpath = [
          "~/Documents/Work"
        ];
        dirHashes = {
          d = "$HOME/.dotfiles";
          w = "$HOME/Documents/Work";
          s = "$HOME/.dotfiles/secrets";
          pr = "$HOME/Documents/Private";
          ac = "$HOME/.ansible/collections/ansible_collections/vbc/linux/roles";
        };
      };


      ssh = {
        matchBlocks = {
          "uc" = {
            hostname = "uc.clip.vbc.ac.at";
            user = "stack";
          };
          "uc-stg" = {
            hostname = "uc.staging.clip.vbc.ac.at";
            user = "stack";
          };
          "cbe" = {
            hostname = "cbe.vbc.ac.at";
            user = "dc_adm_schwarzaeugl";
          };
          "cbe-stg" = {
            hostname = "cbe.staging.vbc.ac.at";
            user = "dc_adm_schwarzaeugl";
          };
          "*.vbc.ac.at" = {
            user = "dc_adm_schwarzaeugl";
          };
        };
      };

      firefox = {
        profiles = {
          dc_adm = lib.recursiveUpdate { id = 1; } config.swarselsystems.firefox;
          cl_adm = lib.recursiveUpdate { id = 2; } config.swarselsystems.firefox;
          ws_adm = lib.recursiveUpdate { id = 3; } config.swarselsystems.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"
        ];
      };
    };

    xdg = {
      mimeApps = {
        defaultApplications = {
          "x-scheme-handler/msteams" = [ "teams-for-linux.desktop"] ;
        };
      };
      desktopEntries =
      let
        terminal = false;
        categories = [ "Application" ];
        icon = "firefox";
      in
      {
        firefox_dc = {
          name = "Firefox (dc_adm)";
          genericName = "Firefox dc";
          exec = "firefox -p dc_adm";
          inherit terminal categories icon;
        };

        firefox_ws = {
          name = "Firefox (ws_adm)";
          genericName = "Firefox ws";
          exec = "firefox -p ws_adm";
          inherit terminal categories icon;
        };

        firefox_cl = {
          name = "Firefox (cl_adm)";
          genericName = "Firefox cl";
          exec = "firefox -p cl_adm";
          inherit terminal categories icon;
        };

      };
    };

  }

Emacs

Initialization (early-init.el)

In this section I handle my early init file; it takes care of frame-setup for emacsclient buffers.

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.

  (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 (* 1000 1000 8)
                ;; (setq gc-cons-threshold #x40000000
                   (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)
                )))

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))))

Make C-i, C-m, C-[ available in graphic sessions

By default, emacs binds

  • C-i to the TAB key
  • C-m to the RET key
  • C-[ to the ECS key

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])
         ))))

Personal settings

This section is used to define my own functions, own variables, and own keybindings.

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.

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.

  (defun swarsel/toggle-evil-state ()
    (interactive)
    (if (or (evil-emacs-state-p) (evil-insert-state-p))
        (evil-normal-state)
      (evil-emacs-state)))
Switching to last used buffer

I often find myself bouncing between two buffers when I do not want to use a window split. This funnction simply jumps to the last used buffer.

  (defun swarsel/last-buffer () (interactive) (switch-to-buffer nil))
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)
            (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 "leon@swarsel.win"
          user-full-name "Leon Schwarzäugl"))
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)
[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

  1. screw with undo/redo
  2. 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.

  (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))))
[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))
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?)
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)
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 ()
    ;; (org-indent-mode)
    (variable-pitch-mode 1)
    ;;(auto-fill-mode 0)
    ;; (setq display-line-numbers-type 'relative
    ;;       display-line-numbers-current-absolute 1
    ;;       display-line-numbers-width-start nil
    ;;       display-line-numbers-width 6
    ;;       display-line-numbers-grow-only 1)
    (add-hook 'org-tab-first-hook 'org-end-of-line)
    (visual-line-mode 1))
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))
org-mode: Upon-save actions (Auto-tangle, export to html, formatting)

This section handles everything that shoudld happen when I save SwarselSystems.org. It:

  1. automatically tangles all configuration blocks in this file
  2. exports the configuration file as html for an easier reading experience with working links and index
  3. formats the generated .nix files in accordance to the Alejandra-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)))
org-mode: Fold current heading

Normally emacs cycles between three states:

  1. fully folded
  2. One heading expanded
  3. All headings expanded

However, I want to be able to fold a single heading consistently.

  (defun org-fold-outer ()
    (interactive)
    (org-beginning-of-line)
    (if (string-match "^*+" (thing-at-point 'line t))
        (outline-up-heading 1))
    (outline-hide-subtree)
    )
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))
python shell reloading

The standard Emacs behaviour for the Python process shell is a bit annoying. This is my attempt at making it show automatically on opening a python buffer and making it refresh on its own as well. This does not nicely work yet.

  ;; run the python inferior shell immediately upon entering a python buffer
  ;; (add-hook 'python-mode-hook 'swarsel/run-python)

  ;; (defun swarsel/run-python ()
  ;;   (save-selected-window
  ;;     (switch-to-buffer-other-window (process-buffer (python-shell-get-or-create-process (python-shell-parse-command))))))

  ;; reload python shell automatically
  (defun my-python-shell-run ()
    (interactive)
    (when (get-buffer-process "*Python*")
      (set-process-query-on-exit-flag (get-buffer-process "*Python*") nil)
      (kill-process (get-buffer-process "*Python*"))
      ;; Uncomment If you want to clean the buffer too.
      ;;(kill-buffer "*Python*")
      ;; Not so fast!
      (sleep-for 0.5))
    (run-python (python-shell-parse-command) nil nil)
    (python-shell-send-buffer)
    ;; Pop new window only if shell isnt visible
    ;; in any frame.
    (unless (get-buffer-window "*Python*" t)
      (python-shell-switch-to-shell)))

  (defun my-python-shell-run-region ()
    (interactive)
    (python-shell-send-region (region-beginning) (region-end))
    (python-shell-switch-to-shell))
Nix common prefix bracketer

This function searches for common delimiters in region and removes them, summarizing all captured lines by it.

(defun swarsel/prefix-block (start end)
  (interactive "r")
  (save-excursion
    (goto-char start)
    (setq start (line-beginning-position))
    (goto-char end)
    (setq end (line-end-position))
    (let ((common-prefix (save-excursion
                           (goto-char start)
                           (if (re-search-forward "^\\([^.\n]+\\)\\." end t)
                               (match-string 1)
                             (error "No common prefix found")))))
      (save-excursion
        (goto-char start)
        (insert common-prefix " = {\n")
        (goto-char (+ end (length common-prefix) 6))
        (insert "};\n")
        (goto-char start)
        (while (re-search-forward (concat "^" (regexp-quote common-prefix) "\\.") end t)
          (replace-match ""))))))
Nix formatters

This formats the org code block at point in accordance to the nixpkgs-fmt formatter

  (defun swarsel/org-nixpkgs-fmt-block-lite ()
    (interactive)
    (org-babel-mark-block)
    (call-interactively 'nixpkgs-fmt-region))


    (defun swarsel/org-nixpkgs-fmt-block ()
      (interactive)
      (save-excursion
        (let* ((element (org-element-at-point))
               (begin (org-element-property :begin element))
               (end (org-element-property :end element))
               (lang (org-element-property :language element)))
          (when lang
            (goto-char begin)
            (forward-line)
            (insert "{")
            (goto-char end)
            (forward-line -1)
            (beginning-of-line)
            (forward-char -1)
            (insert "}")
            (org-babel-mark-block)
            (call-interactively 'nixpkgs-fmt-region)))))
Disable garbace 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)

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" '(elfeed :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")
        ;; "cj" '((lambda () (interactive) (org-capture nil "jj")) :which-key "journal")
        ;; "cs" '(markdown-download-screenshot :which-key "screenshot")
        "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")
        "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")
        "ls" '((lambda () (interactive) (find-file "/smb:Swarsel@winters:")) :which-key "Server")
        "lo" '(dired swarsel-obsidian-vault-directory :which-key "obsidian")
        ;; "la" '((lambda () (interactive) (find-file swarsel-org-anki-filepath)) :which-key "anki")
        ;; "ln" '((lambda () (interactive) (find-file swarsel-nix-org-filepath)) :which-key "Nix.org")
        "lp" '((lambda () (interactive) (projectile-switch-project)) :which-key "switch project")
        "lg" '((lambda () (interactive) (magit-list-repositories)) :which-key "list git repos")
        ;; "a"   '(:ignore a :which-key "anki")
        ;; "ap"  '(anki-editor-push-tree :which-key "push new cards")
        ;; "an"  '((lambda () (interactive) (org-capture nil "a")) :which-key "new card")
        ;; "as"  '(swarsel-anki-set-deck-and-notetype :which-key "change deck and notetype")
        "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-M-d" 'swarsel-obsidian-daily ; open daily obsidian file and create if not exist
     ;; "C-M-S" 'swarsel-anki-set-deck-and-notetype ; switch deck and notetype for new anki cards
     ;; "C-M-s" 'markdown-download-screenshot ; wrapper for org-download-screenshot
     "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
     "C-<f9>" 'my-python-shell-run
     "<Paste>" 'yank
     "<Cut>" 'kill-region
     "<Copy>" 'kill-ring-save
     "<undo>" 'evil-undo
     "<redo>" 'evil-redo
     )

Directory setup / File structure

In this section I setup some aliases that I use for various directories on my system. Some of these are actually used for magit repository finding etc., but many of them serve no real use and I need to clean this up someday.

  ;; set Nextcloud directory for journals etc.
  (setq swarsel-sync-directory "~/Nextcloud"
        swarsel-emacs-directory "~/.emacs.d"
        swarsel-dotfiles-directory "~/.dotfiles"
        swarsel-projects-directory "~/Documents/GitHub")

  (setq swarsel-emacs-org-filepath (expand-file-name "Emacs.org" swarsel-dotfiles-directory)
        swarsel-nix-org-filepath (expand-file-name "Nix.org" swarsel-dotfiles-directory)
        swarsel-swarsel-org-filepath (expand-file-name "SwarselSystems.org" swarsel-dotfiles-directory)
        )


  ;; set Emacs main configuration .org names
  (setq swarsel-emacs-org-file "Emacs.org"
        swarsel-anki-org-file "Anki.org"
        swarsel-tasks-org-file "Tasks.org"
        swarsel-archive-org-file "Archive.org"
        swarsel-org-folder-name "Org"
        swarsel-obsidian-daily-folder-name "⭐ Personal/Journal"
        swarsel-obsidian-folder-name "Obsidian"
        swarsel-obsidian-vault-name "Main")


  ;; set directory paths
  (setq swarsel-org-directory (expand-file-name swarsel-org-folder-name  swarsel-sync-directory)) ; path to org folder
  (setq swarsel-obsidian-directory (expand-file-name swarsel-obsidian-folder-name swarsel-sync-directory)) ; path to obsidian
  (setq swarsel-obsidian-vault-directory (expand-file-name swarsel-obsidian-vault-name swarsel-obsidian-directory)) ; path to obsidian vault
  (setq swarsel-obsidian-daily-directory (expand-file-name swarsel-obsidian-daily-folder-name swarsel-obsidian-vault-directory)) ; path to obsidian daily folder

  ;; filepaths to certain documents
  (setq swarsel-org-anki-filepath (expand-file-name swarsel-anki-org-file swarsel-org-directory) ; path to anki export file
        swarsel-org-tasks-filepath (expand-file-name swarsel-tasks-org-file swarsel-org-directory)
        swarsel-org-archive-filepath (expand-file-name swarsel-archive-org-file swarsel-org-directory))

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)

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

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.

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)
    ;; 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)

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)

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).

  (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

Better garbage collection

(setq garbage-collection-messages t)
(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))))))

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)

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)

Evil

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)))
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))
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))
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)
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))

ispell

This should setup a wordlist that can be used as a dictionary. However, for some reason this does not work, and I will need to further investigate this issue.

  ;; set the NixOS wordlist by hand
  (setq ispell-alternate-dictionary (getenv "WORDLIST"))

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)

  ;; these settings used to be in custom.el

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))

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)

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))

Modeline

Here I set up the modeline with some information that I find useful. Specficially I am using the doom modeline. Most informations I disable for it, except for the cursor information (row + column) as well as a widget for mu4e and git information.

  (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)))

Helper Modes

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:

  • Vertico simply provides a vertically stacking completion
  • Marginalia adds more information to completion results
  • Orderless allows for fuzzy matching
  • Consult provides better implementations for several user functions, e.g. consult-line or consult-outline
  • 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.

Nerd icons is originally enabled here: Icons

vertico
  (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))
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))
orderless

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)))
consult

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)))
embark

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)))))
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))
marginalia

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
    :init
    (marginalia-mode)
    (setq marginalia-annotators '(marginalia-annotators-heavy marginalia-annotators-light nil)))
nerd-icons-completion

As stated above, this simply provides nerd-icons to the completion framework.

  (use-package nerd-icons-completion
    :after (marginalia nerd-icons)
    :hook (marginalia-mode . nerd-icons-completion-marginalia-setup)
    :init
    (nerd-icons-completion-mode))
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))

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" "&&" "^=" "~~" "~@" "~="
                              "~>" "~-" "**" "*>" "*/" "||" "|}" "|]" "|=" "|>" "|-" "{|"
                              "[|" "]#" "::" ":=" ":>" ":<" "$>" "==" "=>" "!=" "!!" ">:"
                              ">=" ">>" ">-" "-~" "-|" "->" "--" "-<" "<~" "<*" "<|" "<:"
                              "<$" "<=" "<>" "<-" "<<" "<+" "</" "#{" "#[" "#:" "#=" "#!"
                              "##" "#(" "#?" "#_" "%%" ".=" ".." ".?" "+>" "++" "?:" "?="
                              "?." "??" "/*" "/=" "/>" "//" "__" "~~" "(*" "*)" "\\\\"
                              "://" ";;")))

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))

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)

Authentication

This defines the authentication sources used by org-calfw (Calendar) and Forge.

  ;; (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)

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.

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.

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

  (use-package org
    ;;:diminish (org-indent-mode)
    :hook (org-mode . swarsel/org-mode-setup)
    :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" "$" "$$" "\\(" "\\[")))
org-agenda

Here I setup a plethora of keywords, keybinds and paths to give my org-agenda more power.

  (setq org-agenda-files '("/home/swarsel/Nextcloud/Org/Tasks.org"
                           "/home/swarsel/Nextcloud/Org/Archive.org"
                           "/home/swarsel/Nextcloud/Org/Anki.org"
                           "/home/swarsel/Calendars/leon_cal.org"))

  (setq org-refile-targets
        '((swarsel-archive-org-file :maxlevel . 1)
          (swarsel-anki-org-file :maxlevel . 1)
          (swarsel-tasks-org-file :maxlevel . 1)))

  (setq org-todo-keywords
        '((sequence "TODO(t)" "NEXT(n)" "|" "DONE(d!)")
          (sequence "BACKLOG(b)" "PLAN(p)" "READY(r)" "ACTIVE(a)" "REVIEW(v)" "WAIT(w@/!)" "HOLD(h)" "|" "COMPLETED(c)" "CANC(k@)")))


  ;; Configure custom agenda views
  (setq org-agenda-custom-commands
        '(("d" "Dashboard"
           ((agenda "" ((org-deadline-warning-days 7)))
            (todo "NEXT"
                  ((org-agenda-overriding-header "Next Tasks")))
            (tags-todo "agenda/ACTIVE" ((org-agenda-overriding-header "Active Projects")))))

          ("n" "Next Tasks"
           ((todo "NEXT"
                  ((org-agenda-overriding-header "Next Tasks")))))

          ("W" "Work Tasks" tags-todo "+work-email")


          ("w" "Workflow Status"
           ((todo "WAIT"
                  ((org-agenda-overriding-header "Waiting on External")
                   (org-agenda-files org-agenda-files)))
            (todo "REVIEW"
                  ((org-agenda-overriding-header "In Review")
                   (org-agenda-files org-agenda-files)))
            (todo "PLAN"
                  ((org-agenda-overriding-header "In Planning")
                   (org-agenda-todo-list-sublevels nil)
                   (org-agenda-files org-agenda-files)))
            (todo "BACKLOG"
                  ((org-agenda-overriding-header "Project Backlog")
                   (org-agenda-todo-list-sublevels nil)
                   (org-agenda-files org-agenda-files)))
            (todo "READY"
                  ((org-agenda-overriding-header "Ready for Work")
                   (org-agenda-files org-agenda-files)))
            (todo "ACTIVE"
                  ((org-agenda-overriding-header "Active Projects")
                   (org-agenda-files org-agenda-files)))
            (todo "COMPLETED"
                  ((org-agenda-overriding-header "Completed Projects")
                   (org-agenda-files org-agenda-files)))
            (todo "CANC"
                  ((org-agenda-overriding-header "Cancelled Projects")
                   (org-agenda-files org-agenda-files)))))))
org capture templates

I wrote these capture templates to allow myself to quickly create Anki cards from within Emacs. I nearly never use this feature, but it cannot hurt to have.

  (setq org-capture-templates
        `(
          ("a" "Anki basic"
           entry
           (file+headline swarsel-org-anki-filepath "Dispatch")
           (function swarsel-anki-make-template-string))

          ("A" "Anki cloze"
           entry
           (file+headline org-swarsel-anki-file "Dispatch")
           "* %<%H:%M>\n:PROPERTIES:\n:ANKI_NOTE_TYPE: Cloze\n:ANKI_DECK: 🦁 All::01 ❤️ Various::00 ✨ Allgemein\n:END:\n** Text\n%?\n** Extra\n")
          ("t" "Tasks / Projects")
          ("tt" "Task" entry (file+olp swarsel-org-tasks-filepath "Inbox")
           "* TODO %?\n  %U\n  %a\n  %i" :empty-lines 1)
          ))
  )
Font Faces

Again, my understanding of the font-faces in Emacs is limited. This is mostly just tuned so that my org-files look acceptable.

  ;; Set faces for heading levels
  (with-eval-after-load 'org-faces  (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)))

                        ;; Ensure that anything that should be fixed-pitch in Org files appears that way
                        (set-face-attribute 'org-block nil   :inherit 'fixed-pitch)
                        (set-face-attribute 'org-table nil   :inherit 'fixed-pitch)
                        (set-face-attribute 'org-formula nil   :inherit 'fixed-pitch)
                        (set-face-attribute 'org-code nil :inherit '(shadow fixed-pitch))
                        (set-face-attribute 'org-verbatim nil :inherit '(shadow fixed-pitch))
                        (set-face-attribute 'org-special-keyword nil :inherit '(font-lock-comment-face fixed-pitch))
                        (set-face-attribute 'org-meta-line nil :inherit '(font-lock-comment-face fixed-pitch))
                        (set-face-attribute 'org-checkbox nil :inherit 'fixed-pitch))
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))
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))
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)
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.

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)
old easy structure templates
  • 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 press RET. 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 :tangle"))
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-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)
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.

  (use-package org-download
    :after org
    :defer nil
    :custom
    (org-download-method 'directory)
    (org-download-image-dir "./images")
    (org-download-heading-lvl 0)
    (org-download-timestamp "org_%Y%m%d-%H%M%S_")
    ;;(org-image-actual-width 500)
    (org-download-screenshot-method "grim -g \"$(slurp)\" %s")
    :bind
    ("C-M-y" . org-download-screenshot)
    :config
    (require 'org-download))
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)
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))
Presentations

Recently I have grown fond of holding presentations using Emacs :)

  (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)

Nix Mode

This adds a rudimentary nix-mode to Emacs. I have not really tried this out, as I am mostly editing nix-files in org-mode anyways.

  (use-package nix-mode)

  (use-package nix-ts-mode
    :mode "\\.nix\\'")

HCL Mode

This adds support for Hashicorp Configuration Language. I need this at work.

  (use-package hcl-mode
    :mode "\\.hcl\\'"
    :config
    (setq hcl-indent-level 2))

Jenkinsfile/Groovy

This adds support for Groovy, which I specifically need to work with Jenkinsfiles. I need this at work.

  (use-package groovy-mode)

  (use-package jenkinsfile-mode
    :mode "Jenkinsfile")

Dockerfile

This adds support for Dockerfiles. I need this at work.

  (use-package dockerfile-mode
    :mode "Dockerfile")

Terraform Mode

This adds support for Terraform configuration files. I need this at work.

  (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)

nixpkgs-fmt

Adds functions for formatting nix code.

  (use-package nixpkgs-fmt)

shfmt

Adds functions for formatting shellscripts.

  (use-package shfmt
    :config
    (setq shfmt-command "shfmt")
    (setq shfmt-arguments '("-i" "4" "-s" "-sr")))

Markdown Mode

Mode
  (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)))
LaTeX in Markdown
  (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)
              ))

Olivetti

Olivetti is a mode specialized for writing prose in Emacs. I went for a very simple setup with little distractions.

This mode is not automatically activated anywhere because I only rarely need it.

  (use-package olivetti
    :init
    (setq olivetti-body-width 100)
    (setq olivetti-recall-visual-line-mode-entry-state t))

elfeed

  ;; (setq elfeed-feeds
  ;;       '("https://www.coindesk.com/arc/outboundfeeds/rss/"
  ;;         "https://feed.phenx.de/lootscraper_gog_game.xml"
  ;;         "https://feed.phenx.de/lootscraper_ubisoft_game.xml"
  ;;         "https://hnrss.org/frontpage"
  ;;         "https://www.derstandard.at/rss/inland"
  ;;         "https://www.derstandard.at/rss/international"
  ;;         "https://www.derstandard.at/rss/kultur"
  ;;         "https://www.derstandard.at/rss/wissenschaft"
  ;;         "https://www.rfc-editor.org/rfcrss.xml"
  ;;         "https://waitbutwhy.com/feed"
  ;;         "https://steamcommunity.com/groups/freegamesfinders/rss/"))

  (use-package elfeed
    :ensure t
    :bind (:map elfeed-search-mode-map
                                          ;              ("A" . bjm/elfeed-show-all)
                                          ;              ("E" . bjm/elfeed-show-emacs)
                                          ;              ("D" . bjm/elfeed-show-daily)
                ("q" . bjm/elfeed-save-db-and-bury)))


  (require 'elfeed)

  ;; Load elfeed-org
  (use-package elfeed-org
    :config
    (elfeed-org)
    (setq rmh-elfeed-org-files (list "~/.elfeed/elfeed.org"))
    )

  (use-package elfeed-goodies)
  (elfeed-goodies/setup)

  (use-package elfeed-web)

  ;;functions to support syncing .elfeed between machines
  ;;makes sure elfeed reads index from disk before launching
  (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))

  ;;write to disk when quiting
  (defun bjm/elfeed-save-db-and-bury ()
    "Wrapper to save the elfeed db to disk before burying buffer"
    (interactive)
    (elfeed-db-save)
    (quit-window))


  (global-set-key (kbd "C-c w") 'bjm/elfeed-load-db-and-open)


  (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)

darkroom

Darkroom is package that reduces all forms of distraction to a minimum - this can be useful when simply reading a file for example. For this mode I have increased the text scale by a large margin to make for comfortable reading This mode is not automatically activated anywhere because I only rarely need it.

  (use-package darkroom
    :init
    (setq darkroom-text-scale-increase 3))

Ripgrep

This is the ripgrep command for Emacs.

  (use-package rg)

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.

  (mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist))
bash c cmake cpp css elisp go html javascript json julia latex make markdown R python typescript rust sql toml tsx yaml
  (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
    :config
    (global-treesit-auto-mode)
    (setq treesit-auto-install 'prompt))

direnv (envrc)

  (use-package direnv
    :custom (direnv-always-show-summary nil)
    :config (direnv-mode))

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))

crdt (Collaborative Editing)

With this it is possible to work on the same file collaboratively. I have never tried it out, but it sounds cool.

  (use-package crdt)

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)

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-projects-directory)
      (setq projectile-project-search-path (list swarsel-projects-directory)))
    (setq projectile-switch-project-action #'magit-status))

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 also set that here.

  (use-package magit
    :config
    (setq magit-repository-directories `((,swarsel-projects-directory  . 1)
                                         (,swarsel-emacs-directory . 0)
                                         (,swarsel-obsidian-directory . 0)
                                         ("~/.dotfiles/" . 0)))
    :custom
    (magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)) ; stay in the same window

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.

  ;; 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")))

Forge

NOTE: Make sure to configure a GitHub token before using this package!

  (use-package forge
    :after magit)

  (with-eval-after-load 'forge
    (add-to-list 'forge-alist
                 '("sgit.iue.tuwien.ac.at"
                   "sgit.iue.tuwien.ac.at/api/v1"
                   "sgit.iue.tuwien.ac.at"
                   forge-gitea-repository)))

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))

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))))))

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))

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
  ;;   :custom
  ;;   (corfu-cycle t)
  ;;   :init
  ;;   (global-corfu-mode))

  (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 0.3)
    (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)))

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)
    ;; Add to the global default value of `completion-at-point-functions' which is
    ;; used by `completion-at-point'.  The order of the functions matters, the
    ;; first function returning a result wins.  Note that the list of buffer-local
    ;; completion functions takes precedence over the global list.
    ;; (add-to-list 'completion-at-point-functions #'cape-dabbrev)
    ;; (add-to-list 'completion-at-point-functions #'cape-file)
    ;; (add-to-list 'completion-at-point-functions #'cape-elisp-block)
    ;; (add-to-list 'completion-at-point-functions #'cape-history)
    ;; (add-to-list 'completion-at-point-functions #'cape-keyword)
    ;; (add-to-list 'completion-at-point-functions #'cape-tex)
    ;; (add-to-list 'completion-at-point-functions #'cape-sgml)
    ;; (add-to-list 'completion-at-point-functions #'cape-rfc1345)
    ;; (add-to-list 'completion-at-point-functions #'cape-abbrev)
    ;; (add-to-list 'completion-at-point-functions #'cape-dict)
    ;; (add-to-list 'completion-at-point-functions #'cape-elisp-symbol)
    ;; (add-to-list 'completion-at-point-functions #'cape-line)
    )

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))

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
    :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'")))

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))

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))

yasnippet

yasnippet allows to define snippets that can be quickly expanded by hitting the TAB key after inputting a keyword.

I used to run this together with the yasnippet-snippets package, but the snippets in there I did not find all too useful for myself. I need to create some custom snippets here one day.

  (use-package yasnippet
    :init (yas-global-mode 1)
    :config
    (yas-reload-all))
yasnippet math-snippets

The following block is mostly inspired from https://code.kulupu.party/thesuess/WTFmacs/ and sets up a few prefixes that make LaTeX-math-mode nicer to use even with auctex and cape enabled.

  (setq wtf/latex-mathbb-prefix "''")
  (setq swarsel/latex-mathcal-prefix "``")

  (use-package yasnippet
    :config

    (setq wtf/english-alphabet
          '("a" "b" "c" "d" "e" "f" "g" "h" "i" "j" "k" "l" "m" "n" "o" "p" "q" "r" "s" "t" "u" "v" "w" "x" "y" "z"))

    (dolist (elem wtf/english-alphabet)
      (when (string-equal elem (downcase elem))
        (add-to-list 'wtf/english-alphabet (upcase elem))))


    (yas-define-snippets
     'latex-mode
     (mapcar
      (lambda (elem)
        (list (concat wtf/latex-mathbb-prefix elem) (concat "\\mathbb{" elem "}") (concat "Mathbb letter " elem)))
      wtf/english-alphabet))

    (yas-define-snippets
     'latex-mode
     (mapcar
      (lambda (elem)
        (list (concat swarsel/latex-mathcal-prefix elem) (concat "\\mathcal{" elem "}") (concat "Mathcal letter " elem)))
      wtf/english-alphabet))

    (setq swtf/latex-math-symbols
          '(("x" . "\\times")
            ("*" . "\\cdot")
            ("." . "\\ldots")
            ("op" . "\\operatorname{$1}$0")
            ("o" . "\\circ")
            ("V" . "\\forall")
            ("v" . "\\vee")
            ("w" . "\\wedge")
            ("q" . "\\quad")
            ("f" . "\\frac{$1}{$2}$0")
            ("s" . "\\sum_{$1}^{$2}$0")
            ("p" . "\\prod_{$1}^{$2}$0")
            ("e" . "\\exists")
            ("i" . "\\int_{$1}^{$2}$0")
            ("c" . "\\cap")
            ("u" . "\\cup")
            ("0" . "\\emptyset")))

    )

eglot

After having tried out lsp-mode and lsp-bridge for a while each, I must say that eglot feels the most clean and fast to me.

  (use-package eglot
    :config
    (add-to-list 'eglot-server-programs
         '(yaml-ts-mode . ("ansible-language-server" "--stdio")))
    :hook
    ((python-mode
      python-ts-mode
      c-mode
      c-ts-mode
      c++-mode
      c++-ts-mode
      rust-ts-mode
      rustic-mode
      tex-mode
      LaTeX-mode
      yaml-ts-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)

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)))

Breadcrumb

This simple shows the path to the current file on the top of the buffer - I just think it looks kind of neat, even though it is not extremely useful :)

    (use-package breadcrumb
      ;; :config (breadcrumb-mode)
      )

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)

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)))

pdf-tools: pdf-viewer and support for dirvish

This enables pdf-previewing in dirvish and gives a much better pdf-viewer than is shipped normally by emacs.

  ;; (use-package pdf-tools
  ;;   :init
  ;;   (if (not (boundp 'pdf-tools-directory))
  ;;       (pdf-tools-install))
  ;;   :mode ("\\.pdf" . pdf-view-mode))

Jupyter

This is a jupyter client. Using it is a bit cumbersome though, so I have not fully explored all features.

  (use-package ein)

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"))))

  ;; (add-hook 'prog-mode-hook 'undo-tree-mode)
  ;; (add-hook 'text-mode-hook 'undo-tree-mode)
  ;; (add-hook 'org-mode-hook 'undo-tree-mode)
  ;; (add-hook 'latex-mode-hook 'undo-tree-mode)

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)
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))

External Applications

Obsidian

This provides an interface to Obsidian for Emacs - as much as I want to like it, I actually enjoy using the official Obsidian app more - even though that cannot be used by Emacs directly.

My workflow for Obsidian is now as follows:

  1. create notes either in Emacs or Obsidian
  2. look at them in the official client I hope that this package will improve, then I will come back to it one day.
  ;; (use-package obsidian
  ;;   :ensure t
  ;;   :demand t
  ;;   :config
  ;;   (obsidian-specify-path swarsel-obsidian-vault-directory)
  ;;   (global-obsidian-mode t)
  ;;   :custom
  ;;   ;; This directory will be used for `obsidian-capture' if set.
  ;;   (obsidian-inbox-directory "Inbox")
  ;;   (bind-key (kbd "C-c M-o") 'obsidian-hydra/body 'obsidian-mode-map)
  ;;   :bind (:map obsidian-mode-map
  ;;               ;; Replace C-c C-o with Obsidian.el's implementation. It's ok to use another key binding.
  ;;               ("C-c C-o" . obsidian-follow-link-at-point)
  ;;               ;; Jump to backlinks
  ;;               ("C-c C-b" . obsidian-backlink-jump)
  ;;               ;; If you prefer you can use `obsidian-insert-link'
  ;;               ("C-c C-l" . obsidian-insert-wikilink)))
Anki

This section is here to make Anki usable from within Emacs - an endeavour that I have mostly given up on.

Basic Anki setup
  ;; (use-package anki-editor
  ;;   :after org
  ;;   :bind (:map org-mode-map
  ;;               ("<f12>" . anki-editor-cloze-region-auto-incr)
  ;;               ("<f11>" . anki-editor-cloze-region-dont-incr)
  ;;               ("<f10>" . anki-editor-reset-cloze-number)
  ;;               ("<f9>"  . anki-editor-push-tree))
  ;;   :hook (org-capture-after-finalize . anki-editor-reset-cloze-number) ; Reset cloze-number after each capture.
  ;;   :config
  ;;   (setq anki-editor-create-decks t ;; Allow anki-editor to create a new deck if it doesn't exist
  ;;         anki-editor-org-tags-as-anki-tags t)

  ;;   (defun anki-editor-cloze-region-auto-incr (&optional arg)
  ;;     "Cloze region without hint and increase card number."
  ;;     (interactive)
  ;;     (anki-editor-cloze-region swarsel-anki-editor-cloze-number "")
  ;;     (setq swarsel-anki-editor-cloze-number (1+ swarsel-anki-editor-cloze-number))
  ;;     (forward-sexp))
  ;;   (defun anki-editor-cloze-region-dont-incr (&optional arg)
  ;;     "Cloze region without hint using the previous card number."
  ;;     (interactive)
  ;;     (anki-editor-cloze-region (1- swarsel-anki-editor-cloze-number) "")
  ;;     (forward-sexp))
  ;;   (defun anki-editor-reset-cloze-number (&optional arg)
  ;;     "Reset cloze number to ARG or 1"
  ;;     (interactive)
  ;;     (setq swarsel-anki-editor-cloze-number (or arg 1)))
  ;;   (defun anki-editor-push-tree ()
  ;;     "Push all notes under a tree."
  ;;     (interactive)
  ;;     (anki-editor-push-notes '(4))
  ;;     (anki-editor-reset-cloze-number))
  ;;   ;; Initialize
  ;;   (anki-editor-reset-cloze-number)
  ;;   )

  ;; (require 'anki-editor)
Own Anki functions
  • These functions enable you to quickly set the destination note type and deck
  ;; (defvar swarsel-anki-deck nil)
  ;; (defvar swarsel-anki-notetype nil)
  ;; (defvar swarsel-anki-fields nil)

  ;; (defun swarsel-anki-set-deck-and-notetype ()
  ;;   (interactive)
  ;;   (setq swarsel-anki-deck  (completing-read "Choose a deck: "
  ;;                                             (sort (anki-editor-deck-names) #'string-lessp)))
  ;;   (setq swarsel-anki-notetype (completing-read "Choose a note type: "
  ;;                                                (sort (anki-editor-note-types) #'string-lessp)))
  ;;   (setq swarsel-anki-fields (progn
  ;;                               (anki-editor--anki-connect-invoke-result "modelFieldNames" `((modelName . ,swarsel-anki-notetype)))))
  ;;   )

  ;; (defun swarsel-anki-make-template-string ()
  ;;   (if (not swarsel-anki-deck)
  ;;       (call-interactively 'swarsel-anki-set-deck-and-notetype))
  ;;   (setq swarsel-temp swarsel-anki-fields)
  ;;   (concat (concat "* %<%H:%M>\n:PROPERTIES:\n:ANKI_NOTE_TYPE: " swarsel-anki-notetype "\n:ANKI_DECK: " swarsel-anki-deck "\n:END:\n** ")(pop swarsel-temp) "\n%?\n** " (mapconcat 'identity swarsel-temp "\n\n** ") "\n\n"))

  ;; (defun swarsel-today()
  ;;   (format-time-string "%Y-%m-%d"))

  ;; (defun swarsel-obsidian-daily ()
  ;;   (interactive)
  ;;   (if (not (file-exists-p (expand-file-name (concat (swarsel-today) ".md") swarsel-obsidian-daily-directory)))
  ;;       (write-region "" nil (expand-file-name (concat (swarsel-today) ".md") swarsel-obsidian-daily-directory))
  ;;     )
  ;;   (find-file (expand-file-name (concat (swarsel-today) ".md") swarsel-obsidian-daily-directory)))

Email

make sure mu4e is found

This seems not to be needed - I do not yet dare to delete it though.

  ;; (let ((mu4epath
  ;;        (concat
  ;;         (f-dirname
  ;;          (file-truename
  ;;           (executable-find "mu")))
  ;;         "/../share/emacs/site-lisp/mu4e")))
  ;;   (when (and
  ;;          (string-prefix-p "/nix/store/" mu4epath)
  ;;          (file-directory-p mu4epath))
  ;;     (add-to-list 'load-path mu4epath)))
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)

    (setq mu4e-update-interval 180)
    (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 "/Sent Mail"     :key ?s)
            (:maildir "/Trash"     :key ?t)
            (:maildir "/Drafts"     :key ?d)
            (:maildir "/All Mail"     :key ?a)))

    (setq user-mail-address "leon@swarsel.win"
          user-full-name "Leon Schwarzäugl")


    (setq mu4e-user-mail-address-list '(leon.schwarzaeugl@gmail.com leon@swarsel.win nautilus.dw@gmail.com mrswarsel@gmail.com)))


  (add-hook 'mu4e-compose-mode-hook #'swarsel/mu4e-send-from-correct-address)
  (add-hook 'mu4e-compose-post-hook #'swarsel/mu4e-restore-default)
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
    (setq mu4e-alert-set-default-style 'libnotify))

  (add-hook 'after-init-hook #'mu4e-alert-enable-notifications)

  (mu4e t)

Calendar

This provides a beautiful calender to emacs.

Yes, I am aware that I am exposing my university-calendar to the public here. I can imagine worse things ;) if you however know how to obscure this, let me know!

  (use-package org-caldav
    :init
    ;; set org-caldav-sync-initalization
    (setq swarsel-caldav-synced 0)
    (setq org-caldav-url "https://stash.swarsel.win/remote.php/dav/calendars/Swarsel")
    (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)
    ;; (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 "Purple")  ; orgmode source
      (cfw:ical-create-source "TISS" "https://tiss.tuwien.ac.at/events/rest/calendar/personal?locale=de&token=4463bf7a-87a3-490a-b54c-99b4a65192f3" "Cyan"))))

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*")))
    (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/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 "instagram.com/Swarsele")))

             (,""
              "SwarselSound"
              "Browse SwarselSound"
              (lambda (&rest _) (browse-url "sound.swarsel.win")) )
             (,""
              "SwarselSwarsel"
              "Browse Swarsel"
              (lambda (&rest _) (browse-url "github.com/Swarsel")) )
             (,""
              "SwarselStash"
              "Browse SwarselStash"
              (lambda (&rest _) (browse-url "stash.swarsel.win")) )
             (,"󰫑"
              "SwarselSport"
              "Browse SwarselSports"
              (lambda (&rest _) (browse-url "social.parkour.wien/@Lenno")))
             )
            (
             (,"󱄅"
              "swarsel.win"
              "Browse swarsel.win"
              (lambda (&rest _) (browse-url "swarsel.win")))
             )
            )))

vterm

  (use-package vterm
      :ensure t)

  (defun sudo-find-file (file-name)
  "Like find file, but opens the file as root."
  (interactive "FSudo Find File: ")
  (let ((tramp-file-name (concat "/sudo::" (expand-file-name file-name))))
    (find-file tramp-file-name)))
  ;;; vterm/config.el -*- lexical-binding: t; -*-

    ;; Original functions overwrites tramp path with a guessed path.
    ;; However it breaks if remote fqdn/hostname is not resolvale by local machine
    ;; could also break on port forwarding, multihops,
    ;; custom protocol such as: docker, vagrant, ...
    ;; *if* you try to shell-side configure them.
    ;; Easily testable with vagrant ssh port on localhost.
    ;; My workflow is to open a tramp dired on / of the remote to get a
    ;; "foothold" then open vterms from there.
    (defun vterm--get-directory (path)
      "[OVERLOADED] Get normalized directory to PATH."
      (when path
        (let (directory)
          (if (string-match "^\\(.*?\\)@\\(.*?\\):\\(.*?\\)$" path)
              (progn
                (let ((user (match-string 1 path))
                      (host (match-string 2 path))
                      (dir (match-string 3 path)))
                  (if (and (string-equal user user-login-name)
                           (string-equal host (system-name)))
                      (progn
                        (when (file-directory-p dir)
                          (setq directory (file-name-as-directory dir))))
                    (setq directory
                          ;; Bellow is what i altered
                          (file-name-as-directory (concat (file-remote-p default-directory) dir))))))
            (when (file-directory-p path)
              (setq directory (file-name-as-directory path))))
          directory)))
    ;; Injects the payload to the vterm buffer.
    (defun me/vterm-load-config ()
      "Pass local configuration files to vterm.

  Allows remote vterm to be shell-side configured,
  without altering remote config.
  Also adds my personal configuration that does not rely
  too much on external packages.
  Prints a reasuring message to proove good faith."
      (interactive)
      (let (;; Bellow messages to reassure other users that look at history
            (reasuring-message (format "Configuring shell of user %s to be emacs comptible"
                                       user-full-name))
            (reasuring-notice "This action is shell local, it will not affect other shells")
            ;; Bellow lies my configuration
            (basic-func-script (f-read-text (concat (getenv "HOME")
                                                    "/.emacs.d/shells/sources/functions.sh")))
            ;; Bellow lies the vterm shell-side configuration
            ;; Must be sourced last
            (vterm-func-script (f-read-text (concat
                                             (file-name-directory (find-library-name "vterm"))
                                             "/etc/emacs-vterm-bash.sh"))))
        (vterm-insert (format "# START: %s\n" reasuring-message))
        (vterm-insert (format "# %s\n" reasuring-notice))
        ;; Create one single block in history
        (vterm-insert "{\n")
        (vterm-insert basic-func-script)
        (vterm-insert vterm-func-script)
        (vterm-insert "}\n")
        ;; End the single block in history
        (vterm-insert (format "# %s\n" reasuring-notice))
        (vterm-insert (format "# STOP: %s\n" reasuring-message))
        )
      )

    ;; find-file-other-window does not works great on remote:
    ;; if given an absolute path on a remote host,
    ;; the path will be understood as a local file since no
    ;; tramp prefix is present, and bash does not care
    ;; about tramp prefixes.
    ;; Bellow we solve context before sending it to
    ;; ffow
    (defun me/vterm--find-file-other-window-wrapper (file)
      "Help vterm find a FILE."
      (find-file-other-window (me/vterm--ffow-resolver file)))
    (defun me/vterm--ffow-resolver (file)
      "Help vterm resolve FILE."
      (cond
       ;; "/sudo::"
       ;; doom--sudo-file-path do the trick for us
       ((s-starts-with-p "/sudo::" file)
         (sudo-find-file
          (concat (file-remote-p default-directory)
                  (substring-no-properties file 7))))
       ;; "/" means we want the "Relative root"
       ;; try appending the remote prefix if relevent
       ((s-starts-with-p "/" file)
        (concat (file-remote-p default-directory) file))
       ;; we got a relative path
       ;; we don't need to help ffow to find it
       (t
        file)))

    ;; The variable vterm-eval-cmds is a SERIOUSLY SENSIBLE variable !
    ;; Do not be the guy that adds RCE into their config !

    ;; Allow customed ffow to be called from vterm
    ;; ffow should be as safe as find-file which is already trusted
    ;; we append our resolver that only manipulate strings,
    ;; Proove me wrong but i think it's safe.
    (add-to-list 'vterm-eval-cmds '("find-file-other-window"
                                    me/vterm--find-file-other-window-wrapper))