#+title: SwarselSystems: NixOS + Emacs Configuration
#+PROPERTY: header-args:emacs-lisp :tangle programs/emacs/init.el :mkdirp yes
#+PROPERTY: header-args:nix :mkdirp yes
#+EXPORT_FILE_NAME: index.html
#+OPTIONS: toc:6
#+macro: revision-date (eval (format-time-string "%F %T %z"))
#+macro: count-words (eval (count-words (point-min) (point-max)))
#+macro: count-lines (eval (count-lines (point-min) (point-max)))
*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 [[https://swarsel.github.io/.dotfiles/][html version]].
* Introduction (no code)
:PROPERTIES:
:CUSTOM_ID: h:a86fe971-f169-4052-aacf-15e0f267c6cd
:END:
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:
- [[https:github.com/Swarsel/.dotfiles][~SwarselSystems~ on github.com]]
- [[https:swagit.swarsel.win/Swarsel/.dotfiles][~SwarselSystems~ on swagit.swarsel.win]]
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.
** Structure of this file
This file is structured as follows:
- [[#h:a86fe971-f169-4052-aacf-15e0f267c6cd][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
- [[#h:c7588c0d-2528-485d-b2df-04d6336428d7][flake.nix]]
This block holds everything related to the heart of the nix side of the configuration - the =flake.nix= file.
- [[#h:02cd20be-1ffa-4904-9d5a-da5a89ba1421][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.
- [[#h:ed4cd05c-0879-41c6-bc39-3f1246a96f04][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}}})
#+begin_src emacs-lisp :tangle no :exports both
system-configuration-options
#+end_src
#+RESULTS:
: --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:
#+begin_src emacs-lisp :tangle no :export both :results silent
(org-babel-tangle)
#+end_src
The =.html= version of this page can be generated by calling the chord =C-SPC o e=, or by executing the below block
#+begin_src emacs-lisp :tangle no :export both :results silent
(org-html-export-to-html)
#+end_src
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:
#+begin_src elisp :noweb yes :exports both :results html
(concat
"\n"
"")
#+end_src
#+RESULTS:
#+begin_export html
#+end_export
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.
- [[#h:8fc9f66a-7412-4091-8dee-a06f897baf67][Appendix A: Supplementary Files]]
This section holds files that are not written in nix but are still referenced in the configuration in some way. This is mostly used for configuration of programs that have no native nix support, like tridactyl. Note that shell scripts are still defined under their respective entry in [[#h:64a5cc16-6b16-4802-b421-c67ccef853e1][Packages]].
- Historical Note: Noweb-Ref blocks
These blocks were used in several places throughout the configurations, but not on all machines necessarily. For example, the theming section used need to be in a NixOS block on NixOS machines but in a home-manager block on non-NixOS.
Originally, I used this method a lot throughout my configuration. However, as my knowledge of NixOS grew, I have been weeding these snippets out more and more as I find more efficient native solutions. Now, only the theming block remains. For example, the above problem can be solved by defining a =theme= attribute set and using =lib.recursiveUpdate= as shown in [[#h:79f7150f-b162-4f57-abdf-07f40dffd932][Shared Configuration Options (holds firefox & stylix config parts)]] and [[#h:a92318cd-413e-4e78-a478-e63b09df019c][Theme (stylix)]].
As such, this served to reduce code duplication in this file. The tangled files experienced no size reduction, since noweb-ref only substitutes these blocks in.
For archival reasons, here is shown how to use a noweb-ref block, in case I ever decide to use it again, or it is interesting to you:
#+begin_src nix :tangle no :noweb-ref blockName
enable = true;
#+end_src
which can then be used in a block like:
#+begin_src nix :tangle :noweb yes
<>
#+end_src
not that noweb-reffed blocks will not be indented correctly. You will want to account for that when checking your nix flake with the formatter of your choice. Personally, I have solved this issue using the functions defined in [[#h:59d4306e-9b73-4b2c-b039-6a6518c357fc][org-mode: Upon-save actions (Auto-tangle, export to html, formatting)]]. Originally, I also automatically exported to html there, but it incurred a too high memory penalty which made Emacs become sluggish over time.
** TODO Structure of this flake
The structure of this flake as seen many revisions, however lately I have settled on a system that I have grown to like:
- =hosts=: This folder is used to house all configurations that are used across the infrastructure. At the top level, it splits into the subfolders =nixos=, =home=, =darwin=, and =android=. These folders specify the mode that the configuration is running in:
- nixos: Full NixOS host (may or may not also use home-manager)
- darwin: Host that uses NixOS on MacOS (may or may not use home-manager)
- home: Host that uses only home-manager (no full NixOS)
- android: Phone using nix-on-droid (may or may not use home-manager)
The corresponding configurations are automatically generated by =mkFullHostConfigs= and =mkHalfHostConfigs=. A "full" host either in the nixos or darwin folder, while a "half" host is in either of home or android. This has to do with the scheme in which these configurations are generated.
These folders hold in turn a number of folders, the actual configurations. At this time, the files stored in this folder are:
- default.nix:
This file holds the abstracted configuration of the host. This should mostly be enabling [[#h:f0f1c961-3e7a-47b8-99ab-1654bb45dffc][Profiles]] as well as setting some [[#h:f4f22166-e345-43e6-b15f-b7f5bb886554][Shared Configuration Options]].
- hardware-config.nix:
It is not clearly defined what I hold in this file. Mostly it is just the attributes that nix originally sets when setting up the system for the first time (although at this time modified by me!), bar any filesystem configuration. This makes my deployment in [[#h:74db57ae-0bb9-4257-84be-eddbc85130dd][swarsel-bootstrap]] a little bit simpler.
- disk-config.nix
Holds the aforementioned filesystem configuration and is applied using [[https://github.com/nix-community/disko][disko]].
- The hosts// folders may also have a =secrets= folder, under which a single file =pii.nix.enc= can be stored. As the name suggests, this file should be encrypted. Specifically, it needs to be a [[https://github.com/getsops/sops][sops]]-encrypted file (sops does not seem to suggest a file ending other than .yml or others, which is not verbose enough for me, so I went with =.enc=). This file should have the structure of a nix expression, e.g.:
#+begin_src nix :tangle no
{
my_value = 2;
my_attrSet = {
enable = true;
};
}
#+end_src
Using the mechanisms in [[#h:82b8ede2-02d8-4c43-8952-7200ebd4dc23][PII management]] (which in turn uses [[#h:87c7893e-e946-4fc0-8973-1ca27d15cf0e][extra-builtins]] and [[#h:315e6ef6-27d5-4cd8-85ff-053eabe60ddb][sops-decrypt-and-cache]]), these files are decrypted during evaluation time and stored under a persistent directory. As the name suggests, I am using these files to store personally identifiable information - these "secrets" are stored world-readable in the nix store. As such, this should not be used to store important secrets, but rather information that you would not like everyone on the internet to easily find in your git repo.
- =modules=
This folder holds the most part of the actual system configuration done in this repository. At some point I thought it was cool to have my whole configuration exposed under the flakes =nixosModules=, which is indeed achieved (its usefulness is however debatable). In any way, this folder splits up as:
- nixos: Holds true NixOS configuration
- home: Holds configuration to be used by home-manager (either as a NixOS submodule or not)
- darwin: Holds configuration for nix-darwin. This folder further splits up into a nixos and a home folder, which hold respective nix or home-manager configuration for nix-darwin.
- iso: Holds specific configuration for my installer ISO that I do not want to have loaded in the rest of the configuration.
The nixos and home folders further split up:
- common: Configuration that can be used by all hosts (TODO: this currently includes configuration used by my user devices, which will mostly not be used by servers)
- server: Configuration to be used on servers
- optional: Configuration that will be used rather rarely
This structure is very optionated and highly subjective. I will possibly change this in the future.
By themselves, most of the files in the modules folder will not do anything. In order for them to do something, their corresponding =config.swarselsystems.modules= attribute needs to be enabled. This is done using...
- =profiles=: This folder splits up into =home= and =nixos= subfolders, where groupings of module enablers are stored for the respective home and nix setups. Note that =home= profiles are also used in NixOS setups (extensively even)!
- =nix=: This special folder holds mostly =.nix= files that are not automatically loaded, but rather setup specific things that affect most of the flake. For example, here lies the aforementioned [[#h:87c7893e-e946-4fc0-8973-1ca27d15cf0e][extra-builtins]] as well as the setup for the [[*Globals][Globals]] system. TODO: Move flake-parts units there and explain them here.
- =lib=: This folder holds utility functions that I add to the nixpkgs library under the =swarselsystems= attribute set. An example would be the =mkIfElse= function.
- =pkgs=: This folder holds derivations (mostly packages) that I define myself. This is mostly used to grab versions that are not (yet) in nixpkgs, or modified versions of another package. Each derivation in this folder is in turn in its own folder which holds a defautlt.nix. Using the mechanism in [[#h:64a5cc16-6b16-4802-b421-c67ccef853e1][Packages]], these are automatically built and available to all configurations (packages still need to be installed e.g. in =environment.systemPackages=)
- =checks=: Holds a file that defines my pre-commit-hook checks. TODO: move this to /nix probably
- =scripts=: This folder holds a bunch of shell scripts that I use for various tasks. Nearly all of these are made into a derivation using =pkgs.writeShellApplication=. In the future (TODO?), I might convert these to native nix, but in the past I kept the as true shellfiles in case I ever wanted to move away from nix. This is becoming less and less likely, however. And even in case that this would happen, I could retrieve these files from the nix store and would simply have to remove the nix store paths.
- =secrets=: Unlike the similar folder under =hosts=, this folder holds actual sops-encrypted secrets that are created at activation time and not in the nix store. The folder splits up into a bunch of folders, as well as a =repo= folder, which holds another =pii.nix.enc=, which holds global PII's, and a =certs= folder that holds some longer certificate style secrets.
- =overlays=: This holds a single =default.nix= that defines the overlay I am using in my configuration. It is responsible for adding my defined packages and modifications to the final nixpkgs. Also I add some other conveniences like all past stable nixpkgs and some other package sets.
- =programs=: This folder holds configurations for various programs (most notably emacs' =init.el= and =early-init.el=), that are being rendered using org-babel and loaded using nix.
- =wallpaper=: Holds wallpapers and profile pictures.
- =topology=: Holds the configuration used by [[https://github.com/oddlama/nix-topology][nix-topology]].
* flake.nix
:PROPERTIES:
:CUSTOM_ID: h:c7588c0d-2528-485d-b2df-04d6336428d7
:END:
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: [[#h:aee5ec75-7ca6-40d8-b6ac-a3e7e33a474b][flake.nix template]]. Adding new flake inputs is very easy, you just add them to [[#h:8a411ee2-a58e-4b5b-99bd-4ba772f8f0a2][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 [[#h:df0072bc-853f-438f-bd85-bfc869501015][let]], and the specific setup is done in either [[#h:9c9b9e3b-8771-44fa-ba9e-5056ae809655][nixosConfigurations]] (for NixOS systems), [[#h:f881aa05-a670-48dd-a57b-2916abdcb692][homeConfigurations]] (for home-manager systems), or [[#h:5f6ef553-59f9-4239-b6f3-63d33b57f335][nixOnDroidConfigurations]] (for Nix on Android) and [[#h:f881aa05-a670-48dd-a57b-2916abdcb692][darwinConfigurations]] (for Darwin systems, also known as Macs). There also used to be a [[#h:6a08495a-8566-4bb5-9fac-b03df01f6c81][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: Add several NixOS hosts on Proxmox and Oracle Cloud=.
** flake.nix skeleton
:PROPERTIES:
:CUSTOM_ID: h:aee5ec75-7ca6-40d8-b6ac-a3e7e33a474b
:END:
This sections puts together the =flake.nix= file from the [[#h:d39b8dfb-536d-414f-9fc0-7d67df48cee4][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.=, whereas explicit arguments may just be called by using ==. 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.
#+begin_src nix :noweb yes :tangle flake.nix
{
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 = {
<>
};
outputs =
inputs@{ self
, nixpkgs
, home-manager
, systems
, ...
}:
let
<>
in
inputs.flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
./nix/globals.nix
];
flake = { config, ... }:
let
<>
linuxUser = "swarsel";
macUser = "leon.schwarzaeugl";
mkFullHost = host: type: {
${host} =
let
systemFunc = if (type == "nixos") then lib.nixosSystem else inputs.nix-darwin.lib.darwinSystem;
in
systemFunc {
specialArgs = { inherit inputs outputs lib self; inherit (config) globals; };
modules = [
{
node.name = host;
node.secretsDir = ./hosts/${type}/${host}/secrets;
}
# put inports here that are for all hosts
inputs.disko.nixosModules.disko
inputs.sops-nix.nixosModules.sops
inputs.impermanence.nixosModules.impermanence
inputs.lanzaboote.nixosModules.lanzaboote
inputs.fw-fanctrl.nixosModules.default
"${self}/hosts/${type}/${host}"
{
_module.args.primaryUser = linuxUser;
}
] ++
(if (host == "iso") then [
inputs.nix-topology.nixosModules.default
] else
([
# put nixos imports here that are for all servers and normal hosts
inputs.nix-topology.nixosModules.default
"${self}/modules/${type}/common"
inputs.stylix.nixosModules.stylix
inputs.nswitch-rcm-nix.nixosModules.nswitch-rcm
] ++ (if (type == "nixos") then [
inputs.home-manager.nixosModules.home-manager
"${self}/profiles/nixos"
"${self}/modules/nixos/server"
"${self}/modules/nixos/optional"
{
home-manager.users."${linuxUser}".imports = [
# put home-manager imports here that are for all normal hosts
"${self}/modules/home/common"
"${self}/modules/home/server"
"${self}/modules/home/optional"
"${self}/profiles/home"
];
}
] else [
# put nixos imports here that are for darwin hosts
"${self}/modules/darwin/nixos/common"
"${self}/profiles/darwin"
inputs.home-manager.darwinModules.home-manager
{
home-manager.users."${macUser}".imports = [
# put home-manager imports here that are for darwin hosts
"${self}/modules/darwin/home"
"${self}/modules/home/server"
"${self}/modules/home/optional"
"${self}/profiles/home"
];
}
])
));
};
};
mkHalfHost = host: type: pkgs: {
${host} =
let
systemFunc = if (type == "home") then inputs.home-manager.lib.homeManagerConfiguration else inputs.nix-on-droid.lib.nixOnDroidConfiguration;
in
systemFunc
{
inherit pkgs;
extraSpecialArgs = { inherit inputs outputs lib self; };
modules = [ "${self}/hosts/${type}/${host}" ];
};
};
mkFullHostConfigs = hosts: type: lib.foldl (acc: set: acc // set) { } (lib.map (host: mkFullHost host type) hosts);
mkHalfHostConfigs = hosts: type: pkgs: lib.foldl (acc: set: acc // set) { } (lib.map (host: mkHalfHost host type pkgs) hosts);
in
{
<>
nixosConfigurations = mkFullHostConfigs (lib.swarselsystems.readHosts "nixos") "nixos";
homeConfigurations = mkHalfHostConfigs (lib.swarselsystems.readHosts "home") "home" lib.swarselsystems.pkgsFor.x86_64-linux;
darwinConfigurations = mkFullHostConfigs (lib.swarselsystems.readHosts "darwin") "darwin";
nixOnDroidConfigurations = mkHalfHostConfigs (lib.swarselsystems.readHosts "android") "android" lib.swarselsystems.pkgsFor.aarch64-linux;
topology = lib.swarselsystems.forEachSystem (pkgs: import inputs.nix-topology {
inherit pkgs;
modules = [
"${self}/topology"
{ inherit (self) nixosConfigurations; }
];
});
nodes = config.nixosConfigurations;
};
systems = [
"x86_64-linux"
"aarch64-linux"
];
};
}
#+end_src
** Inputs
:PROPERTIES:
:CUSTOM_ID: h:8a411ee2-a58e-4b5b-99bd-4ba772f8f0a2
:END:
Here we define inputs and outputs of the flake. First, the following list is for the outputs of the flake.
Format: ,
Mind the comma at the end. You need this because the =...= is being passed as the last argument in the template at [[#h:aee5ec75-7ca6-40d8-b6ac-a3e7e33a474b][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:
- [[https://github.com/NixOS/nixpkgs][nixpkgs]]
This is the base repository that I am following for all packages. I follow the unstable branch.
- [[https://github.com/nix-community/home-manager][home-manager]]
This handles user-level configuration and mostly provides dotfiles that are generated and symlinked to =~/.config/=.
- [[https://github.com/nix-community/NUR][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.
- [[https://github.com/nix-community/nixGL][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.
- [[https://github.com/danth/stylix][stylix]]
As described before, this handles all theme related options.
- [[https://github.com/Mic92/sops-nix][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.
- [[https://github.com/nix-community/lanzaboote][Lanzaboote]]
Provides secure boot for NixOS. Needed for my Surface Pro 3.
- [[https://github.com/nix-community/nix-on-droid][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!
- [[https://github.com/NixOS/nixos-hardware][nixos-hardware]]
Provides specific hardware setting for some hardware configurations. For example, this sets some better defaults for my Lenovo Thinkpad P14s Gen2.
- [[https://github.com/thiagokokada/nix-alien][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.
- [[https://github.com/nix-community/nixos-generators][nixos-generators]]
Provides me with images that I can use to create LXCs on Proxmox.
- [[https://github.com/Swarsel/nswitch-rcm-nix][nswitch-rcm-nix]]
Allows auto injection of payloads upon connecting a Nintendo Switch.
- [[https://github.com/nix-community/nix-index-database][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.
- [[https://github.com/nix-community/disko][disko]]
disko provides declarative disk partitioning, which I use for impermanence as well as [[https://github.com/nix-community/nixos-anywhere][nixos-anywhere]].
- [[https://github.com/nix-community/impermanence][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.
- [[https://github.com/dj95/zjstatus][zjstatus]]
This provides utilities for customizing a statusbar in zellij. Currently unused as I prefer tmux for now and might be removed in the future.
- [[https://github.com/TamtamHero/fw-fanctrl][fw-fanctrl]]
This provides access to the internal fans of Frameworks laptops. This is a bit more nice to use than directly using ectool.
- [[https://github.com/LnL7/nix-darwin][nix-darwin]]
After learning that MacOS systems can also be configured using nix, I managed to get access to an old MacBook for testing. This allows to set most general settings that can otherwise be set using the Mac GUI.
- [[https://github.com/cachix/git-hooks.nix][pre-commit-hooks]]
Provides access to several checks that can be hooked to be run before several stages in the process.
- nix-secrets
This is a private repository that I use for settings in modules that do not expose a =secretsFile= (or similar) option. An example is the =LastFM.ApiKey= option in [[#h:f347f3ad-5100-4c4f-8616-cfd7f8e14a72][navidrome]]:
=LastFM.ApiKey = builtins.readFile "${secretsDirectory}/navidrome/lastfm-secret";=
When setting this option normally, the password would normally be written world-readable not only in the nix store, but also in the configuration. Hence, I put such passwords into a private repository. This allows me to keep purity of the flake while keeping a level of security on these secrets.
- [[https://github.com/oddlama/nix-topology][nix-topology]]
This automatically creates a topology diagram of my configuration.
#+begin_src nix :tangle no :noweb-ref flakeinputs
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
nixpkgs-kernel.url = "github:NixOS/nixpkgs/063f43f2dbdef86376cc29ad646c45c46e93234c?narHash=sha256-6m1Y3/4pVw1RWTsrkAK2VMYSzG4MMIj7sqUy7o8th1o%3D"; #specifically pinned for kernel version
nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.05";
nixpkgs-stable24_05.url = "github:NixOS/nixpkgs/nixos-24.05";
nixpkgs-stable24_11.url = "github:NixOS/nixpkgs/nixos-24.11";
systems.url = "github:nix-systems/default-linux";
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
emacs-overlay = {
url = "github:nix-community/emacs-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
nur.url = "github:nix-community/NUR";
nixgl.url = "github:guibou/nixGL";
stylix.url = "github:danth/stylix";
sops-nix.url = "github:Mic92/sops-nix";
lanzaboote.url = "github:nix-community/lanzaboote";
nix-on-droid = {
url = "github:nix-community/nix-on-droid/release-24.05";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-generators = {
url = "github:nix-community/nixos-generators";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-hardware = {
url = "github:NixOS/nixos-hardware/master";
};
nix-alien = {
url = "github:thiagokokada/nix-alien";
};
nswitch-rcm-nix = {
url = "github:Swarsel/nswitch-rcm-nix";
};
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 = { };
};
vbc-nix = {
url = "git+ssh://git@github.com/vbc-it/vbc-nix.git?ref=main";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-topology.url = "github:oddlama/nix-topology";
flake-parts.url = "github:hercules-ci/flake-parts";
#+end_src
** let
:PROPERTIES:
:CUSTOM_ID: h:df0072bc-853f-438f-bd85-bfc869501015
:END:
Here I define a few variables that I need for my system specifications. There used to be more here, but I managed to optimize my configuration, and only two things remain:
- =outputs=, which is needed for =lib=
- =lib=: This exposes a common =lib= for NixOS and home-manager that is extended by my own personal =lib= functions.
#+begin_src nix :tangle no :noweb-ref flakelet
inherit (self) outputs;
lib = (nixpkgs.lib // home-manager.lib).extend (_: _: { swarselsystems = import ./lib { inherit self lib inputs outputs systems; }; });
#+end_src
** General (outputs)
:PROPERTIES:
:CUSTOM_ID: h:54cd8f65-a3ba-43c3-ae37-5f04383fe720
:END:
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.
- =homeModules= 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 [[#h:cbd5002c-e0fa-434a-951b-e05b179e4e3f][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 = ? =. 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 [[#h:5e3e21e0-57af-4dad-b32f-6400af9b7aab][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.
#+begin_src nix :tangle no :noweb-ref flakeoutputgeneral
inherit lib;
# nixosModules = import ./modules/nixos { inherit lib; };
# homeModules = import ./modules/home { inherit lib; };
packages = lib.swarselsystems.forEachSystem (pkgs: import ./pkgs { inherit lib pkgs; });
formatter = lib.swarselsystems.forEachSystem (pkgs: pkgs.nixpkgs-fmt);
overlays = import ./overlays { inherit self lib inputs; };
apps = lib.swarselsystems.forAllSystems (system:
let
appNames = [
"swarsel-bootstrap"
"swarsel-install"
"swarsel-rebuild"
"swarsel-postinstall"
];
appSet = lib.swarselsystems.mkApps system appNames self;
in
appSet // {
default = appSet.swarsel-bootstrap;
}
);
devShells = lib.swarselsystems.forAllSystems (system:
let
pkgs = lib.swarselsystems.pkgsFor.${system};
checks = self.checks.${system};
in
{
default = pkgs.mkShell {
# plugin-files = ${pkgs.nix-plugins.overrideAttrs (o: {
# buildInputs = [pkgs.nixVersions.latest pkgs.boost];
# patches = (o.patches or []) ++ [ "${self}/nix/nix-plugins.patch" ];
# })}/lib/nix/plugins
NIX_CONFIG = ''
plugin-files = ${pkgs.nix-plugins}/lib/nix/plugins
extra-builtins-file = ${self + /nix/extra-builtins.nix}
'';
inherit (checks.pre-commit-check) shellHook;
buildInputs = checks.pre-commit-check.enabledPackages;
nativeBuildInputs = [
(builtins.trace "alarm: we pinned nix_2_24 because of https://github.com/shlevy/nix-plugins/issues/20" pkgs.nixVersions.nix_2_24) # Always use the nix version from this flake's nixpkgs version, so that nix-plugins (below) doesn't fail because of different nix versions.
# pkgs.nix
pkgs.home-manager
pkgs.git
pkgs.just
pkgs.age
pkgs.ssh-to-age
pkgs.sops
pkgs.statix
pkgs.deadnix
pkgs.nixpkgs-fmt
];
};
}
);
templates = import ./templates { inherit lib; };
checks = lib.swarselsystems.forAllSystems (system:
let
pkgs = lib.swarselsystems.pkgsFor.${system};
in
import ./checks { inherit self inputs system pkgs; }
);
diskoConfigurations.default = import .templates/hosts/nixos/disk-config.nix;
#+end_src
** Pre-commit-hooks (Checks)
:PROPERTIES:
:CUSTOM_ID: h:cbd5002c-e0fa-434a-951b-e05b179e4e3f
:END:
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=)
#+begin_src nix :tangle checks/default.nix
{ 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";
};
};
};
}
#+end_src
** Templates
:PROPERTIES:
:CUSTOM_ID: h:e817f769-9aa9-4192-b649-c269080f4fee
:END:
This file defines the templates that are being exposed by the flake. These can be used by running =nix flake init -t github:Swarsel/.dotfiles#=.
#+begin_src nix :tangle templates/default.nix
{ lib, ... }:
let
templateNames = [
"python"
"rust"
"go"
"cpp"
"latex"
"default"
];
in
lib.swarselsystems.mkTemplates templateNames
#+end_src
** nixosConfigurations
:PROPERTIES:
:CUSTOM_ID: h:9c9b9e3b-8771-44fa-ba9e-5056ae809655
:END:
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 [[#h:aee5ec75-7ca6-40d8-b6ac-a3e7e33a474b][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.
#+begin_src nix :tangle no :noweb-ref flakenixosconf
#+end_src
** darwinConfigurations
:PROPERTIES:
:CUSTOM_ID: h:f881aa05-a670-48dd-a57b-2916abdcb692
:END:
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 [[#h:aee5ec75-7ca6-40d8-b6ac-a3e7e33a474b][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.
#+begin_src nix :tangle no :noweb-ref flakedarwinconf
#+end_src
** homeConfigurations
:PROPERTIES:
:CUSTOM_ID: h:f881aa05-a670-48dd-a57b-2916abdcb692
:END:
In contrast, this defines home-manager systems, which I only have one of, that serves as a template mostly.
#+begin_src nix :tangle no :noweb-ref flakehomeconf
# "swarsel@home-manager" = inputs.home-manager.lib.homeManagerConfiguration {
# pkgs = lib.swarselsystems.pkgsFor.x86_64-linux;
# extraSpecialArgs = { inherit inputs outputs; };
# modules = homeModules ++ mixedModules ++ [
# ./hosts/home-manager
# ];
# };
#+end_src
** nixOnDroidConfigurations
:PROPERTIES:
:CUSTOM_ID: h:5f6ef553-59f9-4239-b6f3-63d33b57f335
:END:
Nix on Android also demands an own flake output, which is provided here.
#+begin_src nix :tangle no :noweb-ref flakedroidconf
# magicant = inputs.nix-on-droid.lib.nixOnDroidConfiguration {
# pkgs = lib.swarselsystems.pkgsFor.aarch64-linux;
# modules = [
# ./hosts/magicant
# ];
# };
#+end_src
** topologyConfigurations
:PROPERTIES:
:CUSTOM_ID: h:1337e267-bca3-4c65-863f-9f44b753dee4
:END:
#+begin_src nix :tangle no :noweb-ref topologyconf
#+end_src
* System
:PROPERTIES:
:CUSTOM_ID: h:02cd20be-1ffa-4904-9d5a-da5a89ba1421
:END:
This holds most of the NixOS side of configuration.
** System specific configuration
:PROPERTIES:
:CUSTOM_ID: h:88bf4b90-e94b-46fb-aaf1-a381a512860d
:END:
This section mainly exists house different `default.nix` files to define some modules that should be loaded on respective systems.
Every host is housed in the =hosts/= directory, which is then subdivided by each respective system (=nixos/=, =home-manager/=, =nix-on-droid/=, =darwin/=). As described earlier, some of these configurations (nixos and darwin) can be defined automatically in this flake. For home-manager and nix-on-droid, the system architecture must be defined manually.
*** Template
:PROPERTIES:
:CUSTOM_ID: h:373bd9e8-616e-434e-bfab-c216ce4470e9
:END:
This is the template that I use for new deployments of personal machines. Servers are usually highly tailored to their specific task and I do not consider it worth a time to craft a template for that. Also, at least at the current time, I only provide a template for NixOS hosts, as I rarely ever use anything else.
**** Main Configuration
:PROPERTIES:
:CUSTOM_ID: h:859aec97-65a2-4633-b7d8-73d4ccf89cc5
:END:
#+begin_src nix :tangle templates/hosts/nixos/default.nix
{ self, inputs, pkgs, lib, primaryUser, ... }:
let
modulesPath = "${self}/modules";
sharedOptions = {
isBtrfs = true;
};
in
{
imports = [
# ---- nixos-hardware here ----
./hardware-configuration.nix
./disk-config.nix
"${modulesPath}/nixos/optional/virtualbox.nix"
# "${modulesPath}/nixos/optional/vmware.nix"
"${modulesPath}/nixos/optional/autologin.nix"
"${modulesPath}/nixos/optional/nswitch-rcm.nix"
"${modulesPath}/nixos/optional/gaming.nix"
inputs.home-manager.nixosModules.home-manager
{
home-manager.users."${primaryUser}".imports = [
"${modulesPath}/home/optional/gaming.nix"
];
}
];
boot = {
kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
};
networking = {
hostName = "TEMPLATE";
firewall.enable = true;
};
swarselsystems = lib.recursiveUpdate
{
wallpaper = self + /wallpaper/lenovowp.png;
hasBluetooth = true;
hasFingerprint = true;
isImpermanence = true;
isSecureBoot = true;
isCrypted = true;
isSwap = true;
swapSize = "32G";
rootDisk = "TEMPLATE";
}
sharedOptions;
home-manager.users."${primaryUser}".swarselsystems = lib.recursiveUpdate
{
isLaptop = true;
isNixos = true;
cpuCount = 16;
}
sharedOptions;
}
#+end_src
**** disko
:PROPERTIES:
:CUSTOM_ID: h:24757d1e-6e88-4843-ab20-5e0c1b7ae29e
:END:
Acceptance of arbitraty argumments is here needed because =disko= passes =diskoFile= to this file.
#+begin_src nix :tangle templates/hosts/nixos/disk-config.nix
{ lib, pkgs, config, rootDisk, ... }:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko.devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
environment.systemPackages = [
pkgs.yubikey-manager
];
}
#+end_src
*** Physical hosts
:PROPERTIES:
:CUSTOM_ID: h:58dc6384-0d19-4f71-9043-4014bd033ba2
:END:
This is a list of all physical machines that I maintain.
**** nbl-imba-2 (Framework Laptop 16)
:PROPERTIES:
:CUSTOM_ID: h:6c6e9261-dfa1-42d8-ab2a-8b7c227be6d9
:END:
My work machine. Built for more security, this is the gold standard of my configurations at the moment.
***** Main Configuration
:PROPERTIES:
:CUSTOM_ID: h:567c0055-f5f7-4e53-8f13-d767d7166e9d
:END:
#+begin_src nix :tangle hosts/nixos/nbl-imba-2/default.nix
{ self, config, inputs, lib, primaryUser, ... }:
let
sharedOptions = {
isBtrfs = true;
isLinux = true;
sharescreen = "eDP-2";
profiles = {
personal = true;
work = true;
framework = true;
};
};
in
{
imports = [
inputs.nixos-hardware.nixosModules.framework-16-7040-amd
./disk-config.nix
./hardware-configuration.nix
];
swarselsystems = lib.recursiveUpdate
{
info = "Framework Laptop 16, 7940HS, RX7700S, 64GB RAM";
firewall = lib.mkForce true;
wallpaper = self + /wallpaper/lenovowp.png;
hasBluetooth = true;
hasFingerprint = true;
isImpermanence = false;
isSecureBoot = true;
isCrypted = true;
inherit (config.repo.secrets.local) hostName;
inherit (config.repo.secrets.local) fqdn;
hibernation.offset = 533760;
profiles = {
amdcpu = true;
amdgpu = true;
hibernation = true;
btrfs = true;
};
}
sharedOptions;
home-manager.users."${primaryUser}" = {
# home.stateVersion = lib.mkForce "23.05";
swarselsystems = lib.recursiveUpdate
{
isLaptop = true;
isNixos = true;
isSecondaryGpu = true;
SecondaryGpuCard = "pci-0000_03_00_0";
cpuCount = 16;
temperatureHwmon = {
isAbsolutePath = true;
path = "/sys/devices/virtual/thermal/thermal_zone0/";
input-filename = "temp4_input";
};
lowResolution = "1280x800";
highResolution = "2560x1600";
monitors = {
main = {
name = "BOE 0x0BC9 Unknown";
mode = "2560x1600"; # TEMPLATE
scale = "1";
position = "2560,0";
workspace = "15:L";
output = "eDP-2";
};
};
}
sharedOptions;
};
}
#+end_src
***** hardware-configuration
:PROPERTIES:
:CUSTOM_ID: h:25115a54-c634-4896-9a41-254064ce9fcc
:END:
#+begin_src nix :tangle hosts/nixos/nbl-imba-2/hardware-configuration.nix
{ config, lib, pkgs, modulesPath, ... }:
{
imports =
[
(modulesPath + "/installer/scan/not-detected.nix")
];
# Fix Wlan after suspend or Hibernate
# environment.etc."systemd/system-sleep/fix-wifi.sh".source =
# pkgs.writeShellScript "fix-wifi.sh" ''
# case $1/$2 in
# pre/*)
# ${pkgs.kmod}/bin/modprobe -r mt7921e mt792x_lib mt76
# echo 1 > /sys/bus/pci/devices/0000:04:00.0/remove
# ;;
# post/*)
# ${pkgs.kmod}/bin/modprobe mt7921e
# echo 1 > /sys/bus/pci/rescan
# ;;
# esac
# '';
boot = {
kernelPackages = lib.mkDefault pkgs.kernel.linuxPackages;
binfmt.emulatedSystems = [ "aarch64-linux" ];
initrd = {
availableKernelModules = [ "nvme" "xhci_pci" "thunderbolt" "usb_storage" "cryptd" "usbhid" "sd_mod" "r8152" ];
# allow to remote build on arm (needed for moonside)
kernelModules = [ "sg" ];
luks.devices."cryptroot" = {
# improve performance on ssds
bypassWorkqueues = true;
preLVM = true;
};
};
kernelModules = [ "kvm-amd" ];
kernelParams = [
"mem_sleep_default=deep"
# supposedly, this helps save power on laptops
# in reality (at least on this model), this just generate excessive heat on the CPUs
# "amd_pstate=passive"
# Fix screen flickering issue at the cost of battery life (disable PSR and PSR-SU, keep PR enabled)
# TODO: figure out if this is worth it
# test PSR/PR state with 'sudo grep '' /sys/kernel/debug/dri/0000*/eDP-2/*_capability'
# ref:
# https://old.reddit.com/r/framework/comments/1goh7hc/anyone_else_get_this_screen_flickering_issue/
# https://www.reddit.com/r/NixOS/comments/1hjruq1/graphics_corruption_on_kernel_6125_and_up/
# https://gitlab.freedesktop.org/drm/amd/-/issues/3797
"amdgpu.dcdebugmask=0x410"
];
extraModulePackages = [ ];
};
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces..useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp196s0f3u1c2.useDHCP = lib.mkDefault true;
# networking.interfaces.wlp4s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
#+end_src
***** disko
:PROPERTIES:
:CUSTOM_ID: h:e0da04c7-4199-44b0-b525-6cfc64072b45
:END:
#+begin_src nix :tangle hosts/nixos/nbl-imba-2/disk-config.nix
{
disko.devices = {
disk = {
nvme0n1 = {
type = "disk";
device = "/dev/nvme0n1";
content = {
type = "gpt";
partitions = {
ESP = {
label = "boot";
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [
"defaults"
];
};
};
luks = {
size = "100%";
label = "luks";
content = {
type = "luks";
name = "cryptroot";
extraOpenArgs = [
"--allow-discards"
"--perf-no_read_workqueue"
"--perf-no_write_workqueue"
];
# https://0pointer.net/blog/unlocking-luks2-volumes-with-tpm2-fido2-pkcs11-security-hardware-on-systemd-248.html
settings = { crypttabExtraOpts = [ "fido2-device=auto" "token-timeout=10" ]; };
content = {
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ];
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [ "subvol=root" "compress=zstd" "noatime" ];
};
"/home" = {
mountpoint = "/home";
mountOptions = [ "subvol=home" "compress=zstd" "noatime" ];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [ "subvol=nix" "compress=zstd" "noatime" ];
};
"/persist" = {
mountpoint = "/persist";
mountOptions = [ "subvol=persist" "compress=zstd" "noatime" ];
};
"/log" = {
mountpoint = "/var/log";
mountOptions = [ "subvol=log" "compress=zstd" "noatime" ];
};
"/swap" = {
mountpoint = "/swap";
swap.swapfile.size = "64G";
};
};
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = true;
fileSystems."/var/log".neededForBoot = true;
}
#+end_src
**** Winters (Server)
:PROPERTIES:
:CUSTOM_ID: h:932ef6b0-4c14-4200-8e3f-2e208e748746
:END:
This is my main server that I run at home. It handles most tasks that require bigger amounts of storage than I can receive for free at OCI. Also it houses some data that I find too sensitive to hand over to Oracle.
***** Main Configuration
:PROPERTIES:
:CUSTOM_ID: h:8ad68406-4a75-45ba-97ad-4c310b921124
:END:
#+begin_src nix :tangle hosts/nixos/winters/default.nix
{ lib, config, primaryUser, ... }:
let
sharedOptions = {
isBtrfs = false;
isLinux = true;
profiles = {
server.local = true;
};
};
in
{
imports = [
./hardware-configuration.nix
];
boot = {
loader.systemd-boot.enable = true;
loader.efi.canTouchEfiVariables = true;
};
networking = {
inherit (config.repo.secrets.local) hostId;
hostName = "winters";
firewall.enable = true;
enableIPv6 = false;
firewall.allowedTCPPorts = [ 80 443 ];
};
swarselsystems = lib.recursiveUpdate
{
info = "ASRock J4105-ITX, 32GB RAM";
isImpermanence = false;
isSecureBoot = true;
isCrypted = true;
}
sharedOptions;
home-manager.users."${primaryUser}" = {
home.stateVersion = lib.mkForce "23.05";
swarselsystems = lib.recursiveUpdate
{ }
sharedOptions;
};
}
#+end_src
***** hardware-configuration
:PROPERTIES:
:CUSTOM_ID: h:0fdefb4f-ce53-4caf-89ed-5d79646f70f0
:END:
#+begin_src nix :tangle hosts/nixos/winters/hardware-configuration.nix
{ config, lib, modulesPath, ... }:
{
imports =
[
(modulesPath + "/installer/scan/not-detected.nix")
];
boot = {
initrd.availableKernelModules = [ "ahci" "xhci_pci" "usbhid" "usb_storage" "sd_mod" ];
initrd.kernelModules = [ ];
kernelModules = [ "kvm-intel" ];
extraModulePackages = [ ];
supportedFilesystems = [ "zfs" ];
zfs.extraPools = [ "Vault" ];
};
fileSystems = {
"/" =
{
device = "/dev/disk/by-uuid/30e2f96a-b01d-4c27-9ebb-d5d7e9f0031f";
fsType = "ext4";
};
"/boot" =
{
device = "/dev/disk/by-uuid/F0D8-8BD1";
fsType = "vfat";
};
};
swapDevices =
[{ device = "/dev/disk/by-uuid/a8eb6f3b-69bf-4160-90aa-9247abc108e0"; }];
# Enables DHCP on each ethernet and wireless interface. In case of scripted networking
# (the default) this is the recommended approach. When using systemd-networkd it's
# still possible to use this option, but it's recommended to use it in conjunction
# with explicit per-interface declarations with `networking.interfaces..useDHCP`.
networking.useDHCP = lib.mkDefault true;
# networking.interfaces.enp3s0.useDHCP = lib.mkDefault true;
nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;
}
#+end_src
**** nbm-imba-166 (MacBook Pro)
:PROPERTIES:
:CUSTOM_ID: h:28e1a7eb-356b-4015-83f7-9c552c8c0e9d
:END:
A Mac notebook that I have received from work. I use this machine for getting accustomed to the Apple ecosystem as well as as a sandbox for nix-darwin configurations.
#+begin_src nix :tangle hosts/darwin/nbm-imba-166/default.nix
{ lib, ... }:
let
inherit (config.repo.secrets.local) workUser;
in
{
# Auto upgrade nix package and the daemon service.
services.nix-daemon.enable = true;
services.karabiner-elements.enable = true;
home-manager.users.workUser.home = {
username = lib.mkForce workUser;
swarselsystems = {
isDarwin = true;
isLaptop = true;
isNixos = false;
isBtrfs = false;
mainUser = workUser;
homeDir = "/home/${workUser}";
flakePath = "/home/${workUser}/.dotfiles";
};
};
}
#+end_src
**** Magicant (Phone)
:PROPERTIES:
:CUSTOM_ID: h:729af373-37e7-4379-9a3d-b09792219415
:END:
My phone. I use only a minimal config for remote debugging here.
#+begin_src nix :tangle hosts/android/magicant/default.nix
{ pkgs, ... }: {
environment = {
packages = with pkgs; [
vim
git
openssh
# toybox
dig
man
gnupg
curl
deadnix
statix
nixpgks-fmt
nvd
];
etcBackupExtension = ".bak";
extraOutputsToInstall = [
"doc"
"info"
"devdoc"
];
motd = null;
};
android-integration = {
termux-open.enable = true;
xdg-open.enable = true;
termux-open-url.enable = true;
termux-reload-settings.enable = true;
termux-setup-storage.enable = true;
};
# Backup etc files instead of failing to activate generation if a file already exists in /etc
# Read the changelog before changing this value
system.stateVersion = "23.05";
# Set up nix for flakes
nix.extraOptions = ''
experimental-features = nix-command flakes
'';
}
#+end_src
*** Virtual hosts
:PROPERTIES:
:CUSTOM_ID: h:4dc59747-9598-4029-aa7d-92bf186d6c06
:END:
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)
:PROPERTIES:
:CUSTOM_ID: h:4c5febb0-fdf6-44c5-8d51-7ea0f8930abf
:END:
This machine mainly acts as an external sync helper. It manages the following things:
- Anki syncing
- Forgejo git server
- Elfeed sync server (RSS)
- Syncthing backup of replaceable data
All of these are processes that use little cpu but can take a lot of storage. For this I use a free Ampere instance from OCI with 50G of space. In case my account gets terminated, all of this data is easily replaceable or backed up regularly anyways.
#+begin_src nix :tangle hosts/nixos/sync/default.nix
{ lib, config, primaryUser, ... }:
let
sharedOptions = {
isBtrfs = false;
isLinux = true;
};
inherit (config.repo.secrets.common) workHostName;
inherit (config.repo.secrets.local.syncthing) dev1 dev2 dev3 loc1;
in
{
imports = [
./hardware-configuration.nix
];
sops = {
defaultSopsFile = lib.mkForce "/root/.dotfiles/secrets/sync/secrets.yaml";
};
boot = {
tmp.cleanOnBoot = true;
loader.grub.device = "nodev";
};
zramSwap.enable = false;
networking = {
nftables.enable = lib.mkForce false;
hostName = "sync";
enableIPv6 = false;
domain = "subnet03112148.vcn03112148.oraclevcn.com";
firewall = {
allowedTCPPorts = [ 80 443 8384 9812 22000 27701 ];
allowedUDPPorts = [ 21027 22000 ];
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
iptables -I INPUT -m state --state NEW -p tcp --dport 9812 -j ACCEPT
'';
};
};
hardware = {
enableAllFirmware = lib.mkForce false;
};
system.stateVersion = "23.11";
services = {
nginx = {
virtualHosts = {
"sync.swarsel.win" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://localhost:8384";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
syncthing = {
enable = true;
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";
};
"winters" = {
id = "O7RWDMD-AEAHPP7-7TAVLKZ-BSWNBTU-2VA44MS-EYGUNBB-SLHKB3C-ZSLMOAA";
};
"${workHostName}" = {
id = "YAPV4BV-I26WPTN-SIP32MV-SQP5TBZ-3CHMTCI-Z3D6EP2-MNDQGLP-53FT3AB";
};
"${dev1}" = {
id = "OCCDGDF-IPZ6HHQ-5SSLQ3L-MSSL5ZW-IX5JTAM-PW4PYEK-BRNMJ7E-Q7YDMA7";
};
"${dev2}" = {
id = "LPCFIIB-ENUM2V6-F2BWVZ6-F2HXCL2-BSBZXUF-TIMNKYB-7CATP7H-YU5D3AH";
};
"${dev3}" = {
id = "LAUT2ZP-KEZY35H-AHR3ARD-URAREJI-2B22P5T-PIMUNWW-PQRDETU-7KIGNQR";
};
};
folders = {
"Default Folder" = lib.mkForce {
path = "/var/lib/syncthing/Sync";
type = "receiveonly";
versioning = null;
devices = [ "winters" "magicant" "${workHostName}" ];
id = "default";
};
"Obsidian" = {
path = "/var/lib/syncthing/Obsidian";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "winters" "magicant" "${workHostName}" ];
id = "yjvni-9eaa7";
};
"Org" = {
path = "/var/lib/syncthing/Org";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "winters" "magicant" "${workHostName}" ];
id = "a7xnl-zjj3d";
};
"Vpn" = {
path = "/var/lib/syncthing/Vpn";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "winters" "magicant" "${workHostName}" ];
id = "hgp9s-fyq3p";
};
"${loc1}" = {
path = "/var/lib/syncthing/${loc1}";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "3";
};
devices = [ dev1 dev2 dev3 ];
id = "5gsxv-rzzst";
};
};
};
};
};
swarselsystems = lib.recursiveUpdate
{
info = "VM.Standard.E2.1.Micro";
flakePath = "/root/.dotfiles";
isImpermanence = false;
isSecureBoot = false;
isCrypted = false;
profiles = {
server.sync = true;
};
}
sharedOptions;
home-manager.users."${primaryUser}" = {
home.stateVersion = lib.mkForce "23.05";
swarselsystems = lib.recursiveUpdate
{ }
sharedOptions;
};
}
#+end_src
**** Moonside (OCI)
:PROPERTIES:
:CUSTOM_ID: h:f547ed16-5e6e-4744-9e33-af090e0a175b
:END:
***** Main Configuration
:PROPERTIES:
:CUSTOM_ID: h:a8f20a56-ce92-43d8-8bfe-3edccebf2bf9
:END:
#+begin_src nix :tangle hosts/nixos/moonside/default.nix
{ lib, config, primaryUser, ... }:
let
inherit (config.repo.secrets.common) workHostName;
inherit (config.repo.secrets.local.syncthing) dev1 dev2 dev3 loc1;
sharedOptions = {
isBtrfs = true;
isLinux = true;
};
in
{
imports = [
./hardware-configuration.nix
./disk-config.nix
];
sops = {
age.sshKeyPaths = lib.mkDefault [ "/etc/ssh/ssh_host_ed25519_key" ];
defaultSopsFile = lib.mkForce "/home/swarsel/.dotfiles/secrets/moonside/secrets.yaml";
secrets = {
wireguard-private-key = { };
};
};
boot = {
loader.systemd-boot.enable = true;
tmp.cleanOnBoot = true;
};
environment = {
etc."issue".text = "\4";
persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{
directory = "/var/lib/syncthing";
user = "syncthing";
group = "syncthing";
mode = "0700";
}
];
};
topology.self.interfaces.wg = {
addresses = ["192.168.3.4"];
renderer.hidePhysicalConnections = true;
virtual = true;
type = "wireguard";
};
networking = {
nftables.enable = lib.mkForce false;
hostName = "moonside";
enableIPv6 = false;
domain = "subnet03291956.vcn03291956.oraclevcn.com";
firewall = {
allowedTCPPorts = [ 80 443 8384 ];
};
wireguard = {
enable = true;
interfaces = {
home-vpn = {
privateKeyFile = config.sops.secrets.wireguard-private-key.path;
ips = [ "192.168.3.4/32" ];
peers = [
{
publicKey = "NNGvakADslOTCmN9HJOW/7qiM+oJ3jAlSZGoShg4ZWw=";
name = "moonside";
persistentKeepalive = 25;
endpoint = "${config.repo.secrets.common.ipv4}:51820";
allowedIPs = [
"192.168.3.0/24"
"192.168.1.0/24"
];
}
];
};
};
};
};
hardware = {
enableAllFirmware = lib.mkForce false;
};
system.stateVersion = "23.11";
services = {
nginx = {
virtualHosts = {
"syncthing.swarsel.win" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://localhost:8384";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
syncthing = {
enable = true;
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";
};
"winters" = {
id = "O7RWDMD-AEAHPP7-7TAVLKZ-BSWNBTU-2VA44MS-EYGUNBB-SLHKB3C-ZSLMOAA";
};
"${workHostName}" = {
id = "YAPV4BV-I26WPTN-SIP32MV-SQP5TBZ-3CHMTCI-Z3D6EP2-MNDQGLP-53FT3AB";
};
"${dev1}" = {
id = "OCCDGDF-IPZ6HHQ-5SSLQ3L-MSSL5ZW-IX5JTAM-PW4PYEK-BRNMJ7E-Q7YDMA7";
};
"${dev2}" = {
id = "LPCFIIB-ENUM2V6-F2BWVZ6-F2HXCL2-BSBZXUF-TIMNKYB-7CATP7H-YU5D3AH";
};
"${dev3}" = {
id = "LAUT2ZP-KEZY35H-AHR3ARD-URAREJI-2B22P5T-PIMUNWW-PQRDETU-7KIGNQR";
};
};
folders = {
"Default Folder" = lib.mkForce {
path = "/sync/Sync";
type = "receiveonly";
versioning = null;
devices = [ "winters" "magicant" "${workHostName}" ];
id = "default";
};
"Obsidian" = {
path = "/sync/Obsidian";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "winters" "magicant" "${workHostName}" ];
id = "yjvni-9eaa7";
};
"Org" = {
path = "/sync/Org";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "winters" "magicant" "${workHostName}" ];
id = "a7xnl-zjj3d";
};
"Vpn" = {
path = "/sync/Vpn";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "winters" "magicant" "${workHostName}" ];
id = "hgp9s-fyq3p";
};
"Documents" = {
path = "/sync/Documents";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "2";
};
devices = [ "winters" ];
id = "hgr3d-pfu3w";
};
"runandbun" = {
path = "/sync/runandbun";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "winters" "magicant" ];
id = "kwnql-ev64v";
};
"${loc1}" = {
path = "/sync/${loc1}";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "3";
};
devices = [ dev1 dev2 dev3 ];
id = "5gsxv-rzzst";
};
};
};
};
};
swarselsystems = lib.recursiveUpdate
{
info = "VM.Standard.A1.Flex, 4 OCPUs, 24GB RAM";
flakePath = "/home/swarsel/.dotfiles";
isImpermanence = true;
isSecureBoot = false;
isCrypted = false;
isSwap = false;
rootDisk = "/dev/sda";
profiles = {
server.moonside = true;
};
}
sharedOptions;
home-manager.users."${primaryUser}" = {
home.stateVersion = lib.mkForce "23.11";
swarselsystems = lib.recursiveUpdate
{ }
sharedOptions;
};
}
#+end_src
***** hardware-configuration
:PROPERTIES:
:CUSTOM_ID: h:f99c05ab-f047-4350-b80a-4c1ff55b91bf
:END:
loader.grub = {
efiSupport = true;
efiInstallAsRemovable = true;
device = "nodev";
};
#+begin_src nix :tangle hosts/nixos/moonside/hardware-configuration.nix
{ lib, modulesPath, ... }:
{
imports = [ (modulesPath + "/profiles/qemu-guest.nix") ];
boot = {
initrd = {
availableKernelModules = [ "xhci_pci" "virtio_pci" "virtio_scsi" "usbhid" ];
kernelModules = [ ];
};
kernelModules = [ ];
extraModulePackages = [ ];
};
nixpkgs.hostPlatform = lib.mkForce "aarch64-linux";
}
#+end_src
***** disko
:PROPERTIES:
:CUSTOM_ID: h:cec82b06-39ca-4c0e-b4f5-c1fda9b14e6d
:END:
#+begin_src nix :tangle hosts/nixos/moonside/disk-config.nix
# NOTE: ... is needed because dikso passes diskoFile
{ lib
, config
, rootDisk
, ...
}:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko.devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
disk1 = {
type = "disk";
device = "/dev/sdb";
content = {
type = "gpt";
partitions = {
sync = {
size = "100%";
content = {
type = "btrfs";
extraArgs = [ "-L" "sync" "-f" ]; # force overwrite
subvolumes = {
"/sync" = {
mountpoint = "/sync";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
}
#+end_src
*** Utility hosts
:PROPERTIES:
:CUSTOM_ID: h:89ce533d-4856-4988-b456-0951d4453db8
:END:
**** Toto (Physical/VM)
:PROPERTIES:
:CUSTOM_ID: h:6b495f0e-fc11-44c8-a9e8-83f3d95c8857
:END:
This is a slim setup for developing base configuration. I do not track the hardware-configuration for this host here because I often switch this configuration between running on a QEMU VM and a physical laptop and do not want to constantly adapt the config here to reflect the current state.
***** Main Configuration
:PROPERTIES:
:CUSTOM_ID: h:4e53b40b-98b2-4615-b1b0-3696a75edd6e
:END:
#+begin_src nix :tangle hosts/nixos/toto/default.nix
{ self, inputs, pkgs, lib, primaryUser, ... }:
let
modulesPath = "${self}/modules";
sharedOptions = {
isBtrfs = true;
isLinux = true;
profiles = {
toto = true;
};
};
in
{
imports = [
./disk-config.nix
./hardware-configuration.nix
"${modulesPath}/nixos/common/sharedsetup.nix"
"${modulesPath}/home/common/sharedsetup.nix"
"${self}/profiles/nixos"
inputs.home-manager.nixosModules.home-manager
{
home-manager.users."${primaryUser}".imports = [
inputs.sops-nix.homeManagerModules.sops
"${modulesPath}/home/common/sharedsetup.nix"
"${self}/profiles/home"
];
}
];
environment.systemPackages = with pkgs; [
curl
git
gnupg
rsync
ssh-to-age
sops
vim
just
sbctl
];
system.stateVersion = lib.mkForce "23.05";
boot = {
supportedFilesystems = [ "btrfs" ];
kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
};
networking = {
hostName = "toto";
firewall.enable = false;
};
swarselsystems = lib.recursiveUpdate
{
info = "~SwarselSystems~ remote install helper";
wallpaper = self + /wallpaper/lenovowp.png;
isImpermanence = true;
isCrypted = false;
isSecureBoot = false;
isSwap = false;
swapSize = "8G";
# rootDisk = "/dev/nvme0n1";
rootDisk = "/dev/sda";
# rootDisk = "/dev/vda";
}
sharedOptions;
home-manager.users."${primaryUser}" = {
home.stateVersion = lib.mkForce "23.05";
swarselsystems = lib.recursiveUpdate
{
isLaptop = false;
isNixos = true;
}
sharedOptions;
};
}
#+end_src
***** disko
:PROPERTIES:
:CUSTOM_ID: h:cec82b06-39ca-4c0e-b4f5-c1fda9b14e6d
:END:
#+begin_src nix :tangle hosts/nixos/toto/disk-config.nix
# NOTE: ... is needed because dikso passes diskoFile
{ lib
, pkgs
, config
, rootDisk
, ...
}:
let
type = "btrfs";
extraArgs = [ "-L" "nixos" "-f" ]; # force overwrite
subvolumes = {
"/root" = {
mountpoint = "/";
mountOptions = [
"subvol=root"
"compress=zstd"
"noatime"
];
};
"/home" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/home";
mountOptions = [
"subvol=home"
"compress=zstd"
"noatime"
];
};
"/persist" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/persist";
mountOptions = [
"subvol=persist"
"compress=zstd"
"noatime"
];
};
"/log" = lib.mkIf config.swarselsystems.isImpermanence {
mountpoint = "/var/log";
mountOptions = [
"subvol=log"
"compress=zstd"
"noatime"
];
};
"/nix" = {
mountpoint = "/nix";
mountOptions = [
"subvol=nix"
"compress=zstd"
"noatime"
];
};
"/swap" = lib.mkIf config.swarselsystems.isSwap {
mountpoint = "/.swapvol";
swap.swapfile.size = config.swarselsystems.swapSize;
};
};
in
{
disko.devices = {
disk = {
disk0 = {
type = "disk";
device = config.swarselsystems.rootDisk;
content = {
type = "gpt";
partitions = {
ESP = {
priority = 1;
name = "ESP";
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [ "defaults" ];
};
};
root = lib.mkIf (!config.swarselsystems.isCrypted) {
size = "100%";
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/disk/by-label/nixos" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
luks = lib.mkIf config.swarselsystems.isCrypted {
size = "100%";
content = {
type = "luks";
name = "cryptroot";
passwordFile = "/tmp/disko-password"; # this is populated by bootstrap.sh
settings = {
allowDiscards = true;
# https://github.com/hmajid2301/dotfiles/blob/a0b511c79b11d9b4afe2a5e2b7eedb2af23e288f/systems/x86_64-linux/framework/disks.nix#L36
crypttabExtraOpts = [
"fido2-device=auto"
"token-timeout=10"
];
};
content = {
inherit type subvolumes extraArgs;
postCreateHook = lib.mkIf config.swarselsystems.isImpermanence ''
MNTPOINT=$(mktemp -d)
mount "/dev/mapper/cryptroot" "$MNTPOINT" -o subvolid=5
trap 'umount $MNTPOINT; rm -rf $MNTPOINT' EXIT
btrfs subvolume snapshot -r $MNTPOINT/root $MNTPOINT/root-blank
'';
};
};
};
};
};
};
};
};
fileSystems."/persist".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
fileSystems."/home".neededForBoot = lib.mkIf config.swarselsystems.isImpermanence true;
environment.systemPackages = [
pkgs.yubikey-manager
];
}
#+end_src
**** drugstore (ISO)
:PROPERTIES:
:CUSTOM_ID: h:8583371d-5d47-468b-84ba-210aad7e2c90
:END:
This is a live environment ISO that I use to bootstrap new systems. It only loads a minimal configuration and no graphical interface. After booting this image on a host, find out its IP and bootstrap the system using the =bootstrap= utility.
For added convenience, the live environment displays a helpful text on login, we define it here (will be put into =/etc/issue=):
#+begin_src bash :tangle programs/etc/issue
[32m~SwarselSystems~[0m
IP of primary interface: [31m\4[0m
The Password for all users & root is '[31msetup[0m'.
Install the system remotely by running '[33mbootstrap -n -d [0m' on a machine with deployed secrets.
Alternatively, run '[33mswarsel-install -n [0m' for a local install. For your convenience, an example call is in the bash history (press up on the keyboard to access).
#+end_src
Also, an initial bash history is provided to allow for a very quick local deployment:
#+begin_src shell :tangle programs/bash/.bash_history
swarsel-install -n chaostheatre
#+end_src
#+begin_src nix :tangle hosts/nixos/iso/default.nix
{ self, pkgs, inputs, config, lib, modulesPath, primaryUser ? "swarsel", ... }:
let
pubKeys = lib.filesystem.listFilesRecursive "${self}/secrets/keys/ssh";
in
{
imports = [
"${modulesPath}/installer/cd-dvd/installation-cd-minimal.nix"
"${modulesPath}/installer/cd-dvd/channel.nix"
"${self}/modules/iso/minimal.nix"
"${self}/modules/nixos/common/sharedsetup.nix"
"${self}/modules/nixos/common/topology.nix"
"${self}/modules/home/common/sharedsetup.nix"
"${self}/modules/nixos/common/globals.nix"
inputs.home-manager.nixosModules.home-manager
{
home-manager.users."${primaryUser}".imports = [
"${self}/modules/home/common/settings.nix"
"${self}/modules/home/common/sharedsetup.nix"
];
}
];
options.node = {
name = lib.mkOption {
description = "Node Name.";
type = lib.types.str;
};
secretsDir = lib.mkOption {
description = "Path to the secrets directory for this node.";
type = lib.types.path;
default = ./.;
};
};
config = {
node.name = lib.mkForce "drugstore";
swarselsystems = {
info = "~SwarselSystems~ installer ISO";
};
home-manager.users."${primaryUser}" = {
home = {
stateVersion = "23.05";
file = {
".bash_history" = {
source = self + /programs/bash/.bash_history;
};
};
};
swarselsystems = {
modules.general = lib.mkForce true;
};
};
home-manager.users.root.home = {
stateVersion = "23.05";
file = {
".bash_history" = {
source = self + /programs/bash/.bash_history;
};
};
};
# environment.etc."issue".text = "\x1B[32m~SwarselSystems~\x1B[0m\nIP of primary interface: \x1B[31m\\4\x1B[0m\nThe Password for all users & root is '\x1B[31msetup\x1B[0m'.\nInstall the system remotely by running '\x1B[33mbootstrap -n -d [--impermanence] [--encryption]\x1B[0m' on a machine with deployed secrets.\nAlternatively, run '\x1B[33mswarsel-install -d -f \x1B[0m' for a local install.\n";
environment.etc."issue".source = "${self}/programs/etc/issue";
networking.dhcpcd.runHook = "${pkgs.utillinux}/bin/agetty --reload";
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 primaryUser;
users = {
allowNoPasswordLogin = true;
groups.swarsel = { };
users = {
swarsel = {
name = primaryUser;
group = primaryUser;
isNormalUser = true;
password = "setup"; # this is overwritten after install
openssh.authorizedKeys.keys = lib.lists.forEach pubKeys (key: builtins.readFile key);
extraGroups = [ "wheel" ];
};
root = {
# password = lib.mkForce config.users.users.swarsel.password; # this is overwritten after install
openssh.authorizedKeys.keys = config.users.users."${primaryUser}".openssh.authorizedKeys.keys;
};
};
};
boot = {
loader.systemd-boot.enable = lib.mkForce true;
loader.efi.canTouchEfiVariables = true;
};
programs.bash.shellAliases = {
"swarsel-install" = "nix run github:Swarsel/.dotfiles#swarsel-install --";
};
system.activationScripts.cache = {
text = ''
mkdir -p -m=0777 /home/${primaryUser}/.local/state/nix/profiles
mkdir -p -m=0777 /home/${primaryUser}/.local/state/home-manager/gcroots
mkdir -p -m=0777 /home/${primaryUser}/.local/share/nix/
printf '{\"extra-substituters\":{\"https://nix-community.cachix.org\":true,\"https://nix-community.cachix.org https://cache.ngi0.nixos.org/\":true},\"extra-trusted-public-keys\":{\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\":true,\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= cache.ngi0.nixos.org-1:KqH5CBLNSyX184S9BKZJo1LxrxJ9ltnY2uAs5c/f1MA=\":true}}' | tee /home/${primaryUser}/.local/share/nix/trusted-settings.json > /dev/null
mkdir -p /root/.local/share/nix/
printf '{\"extra-substituters\":{\"https://nix-community.cachix.org\":true,\"https://nix-community.cachix.org https://cache.ngi0.nixos.org/\":true},\"extra-trusted-public-keys\":{\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\":true,\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= cache.ngi0.nixos.org-1:KqH5CBLNSyX184S9BKZJo1LxrxJ9ltnY2uAs5c/f1MA=\":true}}' | tee /root/.local/share/nix/trusted-settings.json > /dev/null
'';
};
systemd = {
services.sshd.wantedBy = lib.mkForce [ "multi-user.target" ];
targets = {
sleep.enable = false;
suspend.enable = false;
hibernate.enable = false;
hybrid-sleep.enable = false;
};
};
system.stateVersion = lib.mkForce "23.05";
networking = {
hostName = "drugstore";
wireless.enable = false;
};
};
}
#+end_src
**** Home-manager only (default non-NixOS)
:PROPERTIES:
:CUSTOM_ID: h:7056b9a0-f38b-4bca-b2ba-ab34e2d73493
:END:
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.
#+begin_src nix :tangle hosts/home/default/default.nix
{ self, outputs, config, ... }:
{
imports = [
inputs.stylix.homeManagerModules.stylix
inputs.sops-nix.homeManagerModules.sops
inputs.nix-index-database.hmModules.nix-index
./modules/home/common
"${self}/modules/home/common/sharedsetup.nix"
];
nixpkgs = {
overlays = [ outputs.overlays.default ];
config = {
allowUnfree = true;
};
};
services.xcape = {
enable = true;
mapExpression = {
Control_L = "Escape";
};
};
programs.zsh.initContent = "
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;
};
}
#+end_src
**** ChaosTheatre (Demo Physical/VM)
:PROPERTIES:
:CUSTOM_ID: h:e1498bef-ec67-483d-bf02-76264e30be8e
:END:
This is just a demo host. It applies all the configuration found in the common parts of the flake, but disables all secrets-related features (as they would not work without the proper SSH keys).
I also set the =WLR_RENDERER_ALLOW_SOFTWARE=1= to allow this configuration to run in a virtualized environment. I also enable =qemuGuest= for a smoother experience when testing on QEMU.
***** Main configuration
:PROPERTIES:
:CUSTOM_ID: h:9f1f3439-b0af-4dcd-a96f-b6aa7b6cd2ab
:END:
#+begin_src nix :tangle hosts/nixos/chaostheatre/default.nix
{ self, inputs, config, pkgs, lib, primaryUser, ... }:
let
sharedOptions = {
isBtrfs = false;
isLinux = true;
isPublic = true;
profiles = {
chaostheatre = true;
};
};
in
{
imports = [
./hardware-configuration.nix
./disk-config.nix
{
_module.args.diskDevice = config.swarselsystems.rootDisk;
}
"${self}/hosts/nixos/chaostheatre/options.nix"
inputs.home-manager.nixosModules.home-manager
{
home-manager.users."${primaryUser}".imports = [
"${self}/modules/home/common/settings.nix"
"${self}/hosts/nixos/chaostheatre/options-home.nix"
"${self}/modules/home/common/sharedsetup.nix"
];
}
];
environment.variables = {
WLR_RENDERER_ALLOW_SOFTWARE = 1;
};
services.qemuGuest.enable = true;
boot = {
loader.systemd-boot.enable = lib.mkForce true;
loader.efi.canTouchEfiVariables = true;
kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
};
networking = {
hostName = "chaostheatre";
firewall.enable = true;
};
swarselsystems = lib.recursiveUpdate
{
info = "~SwarselSystems~ demo host";
wallpaper = self + /wallpaper/lenovowp.png;
initialSetup = true;
isImpermanence = true;
isCrypted = true;
isSecureBoot = false;
isSwap = true;
swapSize = "4G";
rootDisk = "/dev/vda";
}
sharedOptions;
home-manager.users."${primaryUser}" = {
home.stateVersion = lib.mkForce "23.05";
swarselsystems = lib.recursiveUpdate
{
isNixos = true;
}
sharedOptions;
};
}
#+end_src
***** NixOS dummy options configuration
:PROPERTIES:
:CUSTOM_ID: h:6f9c1a3b-452e-4944-86e8-cb17603cc3f9
:END:
#+begin_src nix :tangle hosts/nixos/chaostheatre/options.nix
_:
{ }
#+end_src
***** home-manager dummy options configuration
:PROPERTIES:
:CUSTOM_ID: h:88ccb198-74b9-4269-8e22-af1277f44667
:END:
#+begin_src nix :tangle hosts/nixos/chaostheatre/options-home.nix
_:
{ }
#+end_src
** Additions and modifications
:PROPERTIES:
:CUSTOM_ID: h:ab272ab4-3c93-48b1-8f1e-f710aa9aae5d
:END:
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.
Also, this is where I define all of my own modules. These are mostly used for setting some host-specifics directly than opposed to through multiple options.
Lastly, I add some of my own library functions to be used alongside the functions provided by =nixpkgs= and =home-manager=.
*** Packages
:PROPERTIES:
:CUSTOM_ID: h:64a5cc16-6b16-4802-b421-c67ccef853e1
:END:
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.
#+begin_src nix :tangle pkgs/default.nix
{ lib, pkgs, ... }:
let
packageNames = lib.swarselsystems.readNix "pkgs";
in
lib.swarselsystems.mkPackages packageNames pkgs
#+end_src
**** pass-fuzzel
:PROPERTIES:
:CUSTOM_ID: h:4fce458d-7c9c-4bcd-bd90-76b745fe5ce3
:END:
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.
#+begin_src shell :tangle scripts/pass-fuzzel.sh
# 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"
#+end_src
#+begin_src nix :tangle pkgs/pass-fuzzel/default.nix
{ self, name, writeShellApplication, libnotify, pass, fuzzel, wtype }:
writeShellApplication {
inherit name;
runtimeInputs = [ libnotify (pass.withExtensions (exts: [ exts.pass-otp ])) fuzzel wtype ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** cura5
:PROPERTIES:
:CUSTOM_ID: h:799579f3-ddd3-4f76-928a-a8c665980476
:END:
The version of =cura= used to be quite outdated in nixpkgs. I am fetching a newer AppImage here and use that instead.
#+begin_src nix :tangle pkgs/cura5/default.nix
# taken from https://github.com/NixOS/nixpkgs/issues/186570#issuecomment-1627797219
{ appimageTools, fetchurl, writeScriptBin, pkgs, ... }:
let
cura5 = appimageTools.wrapType2 rec {
pname = "cura5";
version = "5.9.0";
src = fetchurl {
url = "https://github.com/Ultimaker/Cura/releases/download/${version}/UltiMaker-Cura-${version}-linux-X64.AppImage";
hash = "sha256-STtVeM4Zs+PVSRO3cI0LxnjRDhOxSlttZF+2RIXnAp4=";
};
extraPkgs = pkgs: with pkgs; [ ];
};
in
writeScriptBin "cura" ''
#! ${pkgs.bash}/bin/bash
# AppImage version of Cura loses current working directory and treats all paths relative to $HOME.
# So we convert each of the files passed as argument to an absolute path.
# This fixes use cases like `cd /path/to/my/files; cura mymodel.stl anothermodel.stl`.
args=()
for a in "$@"; do
if [ -e "$a" ]; then
a="$(realpath "$a")"
fi
args+=("$a")
done
exec "${cura5}/bin/cura5" "''${args[@]}"
''
#+end_src
**** hm-specialisation
:PROPERTIES:
:CUSTOM_ID: h:e6612cff-0804-47ef-9f2b-d2cc6d81a896
:END:
This script allows for quick git home-manager specialisation switching.
#+begin_src nix :tangle pkgs/hm-specialisation/default.nix
{ name, writeShellApplication, fzf, findutils, home-manager, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ fzf findutils home-manager ];
text = ''
genpath=$(home-manager generations | head -1 | awk '{print $7}')
dirs=$(find "$genpath/specialisation" -type l 2>/dev/null; [ -d "$genpath" ] && echo "$genpath")
"$(echo "$dirs" | fzf --prompt="Choose home-manager specialisation to activate")"/activate
'';
}
#+end_src
**** cdw
:PROPERTIES:
:CUSTOM_ID: h:73b14c7a-5444-4fed-b7ac-d65542cdeda3
:END:
This script allows for quick git worktree switching.
#+begin_src nix :tangle pkgs/cdw/default.nix
{ name, writeShellApplication, fzf, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ fzf ];
text = ''
cd "$(git worktree list | fzf | awk '{print $1}')"
'';
}
#+end_src
**** cdb
:PROPERTIES:
:CUSTOM_ID: h:5ad99997-e54c-4f0b-9ab7-15f76b1e16e1
:END:
This script allows for quick git branch switching.
#+begin_src nix :tangle pkgs/cdb/default.nix
{ name, writeShellApplication, fzf, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ fzf ];
text = ''
git checkout "$(git branch --list | grep -v "^\*" | fzf | awk '{print $1}')"
'';
}
#+end_src
**** bak
:PROPERTIES:
:CUSTOM_ID: h:03b1b77b-3ca8-4a8f-8e28-9f29004d96d3
:END:
This script lets me quickly backup files by appending =.bak= to the filename.
#+begin_src nix :tangle pkgs/bak/default.nix
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
text = ''
cp -r "$1"{,.bak}
'';
}
#+end_src
**** timer
:PROPERTIES:
:CUSTOM_ID: h:3c72d263-411c-44f0-90ff-55f14d4d9d49
:END:
This app starts a configuratble timer and uses TTS to say something once the timer runs out.
#+begin_src nix :tangle pkgs/timer/default.nix
{ name, writeShellApplication, speechd, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ speechd ];
text = ''
sleep "$1"; while true; do spd-say "$2"; sleep 0.5; done;
'';
}
#+end_src
**** e
:PROPERTIES:
:CUSTOM_ID: h:1834df06-9238-4efa-9af6-851dafe66c68
:END:
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.
#+begin_src shell :tangle scripts/e.sh
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
#+end_src
#+begin_src nix :tangle pkgs/e/default.nix
{ self, name, writeShellApplication, emacs30-pgtk, sway, jq }:
writeShellApplication {
inherit name;
runtimeInputs = [ emacs30-pgtk sway jq ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** command-not-found
:PROPERTIES:
:CUSTOM_ID: h:10268005-a9cd-4a00-967c-cbe975c552fa
:END:
The normal =command-not-found.sh= uses the outdated =nix-shell= commands as suggestions. This version supplies me with the more modern =nixpkgs#= version.
#+begin_src shell :tangle scripts/command-not-found.sh
# 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 $?
}
#+end_src
**** swarselcheck
:PROPERTIES:
:CUSTOM_ID: h:82f4f414-749b-4d5a-aaaa-6e3ec15fbc3d
:END:
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 [[#h:1834df06-9238-4efa-9af6-851dafe66c68][e]].
#+begin_src shell :tangle scripts/swarselcheck.sh
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 -o confirm_os_window_close=0 zellij attach --create kittyterm &
sleep 1
fi
if [ "$STR" == "" ]; then
exec swaymsg '[title="kittyterm"]' scratchpad show
else
exec swaymsg '[title="kittyterm"]' scratchpad show
fi
elif [[ $element -eq 1 ]]; then
STR=$(swaymsg -t get_tree | grep Element || true)
if [ "$STR" == "" ]; then
exec element-desktop
else
exec swaymsg '[app_id=Element]' kill
fi
elif [[ $vesktop -eq 1 ]]; then
STR=$(swaymsg -t get_tree | grep vesktop || true)
if [ "$STR" == "" ]; then
exec vesktop
else
exec swaymsg '[app_id=vesktop]' kill
fi
elif [[ $spotifyplayer -eq 1 ]]; then
STR=$(swaymsg -t get_tree | jq -r 'recurse(.nodes[]) | select(.name == "__i3_scratch")' | grep spotifytui || true)
CHECK=$(swaymsg -t get_tree | grep spotifytui || true)
if [ "$CHECK" == "" ]; then
exec kitty -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
#+end_src
#+begin_src nix :tangle pkgs/swarselcheck/default.nix
{ self, name, writeShellApplication, kitty, element-desktop, vesktop, spotify-player, jq }:
writeShellApplication {
inherit name;
runtimeInputs = [ kitty element-desktop vesktop spotify-player jq ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** swarselzellij
:PROPERTIES:
:CUSTOM_ID: h:564c102c-e335-4f17-a613-c5a436bb4864
:END:
#+begin_src shell :tangle scripts/swarselzellij.sh
KITTIES=$(($(pgrep -P 1 kitty | wc -l) - 1))
if ((KITTIES < 1)); then
exec kitty -o confirm_os_window_close=0 zellij attach --create main
else
exec kitty -o confirm_os_window_close=0 zellij attach --create "temp $KITTIES"
fi
#+end_src
#+begin_src nix :tangle pkgs/swarselzellij/default.nix
{ self, name, writeShellApplication, kitty }:
writeShellApplication {
inherit name;
runtimeInputs = [ kitty ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** waybarupdate
:PROPERTIES:
:CUSTOM_ID: h:f93f66f9-6b8b-478e-b139-b2f382c1f25e
:END:
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.
#+begin_src shell :tangle scripts/waybarupdate.sh
CFG=$(git --git-dir="$HOME"/.dotfiles/.git --work-tree="$HOME"/.dotfiles/ status -s | wc -l)
CSE=$(git --git-dir="$DOCUMENT_DIR_PRIV"/CSE_TUWIEN/.git --work-tree="$DOCUMENT_DIR_PRIV"/CSE_TUWIEN/ status -s | wc -l)
PASS=$(($(git --git-dir="$HOME"/.local/share/password-store/.git --work-tree="$HOME"/.local/share/password-store/ status -s | wc -l) + $(git --git-dir="$HOME"/.local/share/password-store/.git --work-tree="$HOME"/.local/share/password-store/ diff origin/main..HEAD | wc -l)))
if [[ $CFG != 0 ]]; then
CFG_STR='CONFIG'
else
CFG_STR=''
fi
if [[ $CSE != 0 ]]; then
CSE_STR=' CSE'
else
CSE_STR=''
fi
if [[ $PASS != 0 ]]; then
PASS_STR=' PASS'
else
PASS_STR=''
fi
OUT="$CFG_STR""$CSE_STR""$PASS_STR"
echo "$OUT"
#+end_src
#+begin_src nix :tangle pkgs/waybarupdate/default.nix
{ self, name, writeShellApplication, git }:
writeShellApplication {
inherit name;
runtimeInputs = [ git ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** opacitytoggle
:PROPERTIES:
:CUSTOM_ID: h:a1d94db2-837a-40c4-bbd8-81ce847440ee
:END:
This app quickly toggles between 5% and 0% transparency.
#+begin_src shell :tangle scripts/opacitytoggle.sh
if swaymsg opacity plus 0.01 -q; then
swaymsg opacity 1
else
swaymsg opacity 0.95
fi
#+end_src
#+begin_src nix :tangle pkgs/opacitytoggle/default.nix
{ self, name, writeShellApplication, sway }:
writeShellApplication {
inherit name;
runtimeInputs = [ sway ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** fs-diff
:PROPERTIES:
:CUSTOM_ID: h:7c4e41b3-8c1e-4f71-87a6-30d40baed6a0
:END:
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.
#+begin_src shell :tangle scripts/fs-diff.sh
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
#+end_src
#+begin_src nix :tangle pkgs/fs-diff/default.nix
{ self, name, writeShellApplication }:
writeShellApplication {
inherit name;
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** github-notifications
:PROPERTIES:
:CUSTOM_ID: h:a9398c4e-4d3b-4942-b03c-192f9c0517e5
:END:
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.
#+begin_src nix :tangle pkgs/github-notifications/default.nix
{ name, writeShellApplication, jq, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ jq ];
text = ''
count=$(curl -u Swarsel:"$(cat "$XDG_RUNTIME_DIR/secrets/github_notif")" https://api.github.com/notifications | jq '. | length')
if [[ "$count" != "0" ]]; then
echo "{\"text\":\"$count\"}"
fi
'';
}
#+end_src
**** fullscreen
:PROPERTIES:
:CUSTOM_ID: h:9d49531a-1d9b-4600-b200-18befb5e0f3a
:END:
This application moves the wl-mirror app to the T workspace and makes it fullscreen there.
#+begin_src nix :tangle pkgs/fullscreen/default.nix
{ name, writeShellApplication, sway, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ sway ];
text = ''
swaymsg '[app_id=at.yrlf.wl_mirror] move to workspace 14:T'
swaymsg '[app_id=at.yrlf.wl_mirror] fullscreen'
'';
}
#+end_src
**** screenshare
:PROPERTIES:
:CUSTOM_ID: h:960e539c-2a5a-4e21-b3d4-bcdfc8be8fda
:END:
#+begin_src shell :tangle scripts/screenshare.sh
headless="false"
while [[ $# -gt 0 ]]; do
case "$1" in
-h)
headless="true"
;;
,*)
echo "Invalid option detected."
;;
esac
shift
done
SHARESCREEN="$(nix eval --raw ~/.dotfiles#nixosConfigurations."$(hostname)".config.home-manager.users."$(whoami)".swarselsystems.sharescreen)"
if [[ $headless == "true" ]]; then
wl-mirror "$SHARESCREEN"
else
wl-mirror "$SHARESCREEN" &
sleep 0.1
swaymsg '[app_id=at.yrlf.wl_mirror] move to workspace 14:T'
swaymsg '[app_id=at.yrlf.wl_mirror] fullscreen'
fi
#+end_src
#+begin_src nix :tangle pkgs/screenshare/default.nix
{ self, name, writeShellApplication, sway }:
writeShellApplication {
inherit name;
runtimeInputs = [ sway ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** swarsel-bootstrap
:PROPERTIES:
:CUSTOM_ID: h:74db57ae-0bb9-4257-84be-eddbc85130dd
:END:
This program sets up a new NixOS host remotely. It also takes care of secret management on the new host.
#+begin_src shell :tangle scripts/swarsel-bootstrap.sh
# 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"
persist_dir=""
disk_encryption=0
temp=$(mktemp -d)
function help_and_exit() {
echo
echo "Remotely installs SwarselSystem on a target machine including secret deployment."
echo
echo "USAGE: $0 -n -d [OPTIONS]"
echo
echo "ARGS:"
echo " -n specify target_hostname of the target host to deploy the nixos config on."
echo " -d specify ip or url to the target host."
echo " target during install process."
echo
echo "OPTIONS:"
echo " -u specify target_user with sudo access. nix-config will be cloned to their home."
echo " Default='${target_user}'."
echo " --port specify the ssh port to use for remote access. Default=${ssh_port}."
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
}
function update_sops_file() {
key_name=$1
key_type=$2
key=$3
if [ ! "$key_type" == "hosts" ] && [ ! "$key_type" == "users" ]; then
red "Invalid key type passed to update_sops_file. Must be either 'hosts' or 'users'."
exit 1
fi
cd "${git_root}"
SOPS_FILE=".sops.yaml"
sed -i "{
# Remove any * and & entries for this host
/[*&]$key_name/ d;
# Inject a new age: entry
# n matches the first line following age: and p prints it, then we transform it while reusing the spacing
/age:/{n; p; s/\(.*- \*\).*/\1$key_name/};
# Inject a new hosts or user: entry
/&$key_type/{n; p; s/\(.*- &\).*/\1$key_name $key/}
}" $SOPS_FILE
green "Updating .sops.yaml"
cd -
}
while [[ $# -gt 0 ]]; do
case "$1" in
-n)
shift
target_hostname=$1
;;
-d)
shift
target_destination=$1
;;
-u)
shift
target_user=$1
;;
--port)
shift
ssh_port=$1
;;
--debug)
set -x
;;
-h | --help) help_and_exit ;;
,*)
echo "Invalid option detected."
help_and_exit
;;
esac
shift
done
green "~SwarselSystems~ remote installer"
green "Reading system information for $target_hostname ..."
DISK="$(nix eval --raw ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.rootDisk)"
green "Root Disk: $DISK"
CRYPTED="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isCrypted)"
if [[ $CRYPTED == "true" ]]; then
green "Encryption: ✓"
disk_encryption=1
else
red "Encryption: X"
disk_encryption=0
fi
IMPERMANENCE="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isImpermanence)"
if [[ $IMPERMANENCE == "true" ]]; then
green "Impermanence: ✓"
persist_dir="/persist"
else
red "Impermanence: X"
persist_dir=""
fi
SWAP="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isSwap)"
if [[ $SWAP == "true" ]]; then
green "Swap: ✓"
else
red "Swap: X"
fi
SECUREBOOT="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isSecureBoot)"
if [[ $SECUREBOOT == "true" ]]; then
green "Secure Boot: ✓"
else
red "Secure Boot: X"
fi
ssh_cmd="ssh -oport=${ssh_port} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t $target_user@$target_destination"
# ssh_root_cmd=$(echo "$ssh_cmd" | sed "s|${target_user}@|root@|") # uses @ in the sed switch to avoid it triggering on the $ssh_key value
ssh_root_cmd=${ssh_cmd/${target_user}@/root@}
scp_cmd="scp -oport=${ssh_port} -o StrictHostKeyChecking=no"
if [[ -z ${FLAKE} ]]; then
FLAKE=/home/"$target_user"/.dotfiles
fi
if [ ! -d "$FLAKE" ]; then
cd /home/"$target_user"
yellow "Flake directory not found - cloning repository from GitHub"
git clone git@github.com:Swarsel/.dotfiles.git || (yellow "Could not clone repository via SSH - defaulting to HTTPS" && git clone https://github.com/Swarsel/.dotfiles.git)
FLAKE=/home/"$target_user"/.dotfiles
fi
cd "$FLAKE"
git_root=$(git rev-parse --show-toplevel)
# ------------------------
green "Wiping known_hosts of $target_destination"
sed -i "/$target_hostname/d; /$target_destination/d" ~/.ssh/known_hosts
# ------------------------
green "Preparing a new ssh_host_ed25519_key pair for $target_hostname."
# Create the directory where sshd expects to find the host keys
install -d -m755 "$temp/$persist_dir/etc/ssh"
# Generate host ssh key pair without a passphrase
ssh-keygen -t ed25519 -f "$temp/$persist_dir/etc/ssh/ssh_host_ed25519_key" -C root@"$target_hostname" -N ""
# Set the correct permissions so sshd will accept the key
chmod 600 "$temp/$persist_dir/etc/ssh/ssh_host_ed25519_key"
echo "Adding ssh host fingerprint at $target_destination to ~/.ssh/known_hosts"
# This will fail if we already know the host, but that's fine
ssh-keyscan -p "$ssh_port" "$target_destination" >> ~/.ssh/known_hosts || true
# ------------------------
# when using luks, disko expects a passphrase on /tmp/disko-password, so we set it for now and will update the passphrase later
# via the config
if [ "$disk_encryption" -eq 1 ]; then
while true; do
green "Set disk encryption passphrase:"
read -rs luks_passphrase
green "Please confirm passphrase:"
read -rs luks_passphrase_confirm
if [[ $luks_passphrase == "$luks_passphrase_confirm" ]]; then
$ssh_root_cmd "/bin/sh -c 'echo $luks_passphrase > /tmp/disko-password'"
break
else
red "Passwords do not match"
fi
done
fi
# ------------------------
green "Generating hardware-config.nix for $target_hostname and adding it to the nix-config."
$ssh_root_cmd "nixos-generate-config --force --no-filesystems --root /mnt"
green "Injecting initialSetup"
$ssh_root_cmd "sed -i '/ boot.extraModulePackages /a \ swarselsystems.initialSetup = true;' /mnt/etc/nixos/hardware-configuration.nix"
mkdir -p "$FLAKE"/hosts/nixos/"$target_hostname"
$scp_cmd root@"$target_destination":/mnt/etc/nixos/hardware-configuration.nix "${git_root}"/hosts/nixos/"$target_hostname"/hardware-configuration.nix
# ------------------------
green "Deploying minimal NixOS installation on $target_destination"
SHELL=/bin/sh nix run github:nix-community/nixos-anywhere -- --ssh-port "$ssh_port" --extra-files "$temp" --flake .#"$target_hostname" root@"$target_destination"
echo "Updating ssh host fingerprint at $target_destination to ~/.ssh/known_hosts"
ssh-keyscan -p "$ssh_port" "$target_destination" >> ~/.ssh/known_hosts || true
# ------------------------
while true; do
read -rp "Press Enter to continue once the remote host has finished booting."
if nc -z "$target_destination" "${ssh_port}" 2> /dev/null; then
green "$target_destination is booted. Continuing..."
break
else
yellow "$target_destination is not yet ready."
fi
done
# ------------------------
if [[ $SECUREBOOT == "true" ]]; then
green "Setting up secure boot keys"
$ssh_root_cmd "mkdir -p /var/lib/sbctl"
read -ra scp_call <<< "${scp_cmd}"
sudo "${scp_call[@]}" -r /var/lib/sbctl root@"$target_destination":/var/lib/
$ssh_root_cmd "sbctl enroll-keys --ignore-immutable --microsoft || true"
fi
# ------------------------
green "Disabling initialSetup"
sed -i '/swarselsystems\.initialSetup = true;/d' "$git_root"/hosts/nixos/"$target_hostname"/hardware-configuration.nix
if [ -n "$persist_dir" ]; then
$ssh_root_cmd "cp /etc/machine-id $persist_dir/etc/machine-id || true"
$ssh_root_cmd "cp -R /etc/ssh/ $persist_dir/etc/ssh/ || true"
fi
# ------------------------
green "Generating an age key based on the new ssh_host_ed25519_key."
target_key=$(
ssh-keyscan -p "$ssh_port" -t ssh-ed25519 "$target_destination" 2>&1 |
grep ssh-ed25519 |
cut -f2- -d" " ||
(
red "Failed to get ssh key. Host down?"
exit 1
)
)
host_age_key=$(nix shell nixpkgs#ssh-to-age.out -c sh -c "echo $target_key | ssh-to-age")
if grep -qv '^age1' <<< "$host_age_key"; then
red "The result from generated age key does not match the expected format."
yellow "Result: $host_age_key"
yellow "Expected format: age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
exit 1
else
echo "$host_age_key"
fi
green "Updating nix-secrets/.sops.yaml"
update_sops_file "$target_hostname" "hosts" "$host_age_key"
yellow ".sops.yaml has been updated. There may be superfluous entries, you might need to edit manually."
if yes_or_no "Do you want to manually edit .sops.yaml now?"; then
vim "${git_root}"/.sops.yaml
fi
green "Updating all secrets files to reflect updates .sops.yaml"
sops updatekeys --yes --enable-local-keyservice "${git_root}"/secrets/*/secrets.yaml
# --------------------------
green "Making ssh_host_ed25519_key available to home-manager for user $target_user"
sed -i "/$target_hostname/d; /$target_destination/d" ~/.ssh/known_hosts
$scp_cmd root@"$target_destination":/etc/ssh/ssh_host_ed25519_key root@"$target_destination":/home/"$target_user"/.ssh/ssh_host_ed25519_key
$ssh_root_cmd "mkdir -p /home/$target_user/.ssh; chown $target_user:users /home/$target_user/.ssh/ssh_host_ed25519_key"
# __________________________
if yes_or_no "Add ssh host fingerprints for git upstream repositories? (This is needed for building the full config)"; then
green "Adding ssh host fingerprints for git{lab,hub}"
$ssh_cmd "mkdir -p /home/$target_user/.ssh/; ssh-keyscan -t ssh-ed25519 gitlab.com github.com swagit.swarsel.win >> /home/$target_user/.ssh/known_hosts"
$ssh_root_cmd "mkdir -p /root/.ssh/; ssh-keyscan -t ssh-ed25519 gitlab.com github.com swagit.swarsel.win >> /root/.ssh/known_hosts"
fi
# --------------------------
if yes_or_no "Do you want to copy your full nix-config and nix-secrets to $target_hostname?"; then
green "Adding ssh host fingerprint at $target_destination to ~/.ssh/known_hosts"
ssh-keyscan -p "$ssh_port" "$target_destination" >> ~/.ssh/known_hosts || true
green "Copying full nix-config to $target_hostname"
cd "${git_root}"
just sync "$target_user" "$target_destination"
if [ -n "$persist_dir" ]; then
$ssh_root_cmd "cp -r /home/$target_user/.dotfiles $persist_dir/.dotfiles || true"
$ssh_root_cmd "cp -r /home/$target_user/.ssh $persist_dir/.ssh || true"
fi
if yes_or_no "Do you want to rebuild immediately?"; then
green "Rebuilding nix-config on $target_hostname"
yellow "Reminder: The password is 'setup'"
$ssh_root_cmd "mkdir -p /root/.local/share/nix/; printf '{\"extra-substituters\":{\"https://nix-community.cachix.org\":true,\"https://nix-community.cachix.org https://cache.ngi0.nixos.org/\":true},\"extra-trusted-public-keys\":{\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\":true,\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= cache.ngi0.nixos.org-1:KqH5CBLNSyX184S9BKZJo1LxrxJ9ltnY2uAs5c/f1MA=\":true}}' > /root/.local/share/nix/trusted-settings.json"
$ssh_cmd -oForwardAgent=yes "cd .dotfiles && sudo nixos-rebuild --show-trace --flake .#$target_hostname switch"
fi
else
echo
green "NixOS was successfully installed!"
echo "Post-install config build instructions:"
echo "To copy nix-config from this machine to the $target_hostname, run the following command from ~/nix-config"
echo "just sync $target_user $target_destination"
echo "To rebuild, sign into $target_hostname and run the following command from ~/nix-config"
echo "cd nix-config"
# see above FIXME:(bootstrap)
echo "sudo nixos-rebuild --show-trace --flake .#$target_hostname switch"
# echo "just rebuild"
echo
fi
if yes_or_no "You can now commit and push the nix-config, which includes the hardware-configuration.nix for $target_hostname?"; then
cd "${git_root}"
deadnix hosts/nixos/"$target_hostname"/hardware-configuration.nix -qe
nixpkgs-fmt hosts/nixos/"$target_hostname"/hardware-configuration.nix
(pre-commit run --all-files 2> /dev/null || true) &&
git add "$git_root/hosts/nixos/$target_hostname/hardware-configuration.nix" &&
git add "$git_root/.sops.yaml" &&
git add "$git_root/secrets" &&
(git commit -m "feat: deployed $target_hostname" || true) && git push
fi
#+end_src
#+begin_src nix :tangle pkgs/swarsel-bootstrap/default.nix
{ self, name, writeShellApplication, openssh }:
writeShellApplication {
inherit name;
runtimeInputs = [ openssh ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** swarsel-rebuild
:PROPERTIES:
:CUSTOM_ID: h:1eabdc59-8832-44ca-a22b-11f848ab150a
:END:
#+begin_src shell :tangle scripts/swarsel-rebuild.sh
set -eo pipefail
target_config="chaostheatre"
target_user="swarsel"
function help_and_exit() {
echo
echo "Builds SwarselSystem configuration."
echo
echo "USAGE: $0 [OPTIONS]"
echo
echo "ARGS:"
echo " -n specify nixos config to build."
echo " Default: chaostheatre"
echo " -u specify user to deploy for."
echo " Default: swarsel"
echo " -h | --help Print this help."
exit 0
}
function red() {
echo -e "\x1B[31m[!] $1 \x1B[0m"
if [ -n "${2-}" ]; then
echo -e "\x1B[31m[!] $($2) \x1B[0m"
fi
}
function green() {
echo -e "\x1B[32m[+] $1 \x1B[0m"
if [ -n "${2-}" ]; then
echo -e "\x1B[32m[+] $($2) \x1B[0m"
fi
}
function yellow() {
echo -e "\x1B[33m[*] $1 \x1B[0m"
if [ -n "${2-}" ]; then
echo -e "\x1B[33m[*] $($2) \x1B[0m"
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
-n)
shift
target_config=$1
;;
-u)
shift
target_user=$1
;;
-h | --help) help_and_exit ;;
,*)
echo "Invalid option detected."
help_and_exit
;;
esac
shift
done
cd /home/"$target_user"
if [ ! -d /home/"$target_user"/.dotfiles ]; then
green "Cloning repository from GitHub"
git clone https://github.com/Swarsel/.dotfiles.git
else
red "A .dotfiles repository is in the way. Please (re-)move the repository and try again."
exit 1
fi
local_keys=$(ssh-add -L || true)
pub_key=$(cat /home/"$target_user"/.dotfiles/secrets/keys/ssh/yubikey.pub)
read -ra pub_arr <<< "$pub_key"
cd .dotfiles
if [[ $local_keys != *"${pub_arr[1]}"* ]]; then
yellow "The ssh key for this configuration is not available."
green "Adjusting flake.nix so that the configuration is buildable"
sed -i '/nix-secrets = {/,/^[[:space:]]*};/d' flake.nix
sed -i '/vbc-nix = {/,/^[[:space:]]*};/d' flake.nix
sed -i '/[[:space:]]*\/\/ (inputs.vbc-nix.overlays.default final prev)/d' overlays/default.nix
rm modules/home/common/env.nix
rm modules/home/common/gammastep.nix
rm modules/home/common/git.nix
rm modules/home/common/mail.nix
rm modules/home/common/yubikey.nix
rm modules/nixos/server/restic.nix
rm modules/nixos/common/home-manager-extra.nix
rm hosts/nixos/sync/default.nix
rm -rf modules/nixos/server
rm -rf modules/home/server
nix flake update vbc-nix
git add .
else
green "Valid SSH key found! Continuing with installation"
fi
sudo nixos-generate-config --dir /home/"$target_user"/.dotfiles/hosts/nixos/"$target_config"/
git add /home/"$target_user"/.dotfiles/hosts/nixos/"$target_config"/hardware-configuration.nix
green "Installing flake $target_config"
sudo nixos-rebuild --show-trace --flake .#"$target_config" boot
yellow "Please keep in mind that this is only a demo of the configuration. Things might break unexpectedly."
#+end_src
#+begin_src nix :tangle pkgs/swarsel-rebuild/default.nix
{ self, name, writeShellApplication, git }:
writeShellApplication {
inherit name;
runtimeInputs = [ git ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** swarsel-install
:PROPERTIES:
:CUSTOM_ID: h:fbd8aaf2-9dca-4ca3-aca1-19d0d188a435
:END:
Autoformatting always puts the =EOF= with indentation, which makes shfmt check fail. When editing this block, unindent them manually.
#+begin_src shell :tangle scripts/swarsel-install.sh
set -eo pipefail
target_config="chaostheatre"
target_hostname="chaostheatre"
target_user="swarsel"
persist_dir=""
target_disk="/dev/vda"
disk_encryption=0
function help_and_exit() {
echo
echo "Locally installs SwarselSystem on this machine."
echo
echo "USAGE: $0 -n -d [OPTIONS]"
echo
echo "ARGS:"
echo " -n specify the nixos config to deploy."
echo " Default: chaostheatre"
echo " -d specify disk to install on."
echo " Default: /dev/vda"
echo " -u specify user to deploy for."
echo " Default: swarsel"
echo " -h | --help Print this help."
exit 0
}
function red() {
echo -e "\x1B[31m[!] $1 \x1B[0m"
if [ -n "${2-}" ]; then
echo -e "\x1B[31m[!] $($2) \x1B[0m"
fi
}
function green() {
echo -e "\x1B[32m[+] $1 \x1B[0m"
if [ -n "${2-}" ]; then
echo -e "\x1B[32m[+] $($2) \x1B[0m"
fi
}
function yellow() {
echo -e "\x1B[33m[*] $1 \x1B[0m"
if [ -n "${2-}" ]; then
echo -e "\x1B[33m[*] $($2) \x1B[0m"
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
-n)
shift
target_config=$1
target_hostname=$1
;;
-u)
shift
target_user=$1
;;
-d)
shift
target_disk=$1
;;
-h | --help) help_and_exit ;;
,*)
echo "Invalid option detected."
help_and_exit
;;
esac
shift
done
function cleanup() {
sudo rm -rf .cache/nix
sudo rm -rf /root/.cache/nix
}
trap cleanup exit
green "~SwarselSystems~ local installer"
cd /home/"$target_user"
sudo rm -rf /root/.cache/nix
sudo rm -rf .cache/nix
sudo rm -rf .dotfiles
green "Cloning repository from GitHub"
git clone https://github.com/Swarsel/.dotfiles.git
local_keys=$(ssh-add -L || true)
pub_key=$(cat /home/"$target_user"/.dotfiles/secrets/keys/ssh/yubikey.pub)
read -ra pub_arr <<< "$pub_key"
cd .dotfiles
if [[ $local_keys != *"${pub_arr[1]}"* ]]; then
yellow "The ssh key for this configuration is not available."
green "Adjusting flake.nix so that the configuration is buildable ..."
sed -i '/nix-secrets = {/,/^[[:space:]]*};/d' flake.nix
sed -i '/vbc-nix = {/,/^[[:space:]]*};/d' flake.nix
sed -i '/[[:space:]]*\/\/ (inputs.vbc-nix.overlays.default final prev)/d' overlays/default.nix
rm modules/home/common/env.nix
rm modules/home/common/gammastep.nix
rm modules/home/common/git.nix
rm modules/home/common/mail.nix
rm modules/home/common/yubikey.nix
rm modules/nixos/server/restic.nix
rm modules/nixos/common/home-manager-extra.nix
rm hosts/nixos/sync/default.nix
rm -rf modules/nixos/server
rm -rf modules/home/server
cat > hosts/nixos/chaostheatre/options.nix << EOF
{ self, lib, ... }:
{
options = {
swarselsystems = {
modules = {
home-managerExtra = lib.mkEnableOption "dummy option for chaostheatre";
};
};
};
}
EOF
cat > hosts/nixos/chaostheatre/options-home.nix << EOF
{ self, lib, ... }:
{
options = {
swarselsystems = {
modules = {
yubikey = lib.mkEnableOption "dummy option for chaostheatre";
env = lib.mkEnableOption "dummy option for chaostheatre";
git = lib.mkEnableOption "dummy option for chaostheatre";
mail = lib.mkEnableOption "dummy option for chaostheatre";
gammastep = lib.mkEnableOption "dummy option for chaostheatre";
};
};
};
}
EOF
nix flake update vbc-nix
git add .
else
green "Valid SSH key found! Continuing with installation"
fi
green "Reading system information for $target_config ..."
DISK="$(nix eval --raw ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.rootDisk)"
green "Root Disk in config: $DISK - Root Disk passed in cli: $target_disk"
CRYPTED="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isCrypted)"
if [[ $CRYPTED == "true" ]]; then
green "Encryption: ✓"
disk_encryption=1
else
red "Encryption: X"
disk_encryption=0
fi
IMPERMANENCE="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isImpermanence)"
if [[ $IMPERMANENCE == "true" ]]; then
green "Impermanence: ✓"
persist_dir="/persist"
else
red "Impermanence: X"
persist_dir=""
fi
SWAP="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isSwap)"
if [[ $SWAP == "true" ]]; then
green "Swap: ✓"
else
red "Swap: X"
fi
SECUREBOOT="$(nix eval ~/.dotfiles#nixosConfigurations."$target_hostname".config.swarselsystems.isSecureBoot)"
if [[ $SECUREBOOT == "true" ]]; then
green "Secure Boot: ✓"
else
red "Secure Boot: X"
fi
if [ "$disk_encryption" -eq 1 ]; then
while true; do
green "Set disk encryption passphrase:"
read -rs luks_passphrase
green "Please confirm passphrase:"
read -rs luks_passphrase_confirm
if [[ $luks_passphrase == "$luks_passphrase_confirm" ]]; then
echo "$luks_passphrase" > /tmp/disko-password
break
else
red "Passwords do not match"
fi
done
fi
green "Setting up disk ..."
if [[ $target_config == "chaostheatre" ]]; then
sudo nix --experimental-features "nix-command flakes" run github:nix-community/disko/v1.10.0 -- --mode destroy,format,mount --flake .#"$target_config" --yes-wipe-all-disks --arg diskDevice "$target_disk"
else
sudo nix --experimental-features "nix-command flakes" run github:nix-community/disko/latest -- --mode destroy,format,mount --flake .#"$target_config" --yes-wipe-all-disks
fi
sudo mkdir -p /mnt/"$persist_dir"/home/"$target_user"/
sudo cp -r /home/"$target_user"/.dotfiles /mnt/"$persist_dir"/home/"$target_user"/
sudo chown -R 1000:100 /mnt/"$persist_dir"/home/"$target_user"
green "Generating hardware configuration ..."
sudo nixos-generate-config --root /mnt --no-filesystems --dir /home/"$target_user"/.dotfiles/hosts/nixos/"$target_config"/
green "Injecting initialSetup ..."
sudo sed -i '/ boot.extraModulePackages /a \ swarselsystems.initialSetup = true;' /home/"$target_user"/.dotfiles/hosts/nixos/"$target_config"/hardware-configuration.nix
git add /home/"$target_user"/.dotfiles/hosts/nixos/"$target_config"/hardware-configuration.nix
sudo mkdir -p /root/.local/share/nix/
printf '{\"extra-substituters\":{\"https://nix-community.cachix.org\":true,\"https://nix-community.cachix.org https://cache.ngi0.nixos.org/\":true},\"extra-trusted-public-keys\":{\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=\":true,\"nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs= cache.ngi0.nixos.org-1:KqH5CBLNSyX184S9BKZJo1LxrxJ9ltnY2uAs5c/f1MA=\":true}}' | sudo tee /root/.local/share/nix/trusted-settings.json > /dev/null
green "Installing flake $target_config"
sudo nixos-install --flake .#"$target_config"
green "Installation finished! Reboot to see changes"
#+end_src
#+begin_src nix :tangle pkgs/swarsel-install/default.nix
{ self, name, writeShellApplication, git }:
writeShellApplication {
inherit name;
runtimeInputs = [ git ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** swarsel-postinstall
:PROPERTIES:
:CUSTOM_ID: h:c98a7615-e5da-4f47-8ed1-2b2ea65519e9
:END:
#+begin_src shell :tangle scripts/swarsel-postinstall.sh
set -eo pipefail
target_config="chaostheatre"
target_user="swarsel"
function help_and_exit() {
echo
echo "Locally installs SwarselSystem on this machine."
echo
echo "USAGE: $0 -d [OPTIONS]"
echo
echo "ARGS:"
echo " -d specify disk to install on."
echo " -n specify the nixos config to deploy."
echo " Default: chaostheatre"
echo " Default: chaostheatre"
echo " -u specify user to deploy for."
echo " Default: swarsel"
echo " -h | --help Print this help."
exit 0
}
function green() {
echo -e "\x1B[32m[+] $1 \x1B[0m"
if [ -n "${2-}" ]; then
echo -e "\x1B[32m[+] $($2) \x1B[0m"
fi
}
while [[ $# -gt 0 ]]; do
case "$1" in
-n)
shift
target_config=$1
;;
-u)
shift
target_user=$1
;;
-h | --help) help_and_exit ;;
,*)
echo "Invalid option detected."
help_and_exit
;;
esac
shift
done
function cleanup() {
sudo rm -rf .cache/nix
sudo rm -rf /root/.cache/nix
}
trap cleanup exit
sudo rm -rf .cache/nix
sudo rm -rf /root/.cache/nix
green "~SwarselSystems~ remote post-installer"
cd /home/"$target_user"/.dotfiles
SECUREBOOT="$(nix eval ~/.dotfiles#nixosConfigurations."$target_config".config.swarselsystems.isSecureBoot)"
if [[ $SECUREBOOT == "true" ]]; then
green "Setting up secure boot keys"
sudo mkdir -p /var/lib/sbctl
sbctl create-keys || true
sbctl enroll-keys --ignore-immutable --microsoft || true
fi
green "Disabling initialSetup"
sed -i '/swarselsystems\.initialSetup = true;/d' /home/"$target_user"/.dotfiles/hosts/nixos/"$target_config"/hardware-configuration.nix
sudo nixos-rebuild --flake .#"$target_config" switch
green "Post-install finished!"
#+end_src
#+begin_src nix :tangle pkgs/swarsel-postinstall/default.nix
{ self, name, writeShellApplication, git }:
writeShellApplication {
inherit name;
runtimeInputs = [ git ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** t2ts
:PROPERTIES:
:CUSTOM_ID: h:5ad99997-e54c-4f0b-9ab7-15f76b1e16e1
:END:
#+begin_src nix :tangle pkgs/t2ts/default.nix
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ ];
text = ''
date -d"$1" +%s
'';
}
#+end_src
**** ts2t
:PROPERTIES:
:CUSTOM_ID: h:5ad99997-e54c-4f0b-9ab7-15f76b1e16e1
:END:
#+begin_src nix :tangle pkgs/ts2t/default.nix
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ ];
text = ''
date -d @"$1" 2>/dev/null || date -r "$1"
'';
}
#+end_src
**** vershell
:PROPERTIES:
:CUSTOM_ID: h:7806b129-a4a5-4d10-af27-6cbeafbcb294
:END:
#+begin_src nix :tangle pkgs/vershell/default.nix
{ name, writeShellApplication, ... }:
writeShellApplication {
inherit name;
runtimeInputs = [ ];
text = ''
nix shell github:nixos/nixpkgs/"$1"#"$2";
'';
}
#+end_src
**** eontimer
:PROPERTIES:
:CUSTOM_ID: h:9fda7829-09a4-4b8f-86f6-08b078ab2874
:END:
#+begin_src nix :tangle pkgs/eontimer/default.nix
{ lib
, python3
, fetchFromGitHub
, makeDesktopItem
, writeShellScript
, ...
}:
let
wrapper = writeShellScript "eontimer-wrapper" ''
export QT_QPA_PLATFORM=xcb
exec @out@/bin/EonTimer
'';
in
python3.pkgs.buildPythonApplication rec {
pname = "eontimer";
version = "3.0.0-rc.6";
pyproject = true;
src = fetchFromGitHub {
owner = "DasAmpharos";
repo = "EonTimer";
rev = version;
hash = "sha256-+XN/VGGlEg2gVncRZrWDOZ2bfxt8xyIu22F2wHlG6YI=";
};
build-system = [
python3.pkgs.setuptools
python3.pkgs.wheel
];
dependencies = with python3.pkgs; [
altgraph
certifi
charset-normalizer
idna
libsass
macholib
packaging
pillow
pipdeptree
platformdirs
pyinstaller
pyinstaller-hooks-contrib
pyside6
requests
setuptools
shiboken6
urllib3
];
nativeBuildInputs = [
python3.pkgs.pyinstaller
];
buildPhase = ''
runHook preBuild
pyinstaller --clean --noconfirm EonTimer.spec
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/bin
mkdir -p $out/share/applications
cp dist/EonTimer $out/bin/
install -Dm755 -T ${wrapper} $out/bin/eontimer
substituteInPlace $out/bin/eontimer --subst-var out
runHook postInstall
'';
postInstall = ''
install -Dm755 -t $out/share/applications ${
makeDesktopItem {
name = "eontimer";
desktopName = "EonTimer";
comment = "Start EonTimer";
exec = "eontimer";
}
}/share/applications/eontimer.desktop
'';
meta = {
description = "Pokémon RNG Timer";
homepage = "https://github.com/DasAmpharos/EonTimer";
license = lib.licenses.mit;
maintainers = with lib.maintainers; [ ];
mainProgram = "eon-timer";
};
}
#+end_src
**** project
:PROPERTIES:
:CUSTOM_ID: h:154b6df4-dd50-4f60-9794-05a140d02994
:END:
#+begin_src shell :tangle scripts/project.sh
set -euo pipefail
if [ ! -d "$(pwd)/.git" ]; then
git init
fi
nix flake init --template "$FLAKE"#"$1"
direnv allow
#+end_src
#+begin_src nix :tangle pkgs/project/default.nix
{ self, name, writeShellApplication }:
writeShellApplication {
inherit name;
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** fhs
:PROPERTIES:
:CUSTOM_ID: h:36d6c17c-6d91-4297-b76d-9d7feab6c1a0
:END:
#+begin_src nix :tangle pkgs/fhs/default.nix
{ name, pkgs, ... }:
let
base = pkgs.appimageTools.defaultFhsEnvArgs;
in
pkgs.buildFHSEnv (base // {
name = "fhs";
targetPkgs = pkgs: (base.targetPkgs pkgs) ++ [ pkgs.pkg-config ];
profile = "export FHS=1";
runScript = "zsh";
extraOutputsToInstall = [ "dev" ];
})
#+end_src
**** swarsel-displaypower
:PROPERTIES:
:CUSTOM_ID: h:814d5e7f-4b95-412d-b246-33f888514ec6
:END:
A crude script to power on all displays that might be attached. Needed because sometimes displays do not awake from sleep.
#+begin_src shell :tangle scripts/swarsel-displaypower.sh
swaymsg "output * power on" > /dev/null 2>&1 || true
swaymsg "output * dpms on" > /dev/null 2>&1 || true
#+end_src
#+begin_src nix :tangle pkgs/swarsel-displaypower/default.nix
{ self, name, writeShellApplication, sway }:
writeShellApplication {
inherit name;
runtimeInputs = [ sway ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
**** swarsel-mgba
:PROPERTIES:
:CUSTOM_ID: h:799579f3-ddd3-4f76-928a-a8c665980476
:END:
AppImage version of mgba in which the lua scripting works.
#+begin_src nix :tangle pkgs/swarsel-mgba/default.nix
{ appimageTools, fetchurl, ... }:
let
pname = "mgba";
version = "0.10.4";
src = fetchurl {
url = "https://github.com/mgba-emu/mgba/releases/download/${version}/mGBA-${version}-appimage-x64.appimage";
hash = "sha256-rDihDfuA8DqxvCe6UeavCzpjeU+fSqUbFnyTNC2dc1I=";
};
appimageContents = appimageTools.extractType2 { inherit pname version src; };
in
appimageTools.wrapType2 {
inherit pname version src;
extraInstallCommands = ''
install -Dm444 ${appimageContents}/io.mgba.mGBA.desktop -t $out/share/applications
substituteInPlace $out/share/applications/io.mgba.mGBA.desktop \
--replace-fail 'Exec=mgba-qt %f' 'Exec=mgba'
cp -r ${appimageContents}/usr/share/icons $out/share
'';
}
#+end_src
**** swarsel-deploy
:PROPERTIES:
:CUSTOM_ID: h:c3362d4e-d3a8-43e8-9ef7-272b6de0572e
:END:
#+begin_src nix :tangle pkgs/swarsel-deploy/default.nix
# heavily inspired from https://github.com/oddlama/nix-config/blob/d42cbde676001a7ad8a3cace156e050933a4dcc3/pkgs/deploy.nix
{ name, bc, nix-output-monitor, writeShellApplication, ... }:
writeShellApplication {
runtimeInputs = [ bc nix-output-monitor ];
inherit name;
text = ''
set -euo pipefail
shopt -s lastpipe # allow cmd | readarray
function die() {
echo "error: $*" >&2
exit 1
}
function show_help() {
echo 'Usage: deploy [OPTIONS] [ACTION]'
echo "Builds, pushes and activates nixosConfigurations on target systems."
echo ""
echo 'ACTION:'
echo ' switch [default] Switch immediately to the new configuration and make it the boot default'
echo ' boot Make the configuration the new boot default'
echo " test Activate the configuration but don't make it the boot default"
echo " dry-activate Don't activate, just show what would be done"
echo ""
echo 'OPTIONS: [passed to nix build]'
}
function time_start() {
T_START=$(date +%s.%N)
}
function time_next() {
T_END=$(date +%s.%N)
T_LAST=$(${bc}/bin/bc <<< "scale=1; ($T_END - $T_START)/1")
T_START="$T_END"
}
USER_FLAKE_DIR=$(git rev-parse --show-toplevel 2> /dev/null || pwd) ||
die "Could not determine current working directory. Something went very wrong."
[[ -e "$USER_FLAKE_DIR/flake.nix" ]] ||
die "Could not determine location of your project's flake.nix. Please run this at or below your main directory containing the flake.nix."
cd "$USER_FLAKE_DIR"
[[ $# -gt 0 ]] || {
show_help
exit 1
}
OPTIONS=()
POSITIONAL_ARGS=()
while [[ $# -gt 0 ]]; do
case "$1" in
"help" | "--help" | "-help" | "-h")
show_help
exit 1
;;
-*) OPTIONS+=("$1") ;;
,*) POSITIONAL_ARGS+=("$1") ;;
esac
shift
done
[[ ''${#POSITIONAL_ARGS[@]} -ge 1 ]] ||
die "Missing argument: "
[[ ''${#POSITIONAL_ARGS[@]} -le 2 ]] ||
die "Too many arguments given."
tr , '\n' <<< "''${POSITIONAL_ARGS[0]}" | sort -u | readarray -t HOSTS
ACTION="''${POSITIONAL_ARGS[1]-switch}"
# Expand flake paths for hosts definitions
declare -A TOPLEVEL_FLAKE_PATHS
for host in "''${HOSTS[@]}"; do
TOPLEVEL_FLAKE_PATHS["$host"]=".#nixosConfigurations.$host.config.system.build.toplevel"
done
time_start
# Get outputs of all derivations (should be cached)
declare -A TOPLEVEL_STORE_PATHS
for host in "''${HOSTS[@]}"; do
toplevel="''${TOPLEVEL_FLAKE_PATHS["$host"]}"
# Make sudo call to get prompt out of the way
sudo echo "[1;36m Building [m📦 [34m$host[m"
nix build --no-link "''${OPTIONS[@]}" --show-trace --log-format internal-json -v "$toplevel" |& ${nix-output-monitor}/bin/nom --json ||
die "Failed to get derivation path for $host from ''${TOPLEVEL_FLAKE_PATHS["$host"]}"
TOPLEVEL_STORE_PATHS["$host"]=$(nix build --no-link --print-out-paths "''${OPTIONS[@]}" "$toplevel")
time_next
echo "[1;32m Built [m✅ [34m$host[m [33m''${TOPLEVEL_STORE_PATHS["$host"]}[m [90min ''${T_LAST}s[m"
done
current_host=$(hostname)
for host in "''${HOSTS[@]}"; do
store_path="''${TOPLEVEL_STORE_PATHS["$host"]}"
if [ "$host" = "$current_host" ]; then
echo -e "\033[1;36m Running locally for $host... \033[m"
ssh_prefix="sudo"
else
echo -e "\033[1;36m Copying \033[m➡️ \033[34m$host\033[m"
nix copy --to "ssh://$host" "$store_path"
time_next
echo -e "\033[1;32m Copied \033[m✅ \033[34m$host\033[m \033[90min ''${T_LAST}s\033[m"
ssh_prefix="ssh $host --"
fi
echo -e "\033[1;36m Applying \033[m⚙️ \033[34m$host\033[m"
prev_system=$($ssh_prefix readlink -e /nix/var/nix/profiles/system)
$ssh_prefix /run/current-system/sw/bin/nix-env --profile /nix/var/nix/profiles/system --set "$store_path" ||
die "Failed to set system profile"
$ssh_prefix "$store_path"/bin/switch-to-configuration "$ACTION" ||
echo "Error while activating new system" >&2
if [[ -n $prev_system ]]; then
$ssh_prefix nvd --color always diff "$prev_system" "$store_path" || true
fi
time_next
echo -e "\033[1;32m Applied \033[m✅ \033[34m$host\033[m \033[90min ''${T_LAST}s\033[m"
done
'';
}
#+end_src
**** sshrm
:PROPERTIES:
:CUSTOM_ID: h:02842543-caca-4d4c-a4d2-7ac749b5c136
:END:
This programs simply runs ssh-keygen on the last host that I tried to ssh into. I need this frequently when working with cloud-init usually.
#+begin_src shell :tangle scripts/sshrm.sh
HISTFILE="$HOME"/.histfile
last_ssh_cmd=$(grep -E "ssh " "$HISTFILE" | sed -E 's/^: [0-9]+:[0-9]+;//' | grep "^ssh " | tail -1)
host=$(echo "$last_ssh_cmd" | sed -E 's/.*ssh ([^@ ]+@)?([^ ]+).*/\2/')
if [[ -n $host ]]; then
echo "Removing SSH host key for: $host"
ssh-keygen -R "$host"
else
echo "No valid SSH command found in history."
fi
#+end_src
#+begin_src nix :tangle pkgs/sshrm/default.nix
{ self, name, writeShellApplication, openssh }:
writeShellApplication {
inherit name;
runtimeInputs = [ openssh ];
text = builtins.readFile "${self}/scripts/${name}.sh";
}
#+end_src
*** Overlays (additions, overrides, nixpkgs-stable)
:PROPERTIES:
:CUSTOM_ID: h:5e3e21e0-57af-4dad-b32f-6400af9b7aab
:END:
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.
When adding a new entry here, do not forget to add it in the default output of this file, otherwise it will not be exposed to the rest of the system.
#+begin_src nix :tangle overlays/default.nix
{ self, inputs, lib, ... }:
let
additions = final: _: import "${self}/pkgs" { pkgs = final; inherit lib; };
modifications = final: prev: {
vesktop = prev.vesktop.override {
withSystemVencord = true;
};
firefox = prev.firefox.override {
nativeMessagingHosts = [
prev.tridactyl-native
prev.browserpass
prev.plasma5Packages.plasma-browser-integration
];
};
mgba = final.swarsel-mgba;
retroarch = prev.retroarch.withCores (cores: with cores; [
snes9x # snes
nestopia # nes
dosbox # dos
scummvm # scumm
vba-m # gb/a
mgba # gb/a
melonds # ds
dolphin # gc/wii
]);
};
nixpkgs-stable = final: _: {
stable = import inputs.nixpkgs-stable {
inherit (final) system;
config.allowUnfree = true;
};
};
nixpkgs-kernel = final: _: {
kernel = import inputs.nixpkgs-kernel {
inherit (final) system;
config.allowUnfree = true;
};
};
nixpkgs-stable24_05 = final: _: {
stable24_05 = import inputs.nixpkgs-stable24_05 {
inherit (final) system;
config.allowUnfree = true;
};
};
nixpkgs-stable24_11 = final: _: {
stable24_11 = import inputs.nixpkgs-stable24_11 {
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)
// (nixpkgs-kernel final prev)
// (nixpkgs-stable24_05 final prev)
// (nixpkgs-stable24_11 final prev)
// (zjstatus final prev)
// (inputs.vbc-nix.overlays.default final prev)
// (inputs.nur.overlays.default final prev)
// (inputs.emacs-overlay.overlay final prev)
// (inputs.nix-topology.overlays.default final prev)
// (inputs.nixgl.overlay final prev);
}
#+end_src
*** Profiles
:PROPERTIES:
:CUSTOM_ID: h:f0f1c961-3e7a-47b8-99ab-1654bb45dffc
:END:
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
:PROPERTIES:
:CUSTOM_ID: h:14e68518-8ec7-48ec-b208-0e3d6d49954d
:END:
Modules that need to be loaded on the NixOS level. Note that these will not be available on systems that are not running NixOS.
#+begin_src nix :tangle profiles/nixos/default.nix
{ lib, ... }:
let
profileNames = lib.swarselsystems.readNix "profiles/nixos";
in
{
imports = lib.swarselsystems.mkImports profileNames "profiles/nixos";
}
#+end_src
***** Personal
:PROPERTIES:
:CUSTOM_ID: h:32d654de-8db2-403a-9a27-4c46d7b9172d
:END:
#+begin_src nix :tangle profiles/nixos/personal/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.personal = lib.mkEnableOption "is this a personal host";
config = lib.mkIf config.swarselsystems.profiles.personal {
swarselsystems.modules = {
packages = lib.mkDefault true;
general = lib.mkDefault true;
home-manager = lib.mkDefault true;
home-managerExtra = lib.mkDefault true;
xserver = lib.mkDefault true;
users = lib.mkDefault true;
env = lib.mkDefault true;
security = lib.mkDefault true;
systemdTimeout = lib.mkDefault true;
hardware = lib.mkDefault true;
pulseaudio = lib.mkDefault true;
pipewire = lib.mkDefault true;
network = lib.mkDefault true;
time = lib.mkDefault true;
commonSops = lib.mkDefault true;
pii = lib.mkDefault true;
stylix = lib.mkDefault true;
programs = lib.mkDefault true;
zsh = lib.mkDefault true;
syncthing = lib.mkDefault true;
blueman = lib.mkDefault true;
networkDevices = lib.mkDefault true;
gvfs = lib.mkDefault true;
interceptionTools = lib.mkDefault true;
swayosd = lib.mkDefault true;
ppd = lib.mkDefault true;
yubikey = lib.mkDefault true;
ledger = lib.mkDefault true;
keyboards = lib.mkDefault true;
login = lib.mkDefault true;
nix-ld = lib.mkDefault true;
impermanence = lib.mkDefault true;
nvd = lib.mkDefault true;
gnome-keyring = lib.mkDefault true;
sway = lib.mkDefault true;
xdg-portal = lib.mkDefault true;
distrobox = lib.mkDefault true;
appimage = lib.mkDefault true;
lid = lib.mkDefault true;
lowBattery = lib.mkDefault true;
lanzaboote = lib.mkDefault true;
optional = {
gaming = lib.mkDefault true;
virtualbox = lib.mkDefault true;
autologin = lib.mkDefault true;
nswitch-rcm = lib.mkDefault true;
};
server = {
ssh = lib.mkDefault true;
};
};
};
}
#+end_src
***** Chaostheatre
:PROPERTIES:
:CUSTOM_ID: h:b79fbb59-9cf2-48eb-b469-2589223dda95
:END:
#+begin_src nix :tangle profiles/nixos/chaostheatre/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.chaostheatre = lib.mkEnableOption "is this a chaostheatre host";
config = lib.mkIf config.swarselsystems.profiles.chaostheatre {
swarselsystems.modules = {
packages = lib.mkDefault true;
general = lib.mkDefault true;
home-manager = lib.mkDefault true;
home-managerExtra = lib.mkDefault false;
xserver = lib.mkDefault true;
users = lib.mkDefault true;
env = lib.mkDefault true;
security = lib.mkDefault true;
systemdTimeout = lib.mkDefault true;
hardware = lib.mkDefault true;
pulseaudio = lib.mkDefault true;
pipewire = lib.mkDefault true;
network = lib.mkDefault true;
time = lib.mkDefault true;
commonSops = lib.mkDefault true;
stylix = lib.mkDefault true;
programs = lib.mkDefault true;
zsh = lib.mkDefault true;
syncthing = lib.mkDefault true;
blueman = lib.mkDefault true;
networkDevices = lib.mkDefault true;
gvfs = lib.mkDefault true;
interceptionTools = lib.mkDefault true;
swayosd = lib.mkDefault true;
ppd = lib.mkDefault true;
yubikey = lib.mkDefault true;
ledger = lib.mkDefault true;
keyboards = lib.mkDefault true;
login = lib.mkDefault true;
nix-ld = lib.mkDefault true;
impermanence = lib.mkDefault true;
nvd = lib.mkDefault true;
gnome-keyring = lib.mkDefault true;
sway = lib.mkDefault true;
xdg-portal = lib.mkDefault true;
distrobox = lib.mkDefault true;
appimage = lib.mkDefault true;
lid = lib.mkDefault true;
lowBattery = lib.mkDefault true;
lanzaboote = lib.mkDefault true;
optional = {
autologin = lib.mkDefault true;
};
};
};
}
#+end_src
***** toto
:PROPERTIES:
:CUSTOM_ID: h:125443fb-deb6-44c9-83ee-bbd10daf78dd
:END:
#+begin_src nix :tangle profiles/nixos/toto/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.toto = lib.mkEnableOption "is this a toto (setup) host";
config = lib.mkIf config.swarselsystems.profiles.toto {
swarselsystems.modules = {
general = lib.mkDefault true;
home-manager = lib.mkDefault true;
home-managerExtra = lib.mkDefault true;
xserver = lib.mkDefault true;
users = lib.mkDefault true;
commonSops = lib.mkDefault true;
impermanence = lib.mkDefault true;
lanzaboote = lib.mkDefault true;
server = {
ssh = lib.mkDefault true;
};
optional = {
autologin = lib.mkDefault true;
};
};
};
}
#+end_src
***** Work
:PROPERTIES:
:CUSTOM_ID: h:cb3631a8-9c1b-42f2-ab01-502c7b4c273d
:END:
#+begin_src nix :tangle profiles/nixos/work/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.work = lib.mkEnableOption "is this a work host";
config = lib.mkIf config.swarselsystems.profiles.work {
swarselsystems.modules = {
optional = {
work = lib.mkDefault true;
};
};
};
}
#+end_src
***** Framework
:PROPERTIES:
:CUSTOM_ID: h:eb272c99-842a-4095-bc65-283562749300
:END:
#+begin_src nix :tangle profiles/nixos/framework/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.framework = lib.mkEnableOption "is this a framework brand host";
config = lib.mkIf config.swarselsystems.profiles.framework {
swarselsystems.modules = {
optional = {
framework = lib.mkDefault true;
};
};
};
}
#+end_src
***** AMD CPU
:PROPERTIES:
:CUSTOM_ID: h:b7beb4a5-8808-438d-8799-7f08f38fd1ba
:END:
#+begin_src nix :tangle profiles/nixos/amdcpu/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.amdcpu = lib.mkEnableOption "is this a host with amd cpu";
config = lib.mkIf config.swarselsystems.profiles.amdcpu {
swarselsystems.modules = {
optional = {
amdcpu = lib.mkDefault true;
};
};
};
}
#+end_src
***** AMD GPU
:PROPERTIES:
:CUSTOM_ID: h:79c71b6d-a1ad-447d-8940-bb5bfd71dced
:END:
#+begin_src nix :tangle profiles/nixos/amdgpu/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.amdgpu = lib.mkEnableOption "is this a host with amd gpu";
config = lib.mkIf config.swarselsystems.profiles.amdgpu {
swarselsystems.modules = {
optional = {
amdgpu = lib.mkDefault true;
};
};
};
}
#+end_src
***** Hibernation
:PROPERTIES:
:CUSTOM_ID: h:641d0a2a-0592-448a-a6e3-d0a9c330293e
:END:
#+begin_src nix :tangle profiles/nixos/hibernation/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.hibernation = lib.mkEnableOption "is this a host using hibernation";
config = lib.mkIf config.swarselsystems.profiles.hibernation {
swarselsystems.modules = {
optional = {
hibernation = lib.mkDefault true;
};
};
};
}
#+end_src
***** BTRFS
:PROPERTIES:
:CUSTOM_ID: h:0bb401e3-b195-4ff2-bc74-23c5a54d83d2
:END:
#+begin_src nix :tangle profiles/nixos/btrfs/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.btrfs = lib.mkEnableOption "is this a host using btrfs";
config = lib.mkIf config.swarselsystems.profiles.btrfs {
swarselsystems.modules = {
optional = {
btrfs = lib.mkDefault true;
};
};
};
}
#+end_src
***** Local Server
:PROPERTIES:
:CUSTOM_ID: h:dfc076fd-ee74-4663-b164-653370c52b75
:END:
#+begin_src nix :tangle profiles/nixos/localserver/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.server.local = lib.mkEnableOption "is this a local server";
config = lib.mkIf config.swarselsystems.profiles.server.local {
swarselsystems = {
modules = {
general = lib.mkDefault true;
pii = lib.mkDefault true;
home-manager = lib.mkDefault true;
home-managerExtra = lib.mkDefault true;
xserver = lib.mkDefault true;
time = lib.mkDefault true;
users = lib.mkDefault true;
server = {
general = lib.mkDefault true;
packages = lib.mkDefault true;
sops = lib.mkDefault true;
nfs = lib.mkDefault true;
nginx = lib.mkDefault true;
ssh = lib.mkDefault true;
kavita = lib.mkDefault true;
restic = lib.mkDefault true;
jellyfin = lib.mkDefault true;
navidrome = lib.mkDefault true;
spotifyd = lib.mkDefault true;
mpd = lib.mkDefault true;
postgresql = lib.mkDefault true;
matrix = lib.mkDefault true;
nextcloud = lib.mkDefault true;
immich = lib.mkDefault true;
paperless = lib.mkDefault true;
transmission = lib.mkDefault true;
syncthing = lib.mkDefault true;
monitoring = lib.mkDefault true;
emacs = lib.mkDefault true;
freshrss = lib.mkDefault true;
jenkins = lib.mkDefault false;
kanidm = lib.mkDefault true;
firefly = lib.mkDefault true;
koillection = lib.mkDefault true;
radicale = lib.mkDefault true;
atuin = lib.mkDefault true;
};
};
};
};
}
#+end_src
***** OCI Sync Server
:PROPERTIES:
:CUSTOM_ID: h:9b7b50d1-57ad-41ca-94ab-74393aae01bf
:END:
#+begin_src nix :tangle profiles/nixos/syncserver/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.server.sync = lib.mkEnableOption "is this a oci sync server";
config = lib.mkIf config.swarselsystems.profiles.server.sync {
swarselsystems = {
modules = {
general = lib.mkDefault true;
nix-ld = lib.mkDefault true;
pii = lib.mkDefault true;
home-manager = lib.mkDefault true;
home-managerExtra = lib.mkDefault true;
xserver = lib.mkDefault true;
time = lib.mkDefault true;
users = lib.mkDefault true;
server = {
general = lib.mkDefault true;
packages = lib.mkDefault true;
sops = lib.mkDefault true;
nginx = lib.mkDefault true;
ssh = lib.mkDefault true;
forgejo = lib.mkDefault true;
ankisync = lib.mkDefault true;
};
};
};
};
}
#+end_src
***** Moonside
:PROPERTIES:
:CUSTOM_ID: h:cc780ef2-7e5e-4835-b659-c731b306a320
:END:
#+begin_src nix :tangle profiles/nixos/moonside/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.server.moonside = lib.mkEnableOption "is this a moonside server";
config = lib.mkIf config.swarselsystems.profiles.server.moonside {
swarselsystems = {
modules = {
general = lib.mkDefault true;
pii = lib.mkDefault true;
home-manager = lib.mkDefault true;
home-managerExtra = lib.mkDefault true;
xserver = lib.mkDefault true;
time = lib.mkDefault true;
users = lib.mkDefault true;
impermanence = lib.mkDefault true;
server = {
general = lib.mkDefault true;
packages = lib.mkDefault true;
sops = lib.mkDefault true;
nginx = lib.mkDefault true;
ssh = lib.mkDefault true;
oauth2Proxy = lib.mkDefault true;
croc = lib.mkDefault true;
microbin = lib.mkDefault true;
shlink = lib.mkDefault true;
};
};
};
};
}
#+end_src
**** home-manager
:PROPERTIES:
:CUSTOM_ID: h:ced5841f-c088-4d88-b3a1-7d62aad8837b
:END:
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.
#+BEGIN_src nix :tangle profiles/home/default.nix
{ lib, ... }:
let
profileNames = lib.swarselsystems.readNix "profiles/home";
in
{
imports = lib.swarselsystems.mkImports profileNames "profiles/home";
}
#+end_src
***** Personal
:PROPERTIES:
:CUSTOM_ID: h:26512487-8c29-4b92-835b-d67394c3f5ef
:END:
#+begin_src nix :tangle profiles/home/personal/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.personal = lib.mkEnableOption "is this a personal host";
config = lib.mkIf config.swarselsystems.profiles.personal {
swarselsystems.modules = {
packages = lib.mkDefault true;
ownpackages = lib.mkDefault true;
general = lib.mkDefault true;
nixgl = lib.mkDefault true;
sops = lib.mkDefault true;
yubikey = lib.mkDefault true;
ssh = lib.mkDefault true;
stylix = lib.mkDefault true;
desktop = lib.mkDefault true;
symlink = lib.mkDefault true;
env = lib.mkDefault true;
programs = lib.mkDefault true;
nix-index = lib.mkDefault true;
passwordstore = lib.mkDefault true;
direnv = lib.mkDefault true;
eza = lib.mkDefault true;
atuin = lib.mkDefault true;
git = lib.mkDefault true;
fuzzel = lib.mkDefault true;
starship = lib.mkDefault true;
kitty = lib.mkDefault true;
zsh = lib.mkDefault true;
zellij = lib.mkDefault true;
tmux = lib.mkDefault true;
mail = lib.mkDefault true;
emacs = lib.mkDefault true;
waybar = lib.mkDefault true;
firefox = lib.mkDefault true;
gnome-keyring = lib.mkDefault true;
kdeconnect = lib.mkDefault true;
mako = lib.mkDefault true;
swayosd = lib.mkDefault true;
yubikeytouch = lib.mkDefault true;
sway = lib.mkDefault true;
kanshi = lib.mkDefault true;
gpgagent = lib.mkDefault true;
gammastep = lib.mkDefault true;
optional = {
gaming = lib.mkDefault true;
};
};
};
}
#+end_src
***** Chaostheatre
:PROPERTIES:
:CUSTOM_ID: h:36a0209f-2c17-4808-a1d0-a9e1920c307a
:END:
#+begin_src nix :tangle profiles/home/chaostheatre/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.chaostheatre = lib.mkEnableOption "is this a chaostheatre host";
config = lib.mkIf config.swarselsystems.profiles.chaostheatre {
swarselsystems.modules = {
packages = lib.mkDefault true;
ownpackages = lib.mkDefault true;
general = lib.mkDefault true;
nixgl = lib.mkDefault true;
sops = lib.mkDefault true;
yubikey = lib.mkDefault false;
ssh = lib.mkDefault true;
stylix = lib.mkDefault true;
desktop = lib.mkDefault true;
symlink = lib.mkDefault true;
env = lib.mkDefault false;
programs = lib.mkDefault true;
nix-index = lib.mkDefault true;
direnv = lib.mkDefault true;
eza = lib.mkDefault true;
git = lib.mkDefault false;
fuzzel = lib.mkDefault true;
starship = lib.mkDefault true;
kitty = lib.mkDefault true;
zsh = lib.mkDefault true;
zellij = lib.mkDefault true;
tmux = lib.mkDefault true;
mail = lib.mkDefault false;
emacs = lib.mkDefault true;
waybar = lib.mkDefault true;
firefox = lib.mkDefault true;
gnome-keyring = lib.mkDefault true;
kdeconnect = lib.mkDefault true;
mako = lib.mkDefault true;
swayosd = lib.mkDefault true;
yubikeytouch = lib.mkDefault true;
sway = lib.mkDefault true;
kanshi = lib.mkDefault true;
gpgagent = lib.mkDefault true;
gammastep = lib.mkDefault false;
};
};
}
#+end_src
***** toto
:PROPERTIES:
:CUSTOM_ID: h:e1d4f141-af11-448a-9796-fc822a8f77ec
:END:
#+begin_src nix :tangle profiles/home/toto/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.toto = lib.mkEnableOption "is this a toto (setup) host";
config = lib.mkIf config.swarselsystems.profiles.toto {
swarselsystems.modules = {
general = lib.mkDefault true;
sops = lib.mkDefault true;
ssh = lib.mkDefault true;
};
};
}
#+end_src
***** Work
:PROPERTIES:
:CUSTOM_ID: h:7b091523-a5b0-48b6-8b03-4dc2405e2d81
:END:
#+begin_src nix :tangle profiles/home/work/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.work = lib.mkEnableOption "is this a work host";
config = lib.mkIf config.swarselsystems.profiles.work {
swarselsystems.modules = {
optional = {
work = lib.mkDefault true;
};
};
};
}
#+end_src
***** Framework
:PROPERTIES:
:CUSTOM_ID: h:712b9d7f-16c0-42b3-b02b-6d79ee15cfcc
:END:
#+begin_src nix :tangle profiles/home/framework/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.framework = lib.mkEnableOption "is this a framework brand host";
config = lib.mkIf config.swarselsystems.profiles.framework {
swarselsystems.modules = {
optional = {
framework = lib.mkDefault true;
};
};
};
}
#+end_src
***** Darwin
:PROPERTIES:
:CUSTOM_ID: h:24e6d661-f498-478c-9008-e8d8c17432ca
:END:
#+begin_src nix :tangle profiles/home/darwin/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.darwin = lib.mkEnableOption "is this a darwin host";
config = lib.mkIf config.swarselsystems.profiles.darwin {
swarselsystems.modules = {
general = lib.mkDefault true;
};
};
}
#+end_src
***** Local Server
:PROPERTIES:
:CUSTOM_ID: h:8027b858-369e-4f12-bbaf-f15eeee3d904
:END:
#+begin_src nix :tangle profiles/home/localserver/default.nix :mkdirp yes
{ lib, config, ... }:
{
options.swarselsystems.profiles.server.local = lib.mkEnableOption "is this a local server";
config = lib.mkIf config.swarselsystems.profiles.server.local {
swarselsystems.modules = {
general = lib.mkDefault true;
server = {
dotfiles = lib.mkDefault true;
};
};
};
}
#+end_src
*** Library functions
:PROPERTIES:
:CUSTOM_ID: h:4d38c9f7-2680-4c02-a1f4-ed8db0d55ce4
:END:
This section defines all functions of my own that I add to =lib=. These are used in all places over the config, with many of them being used in =flake.nix=.
A breakdown of each function:
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.
- =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 [[#h:9c9b9e3b-8771-44fa-ba9e-5056ae809655][nixosConfigurations]] or [[#h:f881aa05-a670-48dd-a57b-2916abdcb692][darwinConfigurations]].
- =mkFullHost=:
This 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). This is used in mkFullHostConfigs. In more detail, it dynamically creates a nixosConfiguration host, setting its =speciaArgs= and =modules= attributes. The modules are populated based on whether this is a NixOS or darwin host. For the latter, I will only ever use machines that I get for testing from work, and for these my username is different, so I implemented an if-condition for it. This could be done more cleanly using variables, but some care needs to be taken with the home-manager imports and this approach works, so for now this is fine. Thanks to this function, the import sections of the host configs are pretty clean for most hosts.
=lib.optionals= evaluates to an empty list (=[]=) in case that the conditional is not met.
TODO
#+begin_src nix :tangle lib/default.nix
{ self, lib, systems, inputs, ... }:
{
mkIfElseList = p: yes: no: lib.mkMerge [
(lib.mkIf p yes)
(lib.mkIf (!p) no)
];
mkIfElse = p: yes: no: if p then yes else no;
forAllSystems = lib.genAttrs [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
pkgsFor = lib.genAttrs (import systems) (system:
import inputs.nixpkgs {
inherit system;
overlays = [ self.overlays.default ];
config.allowUnfree = true;
}
);
# mkUser = name: {
# config.users.users.${name} = {
# group = name;
# isSystemUser = true;
# };
# config.users.groups.${name} = {};
# };
mkTrueOption = lib.mkOption {
type = lib.types.bool;
default = true;
};
mkStrong = lib.mkOverride 60;
getSecret = filename: lib.strings.trim (builtins.readFile "${filename}");
forEachSystem = f: lib.genAttrs (import systems) (system: f lib.swarselsystems.pkgsFor.${system});
readHosts = type: lib.attrNames (builtins.readDir "${self}/hosts/${type}");
readNix = type: lib.filter (name: name != "default.nix") (lib.attrNames (builtins.readDir "${self}/${type}"));
mkApps = system: names: self: builtins.listToAttrs (map
(name: {
inherit name;
value = {
type = "app";
program = "${self.packages.${system}.${name}}/bin/${name}";
meta = {
description = "Custom app ${name}.";
};
};
})
names);
mkPackages = names: pkgs: builtins.listToAttrs (map
(name: {
inherit name;
value = pkgs.callPackage "${self}/pkgs/${name}" { inherit self name; };
})
names);
mkModules = names: type: builtins.listToAttrs (map
(name: {
inherit name;
value = import "${self}/modules/${type}/${name}";
})
names);
mkProfiles = names: type: builtins.listToAttrs (map
(name: {
inherit name;
value = import "${self}/profiles/${type}/${name}";
})
names);
mkTemplates = names: builtins.listToAttrs (map
(name: {
inherit name;
value = {
path = "${self}/templates/${name}";
description = "${name} project ";
};
})
names);
mkImports = names: baseDir: lib.map (name: "${self}/${baseDir}/${name}") names;
eachMonitor = _: monitor: {
inherit (monitor) name;
value = builtins.removeAttrs monitor [ "workspace" "name" "output" ];
};
eachOutput = _: monitor: {
inherit (monitor) name;
value = builtins.removeAttrs monitor [ "mode" "name" "scale" "transform" "position" ];
};
}
#+end_src
*** Auxiliary files
:PROPERTIES:
:CUSTOM_ID: h:23602ad9-91f6-4eba-943a-2308070fbaec
:END:
**** extra-builtins
:PROPERTIES:
:CUSTOM_ID: h:87c7893e-e946-4fc0-8973-1ca27d15cf0e
:END:
#+begin_src nix :tangle nix/extra-builtins.nix
# adapted from https://github.com/oddlama/nix-config/blob/main/nix/extra-builtins.nix
{ exec, ... }:
let
assertMsg = pred: msg: pred || builtins.throw msg;
hasSuffix =
suffix: content:
let
lenContent = builtins.stringLength content;
lenSuffix = builtins.stringLength suffix;
in
lenContent >= lenSuffix && builtins.substring (lenContent - lenSuffix) lenContent content == suffix;
in
{
# Instead of calling sops directly here, we call a wrapper script that will cache the output
# in a predictable path in /tmp, which allows us to only require the password for each encrypted
# file once.
sopsImportEncrypted =
nixFile:
assert assertMsg (builtins.isPath nixFile)
"The file to decrypt must be given as a path (not a string) to prevent impurity.";
assert assertMsg (hasSuffix ".nix.enc" nixFile)
"The content of the decrypted file must be a nix expression and should therefore end in .nix.enc";
exec [
./sops-decrypt-and-cache.sh
nixFile
];
}
#+end_src
**** sops-decrypt-and-cache
:PROPERTIES:
:CUSTOM_ID: h:315e6ef6-27d5-4cd8-85ff-053eabe60ddb
:END:
#+begin_src shell :tangle nix/sops-decrypt-and-cache.sh
#!/usr/bin/env bash
# adapted from https://github.com/oddlama/nix-config/blob/main/nix/rage-decrypt-and-cache.sh
set -euo pipefail
print_out_path=false
if [[ $1 == "--print-out-path" ]]; then
print_out_path=true
shift
fi
file="$1"
shift
basename="${file%".enc"}"
# store path prefix or ./ if applicable
[[ $file == "/nix/store/"* ]] && basename="${basename#*"-"}"
[[ $file == "./"* ]] && basename="${basename#"./"}"
# Calculate a unique content-based identifier (relocations of
# the source file in the nix store should not affect caching)
new_name="$(sha512sum "$file")"
new_name="${new_name:0:32}-${basename//"/"/"%"}"
# Derive the path where the decrypted file will be stored
out="/var/tmp/nix-import-encrypted/$UID/$new_name"
umask 077
mkdir -p "$(dirname "$out")"
# Decrypt only if necessary
if [[ ! -e $out ]]; then
agekey=$(sudo ssh-to-age -private-key -i /etc/ssh/sops || sudo ssh-to-age -private-key -i /etc/ssh/ssh_host_ed25519_key)
SOPS_AGE_KEY="$agekey" sops decrypt --output "$out" "$file"
fi
# Print out path or decrypted content
if [[ $print_out_path == true ]]; then
echo "$out"
else
cat "$out"
fi
#+end_src
**** nix-topology
:PROPERTIES:
:CUSTOM_ID: h:46458265-074e-4368-ad9a-055877754914
:END:
#+begin_src nix :tangle topology/default.nix
{ config, ... }:
let
inherit (config.lib.topology)
mkInternet
mkDevice
mkSwitch
mkRouter
mkConnection
;
in
{
renderer = "elk";
networks = {
home-lan = {
name = "Home LAN";
cidrv4 = "192.168.1.0/24";
};
wg = {
name = "Wireguard Tunnel";
cidrv4 = "192.168.3.0/24";
};
};
nodes = {
internet = mkInternet {
connections = [
(mkConnection "moonside" "wan")
(mkConnection "pfsense" "wan")
(mkConnection "sync" "wan")
];
};
sync.interfaces.wan = { };
moonside.interfaces.wan = { };
pfsense = mkRouter "pfSense" {
info = "HUNSN RM02";
image = ../topology/images/hunsn.png;
interfaceGroups = [
[
"eth2"
"eth3"
"eth4"
"eth5"
"eth6"
]
[ "wan" ]
];
interfaces.wg0 = {
addresses = [ "192.168.3.1" ];
network = "wg";
virtual = true;
type = "wireguard";
};
connections = {
eth2 = mkConnection "switch-livingroom" "eth1";
eth4 = mkConnection "winters" "eth1";
eth3 = mkConnection "switch-bedroom" "eth1";
eth6 = mkConnection "wifi-ap" "eth1";
wg = mkConnection "moonside" "wg";
};
interfaces = {
eth2 = {
addresses = [ "192.168.1.1" ];
network = "home-lan";
};
eth3 = {
addresses = [ "192.168.1.1" ];
network = "home-lan";
};
eth4 = {
addresses = [ "192.168.1.1" ];
network = "home-lan";
};
eth6 = {
addresses = [ "192.168.1.1" ];
network = "home-lan";
};
};
};
winters.interfaces."eth1" = { };
wifi-ap = mkSwitch "Wi-Fi AP" {
info = "Huawei";
image = ../topology/images/huawei.png;
interfaceGroups = [
[
"eth1"
"wifi"
]
];
};
switch-livingroom = mkSwitch "Switch Livingroom" {
info = "TL-SG108";
image = ../topology/images/TL-SG108.png;
interfaceGroups = [
[
"eth1"
"eth2"
"eth3"
"eth4"
"eth5"
"eth6"
"eth7"
"eth8"
]
];
connections = {
eth2 = mkConnection "nswitch" "eth1";
eth7 = mkConnection "pc" "eth1";
eth8 = mkConnection "nbl-imba-2" "eth1";
};
};
nswitch = mkDevice "Nintendo Switch" {
info = "Nintendo Switch";
image = ../topology/images/nintendo-switch.png;
interfaces.eth1 = { };
};
pc = mkDevice "Windows Gaming Server" {
info = "i7-4790k, GTX970, 32GB RAM";
image = ../topology/images/pc.png;
interfaces.eth1 = { };
};
nbl-imba-2.interfaces.eth1 = { };
switch-bedroom = mkSwitch "Switch Bedroom" {
info = "TL-SG1005D";
image = ../topology/images/TL-SG1005D.png;
interfaceGroups = [
[
"eth1"
"eth2"
"eth3"
"eth4"
"eth5"
]
];
connections.eth2 = mkConnection "printer" "eth1";
};
printer = mkDevice "Printer" {
info = "DELL C2665dnf";
image = ../topology/images/DELL-C2665dnf.png;
interfaces.eth1 = { };
};
};
}
#+end_src
**** Globals
#+begin_src nix :tangle nix/globals.nix
{ inputs, ... }:
{
flake =
{
config,
lib,
...
}:
{
globals =
let
globalsSystem = lib.evalModules {
prefix = [ "globals" ];
specialArgs = {
inherit lib;
inherit inputs;
inherit (config) nodes;
};
modules = [
../modules/nixos/common/globals.nix
(
{ lib, ... }:
{
globals = lib.mkMerge (
lib.concatLists (
lib.flip lib.mapAttrsToList config.nodes (
name: cfg:
builtins.addErrorContext "while aggregating globals from nixosConfigurations.${name} into flake-level globals:" cfg.config._globalsDefs
)
)
);
}
)
];
};
in
{
# Make sure the keys of this attrset are trivially evaluatable to avoid infinite recursion,
# therefore we inherit relevant attributes from the config.
inherit (globalsSystem.config.globals)
domains
services
macs
myuser
root
;
};
};
}
#+end_src
** NixOS
:PROPERTIES:
:CUSTOM_ID: h:6da812f5-358c-49cb-aff2-0a94f20d70b3
:END:
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
:PROPERTIES:
:CUSTOM_ID: h:1c1250cd-e9b4-4715-8d9f-eb09e64bfc7f
:END:
These are system-level settings specific to NixOS machines. All settings that are required on all machines go here.
**** Imports, non-server settings
:PROPERTIES:
:CUSTOM_ID: h:4acbe063-188b-42e7-b75c-b6d2e232e784
:END:
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.
#+begin_src nix :tangle modules/nixos/common/default.nix
{ self, lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/nixos/common";
modulesPath = "${self}/modules";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/nixos/common" ++ [
"${modulesPath}/home/common/sharedsetup.nix"
];
}
#+end_src
**** Shared Configuration Options
:PROPERTIES:
:CUSTOM_ID: h:f4f22166-e345-43e6-b15f-b7f5bb886554
:END:
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.
#+begin_src nix :tangle modules/nixos/common/sharedsetup.nix
{ lib, ... }:
{
options = {
swarselsystems = {
withHomeManager = lib.mkOption {
type = lib.types.bool;
default = true;
};
isSwap = lib.mkOption {
type = lib.types.bool;
default = true;
};
swapSize = lib.mkOption {
type = lib.types.str;
default = "8G";
};
rootDisk = lib.mkOption {
type = lib.types.str;
default = "";
};
isCrypted = lib.mkEnableOption "uses full disk encryption";
initialSetup = lib.mkEnableOption "initial setup (no sops keys available)";
isImpermanence = lib.mkEnableOption "use impermanence on this system";
isSecureBoot = lib.mkEnableOption "use secure boot on this system";
};
};
}
#+end_src
**** General NixOS settings (stateVersion)
:PROPERTIES:
:CUSTOM_ID: h:24c9146f-2147-4fd5-bafc-d5853e15cf12
:END:
We disable the warnings that trigger when rebuilding with a dirty flake. At this point, I am also disabling channels and pinning the flake registry - the latter lets me use the local version of nixpkgs for commands like =nix shell= (without it, we will always download the newest version of nixpkgs for these commands).
Also, the system state version is set here. No need to touch it.
A breakdown of the flags being set:
- =nixpgks.config.allowUnfree=: allows packages with an unfree license to be built
- nix.settings:
- experimental-features:
- nix-command: Enables the =nix= command from nix 2.4
- flakes: Enables flakes to be used
- ca-derivations: Enables content-addressed derivations, which stops unnecessary rebuiluds - to be used with my TODO private hydra and the binary cache =cache.ngi0.nixos.org= in [[#h:aee5ec75-7ca6-40d8-b6ac-a3e7e33a474b][flake.nix template]]
- cgroups: allows the use of cgroups in builds
- pipe-operators: Enables 'piping' instead of the classic currying syntax - =fun arg= can be expressed as =arg |> fun=. Associatively, it is weaker than functions: =a |> b |> d c |> e = e ((d c) (b a))=
- trusted-users: these users have elevated privileges in nix (mostly used to acknowledge binary caches) - root is added per default here
- connect-timeout: normally, nix tries to reach the cache for 300 seconds for each derivation per cache. This setting lets me change that
- bash-prompt-prefix: adds a prefix to shells spawned by =nix develop=
- [min,max]-free: amounts of space where intermittent GC will be run during builds
- flake registry: URI of the global flake registry (I disable it)
- auto-optimise-store: create hardlinks in the nix store to save space
- warn-dirty: I do not need to see the warning when I have uncommited changes
- max-jobs: How many build jobs should be run in parallel. =auto= sets this to the number of CPUs (which is all) - on systems with many cores this can lead to OOM situations. The default is now =1=, but used to be =auto=, I set this manually just to be safe in the future.
- use-cgroups: Actually run builds within cgroups
- nix.channel.enable: whether to use channels
- nix.registry: Sets the registry for this flake, which I set to its inputs. This allows me to use e.g. =nixpkgs= directly in =nix repl=
- nix.nixPath: Basically the same as =nix.registry=, but for the legacy nix commands
#+begin_src nix :tangle modules/nixos/common/settings.nix
{ lib, pkgs, config, outputs, inputs, ... }:
{
options.swarselsystems.modules.general = lib.mkEnableOption "general nix settings";
config = lib.mkIf config.swarselsystems.modules.general {
nixpkgs = {
overlays = [ outputs.overlays.default ];
config = {
allowUnfree = true;
};
};
environment.etc."nixos/configuration.nix".source = pkgs.writeText "configuration.nix" ''
assert builtins.trace "This location is not used. The config is found in ${config.swarselsystems.flakePath}!" false;
{ }
'';
nix =
let
flakeInputs = lib.filterAttrs (_: lib.isType "flake") inputs;
in
{
settings = {
experimental-features = [
"nix-command"
"flakes"
"ca-derivations"
"cgroups"
"pipe-operators"
];
trusted-users = [ "@wheel" "${config.swarselsystems.mainUser}" ];
connect-timeout = 5;
bash-prompt-prefix = "[33m$SHLVL:\\w [0m";
bash-prompt = "$(if [[ $? -gt 0 ]]; then printf \"[31m\"; else printf \"[32m\"; fi)\[\e[1m\]λ\[\e[0m\] [0m";
fallback = true;
min-free = 128000000;
max-free = 1000000000;
flake-registry = "";
auto-optimise-store = true;
warn-dirty = false;
max-jobs = 1;
use-cgroups = lib.mkIf config.swarselsystems.isLinux true;
};
gc = {
automatic = true;
dates = "weekly";
options = "--delete-older-than 10d";
};
optimise = {
automatic = true;
dates = "weekly";
};
channel.enable = false;
registry = rec {
nixpkgs.flake = inputs.nixpkgs;
p = nixpkgs;
};
nixPath = lib.mapAttrsToList (n: _: "${n}=flake:${n}") flakeInputs;
};
services.dbus.implementation = "broker";
system.stateVersion = lib.mkDefault "23.05";
};
}
#+end_src
**** Share configuration between nodes
:PROPERTIES:
:CUSTOM_ID: h:5c3027b4-ba66-445e-9c5f-c27e332c90e5
:END:
#+begin_src nix :tangle modules/nixos/common/nodes.nix
# adapted from https://github.com/oddlama/nix-config/blob/main/modules/distributed-config.nix
{ config, lib, outputs, ... }:
let
nodeName = config.node.name;
mkForwardedOption =
path:
lib.mkOption {
type = lib.mkOptionType {
name = "Same type that the receiving option `${lib.concatStringsSep "." path}` normally accepts.";
merge =
_loc: defs:
builtins.filter (x: builtins.isAttrs x -> ((x._type or "") != "__distributed_config_empty")) (
map (x: x.value) defs
);
};
default = {
_type = "__distributed_config_empty";
};
description = ''
Anything specified here will be forwarded to `${lib.concatStringsSep "." path}`
on the given node. Forwarding happens as-is to the raw values,
so validity can only be checked on the receiving node.
'';
};
forwardedOptions = [
[
"services"
"nginx"
"upstreams"
]
[
"services"
"nginx"
"virtualHosts"
]
];
attrsForEachOption =
f: lib.foldl' (acc: path: lib.recursiveUpdate acc (lib.setAttrByPath path (f path))) { } forwardedOptions;
in
{
options.nodes = lib.mkOption {
description = "Options forwarded to the given node.";
default = { };
type = lib.types.attrsOf (
lib.types.submodule {
options = attrsForEachOption mkForwardedOption;
}
);
};
config =
let
getConfig =
path: otherNode:
let
cfg = outputs.nixosConfigurations.${otherNode}.config.nodes.${nodeName} or null;
in
lib.optionals (cfg != null) (lib.getAttrFromPath path cfg);
mergeConfigFromOthers = path: lib.mkMerge (lib.concatMap (getConfig path) (lib.attrNames outputs.nixosConfigurations));
in
attrsForEachOption mergeConfigFromOthers;
}
#+end_src
**** Global options
#+begin_src nix :tangle modules/nixos/common/globals.nix
{ lib, options, ... }:
let
inherit (lib)
mkOption
types
;
in
{
options = {
globals = mkOption {
default = { };
type = types.submodule {
options = {
root = {
hashedPassword = mkOption {
type = types.str;
description = "My root user's password hash.";
};
};
myuser = {
name = mkOption {
type = types.str;
description = "My unix username.";
};
hashedPassword = mkOption {
type = types.str;
description = "My unix password hash.";
};
};
services = mkOption {
type = types.attrsOf (
types.submodule {
options = {
domain = mkOption {
type = types.str;
description = "The domain under which this service can be reached";
};
};
}
);
};
domains = {
me = mkOption {
type = types.str;
description = "My main domain.";
};
personal = mkOption {
type = types.str;
description = "My personal domain.";
};
};
macs = mkOption {
default = { };
type = types.attrsOf types.str;
description = "Known MAC addresses for external devices.";
};
};
};
};
_globalsDefs = mkOption {
type = types.unspecified;
default = options.globals.definitions;
readOnly = true;
internal = true;
};
};
}
#+end_src
**** System Packages
:PROPERTIES:
:CUSTOM_ID: h:0e7e8bea-ec58-499c-9731-09dddfc39532
:END:
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 [[#h:893a7f33-7715-415b-a895-2687ded31c18][Installed packages]].
#+begin_src nix :tangle modules/nixos/common/packages.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.packages = lib.mkEnableOption "install packages";
config = lib.mkIf config.swarselsystems.modules.packages {
environment.systemPackages = with pkgs; [
# yubikey packages
gnupg
yubikey-personalization
yubico-pam
yubioath-flutter
yubikey-manager
yubikey-touch-detector
yubico-piv-tool
cfssl
pcsctools
pcscliteWithPolkit.out
# ledger packages
ledger-live-desktop
# pinentry
dbus
swaylock-effects
syncthingtray-minimal
wl-mirror
swayosd
# secure boot
sbctl
libsForQt5.qt5.qtwayland
# nix package database
nix-index
nixos-generators
# commit hooks
pre-commit
# proc info
acpi
# pci info
pciutils
usbutils
# better make for general tasks
just
screenshare
fullscreen
# keyboards
qmk
vial
via
# theme related
adwaita-icon-theme
# kde-connect
xdg-desktop-portal
xdg-desktop-portal-wlr
# bluetooth
bluez
ghostscript_headless
wireguard-tools
nixd
zig
zls
ansible-language-server
elk-to-svg
];
nixpkgs.config.permittedInsecurePackages = [
"jitsi-meet-1.0.8043"
"electron-29.4.6"
"SDL_ttf-2.0.11"
];
};
}
#+end_src
**** Setup home-manager base
:PROPERTIES:
:CUSTOM_ID: h:7f6d6908-4d02-4907-9c70-f802f4358520
:END:
We enable the use of =home-manager= as a NixoS module. A nice trick here is the =extraSpecialArgs = inputs= line, which enables the use of =seflf= in most parts of the configuration. This is useful to refer to the root of the flake (which is otherwise quite hard while maintaining flake purity).
#+begin_src nix :tangle modules/nixos/common/home-manager.nix
{ inputs, config, lib, ... }:
{
options.swarselsystems.modules.home-manager = lib.mkEnableOption "home-manager";
config = lib.mkIf config.swarselsystems.modules.home-manager {
home-manager = lib.mkIf config.swarselsystems.withHomeManager {
useGlobalPkgs = true;
useUserPackages = true;
verbose = true;
sharedModules = [
inputs.nix-index-database.hmModules.nix-index
inputs.sops-nix.homeManagerModules.sops
{
home.stateVersion = lib.mkDefault config.system.stateVersion;
}
];
extraSpecialArgs = { inherit (inputs) self; };
};
};
}
#+end_src
**** Setup home-manager specialArgs
:PROPERTIES:
:CUSTOM_ID: h:41d1b7c6-52bf-45f3-9d83-610b469dffc0
:END:
This sets up the =nix-secrets= extraSpeciaArgs. This should not be present on the =chaostheatre= configuration, which is why I split this section into its own file, which makes removal easier when setting that system up.
#+begin_src nix :tangle modules/nixos/common/home-manager-extra.nix
{ inputs, config, lib, ... }:
{
options.swarselsystems.modules.home-managerExtra = lib.mkEnableOption "home-manager extras for non-chaostheatre";
config = lib.mkIf config.swarselsystems.modules.home-managerExtra {
home-manager = lib.mkIf config.swarselsystems.withHomeManager {
extraSpecialArgs = { inherit (inputs) nix-secrets nixgl; };
};
};
}
#+end_src
**** Setup login keymap
:PROPERTIES:
:CUSTOM_ID: h:7248f338-8cad-4443-9060-deae7955b26f
:END:
Next, we setup the keymap in case we are not in a graphical session. At this point, I always resort to us/altgr-intl, as it is comfortable to use and I do not write too much German anyways.
#+begin_src nix :tangle modules/nixos/common/xserver.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.xserver = lib.mkEnableOption "xserver keymap";
config = lib.mkIf config.swarselsystems.modules.packages {
services.xserver = {
xkb = {
layout = "us";
variant = "altgr-intl";
};
};
};
}
#+end_src
**** User setup, Make users non-mutable
:PROPERTIES:
:CUSTOM_ID: h:48959890-fbc7-4d28-b33c-f33e028ab473
:END:
This ensures that all user-configuration happens here in the config file.
In case of using a fully setup system, this makes also sure that no further user level modifications can be made using CLI utilities (e.g. usermod etc.). Everything must be defined in the flake.
For that reason, make sure that =sops-nix= is properly working before setting the =initialSetup= flag, otherwise you might lose user access.
#+begin_src nix :tangle modules/nixos/common/users.nix
{ self, pkgs, config, lib, ... }:
let
sopsFile = self + /secrets/general/secrets.yaml;
in
{
options.swarselsystems.modules.users = lib.mkEnableOption "user config";
config = lib.mkIf config.swarselsystems.modules.users {
sops.secrets.swarseluser = lib.mkIf (!config.swarselsystems.isPublic) { inherit sopsFile; neededForUsers = true; };
users = {
mutableUsers = lib.mkIf (!config.swarselsystems.initialSetup) false;
users."${config.swarselsystems.mainUser}" = {
isNormalUser = true;
description = "Leon S";
password = lib.mkIf config.swarselsystems.initialSetup "setup";
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; [ ];
};
};
};
}
#+end_src
**** Environment setup
:PROPERTIES:
:CUSTOM_ID: h:f4006367-0965-4b4f-a3b0-45f63b07d2b8
:END:
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).
#+begin_src nix :tangle modules/nixos/common/env.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.env = lib.mkEnableOption "environment config";
config = lib.mkIf config.swarselsystems.modules.env {
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
]);
};
};
};
}
#+end_src
**** Security
:PROPERTIES:
:CUSTOM_ID: h:e2d40df9-0026-4caa-8476-9dc2353055a1
:END:
Needed for control over system-wide privileges etc. Also I make sure that the root user has access to =SSH_AUTH_SOCK= (without this, root will not be able to read my =nix-secrets= repository).
#+begin_src nix :tangle modules/nixos/common/polkit.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.security = lib.mkEnableOption "security config";
config = lib.mkIf config.swarselsystems.modules.security {
security = {
pam.services = {
login.u2fAuth = true;
sudo.u2fAuth = true;
swaylock.u2fAuth = true;
swaylock.fprintAuth = false;
};
polkit.enable = true;
sudo.extraConfig = ''
Defaults env_keep+=SSH_AUTH_SOCK
Defaults env_keep+=XDG_RUNTIME_DIR
Defaults env_keep+=WAYLAND_DISPLAY
'';
};
};
}
#+end_src
**** Reduce systemd timeouts
:PROPERTIES:
:CUSTOM_ID: h:12858442-c129-4aa1-9c9c-a0916e36b302
:END:
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.
#+begin_src nix :tangle modules/nixos/common/systemd.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.systemdTimeout = lib.mkEnableOption "systemd timeout config";
config = lib.mkIf config.swarselsystems.modules.systemdTimeout {
# systemd
systemd.extraConfig = ''
DefaultTimeoutStartSec=60s
DefaultTimeoutStopSec=15s
'';
};
}
#+end_src
**** Hardware settings
:PROPERTIES:
:CUSTOM_ID: h:1fa7cf61-5c03-43a3-a7f0-3d6ee246b31b
:END:
Enable OpenGL, Sound, Bluetooth and various drivers.
#+begin_src nix :tangle modules/nixos/common/hardware.nix
{ pkgs, config, lib, ... }:
{
options.swarselsystems = {
modules.hardware = lib.mkEnableOption "hardware config";
hasBluetooth = lib.mkEnableOption "bluetooth availability";
hasFingerprint = lib.mkEnableOption "fingerprint sensor availability";
trackpoint = {
isAvailable = lib.mkEnableOption "trackpoint availability";
trackpoint.device = lib.mkOption {
type = lib.types.str;
default = "";
};
};
};
config = lib.mkIf config.swarselsystems.modules.hardware {
hardware = {
# opengl.driSupport32Bit = true is replaced with graphics.enable32Bit and hence redundant
graphics = {
enable = true;
enable32Bit = true;
};
trackpoint = lib.mkIf config.swarselsystems.trackpoint.isAvailable {
enable = true;
inherit (config.swarselsystems.trackpoint) device;
};
keyboard.qmk.enable = true;
enableAllFirmware = lib.mkDefault true;
bluetooth = lib.mkIf config.swarselsystems.hasBluetooth {
enable = true;
package = pkgs.stable.bluez;
powerOnBoot = true;
settings = {
General = {
Enable = "Source,Sink,Media,Socket";
};
};
};
};
services.fprintd.enable = lib.mkIf config.swarselsystems.hasFingerprint true;
};
}
#+end_src
**** Pulseaudio
:PROPERTIES:
:CUSTOM_ID: h:63f6773e-b321-4b1d-a206-3913658cf62d
:END:
This is only used on systems not running Pipewire.
#+begin_src nix :tangle modules/nixos/common/pulseaudio.nix
{ config, pkgs, lib, ... }: {
options.swarselsystems.modules.pulseaudio = lib.mkEnableOption "pulseaudio config";
config = lib.mkIf config.swarselsystems.modules.pulseaudio {
services.pulseaudio = {
enable = lib.mkIf (!config.services.pipewire.enable) true;
package = pkgs.pulseaudioFull;
};
};
}
#+end_src
**** Pipewire
:PROPERTIES:
:CUSTOM_ID: h:aa433f5e-a455-4414-b76b-0a2692fa06aa
:END:
Pipewire handles communication on Wayland. This enables several sound tools as well as screen sharing in combinaton with =xdg-desktop-portal-wlr=.
#+begin_src nix :tangle modules/nixos/common/pipewire.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.pipewire = lib.mkEnableOption "pipewire config";
config = lib.mkIf config.swarselsystems.modules.pipewire {
security.rtkit.enable = true; # this is required for pipewire real-time access
services.pipewire = {
enable = true;
package = pkgs.stable.pipewire;
pulse.enable = true;
jack.enable = true;
audio.enable = true;
wireplumber.enable = true;
alsa = {
enable = true;
support32Bit = true;
};
};
};
}
#+end_src
**** Common network settings
:PROPERTIES:
:CUSTOM_ID: h:7d696b64-debe-4a95-80b5-1e510156a6c6
:END:
Here I only enable =networkmanager= and a few default networks. The rest of the network config is done separately in [[#h:88bf4b90-e94b-46fb-aaf1-a381a512860d][System specific configuration]].
#+begin_src nix :tangle modules/nixos/common/network.nix
{ lib, config, ... }:
{
options.swarselsystems = {
modules.network = lib.mkEnableOption "network config";
firewall = lib.swarselsystems.mkTrueOption;
};
config = lib.mkIf config.swarselsystems.modules.network {
networking = {
nftables.enable = lib.mkDefault true;
enableIPv6 = lib.mkDefault true;
firewall = {
enable = lib.swarselsystems.mkStrong config.swarselsystems.firewall;
checkReversePath = lib.mkDefault false;
allowedUDPPorts = [ 51820 ]; # 51820: wireguard
allowedTCPPortRanges = [
{ from = 1714; to = 1764; } # kde-connect
];
allowedUDPPortRanges = [
{ from = 1714; to = 1764; } # kde-connect
];
};
networkmanager = {
enable = true;
ensureProfiles = lib.mkIf (!config.swarselsystems.isPublic) {
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.sops.secrets."sweden-aes-128-cbc-udp-dns-ca.pem".path;
challenge-response-flags = "2";
cipher = "aes-128-cbc";
compress = "yes";
connection-type = "password";
crl-verify-file = config.sops.secrets."sweden-aes-128-cbc-udp-dns-crl-verify.pem".path;
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-${config.swarselsystems.mainUser}";
};
wifi-security = {
group = "ccmp;";
key-mgmt = "wpa-psk";
pairwise = "ccmp;";
proto = "rsn;";
psk = "$HOTSPOT";
};
};
};
};
};
};
systemd.services.NetworkManager-ensure-profiles.after = [ "NetworkManager.service" ];
};
}
#+end_src
**** Time, locale settings
:PROPERTIES:
:CUSTOM_ID: h:852d59ab-63c3-4831-993d-b5e23b877796
:END:
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.
#+begin_src nix :tangle modules/nixos/common/time.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.time = lib.mkEnableOption "time config";
config = lib.mkIf config.swarselsystems.modules.time {
time = {
timeZone = "Europe/Vienna";
# hardwareClockInLocalTime = true;
};
i18n = {
defaultLocale = "en_US.UTF-8";
extraLocaleSettings = {
LC_ADDRESS = "de_AT.UTF-8";
LC_IDENTIFICATION = "de_AT.UTF-8";
LC_MEASUREMENT = "de_AT.UTF-8";
LC_MONETARY = "de_AT.UTF-8";
LC_NAME = "de_AT.UTF-8";
LC_NUMERIC = "de_AT.UTF-8";
LC_PAPER = "de_AT.UTF-8";
LC_TELEPHONE = "de_AT.UTF-8";
LC_TIME = "de_AT.UTF-8";
};
};
};
}
#+end_src
**** Meta options
:PROPERTIES:
:CUSTOM_ID: h:30b81bf9-1e69-4ce8-88af-5592896bcee4
:END:
#+begin_src nix :tangle modules/nixos/common/meta.nix
{ lib, ... }:
{
options = {
node = {
secretsDir = lib.mkOption {
description = "Path to the secrets directory for this node.";
type = lib.types.path;
default = ./.;
};
name = lib.mkOption {
description = "Node Name.";
type = lib.types.str;
};
};
};
}
#+end_src
**** Topology
:PROPERTIES:
:CUSTOM_ID: h:e2e7444b-cb85-4719-b154-e5f37274d02d
:END:
#+begin_src nix :tangle modules/nixos/common/topology.nix
{ self, lib, config, ... }:
{
options.swarselsystems.info = lib.mkOption {
type = lib.types.str;
default = "";
};
config.topology = {
id = config.node.name;
self = {
hardware.info = config.swarselsystems.info;
icon = lib.mkIf config.swarselsystems.isLaptop "devices.laptop";
};
};
}
#+end_src
**** sops
:PROPERTIES:
:CUSTOM_ID: h:d87d80fd-2ac7-4f29-b338-0518d06b4deb
:END:
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
#+begin_src nix :tangle modules/nixos/common/sops.nix
{ self, config, lib, ... }:
let
certsSopsFile = self + /secrets/certs/secrets.yaml;
inherit (config.swarselsystems) mainUser homeDir;
in
{
options.swarselsystems.modules.commonSops = lib.mkEnableOption "sops config";
config = lib.mkIf config.swarselsystems.modules.commonSops {
sops = lib.mkIf (!config.swarselsystems.isPublic) {
age.sshKeyPaths = lib.swarselsystems.mkIfElseList config.swarselsystems.isBtrfs [ "/persist/.ssh/sops" "/persist/.ssh/ssh_host_ed25519_key" ] [ "${homeDir}/.ssh/sops" "/etc/ssh/ssh_host_ed25519_key" ];
defaultSopsFile = lib.swarselsystems.mkIfElseList config.swarselsystems.isBtrfs "/persist/.dotfiles/secrets/general/secrets.yaml" "${homeDir}/.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 = { };
"sweden-aes-128-cbc-udp-dns-crl-verify.pem" = { sopsFile = certsSopsFile; owner = mainUser; };
"sweden-aes-128-cbc-udp-dns-ca.pem" = { sopsFile = certsSopsFile; owner = mainUser; };
};
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}
'';
};
};
};
}
#+end_src
**** PII management
:PROPERTIES:
:CUSTOM_ID: h:82b8ede2-02d8-4c43-8952-7200ebd4dc23
:END:
#+begin_src nix :tangle modules/nixos/common/pii.nix
# largely based on https://github.com/oddlama/nix-config/blob/main/modules/secrets.nix
{ config, inputs, lib, ... }:
let
# If the given expression is a bare set, it will be wrapped in a function,
# so that the imported file can always be applied to the inputs, similar to
# how modules can be functions or sets.
constSet = x: if builtins.isAttrs x then (_: x) else x;
# Try to access the extra builtin we loaded via nix-plugins.
# Throw an error if that doesn't exist.
sopsImportEncrypted =
assert lib.assertMsg (builtins ? extraBuiltins.sopsImportEncrypted)
"The extra builtin 'sopsImportEncrypted' is not available, so repo.secrets cannot be decrypted. Did you forget to add nix-plugins and point it to `/nix/extra-builtins.nix` ?";
builtins.extraBuiltins.sopsImportEncrypted;
# This "imports" an encrypted .nix.age file by evaluating the decrypted content.
importEncrypted =
path:
constSet (
if builtins.pathExists path then
sopsImportEncrypted path
else
{ }
);
in
{
options = {
repo = {
secretFiles = lib.mkOption {
default = { };
type = lib.types.attrsOf lib.types.path;
example = lib.literalExpression "{ local = ./pii.nix.enc; }";
description = ''
This file manages the origin for this machine's repository-secrets. Anything that is
technically not a secret in the classical sense (i.e. that it has to be protected
after it has been deployed), but something you want to keep secret from the public;
Anything that you wouldn't want people to see on GitHub, but that can live unencrypted
on your own devices. Consider it a more ergonomic nix alternative to using git-crypt.
All of these secrets may (and probably will be) put into the world-readable nix-store
on the build and target hosts. You'll most likely want to store personally identifiable
information here, such as:
- MAC Addreses
- Static IP addresses
- Your full name (when configuring your users)
- Your postal address (when configuring e.g. home-assistant)
- ...
Each path given here must be an sops-encrypted .nix file. For each attribute ``,
the corresponding file will be decrypted, imported and exposed as {option}`repo.secrets.`.
'';
};
secrets = lib.mkOption {
readOnly = true;
default = lib.mapAttrs (_: x: importEncrypted x inputs) config.repo.secretFiles;
type = lib.types.unspecified;
description = "Exposes the loaded repo secrets. This option is read-only.";
};
};
swarselsystems.modules.pii = lib.mkEnableOption "enable pii management";
};
config = lib.mkIf config.swarselsystems.modules.pii {
repo.secretFiles =
let
local = config.node.secretsDir + "/pii.nix.enc";
in
(lib.optionalAttrs (lib.pathExists local) { inherit local; }) // {
common = ../../../secrets/repo/pii.nix.enc;
};
};
}
#+end_src
**** Theme (stylix)
:PROPERTIES:
:CUSTOM_ID: h:e6e44705-94af-49fe-9ca0-0629d0f7d932
:END:
By default, [[https://github.com/danth/stylix][stylix]] wants to style GRUB as well. However, I think that looks horrible.
=theme= is defined in [[#h:5bc1b0c9-dc59-4c81-b5b5-e60699deda78][Theme (stylix)]].
#+begin_src nix :noweb yes :tangle modules/nixos/common/stylix.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.stylix = lib.mkEnableOption "stylix config";
config = lib.mkIf config.swarselsystems.modules.stylix {
stylix = lib.recursiveUpdate
{
targets.grub.enable = false; # the styling makes grub more ugly
image = config.swarselsystems.wallpaper;
}
config.swarselsystems.stylix;
home-manager.users."${config.swarselsystems.mainUser}" = {
stylix = {
targets = config.swarselsystems.stylixHomeTargets;
};
};
};
}
#+end_src
**** Programs (including zsh setup)
:PROPERTIES:
:CUSTOM_ID: h:2bbf5f31-246d-4738-925f-eca40681f7b6
:END:
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.
#+begin_src nix :tangle modules/nixos/common/programs.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.programs = lib.mkEnableOption "small program modules config";
config = lib.mkIf config.swarselsystems.modules.programs {
programs = {
dconf.enable = true;
evince.enable = true;
kdeconnect.enable = true;
};
};
}
#+end_src
***** zsh
:PROPERTIES:
:CUSTOM_ID: h:7daa06ff-d3b0-4491-97ce-770b749c52f9
:END:
Here I disable global completion to prevent redundant compinit calls and cache invalidation that slow down shell startup (enabled on the home-manager side).
#+begin_src nix :tangle modules/nixos/common/zsh.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.zsh = lib.mkEnableOption "zsh base config";
config = lib.mkIf config.swarselsystems.modules.zsh {
programs.zsh = {
enable = true;
enableCompletion = false;
};
users.defaultUserShell = pkgs.zsh;
environment.shells = with pkgs; [ zsh ];
environment.pathsToLink = [ "/share/zsh" ];
};
}
#+end_src
***** syncthing
:PROPERTIES:
:CUSTOM_ID: h:1e6d3d56-e415-43a2-8e80-3bad8062ecf8
:END:
#+begin_src nix :tangle modules/nixos/common/syncthing.nix
{ lib, config, pkgs, ... }:
let
inherit (config.swarselsystems) mainUser homeDir;
in
{
options.swarselsystems.modules.syncthing = lib.mkEnableOption "syncthing config";
config = lib.mkIf config.swarselsystems.modules.syncthing {
services.syncthing = {
enable = true;
package = pkgs.stable.syncthing;
user = mainUser;
dataDir = homeDir;
configDir = "${homeDir}/.config/syncthing";
openDefaultPorts = true;
overrideDevices = true;
overrideFolders = true;
settings = {
options = {
urAccepted = -1;
};
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";
};
"moonside@oracle" = {
id = "VPCDZB6-MGVGQZD-Q6DIZW3-IZJRJTO-TCC3QUQ-2BNTL7P-AKE7FBO-N55UNQE";
};
};
folders = {
"Default Folder" = lib.mkDefault {
path = "${homeDir}/Sync";
devices = [ "sync@oracle" "magicant" "winters" "moonside@oracle" ];
id = "default";
};
"Obsidian" = {
path = "${homeDir}/Nextcloud/Obsidian";
devices = [ "sync@oracle" "magicant" "winters" "moonside@oracle" ];
id = "yjvni-9eaa7";
};
"Org" = {
path = "${homeDir}/Nextcloud/Org";
devices = [ "sync@oracle" "magicant" "winters" "moonside@oracle" ];
id = "a7xnl-zjj3d";
};
"Vpn" = {
path = "${homeDir}/Vpn";
devices = [ "sync@oracle" "magicant" "winters" "moonside@oracle" ];
id = "hgp9s-fyq3p";
};
};
};
};
};
}
#+end_src
**** Services
:PROPERTIES:
:CUSTOM_ID: h:79f3258f-ed9d-434d-b50a-e58d57ade2a7
:END:
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
:PROPERTIES:
:CUSTOM_ID: h:b91df05b-113d-4d09-93d1-b271e5b76810
:END:
Enables the blueman service including the nice system tray icon.
#+begin_src nix :tangle modules/nixos/common/blueman.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.blueman = lib.mkEnableOption "blueman config";
config = lib.mkIf config.swarselsystems.modules.blueman {
services.blueman.enable = true;
services.hardware.bolt.enable = true;
};
}
#+end_src
***** Network devices
:PROPERTIES:
:CUSTOM_ID: h:73ed28cb-2f82-47b2-8bc5-208278b55788
:END:
In this section we enable compatibility with several network devices I have at home, mainly printers and scanners.
This allows me to use my big scanner/printer's scanning function over the network.
This also allows me to use my big scanner/printer's printing function over the network. Most of the settings are driver related.
Avahi is the service used for the network discovery.
#+begin_src nix :tangle modules/nixos/common/networkdevices.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.networkDevices = lib.mkEnableOption "network device config";
config = lib.mkIf config.swarselsystems.modules.networkDevices {
# enable scanners over network
hardware.sane = {
enable = true;
extraBackends = [ pkgs.sane-airscan ];
};
# enable discovery and usage of network devices (esp. printers)
services.printing = {
enable = true;
drivers = [
pkgs.gutenprint
pkgs.gutenprintBin
];
browsedConf = ''
BrowseDNSSDSubTypes _cups,_print
BrowseLocalProtocols all
BrowseRemoteProtocols all
CreateIPPPrinterQueues All
BrowseProtocols all
'';
};
services.avahi = {
enable = true;
nssmdns4 = true;
openFirewall = true;
};
};
}
#+end_src
***** enable GVfs
:PROPERTIES:
:CUSTOM_ID: h:f101daa2-604d-4553-99e2-f64b9c207f51
:END:
This is being set to allow myself to use all functions of nautilus in NixOS
#+begin_src nix :tangle modules/nixos/common/gvfs.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.gvfs = lib.mkEnableOption "gvfs config for nautilus";
config = lib.mkIf config.swarselsystems.modules.gvfs {
services.gvfs.enable = true;
};
}
#+end_src
***** interception-tools: Make CAPS work as ESC/CTRL
:PROPERTIES:
:CUSTOM_ID: h:08d213d5-a9f4-4309-8635-ba557b01dc7d
:END:
This is a super-convenient package that lets my remap my =CAPS= key to =ESC= if pressed shortly, and =CTRL= if being held.
#+begin_src nix :tangle modules/nixos/common/interceptiontools.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.interceptionTools = lib.mkEnableOption "interception tools config";
config = lib.mkIf config.swarselsystems.modules.interceptionTools {
# Make CAPS work as a dual function ESC/CTRL key
services.interception-tools = {
enable = true;
udevmonConfig =
let
dualFunctionKeysConfig = builtins.toFile "dual-function-keys.yaml" ''
TIMING:
TAP_MILLISEC: 200
DOUBLE_TAP_MILLISEC: 0
MAPPINGS:
- KEY: KEY_CAPSLOCK
TAP: KEY_ESC
HOLD: KEY_LEFTCTRL
'';
in
''
- JOB: |
${pkgs.interception-tools}/bin/intercept -g $DEVNODE \
| ${pkgs.interception-tools-plugins.dual-function-keys}/bin/dual-function-keys -c ${dualFunctionKeysConfig} \
| ${pkgs.interception-tools}/bin/uinput -d $DEVNODE
DEVICE:
EVENTS:
EV_KEY: [KEY_CAPSLOCK]
'';
};
};
}
#+end_src
***** power-profiles-daemon
:PROPERTIES:
:CUSTOM_ID: h:82fbba41-3a46-4db7-aade-49e4c23fc475
:END:
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.
#+begin_src nix :tangle modules/nixos/common/power-profiles-daemon.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.ppd = lib.mkEnableOption "power profiles daemon config";
config = lib.mkIf config.swarselsystems.modules.ppd {
services.power-profiles-daemon.enable = true;
};
}
#+end_src
***** SwayOSD
:PROPERTIES:
:CUSTOM_ID: h:5db15758-17d8-4bde-811d-d11ccdd3f3d3
:END:
#+begin_src nix :tangle modules/nixos/common/swayosd.nix
{ lib, pkgs, config, ... }:
{
options.swarselsystems.modules.swayosd = lib.mkEnableOption "swayosd settings";
config = lib.mkIf config.swarselsystems.modules.swayosd {
environment.systemPackages = [ pkgs.swayosd ];
services.udev.packages = [ pkgs.swayosd ];
systemd.services.swayosd-libinput-backend = {
description = "SwayOSD LibInput backend for listening to certain keys like CapsLock, ScrollLock, VolumeUp, etc.";
documentation = [ "https://github.com/ErikReider/SwayOSD" ];
wantedBy = [ "graphical.target" ];
partOf = [ "graphical.target" ];
after = [ "graphical.target" ];
serviceConfig = {
Type = "dbus";
BusName = "org.erikreider.swayosd";
ExecStart = "${pkgs.swayosd}/bin/swayosd-libinput-backend";
Restart = "on-failure";
};
};
};
}
#+end_src
**** Hardware compatibility settings (Yubikey, Ledger, Keyboards) - udev rules
:PROPERTIES:
:CUSTOM_ID: h:7a89b5e3-b700-4167-8b14-2b8172f33936
:END:
***** Yubikey
:PROPERTIES:
:CUSTOM_ID: h:49aa792d-edfb-4eac-ae31-ecf23c4dca00
:END:
This takes care of the main Yubikey related configuration on the NixOS side - note that the starting of the gpg-agent is done in the sway settings, to also perform this step of the setup for non NixOS-machines at the same time.
I want to use the ssh-agent from gpg-agent's ssh compatibility, which is why we disable ssh-agent. Also, we load some extra udev rules using =hardware.gpgSmartcards.enable=.
Many guides state that it is needed to enable =pcscd= to use the smartcard mode (CCID) of the Yubikey. However, enabling it causes some problems when locking the screen and unplugging the Yubikey, after which the Yubikey only becomes available again as a smart card after about one minute. I found that is is sufficient to enable =services.gpg-agent.enableScDaemon= in home-manager instead.
Also, since I use a GPG key in sops, it seems that scdaemon creates an instance at boot which sometimes hogs the Yubikey, which leads to significant delays after e.g. locking the screen and unplugging the Yubikey. Since I do not need the GPG key for the actual sops secrets (I use machine age keys instead), I kill that process.
#+begin_src nix :tangle modules/nixos/common/hardwarecompatibility-yubikey.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.yubikey = lib.mkEnableOption "yubikey config";
config = lib.mkIf config.swarselsystems.modules.yubikey {
programs.ssh.startAgent = false;
services.pcscd.enable = false;
hardware.gpgSmartcards.enable = true;
services.udev.packages = with pkgs; [
yubikey-personalization
];
};
}
#+end_src
***** Ledger
:PROPERTIES:
:CUSTOM_ID: h:c3cba64c-cdd7-4d58-a2c2-6a7fb36ad6c4
:END:
This performs the necessary configuration to support this hardware.
#+begin_src nix :tangle modules/nixos/common/hardwarecompatibility-ledger.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.ledger = lib.mkEnableOption "ledger config";
config = lib.mkIf config.swarselsystems.modules.ledger {
hardware.ledger.enable = true;
services.udev.packages = with pkgs; [
ledger-udev-rules
];
};
}
#+end_src
***** Keyboards
:PROPERTIES:
:CUSTOM_ID: h:103b68b6-33a1-4369-a534-5f36dfa95e03
:END:
This loads some udev rules that I need for my split keyboards.
#+begin_src nix :tangle modules/nixos/common/hardwarecompatibility-keyboards.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.keyboards = lib.mkEnableOption "keyboards config";
config = lib.mkIf config.swarselsystems.modules.keyboards {
services.udev.packages = with pkgs; [
qmk-udev-rules
vial
via
];
};
}
#+end_src
**** System Login
:PROPERTIES:
:CUSTOM_ID: h:eae45839-223a-4027-bce3-e26e092c9096
:END:
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
#+begin_src nix :tangle modules/nixos/common/login.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.login = lib.mkEnableOption "login config";
config = lib.mkIf config.swarselsystems.modules.login {
services.greetd = {
enable = true;
settings = {
initial_session.command = "sway";
default_session.command = ''
${pkgs.greetd.tuigreet}/bin/tuigreet \
--time \
--asterisks \
--user-menu \
--cmd sway
'';
};
};
environment.etc."greetd/environments".text = ''
sway
'';
};
}
#+end_src
**** nix-ld
:PROPERTIES:
:CUSTOM_ID: h:404cc18b-b5f8-48d9-a407-a0fd70d57f46
:END:
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 =. This will tell you which library is missing. Afterwards, continue with =nix-locate = 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.
#+begin_src nix :tangle modules/nixos/common/nix-ld.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.nix-ld = lib.mkEnableOption "nix-ld config";
config = lib.mkIf config.swarselsystems.modules.nix-ld {
programs.nix-ld = {
enable = true;
libraries = with pkgs; [
SDL
SDL2
SDL2_image
SDL2_mixer
SDL2_ttf
SDL_image
SDL_mixer
SDL_ttf
alsa-lib
at-spi2-atk
at-spi2-core
atk
bzip2
cairo
cups
curl
dbus
dbus-glib
expat
ffmpeg
flac
fontconfig
freeglut
freetype
fuse3
gdk-pixbuf
glew110
glib
stable.gnome2.GConf
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
libz
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
];
};
};
}
#+end_src
**** Impermanence
:PROPERTIES:
:CUSTOM_ID: h:e7668594-fa8b-4d36-a695-a58222478988
:END:
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.
#+begin_src nix :tangle modules/nixos/common/impermanence.nix
{ config, lib, ... }:
let
mapperTarget = lib.swarselsystems.mkIfElse config.swarselsystems.isCrypted "/dev/mapper/cryptroot" "/dev/disk/by-label/nixos";
inherit (config.swarselsystems) isImpermanence isCrypted;
in
{
options.swarselsystems.modules.impermanence = lib.mkEnableOption "impermanence config";
config = lib.mkIf config.swarselsystems.modules.impermanence {
security.sudo.extraConfig = lib.mkIf isImpermanence ''
# rollback results in sudo lectures after each reboot
Defaults lecture = never
'';
# This script does the actual wipe of the system
# So if it doesn't run, the btrfs system effectively acts like a normal system
# Taken from https://github.com/NotAShelf/nyx/blob/2a8273ed3f11a4b4ca027a68405d9eb35eba567b/modules/core/common/system/impermanence/default.nix
boot.initrd.systemd.enable = lib.mkIf isImpermanence true;
boot.initrd.systemd.services.rollback = lib.mkIf isImpermanence {
description = "Rollback BTRFS root subvolume to a pristine state";
wantedBy = [ "initrd.target" ];
# make sure it's done after encryption
# i.e. LUKS/TPM process
after = lib.swarselsystems.mkIfElseList isCrypted [ "systemd-cryptsetup@cryptroot.service" ] [ "dev-disk-by\\x2dlabel-nixos.device" ];
requires = lib.mkIf (!isCrypted) [ "dev-disk-by\\x2dlabel-nixos.device" ];
# mount the root fs before clearing
before = [ "sysroot.mount" ];
unitConfig.DefaultDependencies = "no";
serviceConfig.Type = "oneshot";
script = ''
mkdir -p /mnt
# We first mount the btrfs root to /mnt
# so we can manipulate btrfs subvolumes.
mount -o subvolid=5 -t btrfs ${mapperTarget} /mnt
btrfs subvolume list -o /mnt/root
# While we're tempted to just delete /root and create
# a new snapshot from /root-blank, /root is already
# populated at this point with a number of subvolumes,
# which makes `btrfs subvolume delete` fail.
# So, we remove them first.
#
# /root contains subvolumes:
# - /root/var/lib/portables
# - /root/var/lib/machines
btrfs subvolume list -o /mnt/root |
cut -f9 -d' ' |
while read subvolume; do
echo "deleting /$subvolume subvolume..."
btrfs subvolume delete "/mnt/$subvolume"
done &&
echo "deleting /root subvolume..." &&
btrfs subvolume delete /mnt/root
echo "restoring blank /root subvolume..."
btrfs subvolume snapshot /mnt/root-blank /mnt/root
# Once we're done rolling back to a blank snapshot,
# we can unmount /mnt and continue on the boot process.
umount /mnt
'';
};
environment.persistence."/persist" = lib.mkIf isImpermanence {
hideMounts = true;
directories =
[
"/etc/nix"
"/etc/NetworkManager/system-connections"
"/var/lib/nixos"
{
directory = "/var/tmp/nix-import-encrypted"; # Decrypted repo-secrets can be kept
mode = "1777";
}
# "/etc/secureboot"
];
files = [
"/etc/ssh/ssh_host_ed25519_key"
"/etc/ssh/ssh_host_ed25519_key.pub"
"/etc/machine-id"
];
};
};
}
#+end_src
**** Summary of nixos-rebuild diff
:PROPERTIES:
:CUSTOM_ID: h:b751d77d-246c-4bd6-b689-3467d82bf9c3
:END:
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.
#+begin_src nix :tangle modules/nixos/common/nvd-rebuild.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.nvd = lib.mkEnableOption "nvd config";
config = lib.mkIf config.swarselsystems.modules.nvd {
system.activationScripts.diff = {
supportsDryActivation = true;
text = ''
${pkgs.nvd}/bin/nvd --color=always --nix-bin-dir=${pkgs.nix}/bin diff \
/run/current-system "$systemConfig"
'';
};
};
}
#+end_src
**** gnome-keyring
:PROPERTIES:
:CUSTOM_ID: h:ce50eb90-8bf4-4203-b502-c3165d2fbf1f
:END:
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.
#+begin_src nix :tangle modules/nixos/common/gnome-keyring.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.gnome-keyring = lib.mkEnableOption "gnome-keyring config";
config = lib.mkIf config.swarselsystems.modules.gnome-keyring {
services.gnome.gnome-keyring = {
enable = true;
};
programs.seahorse.enable = true;
};
}
#+end_src
**** Sway
:PROPERTIES:
:CUSTOM_ID: h:f78ffdd3-232b-4313-bd89-d6fb331fef22
:END:
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.
#+begin_src nix :tangle modules/nixos/common/sway.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.sway = lib.mkEnableOption "sway config";
config = lib.mkIf config.swarselsystems.modules.sway {
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 QT_QPA_PLATFORM_PLUGIN_PATH="${pkgs.libsForQt5.qt5.qtbase.bin}/lib/qt-${pkgs.libsForQt5.qt5.qtbase.version}/plugins";
export MOZ_ENABLE_WAYLAND=1
export MOZ_DISABLE_RDD_SANDBOX=1
'';
};
};
}
#+end_src
**** xdg-portal
:PROPERTIES:
:CUSTOM_ID: h:872d5f46-2ffd-4076-9a2c-98783dd29434
:END:
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.
#+begin_src nix :tangle modules/nixos/common/xdg-portal.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.xdg-portal = lib.mkEnableOption "xdg portal config";
config = lib.mkIf config.swarselsystems.modules.xdg-portal {
xdg.portal = {
enable = true;
config = {
common = {
default = "wlr";
};
};
wlr.enable = true;
wlr.settings.screencast = {
output_name = "eDP-1";
chooser_type = "simple";
chooser_cmd = "${pkgs.slurp}/bin/slurp -f %o -or";
};
};
};
}
#+end_src
**** Podmam (distrobox)
:PROPERTIES:
:CUSTOM_ID: h:1bef3914-a258-4585-b232-e0fbe9e7a9b5
:END:
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.
#+begin_src nix :tangle modules/nixos/common/distrobox.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.distrobox = lib.mkEnableOption "distrobox config";
config = lib.mkIf config.swarselsystems.modules.distrobox {
environment.systemPackages = with pkgs; [
distrobox
boxbuddy
];
virtualisation.podman = {
enable = true;
dockerCompat = true;
package = pkgs.stable.podman;
};
};
}
#+end_src
**** Appimage
:PROPERTIES:
:CUSTOM_ID: h:cfc22f8d-251e-4636-98d6-a43cdb112b68
:END:
Adds the necessary tools to allow .appimage programs easily.
#+begin_src nix :tangle modules/nixos/common/appimage.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.appimage = lib.mkEnableOption "appimage config";
config = lib.mkIf config.swarselsystems.modules.appimage {
programs.appimage = {
enable = true;
binfmt = true;
};
};
}
#+end_src
**** Handle lid switch correctly
:PROPERTIES:
:CUSTOM_ID: h:a5a0d84e-c7b3-4164-a4c7-2e2d8ada69cd
:END:
This turns off the display when the lid is closed.
#+begin_src nix :tangle modules/nixos/common/lid.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.lid = lib.mkEnableOption "lid config";
config = lib.mkIf config.swarselsystems.modules.lid {
services.logind = {
lidSwitch = "suspend";
lidSwitchDocked = "ignore";
};
services.acpid = {
enable = true;
handlers.lidClosed = {
event = "button/lid \\w+ close";
action = ''
cat /sys/class/backlight/amdgpu_bl1/device/enabled
if grep -Fxq disabled /sys/class/backlight/amdgpu_bl1/device/enabled
then
echo "Lid closed. Disabling fprintd."
systemctl stop fprintd
ln -s /dev/null /run/systemd/transient/fprintd.service
systemctl daemon-reload
fi
'';
};
handlers.lidOpen = {
event = "button/lid \\w+ open";
action = ''
if ! $(systemctl is-active --quiet fprintd); then
echo "Lid open. Enabling fprintd."
rm -f /run/systemd/transient/fprintd.service
systemctl daemon-reload
systemctl start fprintd
fi
'';
};
};
};
}
#+end_src
**** Low battery notification
:PROPERTIES:
:CUSTOM_ID: h:adf894d7-b3c6-4b8b-b13f-c28b3a5e1e17
:END:
Since I hide the waybar completely during normal operation, I run the risk of not noticing when my battery is about to run out. This module sends a notification when the battery level falls below 10%. Written by [[https://gist.github.com/cafkafk][cafkafk]].
#+begin_src nix :tangle modules/nixos/common/lowbattery.nix
{ pkgs, lib, config, ... }:
{
options.swarselsystems.modules.lowBattery = lib.mkEnableOption "low battery notification config";
config = lib.mkIf config.swarselsystems.modules.lowBattery {
systemd.user.services."battery-low" = {
enable = true;
description = "Timer for battery check that alerts at 10% or less";
partOf = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
Type = "simple";
ExecStart = pkgs.writeShellScript "battery-low-notification"
''
if (( 10 >= $(${lib.getExe pkgs.acpi} -b | head -n 1 | ${lib.getExe pkgs.ripgrep} -o "\d+%" | ${lib.getExe pkgs.ripgrep} -o "\d+") && $(${lib.getExe pkgs.acpi} -b | head -n 1 | ${lib.getExe pkgs.ripgrep} -o "\d+%" | ${lib.getExe pkgs.ripgrep} -o "\d+") > 0 ));
then ${lib.getExe pkgs.libnotify} --urgency=critical "low battery" "$(${lib.getExe pkgs.acpi} -b | head -n 1 | ${lib.getExe pkgs.ripgrep} -o "\d+%")";
fi;
'';
};
};
systemd.user.timers."battery-low" = {
wantedBy = [ "timers.target" ];
timerConfig = {
# Every Minute
OnCalendar = "*-*-* *:*:00";
Unit = "battery-low.service";
};
};
};
}
#+end_src
**** Lanzaboote
:PROPERTIES:
:CUSTOM_ID: h:d9a89071-b3ba-44d1-b5e0-e9ca6270d377
:END:
This dynamically uses systemd boot or Lanzaboote depending on `config.swarselsystems.initialSetup` and `config.swarselsystems.isSecureBoot`.
#+begin_src nix :tangle modules/nixos/common/lanzaboote.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.lanzaboote = lib.mkEnableOption "lanzaboote config";
config = lib.mkIf config.swarselsystems.modules.lanzaboote {
boot = {
loader = {
efi.canTouchEfiVariables = true;
systemd-boot.enable = lib.swarselsystems.mkIfElse (config.swarselsystems.initialSetup || !config.swarselsystems.isSecureBoot) (lib.mkForce true) (lib.mkForce false);
};
lanzaboote = lib.mkIf (!config.swarselsystems.initialSetup && config.swarselsystems.isSecureBoot) {
enable = true;
pkiBundle = "/var/lib/sbctl";
configurationLimit = 6;
};
};
};
}
#+end_src
*** Server
:PROPERTIES:
:CUSTOM_ID: h:e492c24a-83a0-4bcb-a084-706f49318651
:END:
**** Imports
:PROPERTIES:
:CUSTOM_ID: h:4e64e564-b7cb-469f-bd79-cd3efb3caa62
:END:
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.
#+begin_src nix :tangle modules/nixos/server/default.nix
{ self, lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/nixos/server";
modulesPath = "${self}/modules";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/nixos/server" ++ [
"${modulesPath}/nixos/common/settings.nix"
"${modulesPath}/nixos/common/home-manager.nix"
"${modulesPath}/nixos/common/home-manager-extra.nix"
"${modulesPath}/nixos/common/xserver.nix"
"${modulesPath}/nixos/common/time.nix"
"${modulesPath}/nixos/common/users.nix"
"${modulesPath}/nixos/common/nix-ld.nix"
"${modulesPath}/nixos/common/sharedsetup.nix"
"${modulesPath}/home/common/sharedsetup.nix"
];
}
#+end_src
**** General NixOS Server settings
:PROPERTIES:
:CUSTOM_ID: h:dc365e83-f6c8-4d05-a390-b5f2d01649b4
:END:
Here we just define some aliases for rebuilding the system, and we allow some insecure packages that are needed by some server derivations. It would be more elegant to define these in the respective module, but nixpkgs needs to be defined before we can evaluate modules within it, so this must be a top-level configuration.
#+begin_src nix :tangle modules/nixos/server/settings.nix
{ lib, config, ... }:
let
inherit (config.swarselsystems) flakePath;
in
{
options.swarselsystems = {
modules.server.general = lib.mkEnableOption "general setting on server";
shellAliases = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
};
};
config = lib.mkIf config.swarselsystems.modules.server.general {
environment.shellAliases = lib.recursiveUpdate
{
npswitch = "cd ${flakePath}; git pull; sudo nixos-rebuild --flake .#$(hostname) switch; cd -;";
nswitch = "sudo nixos-rebuild --flake ${flakePath}#$(hostname) switch;";
npiswitch = "cd ${flakePath}; git pull; sudo nixos-rebuild --flake .#$(hostname) switch --impure; cd -;";
nipswitch = "cd ${flakePath}; git pull; sudo nixos-rebuild --flake .#$(hostname) switch --impure; cd -;";
niswitch = "sudo nixos-rebuild --flake ${flakePath}#$(hostname) switch --impure;";
}
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"
#
"SDL_ttf-2.0.11"
];
};
}
#+end_src
**** System Packages
:PROPERTIES:
:CUSTOM_ID: h:6f2967d9-7e32-4605-bb5c-5e27770bec0f
:END:
#+begin_src nix :tangle modules/nixos/server/packages.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.server.packages = lib.mkEnableOption "enable packages on server";
config = lib.mkIf config.swarselsystems.modules.server.packages {
environment.systemPackages = with pkgs; [
gnupg
nix-index
nvd
nix-output-monitor
ssh-to-age
git
emacs
vim
sops
swarsel-deploy
];
};
}
#+end_src
**** sops
:PROPERTIES:
:CUSTOM_ID: h:313f7940-e8bb-4b5d-97cb-e2fea4e665e4
:END:
#+begin_src nix :tangle modules/nixos/server/sops.nix
{ config, lib, ... }:
{
options.swarselsystems.modules.server.sops = lib.mkEnableOption "enable sops on server";
config = lib.mkIf config.swarselsystems.modules.server.sops {
sops = {
age.sshKeyPaths = lib.mkDefault [ "/etc/ssh/sops" ];
defaultSopsFile = lib.mkDefault "${config.swarselsystems.flakePath}/secrets/winters/secrets.yaml";
validateSopsFiles = false;
};
};
}
#+end_src
**** nfs/samba (smb)
:PROPERTIES:
:CUSTOM_ID: h:d6840d31-110c-465f-93fa-0306f755de28
:END:
#+begin_src nix :tangle modules/nixos/server/nfs.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.server.nfs = lib.mkEnableOption "enable nfs on server";
config = lib.mkIf config.swarselsystems.modules.server.nfs {
services = {
# add a user with sudo smbpasswd -a
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;
};
};
};
}
#+end_src
**** NGINX
:PROPERTIES:
:CUSTOM_ID: h:302468d2-106a-41c8-b2bc-9fdc40064a9c
:END:
#+begin_src nix :tangle modules/nixos/server/nginx.nix
{ pkgs, lib, config, ... }:
{
options.swarselsystems.modules.server.nginx = lib.mkEnableOption "enable nginx on server";
config = lib.mkIf config.swarselsystems.modules.server.nginx {
environment.systemPackages = with pkgs; [
lego
];
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";
dnsProvider = "cloudflare";
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
};
};
}
#+end_src
**** ssh
:PROPERTIES:
:CUSTOM_ID: h:f3db197d-1d03-4bf8-b59f-f9891b358f0b
:END:
Here I am forcing =startWhenNeeded= to false so that the value will not be set to true in containers = this would be a problem because it would delay ssh host key generation.
#+begin_src nix :tangle modules/nixos/server/ssh.nix
{ self, lib, config, ... }:
{
options.swarselsystems.modules.server.ssh = lib.mkEnableOption "enable ssh on server";
config = lib.mkIf config.swarselsystems.modules.server.ssh {
services.openssh = {
enable = true;
startWhenNeeded = lib.mkForce false;
settings = {
PasswordAuthentication = false;
KbdInteractiveAuthentication = false;
PermitRootLogin = "yes";
};
hostKeys = [
{
path = "/etc/ssh/ssh_host_ed25519_key";
type = "ed25519";
}
];
};
users.users."${config.swarselsystems.mainUser}".openssh.authorizedKeys.keyFiles = [
(self + /secrets/keys/ssh/yubikey.pub)
(self + /secrets/keys/ssh/magicant.pub)
];
users.users.root.openssh.authorizedKeys.keyFiles = [
(self + /secrets/keys/ssh/yubikey.pub)
(self + /secrets/keys/ssh/magicant.pub)
];
security.sudo.extraConfig = ''
Defaults env_keep+=SSH_AUTH_SOCK
'';
};
}
#+end_src
**** kavita
:PROPERTIES:
:CUSTOM_ID: h:d33f5982-dfe6-42d0-9cf2-2cd8c7b04295
:END:
#+begin_src nix :tangle modules/nixos/server/kavita.nix
{ self, lib, config, pkgs, ... }:
let
serviceName = "kavita";
serviceUser = "kavita";
serviceDomain = config.repo.secrets.common.services.domains.${serviceName};
servicePort = 8080;
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
environment.systemPackages = with pkgs; [
calibre
];
users.users."${serviceUser}" = {
extraGroups = [ "users" ];
};
sops.secrets.kavita = { owner = serviceUser; };
networking.firewall.allowedTCPPorts = [ 8080 ];
topology.self.services.kavita = {
name = "Kavita";
info = "https://${serviceDomain}";
icon = "${self}/topology/images/kavita.png";
};
globals.services.${serviceName}.domain = serviceDomain;
services.kavita = {
enable = true;
user = serviceUser;
settings.Port = servicePort;
tokenKeyFile = config.sops.secrets.kavita.path;
dataDir = "/Vault/data/kavita";
};
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
};
}
#+end_src
**** jellyfin
:PROPERTIES:
:CUSTOM_ID: h:e0d4c16e-ab64-48ac-9734-1ab62953ad4b
:END:
#+begin_src nix :tangle modules/nixos/server/jellyfin.nix
{ pkgs, lib, config, ... }:
let
serviceDomain = "screen.swarsel.win";
servicePort = 8096;
serviceName = "jellyfin";
serviceUser = "jellyfin";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
users.users."${serviceUser}" = {
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
];
};
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals.services.${serviceName}.domain = serviceDomain;
services.jellyfin = {
enable = true;
user = serviceUser;
openFirewall = true; # this works only for the default ports
};
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
};
}
#+end_src
**** navidrome
:PROPERTIES:
:CUSTOM_ID: h:f347f3ad-5100-4c4f-8616-cfd7f8e14a72
:END:
#+begin_src nix :tangle modules/nixos/server/navidrome.nix
{ pkgs, config, lib, ... }:
let
serviceDomain = "sound.swarsel.win";
servicePort = 4040;
serviceName = "navidrome";
serviceUser = "navidrome";
serviceGroup = serviceUser;
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
environment.systemPackages = with pkgs; [
pciutils
alsa-utils
mpv
];
users = {
groups = {
"${serviceGroup}" = {
gid = 61593;
};
};
users = {
"${serviceUser}" = {
isSystemUser = true;
uid = 61593;
group = serviceGroup;
extraGroups = [ "audio" "utmp" "users" "pipewire" ];
};
};
};
hardware = {
enableAllFirmware = lib.mkForce true;
};
networking.firewall.allowedTCPPorts = [ 4040 ];
globals.services.${serviceName}.domain = serviceDomain;
services.navidrome = {
enable = true;
openFirewall = true;
settings = {
LogLevel = "debug";
Address = "0.0.0.0";
Port = servicePort;
MusicFolder = "/Vault/Eternor/Music";
PlaylistsPath = "./Playlists";
AutoImportPlaylists = false;
EnableSharing = true;
EnableTranscodingConfig = true;
Scanner.GroupAlbumReleases = true;
ScanSchedule = "@every 24h";
MPVPath = "${pkgs.mpv}/bin/mpv";
MPVCommandTemplate = "mpv --audio-device=%d --no-audio-display --pause %f";
ReverseProxyWhitelist = "0.0.0.0/0";
ReverseProxyUserHeader = "X-User";
Jukebox = {
Enabled = true;
Default = "default";
Devices = [
# use mpv --audio-device=help to get these
[ "default" "alsa/sysdefault:CARD=PCH" ]
];
};
# Switch using --impure as these credential files are not stored within the flake
# sops-nix is not supported for these which is why we need to resort to these
LastFM = {
inherit (config.repo.secrets.local.LastFM) ApiKey Secret;
};
Spotify = {
inherit (config.repo.secrets.local.Spotify) ID Secret;
};
UILoginBackgroundUrl = "https://i.imgur.com/OMLxi7l.png";
UIWelcomeMessage = "~SwarselSound~";
EnableInsightsCollector = false;
};
};
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
oauth2.enable = true;
oauth2.allowedGroups = [ "navidrome_access" ];
locations =
let
extraConfig = ''
proxy_redirect http:// https://;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_buffering off;
proxy_request_buffering off;
client_max_body_size 0;
'';
in
{
"/" = {
proxyPass = "http://navidrome";
proxyWebsockets = true;
inherit extraConfig;
};
"/share" = {
proxyPass = "http://navidrome";
proxyWebsockets = true;
setOauth2Headers = false;
bypassAuth = true;
inherit extraConfig;
};
"/rest" = {
proxyPass = "http://navidrome";
proxyWebsockets = true;
setOauth2Headers = false;
bypassAuth = true;
inherit extraConfig;
};
};
};
};
};
};
}
#+end_src
**** spotifyd
:PROPERTIES:
:CUSTOM_ID: h:ec9c5a7d-ea8b-46d5-809c-163c917f5c41
:END:
#+begin_src nix :tangle modules/nixos/server/spotifyd.nix
{ lib, config, ... }:
let
servicePort = 1025;
serviceName = "spotifyd";
serviceUser = "spotifyd";
serviceGroup = serviceUser;
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
users.groups."${serviceGroup}" = {
gid = 65136;
};
users.users."${serviceUser}" = {
isSystemUser = true;
uid = 65136;
group = serviceGroup;
extraGroups = [ "audio" "utmp" "pipewire" ];
};
networking.firewall.allowedTCPPorts = [ servicePort ];
services.pipewire.systemWide = true;
services.spotifyd = {
enable = true;
settings = {
global = {
dbus_type = "session";
use_mpris = false;
device = "sysdefault:CARD=PCH";
device_name = "SwarselSpot";
mixer = "alsa";
zeroconf_port = servicePort;
};
};
};
};
}
#+end_src
**** mpd
:PROPERTIES:
:CUSTOM_ID: h:baa4149b-3788-4b05-87ec-0ee9d0726117
:END:
#+begin_src nix :tangle modules/nixos/server/mpd.nix
{ self, lib, config, pkgs, ... }:
{
options.swarselsystems.modules.server.mpd = lib.mkEnableOption "enable mpd on server";
config = lib.mkIf config.swarselsystems.modules.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
];
topology.self.services.mpd = {
name = "MPD";
info = "http://localhost:3254";
icon = "${self}/topology/images/mpd.png";
};
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"
];
}
];
};
};
}
#+end_src
**** pipewire
:PROPERTIES:
:CUSTOM_ID: h:ce6a4371-e44f-419a-be9e-e17c7abdaf3a
:END:
#+begin_src nix :tangle modules/nixos/server/pipewire.nix
{ lib, config, ... }:
{
config = lib.mkIf (config?swarselsystems.modules.server.mpd || config?swarselsystems.modules.server.navidrome) {
security.rtkit.enable = true; # this is required for pipewire real-time access
services.pipewire = {
enable = true;
pulse.enable = true;
jack.enable = true;
audio.enable = true;
wireplumber.enable = true;
alsa = {
enable = true;
support32Bit = true;
};
};
};
}
#+end_src
**** postgresql
:PROPERTIES:
:CUSTOM_ID: h:6ca43d5a-8ba6-4cd1-96b9-f088f11662c0
:END:
#+begin_src nix :tangle modules/nixos/server/postgresql.nix
{ config, lib, pkgs, ... }:
let
serviceName = "postgresql";
postgresVersion = 14;
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
services = {
postgresql = {
enable = true;
package = pkgs."postgresql_${builtins.toString postgresVersion}";
dataDir = "/Vault/data/postgresql/${builtins.toString postgresVersion}";
};
};
};
}
#+end_src
**** matrix
:PROPERTIES:
:CUSTOM_ID: h:1e68d84a-8f99-422f-89ac-78f664ac0013
:END:
#+begin_src nix :tangle modules/nixos/server/matrix.nix
{ lib, config, pkgs, ... }:
let
matrixDomain = "swatrix.swarsel.win";
serviceName = "matrix";
synapsePort = 8008;
synapseUser = "matrix-synapse";
whatsappPort = 29318;
telegramPort = 29317;
signalPort = 29328;
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
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
environment.systemPackages = with pkgs; [
matrix-synapse
lottieconverter
ffmpeg
];
sops = {
secrets = {
matrixsharedsecret = { owner = synapseUser; };
mautrixtelegram_as = { owner = synapseUser; };
mautrixtelegram_hs = { owner = synapseUser; };
mautrixtelegram_api_id = { owner = synapseUser; };
mautrixtelegram_api_hash = { owner = synapseUser; };
};
templates = {
"matrix_user_register.sh".content = ''
register_new_matrix_user -k ${config.sops.placeholder.matrixsharedsecret} http://localhost:${builtins.toString synapsePort}
'';
matrixshared = {
owner = synapseUser;
content = ''
registration_shared_secret: ${config.sops.placeholder.matrixsharedsecret}
'';
};
mautrixtelegram = {
owner = synapseUser;
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}
'';
};
};
};
networking.firewall.allowedTCPPorts = [ 8008 8448 ];
systemd = {
timers."restart-bridges" = {
wantedBy = [ "timers.target" ];
timerConfig = {
OnBootSec = "1d";
OnUnitActiveSec = "1d";
Unit = "restart-bridges.service";
};
};
services = {
"restart-bridges" = {
script = ''
systemctl restart mautrix-whatsapp.service
systemctl restart mautrix-signal.service
systemctl restart mautrix-telegram.service
'';
serviceConfig = {
Type = "oneshot";
User = "root";
};
};
mautrix-telegram.path = with pkgs; [
lottieconverter # for animated stickers conversion, unfree package
ffmpeg # if converting animated stickers to webm (very slow!)
];
};
};
globals.services.${serviceName}.domain = matrixDomain;
services = {
postgresql = {
enable = true;
initialScript = pkgs.writeText "synapse-init.sql" ''
CREATE ROLE "matrix-synapse" WITH LOGIN PASSWORD 'synapse';
CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
CREATE ROLE "mautrix-telegram" WITH LOGIN PASSWORD 'telegram';
CREATE DATABASE "mautrix-telegram" WITH OWNER "mautrix-telegram"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
CREATE ROLE "mautrix-whatsapp" WITH LOGIN PASSWORD 'whatsapp';
CREATE DATABASE "mautrix-whatsapp" WITH OWNER "mautrix-whatsapp"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
CREATE ROLE "mautrix-signal" WITH LOGIN PASSWORD 'signal';
CREATE DATABASE "mautrix-signal" WITH OWNER "mautrix-signal"
TEMPLATE template0
LC_COLLATE = "C"
LC_CTYPE = "C";
'';
};
matrix-synapse = {
enable = true;
dataDir = "/Vault/data/matrix-synapse";
settings = {
app_service_config_files = let
inherit (config.services.matrix-synapse) dataDir;
in
[
"${dataDir}/telegram-registration.yaml"
"${dataDir}/whatsapp-registration.yaml"
"${dataDir}/signal-registration.yaml"
"${dataDir}/doublepuppet.yaml"
];
server_name = matrixDomain;
public_baseurl = "https://${matrixDomain}";
listeners = [
{
port = synapsePort;
bind_addresses = [
"0.0.0.0"
# "::1"
];
type = "http";
tls = false;
x_forwarded = true;
resources = [
{
names = [ "client" "federation" ];
compress = true;
}
];
}
];
};
extraConfigFiles = [
config.sops.templates.matrixshared.path
];
};
mautrix-telegram = {
enable = true;
environmentFile = config.sops.templates.mautrixtelegram.path;
registerToSynapse = false;
settings = {
homeserver = {
address = "http://localhost:${builtins.toString synapsePort}";
domain = matrixDomain;
};
appservice = {
address = "http://localhost:${builtins.toString telegramPort}";
hostname = "0.0.0.0";
port = telegramPort;
provisioning.enabled = true;
id = "telegram";
# ephemeral_events = true; # not needed due to double puppeting
public = {
enabled = false;
};
database = "postgresql:///mautrix-telegram?host=/run/postgresql";
};
bridge = {
relaybot.authless_portals = true;
allow_avatar_remove = true;
allow_contact_info = true;
sync_channel_members = true;
startup_sync = true;
sync_create_limit = 0;
sync_direct_chats = true;
telegram_link_preview = true;
permissions = {
"*" = "relaybot";
"@swarsel:${matrixDomain}" = "admin";
};
animated_sticker = {
target = "gif";
args = {
width = 256;
height = 256;
fps = 30; # only for webm
background = "020202"; # only for gif, transparency not supported
};
};
};
};
};
mautrix-whatsapp = {
enable = true;
registerToSynapse = false;
settings = {
homeserver = {
address = "http://localhost:${builtins.toString synapsePort}";
domain = matrixDomain;
};
appservice = {
address = "http://localhost:${builtins.toString whatsappPort}";
hostname = "0.0.0.0";
port = whatsappPort;
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 = {
"*" = "relay";
"@swarsel:${matrixDomain}" = "admin";
};
};
};
};
mautrix-signal = {
enable = true;
registerToSynapse = false;
settings = {
homeserver = {
address = "http://localhost:${builtins.toString synapsePort}";
domain = matrixDomain;
};
appservice = {
address = "http://localhost:${builtins.toString signalPort}";
hostname = "0.0.0.0";
port = signalPort;
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.
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString synapsePort}" = { };
};
};
};
virtualHosts = {
"${matrixDomain}" = {
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://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
"= /.well-known/matrix/server".extraConfig = mkWellKnown serverConfig;
"= /.well-known/matrix/client".extraConfig = mkWellKnown clientConfig;
};
};
};
};
};
}
#+end_src
**** nextcloud
:PROPERTIES:
:CUSTOM_ID: h:d11ad8d5-25d7-4691-b319-61c16ccef715
:END:
#+begin_src nix :tangle modules/nixos/server/nextcloud.nix
{ pkgs, lib, config, ... }:
let
serviceDomain = "stash.swarsel.win";
serviceUser = "nextcloud";
serviceGroup = serviceUser;
serviceName = "nextcloud";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
sops.secrets = {
nextcloudadminpass = {
owner = serviceUser;
group = serviceGroup;
mode = "0440";
};
kanidm-nextcloud-client = {
owner = serviceUser;
group = serviceGroup;
mode = "0440";
};
};
globals.services.${serviceName}.domain = serviceDomain;
services = {
nextcloud = {
enable = true;
settings = {
trusted_proxies = [ "0.0.0.0" ];
overwriteprotocol = "https";
};
package = pkgs.nextcloud31;
hostName = serviceDomain;
home = "/Vault/data/nextcloud";
datadir = "/Vault/data/nextcloud";
https = true;
configureRedis = true;
maxUploadSize = "4G";
extraApps = {
inherit (pkgs.nextcloud30Packages.apps) mail calendar contacts cospend phonetrack polls tasks sociallogin;
};
extraAppsEnable = true;
config = {
adminuser = "admin";
adminpassFile = config.sops.secrets.nextcloudadminpass.path;
dbtype = "sqlite";
};
};
};
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:80" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
};
};
};
};
};
};
}
#+end_src
**** immich
:PROPERTIES:
:CUSTOM_ID: h:33bad8ad-b362-4bf1-8a49-b9df92329aed
:END:
#+begin_src nix :tangle modules/nixos/server/immich.nix
{ lib, config, globals, ... }:
let
serviceDomain = "shots.swarsel.win";
servicePort = 3001;
serviceUser = "immich";
serviceName = "immich";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
users.users."${serviceUser}" = {
extraGroups = [ "video" "render" "users" ];
};
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals.services.${serviceName}.domain = serviceDomain;
services.immich = {
enable = true;
host = "0.0.0.0";
port = servicePort;
openFirewall = true;
mediaLocation = "/Vault/Eternor/Immich"; # dataDir
environment = {
IMMICH_MACHINE_LEARNING_URL = lib.mkForce "http://localhost:3003";
};
};
networking.firewall.allowedTCPPorts = [ 3001 ];
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
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;
'';
};
};
};
};
};
};
}
#+end_src
**** paperless (tika, gotenberg)
:PROPERTIES:
:CUSTOM_ID: h:89638fb5-0593-4420-9567-f85f0223e341
:END:
This is my personal document management system. It automatically pulls documents from several sources, the only manual step for physical documents is to put them in my scanner and use email delivery.
Also I install Tika and Gotenberg, which are needed to create PDFs out of =.eml='s. This is needed for e.g. online services that only send their invoices through email body text.
#+begin_src nix :tangle modules/nixos/server/paperless.nix
{ lib, pkgs, config, ... }:
let
serviceDomain = "scan.swarsel.win";
servicePort = 28981;
serviceUser = "paperless";
serviceGroup = serviceUser;
serviceName = "paperless";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
users.users."${serviceUser}" = {
extraGroups = [ "users" ];
};
sops.secrets = {
paperless_admin = { owner = serviceUser; };
kanidm-paperless-client = {
owner = serviceUser;
group = serviceGroup;
mode = "0440";
};
};
networking.firewall.allowedTCPPorts = [ servicePort ];
globals.services.${serviceName}.domain = serviceDomain;
services = {
paperless = {
enable = true;
mediaDir = "/Vault/Eternor/Paperless";
dataDir = "/Vault/data/paperless";
user = serviceUser;
port = servicePort;
passwordFile = config.sops.secrets.paperless_admin.path;
address = "0.0.0.0";
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";
};
PAPERLESS_TIKA_ENABLED = "true";
PAPERLESS_TIKA_ENDPOINT = "http://localhost:9998";
PAPERLESS_TIKA_GOTENBERG_ENDPOINT = "http://localhost:3002";
PAPERLESS_APPS = "allauth.socialaccount.providers.openid_connect";
PAPERLESS_SOCIALACCOUNT_PROVIDERS = builtins.toJSON {
openid_connect = {
OAUTH_PKCE_ENABLED = "True";
APPS = [
rec {
provider_id = "kanidm";
name = "Kanidm";
client_id = "paperless";
# secret will be added by paperless-web.service (see below)
#secret = "";
settings.server_url = "https://sso.swarsel.win/oauth2/openid/${client_id}/.well-known/openid-configuration";
}
];
};
};
};
};
tika = {
enable = true;
port = 9998;
openFirewall = false;
listenAddress = "127.0.0.1";
enableOcr = true;
};
gotenberg = {
enable = true;
package = pkgs.stable.gotenberg;
port = 3002;
bindIP = "127.0.0.1";
timeout = "600s";
chromium.package = pkgs.stable.chromium;
};
};
# Add secret to PAPERLESS_SOCIALACCOUNT_PROVIDERS
systemd.services.paperless-web.script = lib.mkBefore ''
oidcSecret=$(< ${config.sops.secrets.kanidm-paperless-client.path})
export PAPERLESS_SOCIALACCOUNT_PROVIDERS=$(
${pkgs.jq}/bin/jq <<< "$PAPERLESS_SOCIALACCOUNT_PROVIDERS" \
--compact-output \
--arg oidcSecret "$oidcSecret" '.openid_connect.APPS.[0].secret = $oidcSecret'
)
'';
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 0;
proxy_connect_timeout 300;
proxy_send_timeout 300;
proxy_read_timeout 300;
send_timeout 300;
'';
};
};
};
};
};
};
}
#+end_src
**** transmission
:PROPERTIES:
:CUSTOM_ID: h:5afeb311-ab86-4029-be53-2160f6d836c3
:END:
#+begin_src nix :tangle modules/nixos/server/transmission.nix
{ self, pkgs, lib, config, ... }:
let
serviceDomain = "store.swarsel.win";
lidarrUser = "lidarr";
lidarrGroup = lidarrUser;
lidarrPort = 8686;
radarrUser = "radarr";
radarrGroup = radarrUser;
radarrPort = 7878;
sonarrUser = "sonarr";
sonarrGroup = sonarrUser;
sonarrPort = 8989;
readarrUser = "readarr";
readarrGroup = readarrUser;
readarrPort = 8787;
prowlarrUser = "prowlarr";
prowlarrGroup = prowlarrUser;
prowlarrPort = 9696;
in
{
options.swarselsystems.modules.server.transmission = lib.mkEnableOption "enable transmission and friends on server";
config = lib.mkIf config.swarselsystems.modules.server.transmission {
# this user/group section is probably unneeded
users = {
groups = {
dockeruser = {
gid = 1155;
};
"${radarrGroup}" = { };
"${readarrGroup}" = { };
"${sonarrGroup}" = { };
"${lidarrGroup}" = { };
"${prowlarrGroup}" = { };
};
users = {
dockeruser = {
isSystemUser = true;
uid = 1155;
group = "docker";
extraGroups = [ "users" ];
};
"${radarrUser}" = {
isSystemUser = true;
group = radarrGroup;
extraGroups = [ "users" ];
};
"${readarrGroup}" = {
isSystemUser = true;
group = readarrGroup;
extraGroups = [ "users" ];
};
"${sonarrGroup}" = {
isSystemUser = true;
group = sonarrGroup;
extraGroups = [ "users" ];
};
"${lidarrUser}" = {
isSystemUser = true;
group = lidarrGroup;
extraGroups = [ "users" ];
};
"${prowlarrGroup}" = {
isSystemUser = true;
group = prowlarrGroup;
extraGroups = [ "users" ];
};
};
};
virtualisation.docker.enable = true;
environment.systemPackages = with pkgs; [
docker
];
topology.self.services = {
radarr.info = "https://${serviceDomain}/radarr";
readarr = {
name = "Readarr";
info = "https://${serviceDomain}/readarr";
icon = "${self}/topology/images/readarr.png";
};
sonarr.info = "https://${serviceDomain}/sonarr";
lidarr.info = "https://${serviceDomain}/lidarr";
prowlarr.info = "https://${serviceDomain}/prowlarr";
};
globals.services.transmission.domain = serviceDomain;
services = {
radarr = {
enable = true;
user = radarrUser;
group = radarrGroup;
settings.server.port = radarrPort;
openFirewall = true;
dataDir = "/Vault/data/radarr";
};
readarr = {
enable = true;
user = readarrUser;
group = readarrGroup;
settings.server.port = readarrPort;
openFirewall = true;
dataDir = "/Vault/data/readarr";
};
sonarr = {
enable = true;
user = sonarrUser;
group = sonarrGroup;
settings.server.port = sonarrPort;
openFirewall = true;
dataDir = "/Vault/data/sonarr";
};
lidarr = {
enable = true;
user = lidarrUser;
group = lidarrGroup;
settings.server.port = lidarrPort;
openFirewall = true;
dataDir = "/Vault/data/lidarr";
};
prowlarr = {
enable = true;
settings.server.port = prowlarrPort;
openFirewall = true;
};
nginx = {
virtualHosts = {
"${serviceDomain}" = {
enableACME = false;
forceSSL = false;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://localhost:9091";
extraConfig = ''
client_max_body_size 0;
'';
};
"/radarr" = {
proxyPass = "http://localhost:${builtins.toString radarrPort}";
extraConfig = ''
client_max_body_size 0;
'';
};
"/readarr" = {
proxyPass = "http://localhost:${builtins.toString readarrPort}";
extraConfig = ''
client_max_body_size 0;
'';
};
"/sonarr" = {
proxyPass = "http://localhost:${builtins.toString sonarrPort}";
extraConfig = ''
client_max_body_size 0;
'';
};
"/lidarr" = {
proxyPass = "http://localhost:${builtins.toString lidarrPort}";
extraConfig = ''
client_max_body_size 0;
'';
};
"/prowlarr" = {
proxyPass = "http://localhost:${builtins.toString prowlarrPort}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
};
};
}
#+end_src
**** syncthing
:PROPERTIES:
:CUSTOM_ID: h:ad2787a2-7b1c-4326-aeff-9d8d6c3f591d
:END:
#+begin_src nix :tangle modules/nixos/server/syncthing.nix
{ lib, config, ... }:
let
inherit (config.repo.secrets.common) workHostName;
serviceDomain = "storync.swarsel.win";
servicePort = 8384;
serviceUser = "syncthing";
serviceGroup = serviceUser;
serviceName = "syncthing";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
users.users."${serviceUser}" = {
extraGroups = [ "users" ];
group = serviceGroup;
isSystemUser = true;
};
users.groups."${serviceGroup}" = { };
networking.firewall.allowedTCPPorts = [ servicePort ];
globals.services.${serviceName}.domain = serviceDomain;
services.syncthing = {
enable = true;
user = serviceUser;
group = serviceGroup;
dataDir = "/Vault/data/syncthing";
configDir = "/Vault/data/syncthing/.config/syncthing";
guiAddress = "0.0.0.0:${builtins.toString servicePort}";
openDefaultPorts = true; # opens ports TCP/UDP 22000 and UDP 21027 for discovery
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";
};
"${workHostName}" = {
id = "YAPV4BV-I26WPTN-SIP32MV-SQP5TBZ-3CHMTCI-Z3D6EP2-MNDQGLP-53FT3AB";
};
"moonside@oracle" = {
id = "VPCDZB6-MGVGQZD-Q6DIZW3-IZJRJTO-TCC3QUQ-2BNTL7P-AKE7FBO-N55UNQE";
};
};
folders = {
"Default Folder" = lib.mkForce {
path = "/Vault/data/syncthing/Sync";
type = "receiveonly";
versioning = null;
devices = [ "sync@oracle" "magicant" "${workHostName}" "moonside@oracle" ];
id = "default";
};
"Obsidian" = {
path = "/Vault/data/syncthing/Obsidian";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "sync@oracle" "magicant" "${workHostName}" "moonside@oracle" ];
id = "yjvni-9eaa7";
};
"Org" = {
path = "/Vault/data/syncthing/Org";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "sync@oracle" "magicant" "${workHostName}" "moonside@oracle" ];
id = "a7xnl-zjj3d";
};
"Vpn" = {
path = "/Vault/data/syncthing/Vpn";
type = "receiveonly";
versioning = {
type = "simple";
params.keep = "5";
};
devices = [ "sync@oracle" "magicant" "${workHostName}" "moonside@oracle" ];
id = "hgp9s-fyq3p";
};
# "Documents" = {
# path = "/Vault/data/syncthing/Documents";
# type = "receiveonly";
# versioning = {
# type = "simple";
# params.keep = "5";
# };
# devices = [ "magicant" "${workHostName}" "moonside@oracle" ];
# id = "hgr3d-pfu3w";
# };
};
};
};
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
};
}
#+end_src
**** restic
:PROPERTIES:
:CUSTOM_ID: h:b73ac8bf-b721-4563-9eff-973925c99a39
:END:
This manages backups for my pictures and obsidian files.
#+begin_src nix :tangle modules/nixos/server/restic.nix
{ lib, pkgs, config, ... }:
let
inherit (config.repo.secrets.local) resticRepo;
in
{
options.swarselsystems.modules.server.restic = lib.mkEnableOption "enable restic backups on server";
config = lib.mkIf config.swarselsystems.modules.server.restic {
sops = {
secrets = {
resticpw = { };
resticaccesskey = { };
resticsecretaccesskey = { };
};
templates = {
"restic-env".content = ''
AWS_ACCESS_KEY_ID=${config.sops.placeholder.resticaccesskey}
AWS_SECRET_ACCESS_KEY=${config.sops.placeholder.resticsecretaccesskey}
'';
};
};
services.restic = {
backups = {
SwarselWinters = {
environmentFile = config.sops.templates."restic-env".path;
passwordFile = config.sops.secrets.resticpw.path;
paths = [
"/Vault/data/paperless"
"/Vault/Eternor/Paperless"
"/Vault/Eternor/Bilder"
"/Vault/Eternor/Immich"
];
pruneOpts = [
"--keep-daily 3"
"--keep-weekly 2"
"--keep-monthly 3"
"--keep-yearly 100"
];
backupPrepareCommand = ''
${pkgs.restic}/bin/restic prune
'';
repository = "${resticRepo}";
initialize = true;
timerConfig = {
OnCalendar = "03:00";
};
};
};
};
};
}
#+end_src
**** monitoring (Grafana)
:PROPERTIES:
:CUSTOM_ID: h:a31c7192-e11d-4a26-915d-1bbc38e373d3
:END:
This section exposes several metrics that I use to check the health of my server. I need to expand on the exporters section at some point, but for now I have everything I need.
#+begin_src nix :tangle modules/nixos/server/monitoring.nix
{ self, lib, config, ... }:
let
serviceDomain = "status.swarsel.win";
servicePort = 3000;
serviceUser = "grafana";
serviceGroup = serviceUser;
moduleName = "monitoring";
grafanaUpstream = "grafana";
prometheusUpstream = "prometheus";
prometheusPort = 9090;
prometheusWebRoot = "prometheus";
in
{
options.swarselsystems.modules.server."${moduleName}" = lib.mkEnableOption "enable ${moduleName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${moduleName}" {
sops.secrets = {
grafanaadminpass = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
prometheusadminpass = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
kanidm-grafana-client = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
users = {
users = {
nextcloud-exporter = {
extraGroups = [ "nextcloud" ];
};
"${serviceUser}" = {
extraGroups = [ "users" ];
};
};
};
networking.firewall.allowedTCPPorts = [ servicePort prometheusPort ];
topology.self.services.prometheus.info = "https://${serviceDomain}/${prometheusWebRoot}";
globals.services.${moduleName}.domain = serviceDomain;
services = {
grafana = {
enable = true;
dataDir = "/Vault/data/grafana";
provision = {
enable = true;
datasources.settings = {
datasources = [
{
name = "prometheus";
type = "prometheus";
url = "https://${serviceDomain}/prometheus";
editable = false;
access = "proxy";
basicAuth = true;
basicAuthUser = "admin";
jsonData = {
httpMethod = "POST";
manageAlerts = true;
prometheusType = "Prometheus";
prometheusVersion = "> 2.50.x";
cacheLevel = "High";
disableRecordingRules = false;
incrementalQueryOverlapWindow = "10m";
};
secureJsonData = {
basicAuthPassword = "$__file{/run/secrets/prometheusadminpass}";
};
}
];
};
};
settings = {
analytics.reporting_enabled = false;
users.allow_sign_up = false;
security = {
admin_password = "$__file{/run/secrets/grafanaadminpass}";
cookie_secure = true;
disable_gravatar = true;
};
server = {
domain = serviceDomain;
root_url = "https://${serviceDomain}";
http_port = servicePort;
http_addr = "0.0.0.0";
protocol = "http";
enforce_domain = true;
enable_gzip = true;
};
"auth.generic_oauth" = {
enabled = true;
name = "Kanidm";
icon = "signin";
allow_sign_up = true;
#auto_login = true;
client_id = "grafana";
client_secret = "$__file{${config.sops.secrets.kanidm-grafana-client.path}}";
scopes = "openid email profile";
login_attribute_path = "preferred_username";
auth_url = "https://sso.swarsel.win/ui/oauth2";
token_url = "https://sso.swarsel.win/oauth2/token";
api_url = "https://sso.swarsel.win/oauth2/openid/grafana/userinfo";
use_pkce = true;
use_refresh_token = true;
# Allow mapping oauth2 roles to server admin
allow_assign_grafana_admin = true;
role_attribute_path = "contains(groups[*], 'server_admin') && 'GrafanaAdmin' || contains(groups[*], 'admin') && 'Admin' || contains(groups[*], 'editor') && 'Editor' || 'Viewer'";
};
};
};
prometheus = {
enable = true;
webExternalUrl = "https://status.swarsel.win/${prometheusWebRoot}";
port = prometheusPort;
listenAddress = "0.0.0.0";
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.modules.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;
};
};
};
};
nodes.moonside.services.nginx = {
upstreams = {
"${grafanaUpstream}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
"${prometheusUpstream}" = {
servers = {
"192.168.1.2:${builtins.toString prometheusPort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${grafanaUpstream}";
proxyWebsockets = true;
extraConfig = ''
client_max_body_size 0;
'';
};
"/${prometheusWebRoot}" = {
proxyPass = "http://${prometheusUpstream}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
};
}
#+end_src
**** Jenkins
:PROPERTIES:
:CUSTOM_ID: h:23452a18-a0a1-4515-8612-ceb19bb5fc22
:END:
This is a WIP Jenkins instance. It is used to automatically build a new system when pushes to the main repository are detected. I have turned this service off for now however, as I actually prefer to start my builds manually.
#+begin_src nix :tangle modules/nixos/server/jenkins.nix
{ pkgs, lib, config, ... }:
let
serviceDomain = "servant.swarsel.win";
servicePort = 8088;
serviceName = "jenkins";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
services.jenkins = {
enable = true;
withCLI = true;
port = 8088;
packages = [ pkgs.stdenv pkgs.git pkgs.jdk17 config.programs.ssh.package pkgs.nix ];
listenAddress = "0.0.0.0";
home = "/Vault/apps/jenkins";
};
services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
};
}
#+end_src
**** Emacs elfeed (RSS Server)
:PROPERTIES:
:CUSTOM_ID: h:4e6824bc-c3db-485d-b543-4072e6283b62
:END:
This was an approach of hosting an RSS server from within emacs. That would have been useful as it would have allowed me to allow my feeds from any device. However, it proved impossible to do bidirectional syncing, so I abandoned this configuration in favor of [[#h:9da3df74-6fc5-4ee1-a345-23ab4e8a613d][FreshRSS]].
#+begin_src nix :tangle modules/nixos/server/emacs.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.server.emacs = lib.mkEnableOption "enable emacs server on server";
config = lib.mkIf config.swarselsystems.modules.server.emacs {
networking.firewall.allowedTCPPorts = [ 9812 ];
services.emacs = {
enable = true;
install = true;
startWithGraphical = false;
};
};
}
#+end_src
**** FreshRSS
:PROPERTIES:
:CUSTOM_ID: h:9da3df74-6fc5-4ee1-a345-23ab4e8a613d
:END:
FreshRSS is a more 'classical' RSS aggregator that I can just host as a distinct service. This also has its upsides because I jave more control over the state this way.
It serves both a Greader API at https://signpost.swarsel.win/api/greader.php, as well as a Fever API at https://signpost.swarsel.win/api/fever.php.
I am using this with CapyReader on my phone, set it up as a FreshRSS account with Server URL =https://signpost.swarsel.win/api/greader.php
FreshRSS claims to support HTTP header auth, but at least it does not work with my oauth2-proxy setup. Until this is fixed, I resorted to the "form" login, since I mostly do not use the web version anyways.
#+begin_src nix :tangle modules/nixos/server/freshrss.nix
{ self, lib, config, ... }:
let
serviceName = "freshrss";
serviceDomain = "signpost.swarsel.win";
serviceUser = "freshrss";
serviceGroup = serviceName;
in
{
options.swarselsystems.modules.server.freshrss = lib.mkEnableOption "enable freshrss on server";
config = lib.mkIf config.swarselsystems.modules.server.freshrss {
users.users."${serviceUser}" = {
extraGroups = [ "users" ];
group = serviceGroup;
isSystemUser = true;
};
users.groups."${serviceGroup}" = { };
sops = {
secrets = {
fresh = { owner = serviceUser; };
"kanidm-freshrss-client" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
"oidc-crypto-key" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
# templates = {
# "freshrss-env" = {
# content = ''
# DATA_PATH=${config.services.freshrss.dataDir}
# OIDC_ENABLED=1
# OIDC_PROVIDER_METADATA_URL=https://sso.swarsel.win/.well-known/openid-configuration
# OIDC_CLIENT_ID=freshrss
# OIDC_CLIENT_SECRET=${config.sops.placeholder.kanidm-freshrss-client}
# OIDC_CLIENT_CRYPTO_KEY=${config.sops.placeholder.oidc-crypto-key}
# OIDC_REMOTE_USER_CLAIM=preferred_username
# OIDC_SCOPES=openid groups email profile
# OIDC_X_FORWARDED_HEADERS=X-Forwarded-Host X-Forwarded-Port X-Forwarded-Proto
# '';
# owner = "freshrss";
# group = "freshrss";
# mode = "0440";
# };
# };
};
topology.self.services.${serviceName} = {
name = "FreshRSS";
info = "https://${serviceDomain}";
icon = "${self}/topology/images/freshrss.png";
};
globals.services.${serviceName}.domain = serviceDomain;
services.freshrss = {
enable = true;
virtualHost = serviceDomain;
baseUrl = "https://${serviceDomain}";
authType = "form";
dataDir = "/Vault/data/tt-rss";
defaultUser = "Swarsel";
passwordFile = config.sops.secrets.fresh.path;
};
# systemd.services.freshrss-config.serviceConfig.EnvironmentFile = [
# config.sops.templates.freshrss-env.path
# ];
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:80" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
oauth2.enable = true;
oauth2.allowedGroups = [ "ttrss_access" ];
locations = {
"/" = {
proxyPass = "http://${serviceName}";
};
"/api" = {
proxyPass = "http://${serviceName}";
setOauth2Headers = false;
bypassAuth = true;
};
};
};
};
};
};
}
#+end_src
**** forgejo (git server)
:PROPERTIES:
:CUSTOM_ID: h:a9965660-4358-4b9a-8c46-d55f28598344
:END:
#+begin_src nix :tangle modules/nixos/server/forgejo.nix
{ lib, config, pkgs, ... }:
let
serviceDomain = "swagit.swarsel.win";
servicePort = 3000;
serviceUser = "forgejo";
serviceGroup = serviceUser;
serviceName = "forgejo";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
networking.firewall.allowedTCPPorts = [ servicePort ];
users.users."${serviceUser}" = {
group = serviceGroup;
isSystemUser = true;
};
users.groups."${serviceGroup}" = { };
sops.secrets = {
kanidm-forgejo-client = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
globals.services.${serviceName}.domain = serviceDomain;
services.forgejo = {
enable = true;
user = serviceUser;
group = serviceGroup;
lfs.enable = lib.mkDefault true;
settings = {
DEFAULT = {
APP_NAME = "~SwaGit~";
};
server = {
PROTOCOL = "http";
HTTP_PORT = servicePort;
HTTP_ADDR = "0.0.0.0";
DOMAIN = serviceDomain;
ROOT_URL = "https://${serviceDomain}";
};
# federation.ENABLED = true;
service = {
DISABLE_REGISTRATION = false;
ALLOW_ONLY_INTERNAL_REGISTRATION = false;
ALLOW_ONLY_EXTERNAL_REGISTRATION = true;
SHOW_REGISTRATION_BUTTON = false;
};
session.COOKIE_SECURE = true;
oauth2_client = {
# Never use auto account linking with this, otherwise users cannot change
# their new user name and they could potentially overtake other users accounts
# by setting their email address to an existing account.
# With "login" linking the user must choose a non-existing username first or login
# with the existing account to link.
ACCOUNT_LINKING = "login";
USERNAME = "nickname";
# This does not mean that you cannot register via oauth, but just that there should
# be a confirmation dialog shown to the user before the account is actually created.
# This dialog allows changing user name and email address before creating the account.
ENABLE_AUTO_REGISTRATION = false;
REGISTER_EMAIL_CONFIRM = false;
UPDATE_AVATAR = true;
};
};
};
systemd.services.forgejo = {
serviceConfig.RestartSec = "60"; # Retry every minute
preStart =
let
exe = lib.getExe config.services.forgejo.package;
providerName = "kanidm";
clientId = "forgejo";
args = lib.escapeShellArgs (
lib.concatLists [
[
"--name"
providerName
]
[
"--provider"
"openidConnect"
]
[
"--key"
clientId
]
[
"--auto-discover-url"
"https://sso.swarsel.win/oauth2/openid/${clientId}/.well-known/openid-configuration"
]
[
"--scopes"
"email"
]
[
"--scopes"
"profile"
]
[
"--group-claim-name"
"groups"
]
[
"--admin-group"
"admin"
]
[ "--skip-local-2fa" ]
]
);
in
lib.mkAfter ''
provider_id=$(${exe} admin auth list | ${pkgs.gnugrep}/bin/grep -w '${providerName}' | cut -f1)
SECRET="$(< ${config.sops.secrets.kanidm-forgejo-client.path})"
if [[ -z "$provider_id" ]]; then
${exe} admin auth add-oauth ${args} --secret "$SECRET"
else
${exe} admin auth update-oauth --id "$provider_id" ${args} --secret "$SECRET"
fi
'';
};
services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
};
}
#+end_src
**** Anki Sync Server
:PROPERTIES:
:CUSTOM_ID: h:cb3f6552-7751-4f9a-b4c7-8d8ba5b255c4
:END:
#+begin_src nix :tangle modules/nixos/server/ankisync.nix
{ lib, config, ... }:
let
serviceDomain = "synki.swarsel.win";
servicePort = 27701;
serviceName = "ankisync";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
networking.firewall.allowedTCPPorts = [ servicePort ];
sops.secrets.swarsel = { owner = "root"; };
topology.self.services.${serviceName} = {
name = lib.mkForce "Anki Sync Server";
info = "https://${serviceDomain}";
};
globals.services.${serviceName}.domain = serviceDomain;
services.anki-sync-server = {
enable = true;
port = servicePort;
address = "0.0.0.0";
openFirewall = true;
users = [
{
username = "Swarsel";
passwordFile = config.sops.secrets.swarsel.path;
}
];
};
services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
};
}
#+end_src
**** kanidm
:PROPERTIES:
:CUSTOM_ID: h:ee625136-29ab-4696-919f-7b0d0042f6dd
:END:
The forgejo configuration is a little broken and will show a 500 error when signing in through kanidm. However, when pressing back and refreshing the page, I am logged in. Currently I cannot be bothered to fix this.
A stupid (but simple) way to get the =originUrl= is to simply set any URL there and try to auth using kanidm. Then check the logs (=journalctl -eu kanidm=) and check for the line that says something along the lines of
`🚧 [warn]: Invalid OAuth2 redirect_uri (must be an exact match to a redirect-url) - got `
To get other URLs (token, etc.), use https:///oauth2/openid//.well-known/oauth-authorization-server, e.g. https://sso.swarsel.win/oauth2/openid/nextcloud/.well-known/oauth-authorization-server, with clienID being the client name as specified in kanidm.
#+begin_src nix :tangle modules/nixos/server/kanidm.nix
{ self, lib, pkgs, config, globals, ... }:
let
certsSopsFile = self + /secrets/certs/secrets.yaml;
serviceDomain = "sso.swarsel.win";
servicePort = 8300;
serviceUser = "kanidm";
serviceGroup = serviceUser;
serviceName = "kanidm";
oauth2ProxyDomain = globals.services.oauth2Proxy.domain;
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
users.users."${serviceUser}" = {
group = serviceGroup;
isSystemUser = true;
};
users.groups."${serviceGroup}" = { };
sops = {
secrets = {
"kanidm-self-signed-crt" = { sopsFile = certsSopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-self-signed-key" = { sopsFile = certsSopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-admin-pw" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-idm-admin-pw" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-immich" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-paperless" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-forgejo" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-grafana" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-nextcloud" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-freshrss" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
"kanidm-oauth2-proxy" = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
};
networking.firewall.allowedTCPPorts = [ servicePort ];
globals.services.${serviceName}.domain = serviceDomain;
services = {
kanidm = {
package = pkgs.kanidmWithSecretProvisioning;
enableServer = true;
serverSettings = {
domain = serviceDomain;
origin = "https://${serviceDomain}";
tls_chain = config.sops.secrets.kanidm-self-signed-crt.path;
tls_key = config.sops.secrets.kanidm-self-signed-key.path;
bindaddress = "0.0.0.0:${toString servicePort}";
trust_x_forward_for = true;
};
enableClient = true;
clientSettings = {
uri = config.services.kanidm.serverSettings.origin;
verify_ca = true;
verify_hostnames = true;
};
provision = {
enable = true;
adminPasswordFile = config.sops.secrets.kanidm-admin-pw.path;
idmAdminPasswordFile = config.sops.secrets.kanidm-idm-admin-pw.path;
groups = {
"immich.access" = { };
"paperless.access" = { };
"forgejo.access" = { };
"forgejo.admins" = { };
"grafana.access" = { };
"grafana.editors" = { };
"grafana.admins" = { };
"grafana.server-admins" = { };
"nextcloud.access" = { };
"nextcloud.admins" = { };
"navidrome.access" = { };
"freshrss.access" = { };
"firefly.access" = { };
"radicale.access" = { };
};
inherit (config.repo.secrets.local) persons;
systems = {
oauth2 = {
immich = {
displayName = "Immich";
originUrl = [
"https://shots.swarsel.win/auth/login"
"https://shots.swarsel.win/user-settings"
"app.immich:///oauth-callback"
"https://shots.swarsel.win/api/oauth/mobile-redirect"
];
originLanding = "https://shots.swarsel.win/";
basicSecretFile = config.sops.secrets.kanidm-immich.path;
preferShortUsername = true;
enableLegacyCrypto = true; # can use RS256 / HS256, not ES256
scopeMaps."immich.access" = [
"openid"
"email"
"profile"
];
};
paperless = {
displayName = "Paperless";
originUrl = "https://scan.swarsel.win/accounts/oidc/kanidm/login/callback/";
originLanding = "https://scan.swarsel.win/";
basicSecretFile = config.sops.secrets.kanidm-paperless.path;
preferShortUsername = true;
scopeMaps."paperless.access" = [
"openid"
"email"
"profile"
];
};
forgejo = {
displayName = "Forgejo";
originUrl = "https://swagit.swarsel.win/user/oauth2/kanidm/callback";
originLanding = "https://swagit.swarsel.win/";
basicSecretFile = config.sops.secrets.kanidm-forgejo.path;
scopeMaps."forgejo.access" = [
"openid"
"email"
"profile"
];
# XXX: PKCE is currently not supported by gitea/forgejo,
# see https://github.com/go-gitea/gitea/issues/21376.
allowInsecureClientDisablePkce = true;
preferShortUsername = true;
claimMaps.groups = {
joinType = "array";
valuesByGroup."forgejo.admins" = [ "admin" ];
};
};
grafana = {
displayName = "Grafana";
originUrl = "https://status.swarsel.win/login/generic_oauth";
originLanding = "https://status.swarsel.win/";
basicSecretFile = config.sops.secrets.kanidm-grafana.path;
preferShortUsername = true;
scopeMaps."grafana.access" = [
"openid"
"email"
"profile"
];
claimMaps.groups = {
joinType = "array";
valuesByGroup = {
"grafana.editors" = [ "editor" ];
"grafana.admins" = [ "admin" ];
"grafana.server-admins" = [ "server_admin" ];
};
};
};
nextcloud = {
displayName = "Nextcloud";
originUrl = " https://stash.swarsel.win/apps/sociallogin/custom_oidc/kanidm";
originLanding = "https://stash.swarsel.win/";
basicSecretFile = config.sops.secrets.kanidm-nextcloud.path;
allowInsecureClientDisablePkce = true;
scopeMaps."nextcloud.access" = [
"openid"
"email"
"profile"
];
preferShortUsername = true;
claimMaps.groups = {
joinType = "array";
valuesByGroup = {
"nextcloud.admins" = [ "admin" ];
};
};
};
oauth2-proxy = {
displayName = "Oauth2-Proxy";
originUrl = "https://${oauth2ProxyDomain}/oauth2/callback";
originLanding = "https://${oauth2ProxyDomain}/";
basicSecretFile = config.sops.secrets.kanidm-oauth2-proxy.path;
scopeMaps = {
"freshrss.access" = [
"openid"
"email"
"profile"
];
"navidrome.access" = [
"openid"
"email"
"profile"
];
"firefly.access" = [
"openid"
"email"
"profile"
];
"radicale.access" = [
"openid"
"email"
"profile"
];
};
preferShortUsername = true;
claimMaps.groups = {
joinType = "array";
valuesByGroup = {
"freshrss.access" = [ "ttrss_access" ];
"navidrome.access" = [ "navidrome_access" ];
"firefly.access" = [ "firefly_access" ];
"radicale.access" = [ "radicale_access" ];
};
};
};
};
};
};
};
};
systemd.services = {
kanidm.serviceConfig.RestartSec = "30";
};
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "https://${serviceName}";
};
};
extraConfig = ''
proxy_ssl_verify off;
'';
};
};
};
};
}
#+end_src
**** oauth2-proxy
:PROPERTIES:
:CUSTOM_ID: h:605f5974-e985-4572-b353-fd1d3ccbadae
:END:
#+begin_src nix :tangle modules/nixos/server/oauth2-proxy.nix
{ lib, config, globals, ... }:
let
kanidmDomain = globals.services.kanidm.domain;
oauth2ProxyDomain = "soauth.swarsel.win";
oauth2ProxyPort = 3004;
in
{
options = {
swarselsystems.modules.server.oauth2Proxy = lib.mkEnableOption "enable oauth2-proxy on server";
# largely based on https://github.com/oddlama/nix-config/blob/main/modules/oauth2-proxy.nix
services.nginx.virtualHosts = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
options.oauth2 = {
enable = lib.mkEnableOption "access protection of this virtualHost using oauth2-proxy.";
allowedGroups = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
A list of kanidm groups that are allowed to access this resource, or the
empty list to allow any authenticated client.
'';
};
X-User = lib.mkOption {
type = lib.types.str;
default = "$upstream_http_x_auth_request_user";
description = "The variable to set as X-User";
};
X-Email = lib.mkOption {
type = lib.types.str;
default = "$upstream_http_x_auth_request_email";
description = "The variable to set as X-Email";
};
X-Access-Token = lib.mkOption {
type = lib.types.str;
default = "$upstream_http_x_auth_request_access_token";
description = "The variable to set as X-Access-Token";
};
};
options.locations = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (locationSubmodule: {
options = {
setOauth2Headers = lib.mkOption {
type = lib.types.bool;
default = true;
description = "Whether to add oauth2 headers to this location. Only takes effect is oauth2 is actually enabled on the parent vhost.";
};
bypassAuth = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Whether to set auth_request off for this location. Only takes effect is oauth2 is actually enabled on the parent vhost.";
};
};
config = lib.mkIf config.oauth2.enable {
extraConfig = lib.optionalString locationSubmodule.config.setOauth2Headers ''
proxy_set_header X-User $user;
proxy_set_header Remote-User $user;
proxy_set_header X-Remote-User $user;
proxy_set_header X-Email $email;
# proxy_set_header X-Access-Token $token;
add_header Set-Cookie $auth_cookie;
'' + lib.optionalString locationSubmodule.config.bypassAuth ''
auth_request off;
'';
};
})
);
};
config = lib.mkIf config.oauth2.enable {
extraConfig = ''
auth_request /oauth2/auth;
error_page 401 = /oauth2/sign_in;
# set variables that can be used in locations..extraConfig
# pass information via X-User and X-Email headers to backend,
# requires running with --set-xauthrequest flag
auth_request_set $user ${config.oauth2.X-User};
auth_request_set $email ${config.oauth2.X-Email};
# if you enabled --pass-access-token, this will pass the token to the backend
# auth_request_set $token ${config.oauth2.X-Access-Token};
# if you enabled --cookie-refresh, this is needed for it to work with auth_request
auth_request_set $auth_cookie $upstream_http_set_cookie;
'';
locations = {
"/oauth2/" = {
proxyPass = "http://oauth2-proxy";
setOauth2Headers = false;
bypassAuth = true;
extraConfig = ''
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
'';
};
"= /oauth2/auth" = {
proxyPass = "http://oauth2-proxy/oauth2/auth" + lib.optionalString (config.oauth2.allowedGroups != [ ]) "?allowed_groups=${lib.concatStringsSep "," config.oauth2.allowedGroups}";
setOauth2Headers = false;
bypassAuth = true;
extraConfig = ''
internal;
proxy_set_header X-Scheme $scheme;
# nginx auth_request includes headers but not body
proxy_set_header Content-Length "";
proxy_pass_request_body off;
'';
};
};
};
}
)
);
};
};
config = lib.mkIf config.swarselsystems.modules.server.oauth2Proxy {
sops = {
secrets = {
"oauth2-cookie-secret" = { owner = "oauth2-proxy"; group = "oauth2-proxy"; mode = "0440"; };
"kanidm-oauth2-proxy-client" = { owner = "oauth2-proxy"; group = "oauth2-proxy"; mode = "0440"; };
};
templates = {
"kanidm-oauth2-proxy-client-env" = {
content = ''
OAUTH2_PROXY_CLIENT_SECRET="${config.sops.placeholder.kanidm-oauth2-proxy-client}"
OAUTH2_PROXY_COOKIE_SECRET=${config.sops.placeholder.oauth2-cookie-secret}
'';
owner = "oauth2-proxy";
group = "oauth2-proxy";
mode = "0440";
};
};
};
networking.firewall.allowedTCPPorts = [ oauth2ProxyPort ];
globals.services.oauth2Proxy.domain = oauth2ProxyDomain;
services = {
oauth2-proxy = {
enable = true;
cookie = {
domain = ".swarsel.win";
secure = true;
expire = "900m";
secret = null; # set by service EnvironmentFile
};
clientSecret = null; # set by service EnvironmentFile
reverseProxy = true;
httpAddress = "0.0.0.0:${builtins.toString oauth2ProxyPort}";
redirectURL = "https://${oauth2ProxyDomain}/oauth2/callback";
setXauthrequest = true;
extraConfig = {
code-challenge-method = "S256";
whitelist-domain = ".swarsel.win";
set-authorization-header = true;
pass-access-token = true;
skip-jwt-bearer-tokens = true;
upstream = "static://202";
oidc-issuer-url = "https://${kanidmDomain}/oauth2/openid/oauth2-proxy";
provider-display-name = "Kanidm";
};
provider = "oidc";
scope = "openid email";
loginURL = "https://${kanidmDomain}/ui/oauth2";
redeemURL = "https://${kanidmDomain}/oauth2/token";
validateURL = "https://${kanidmDomain}/oauth2/openid/oauth2-proxy/userinfo";
clientID = "oauth2-proxy";
email.domains = [ "*" ];
};
};
systemd.services = {
oauth2-proxy = {
# after = [ "kanidm.service" ];
serviceConfig = {
RuntimeDirectory = "oauth2-proxy";
RuntimeDirectoryMode = "0750";
UMask = "007"; # TODO remove once https://github.com/oauth2-proxy/oauth2-proxy/issues/2141 is fixed
RestartSec = "60"; # Retry every minute
EnvironmentFile = [
config.sops.templates.kanidm-oauth2-proxy-client-env.path
];
};
};
};
services.nginx = {
upstreams = {
oauth2-proxy = {
servers = {
"localhost:${builtins.toString oauth2ProxyPort}" = { };
};
};
};
virtualHosts = {
"${oauth2ProxyDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://oauth2-proxy";
};
};
extraConfig = ''
proxy_set_header X-Scheme $scheme;
proxy_set_header X-Auth-Request-Redirect $scheme://$host$request_uri;
'';
};
};
};
};
}
#+end_src
**** Firefly-III
:PROPERTIES:
:CUSTOM_ID: h:4248e9eb-4b9f-4771-bbfb-7186ef7a8331
:END:
#+begin_src nix :tangle modules/nixos/server/firefly-iii.nix
{ self, lib, config, ... }:
let
cfg = config.services.firefly-iii;
serviceDomain = "stonks.swarsel.win";
fireflyUser = "firefly-iii";
serviceName = "firefly";
in
{
options.swarselsystems.modules.server.firefly = lib.mkEnableOption "enable firefly-iii on server";
config = lib.mkIf config.swarselsystems.modules.server.firefly {
users.users.firefly-iii = {
group = "nginx";
isSystemUser = true;
};
sops = {
secrets = {
"firefly-iii-app-key" = { owner = fireflyUser; group = "nginx"; mode = "0440"; };
};
};
topology.self.services.firefly-iii = {
name = "Firefly-III";
info = "https://${serviceDomain}";
icon = "${self}/topology/images/firefly-iii.png";
};
globals.services.${serviceName}.domain = serviceDomain;
services = {
firefly-iii = {
enable = true;
user = fireflyUser;
group = if cfg.enableNginx then "nginx" else fireflyUser;
dataDir = "/Vault/data/firefly-iii";
settings = {
TZ = config.repo.secrets.common.location.timezone;
APP_URL = "https://${serviceDomain}";
APP_KEY_FILE = config.sops.secrets.firefly-iii-app-key.path;
APP_ENV = "local";
DB_CONNECTION = "sqlite";
TRUSTED_PROXIES = "**";
# turning these on breaks api access using the waterfly app
# AUTHENTICATION_GUARD = "remote_user_guard";
# AUTHENTICATION_GUARD_HEADER = "X-User";
# AUTHENTICATION_GUARD_EMAIL = "X-Email";
};
enableNginx = true;
virtualHost = serviceDomain;
};
nginx = {
virtualHosts = {
"${serviceDomain}" = {
locations = {
"/api" = {
setOauth2Headers = false;
extraConfig = ''
index index.php;
try_files $uri $uri/ /index.php?$query_string;
add_header Access-Control-Allow-Methods 'GET, POST, HEAD, OPTIONS';
'';
};
};
};
};
};
};
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:80" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
oauth2.enable = true;
oauth2.allowedGroups = [ "firefly_access" ];
# main config is automatically added by nixos firefly config.
# hence, only provide certificate
locations = {
"/" = {
proxyPass = "http://${serviceName}";
};
"/api" = {
proxyPass = "http://${serviceName}";
setOauth2Headers = false;
bypassAuth = true;
};
};
};
};
};
};
}
#+end_src
**** Koillection
:PROPERTIES:
:CUSTOM_ID: h:09c0fed3-b9c6-487f-a5f6-49be039e5fa2
:END:
#+begin_src nix :tangle modules/nixos/server/koillection.nix
{ self, lib, config, ... }:
let
serviceDomain = "swag.swarsel.win";
serviceUser = "koillection";
serviceDB = "koillection";
serviceName = "koillection";
servicePort = 2282;
postgresUser = config.systemd.services.postgresql.serviceConfig.User; # postgres
postgresPort = config.services.postgresql.settings.port; # 5432
containerRev = "sha256:96693e41a6eb2aae44f96033a090378270f024ddf4e6095edf8d57674f21095d";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
sops.secrets = {
koillection-db-password = { owner = postgresUser; group = postgresUser; mode = "0440"; };
koillection-env-file = { };
};
topology.self.services.koillection = {
name = "Koillection";
info = "https://${serviceDomain}";
icon = "${self}/topology/images/koillection.png";
};
globals.services.${serviceName}.domain = serviceDomain;
virtualisation.oci-containers.containers = {
koillection = {
image = "koillection/koillection@${containerRev}";
ports = [
"${toString servicePort}:80"
];
environment = {
APP_DEBUG = "0";
APP_ENV = "prod";
HTTPS_ENABLED = "1";
UPLOAD_MAX_FILESIZE = "512M";
PHP_MEMORY_LIMIT = "512M";
PHP_TZ = config.repo.secrets.common.location.timezone;
CORS_ALLOW_ORIGIN = "https?://(localhost|swag\\.swarsel\\.win)(:[0-9]+)?$";
DB_DRIVER = "pdo_pgsql";
DB_NAME = serviceDB;
DB_HOST = "host.docker.internal";
DB_USER = serviceUser;
# DB_PASSWORD set in koillection-env-file
DB_PORT = "${toString postgresPort}";
DB_VERSION = "16";
};
environmentFiles = [
config.sops.secrets.koillection-env-file.path
];
extraOptions = [
"--add-host=host.docker.internal:host-gateway" # podman
];
};
};
networking.firewall.allowedTCPPorts = [ servicePort postgresPort ];
systemd.services.postgresql.postStart =
let
passwordPath = config.sops.secrets.koillection-db-password.path;
in
''
$PSQL -tA <<'EOF'
DO $$
DECLARE password TEXT;
BEGIN
password := trim(both from replace(pg_read_file('${passwordPath}'), E'\n', '''));
EXECUTE format('ALTER ROLE ${serviceDB} WITH PASSWORD '''%s''';', password);
END $$;
EOF
'';
services = {
postgresql = {
enable = true;
enableTCPIP = true;
ensureDatabases = [ serviceDB ];
ensureUsers = [
{
name = serviceDB;
ensureDBOwnership = true;
}
];
authentication = ''
host ${serviceDB} ${serviceDB} 10.88.0.0/16 scram-sha-256
'';
};
};
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
};
};
};
};
};
};
}
#+end_src
**** Atuin
:PROPERTIES:
:CUSTOM_ID: h:27eac8b9-c202-4e45-9b80-42592f1e41c8
:END:
#+begin_src nix :tangle modules/nixos/server/atuin.nix
{ lib, config, ... }:
let
serviceDomain = "shellhistory.swarsel.win";
servicePort = 8888;
serviceName = "atuin";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals.services.${serviceName}.domain = serviceDomain;
services.atuin = {
enable = true;
host = "0.0.0.0";
port = servicePort;
openFirewall = true;
openRegistration = false;
};
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 0;
'';
};
};
};
};
};
};
}
#+end_src
**** Radicale
:PROPERTIES:
:CUSTOM_ID: h:c1ca2d28-51d2-45bd-83b5-05007ae94ae6
:END:
#+begin_src nix :tangle modules/nixos/server/radicale.nix
{ self, lib, config, ... }:
let
inherit (config.repo.secrets.local.radicale) user1;
sopsFile = self + /secrets/winters/secrets2.yaml;
serviceDomain = "schedule.swarsel.win";
servicePort = 8000;
serviceName = "radicale";
serviceUser = "radicale";
serviceGroup = serviceUser;
cfg = config.services."${serviceName}";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
sops = {
secrets.radicale-user = { inherit sopsFile; owner = serviceUser; group = serviceGroup; mode = "0440"; };
templates = {
"radicale-users" = {
content = ''
${user1}:${config.sops.placeholder.radicale-user}
'';
owner = serviceUser;
group = serviceGroup;
mode = "0440";
};
};
};
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals.services.${serviceName}.domain = serviceDomain;
services.radicale = {
enable = true;
settings = {
server = {
hosts = [
"0.0.0.0:${builtins.toString servicePort}"
"[::]:${builtins.toString servicePort}"
];
};
auth = {
type = "htpasswd";
htpasswd_filename = config.sops.templates.radicale-users.path;
htpasswd_encryption = "autodetect";
};
storage = {
filesystem_folder = "/Vault/data/radicale/collections";
};
};
rights = {
# all: match authenticated users only
root = {
user = ".+";
collection = "";
permissions = "R";
};
principal = {
user = ".+";
collection = "{user}";
permissions = "RW";
};
calendars = {
user = ".+";
collection = "{user}/[^/]+";
permissions = "rw";
};
};
};
systemd.tmpfiles.rules = [
"d '${cfg.settings.storage.filesystem_folder}' 0750 ${serviceUser} ${serviceGroup} - -"
];
networking.firewall.allowedTCPPorts = [ servicePort ];
networking.firewall.allowedUDPPorts = [ servicePort ];
nodes.moonside.services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"192.168.1.2:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
oauth2.enable = false;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 16M;
'';
};
};
};
};
};
};
}
#+end_src
**** croc
:PROPERTIES:
:CUSTOM_ID: h:f922e8d6-f6e8-4779-a7ad-4037229c9bf0
:END:
#+begin_src nix :tangle modules/nixos/server/croc.nix
{ lib, config, pkgs, ... }:
let
serviceDomain = "send.swarsel.win";
servicePorts = [
9009
9010
9011
9012
9013
];
serviceName = "croc";
cfg = config.services.croc;
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
sops = {
secrets = {
croc-password = { };
};
templates = {
"croc-env" = {
content = ''
CROC_PASS="${config.sops.placeholder.croc-password}"
'';
};
};
};
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals.services.${serviceName}.domain = serviceDomain;
services.croc = {
enable = true;
ports = servicePorts;
pass = config.sops.secrets.croc-password.path;
openFirewall = true;
};
systemd.services = {
"${serviceName}" = {
serviceConfig = {
ExecStart = lib.mkForce "${pkgs.croc}/bin/croc ${lib.optionalString cfg.debug "--debug"} relay --ports ${
lib.concatMapStringsSep "," toString cfg.ports}";
EnvironmentFile = [
config.sops.templates.croc-env.path
];
};
};
};
# ports are opened on the firewall for croc, no nginx config
};
}
#+end_src
**** microbin
:PROPERTIES:
:CUSTOM_ID: h:13071cc3-5cba-44b5-8b5b-2a27be22e021
:END:
#+begin_src nix :tangle modules/nixos/server/microbin.nix
{ lib, config, ... }:
let
serviceDomain = "scratch.swarsel.win";
servicePort = 8777;
serviceName = "microbin";
serviceUser = "microbin";
serviceGroup = serviceUser;
cfg = config.services."${serviceName}";
in
{
options.swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
users = {
groups."${serviceGroup}" = { };
users."${serviceUser}" = {
isSystemUser = true;
group = serviceGroup;
};
};
sops = {
secrets = {
microbin-admin-username = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
microbin-admin-password = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
microbin-uploader-password = { owner = serviceUser; group = serviceGroup; mode = "0440"; };
};
templates = {
"microbin-env" = {
content = ''
MICROBIN_ADMIN_USERNAME="${config.sops.placeholder.microbin-admin-username}"
MICROBIN_ADMIN_PASSWORD="${config.sops.placeholder.microbin-admin-password}"
MICROBIN_UPLOADER_PASSWORD="${config.sops.placeholder.microbin-uploader-password}"
'';
owner = serviceUser;
group = serviceGroup;
mode = "0440";
};
};
};
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals.services.${serviceName}.domain = serviceDomain;
services."${serviceName}" = {
enable = true;
passwordFile = config.sops.templates.microbin-env.path;
dataDir = "/var/lib/microbin";
settings = {
MICROBIN_HIDE_LOGO = true;
MICROBIN_PORT = servicePort;
MICROBIN_EDITABLE = true;
MICROBIN_HIDE_HEADER = true;
MICROBIN_HIDE_FOOTER = true;
MICROBIN_NO_LISTING = false;
MICROBIN_HIGHLIGHTSYNTAX = true;
MICROBIN_BIND = "0.0.0.0";
MICROBIN_PRIVATE = true;
MICROBIN_PUBLIC_PATH = "https://${serviceDomain}";
MICROBIN_READONLY = true;
MICROBIN_SHOW_READ_STATS = true;
MICROBIN_TITLE = "~SwarselScratch~";
MICROBIN_THREADS = 1;
MICROBIN_GC_DAYS = 30;
MICROBIN_ENABLE_BURN_AFTER = true;
MICROBIN_QR = true;
MICROBIN_ETERNAL_PASTA = true;
MICROBIN_ENABLE_READONLY = true;
MICROBIN_DEFAULT_EXPIRY = "1week";
MICROBIN_NO_FILE_UPLOAD = false;
MICROBIN_MAX_FILE_SIZE_ENCRYPTED_MB = 256;
MICROBIN_MAX_FILE_SIZE_UNENCRYPTED_MB = 1024;
MICROBIN_DISABLE_UPDATE_CHECKING = true;
MICROBIN_DISABLE_TELEMETRY = true;
MICROBIN_LIST_SERVER = false;
};
};
systemd.services = {
"${serviceName}" = {
serviceConfig = {
DynamicUser = lib.mkForce false;
User = serviceUser;
Group = serviceGroup;
};
};
};
networking.firewall.allowedTCPPorts = [ servicePort ];
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{ directory = cfg.dataDir; user = serviceUser; group = serviceGroup; mode = "0700"; }
];
services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"localhost:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
extraConfig = ''
client_max_body_size 1G;
'';
};
};
};
};
};
};
}
#+end_src
**** shlink
:PROPERTIES:
:CUSTOM_ID: h:4ccdcd5c-a4dd-49e4-94e7-d81db970059c
:END:
#+begin_src nix :tangle modules/nixos/server/shlink.nix
{ lib, config, ... }:
let
serviceDomain = "s.swarsel.win";
servicePort = 8081;
serviceName = "shlink";
containerRev = "sha256:1a697baca56ab8821783e0ce53eb4fb22e51bb66749ec50581adc0cb6d031d7a";
in
{
options = {
swarselsystems.modules.server."${serviceName}" = lib.mkEnableOption "enable ${serviceName} on server";
};
config = lib.mkIf config.swarselsystems.modules.server."${serviceName}" {
sops = {
secrets = {
shlink-api = { };
};
templates = {
"shlink-env" = {
content = ''
INITIAL_API_KEY=${config.sops.placeholder.shlink-api}
'';
};
};
};
virtualisation.oci-containers.containers."shlink" = {
image = "shlinkio/shlink@${containerRev}";
environment = {
"DEFAULT_DOMAIN" = serviceDomain;
"PORT" = "${builtins.toString servicePort}";
"USE_HTTPS" = "false";
"DEFAULT_SHORT_CODES_LENGTH" = "4";
"WEB_WORKER_NUM" = "1";
"TASK_WORKER_NUM" = "1";
};
environmentFiles = [
config.sops.templates.shlink-env.path
];
ports = [ "${builtins.toString servicePort}:${builtins.toString servicePort}" ];
volumes = [ ];
};
networking.firewall.allowedTCPPorts = [ servicePort ];
environment.persistence."/persist".directories = lib.mkIf config.swarselsystems.isImpermanence [
{ directory = "/var/lib/containers"; }
];
topology.self.services.${serviceName}.info = "https://${serviceDomain}";
globals.services.${serviceName}.domain = serviceDomain;
services.nginx = {
upstreams = {
"${serviceName}" = {
servers = {
"localhost:${builtins.toString servicePort}" = { };
};
};
};
virtualHosts = {
"${serviceDomain}" = {
enableACME = true;
forceSSL = true;
acmeRoot = null;
locations = {
"/" = {
proxyPass = "http://${serviceName}";
};
};
};
};
};
};
}
#+end_src
*** Darwin
:PROPERTIES:
:CUSTOM_ID: h:ac0cd8b3-06cf-4dca-ba73-6100c8fedb47
:END:
**** Imports
:PROPERTIES:
:CUSTOM_ID: h:25a95a30-8e4f-4fe3-9b8e-508a82e0a1b4
:END:
This section sets up all the imports that are used in the home-manager section.
#+begin_src nix :tangle modules/darwin/nixos/default.nix
_:
{
nix.settings.experimental-features = "nix-command flakes";
nixpkgs = {
hostPlatform = "x86_64-darwin";
overlays = [ outputs.overlays.default ];
config = {
allowUnfree = true;
};
};
system.stateVersion = 4;
}
#+end_src
*** Optional
:PROPERTIES:
:CUSTOM_ID: h:f9aa9af0-9b8d-43ff-901d-9ffccdd70589
:END:
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
:PROPERTIES:
:CUSTOM_ID: h:fb3f3e01-7df4-4b06-9e91-aa9cac61a431
:END:
This opens a few gaming ports and installs the steam configuration suite for gaming. There are more options in [[#h:84fd7029-ecb6-4131-9333-289982f24ffa][Gaming]] (home-manager side).
#+begin_src nix :tangle modules/nixos/optional/gaming.nix
{ pkgs, lib, config, ... }:
{
options.swarselsystems.modules.optional.gaming = lib.mkEnableOption "optional gaming settings";
config = lib.mkIf config.swarselsystems.modules.optional.gaming {
programs.steam = {
enable = true;
package = pkgs.steam;
extraCompatPackages = [
pkgs.proton-ge-bin
];
};
# specialisation = {
# gaming.configuration = {
# networking = {
# firewall.enable = lib.mkForce false;
# firewall = {
# allowedUDPPorts = [ 4380 27036 14242 34197 ]; # 34197: factorio; 4380 27036 14242: barotrauma;
# allowedTCPPorts = [ ]; # 34197: factorio; 4380 27036 14242: barotrauma; 51820: wireguard
# allowedTCPPortRanges = [
# { from = 27015; to = 27030; } # barotrauma
# { from = 27036; to = 27037; } # barotrauma
# ];
# allowedUDPPortRanges = [
# { from = 27000; to = 27031; } # barotrauma
# { from = 58962; to = 58964; } # barotrauma
# ];
# };
# };
# hardware.xone.enable = true;
# environment.systemPackages = [
# pkgs.linuxKernel.packages.linux_6_12.xone
# ];
# };
# };
};
}
#+end_src
**** VirtualBox
:PROPERTIES:
:CUSTOM_ID: h:b3523246-14e9-4284-ba22-cebc5ca36732
:END:
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.
#+begin_src nix :tangle modules/nixos/optional/virtualbox.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.optional.virtualbox = lib.mkEnableOption "optional VBox settings";
config = lib.mkIf config.swarselsystems.modules.optional.virtualbox {
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.stable24_05.linuxPackages;
# kernelParams = [
# "amd_iommu=on"
# ];
};
};
};
};
}
#+end_src
**** VmWare
:PROPERTIES:
:CUSTOM_ID: h:34db28fb-62f7-4597-a9ff-0de2991a8415
:END:
This sets the VirtualBox configuration. Guest should not be enabled if not direly needed, it will make rebuilds unbearably slow.
#+begin_src nix :tangle modules/nixos/optional/vmware.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.optional.vmware = lib.mkEnableOption "optional vmware settings";
config = lib.mkIf config.swarselsystems.modules.optional.vmware {
virtualisation.vmware.host.enable = true;
virtualisation.vmware.guest.enable = true;
};
}
#+end_src
**** Auto-login
:PROPERTIES:
:CUSTOM_ID: h:fa8d9ec4-3e22-458a-9239-859cffe7f55c
:END:
Auto login for the initial session.
#+begin_src nix :tangle modules/nixos/optional/autologin.nix
{ lib, config, ... }:
let
inherit (config.swarselsystems) mainUser;
in
{
options.swarselsystems.modules.optional.autologin = lib.mkEnableOption "optional autologin settings";
config = lib.mkIf config.swarselsystems.modules.optional.autologin {
services = {
getty.autologinUser = mainUser;
greetd.settings.initial_session.user = mainUser;
};
};
}
#+end_src
**** nswitch-rcm
:PROPERTIES:
:CUSTOM_ID: h:5c41c4ee-22ca-405b-9e4f-cc4051634edd
:END:
This smashes Atmosphere 1.3.2 on the switch, which is what I am currenty using.
#+begin_src nix :tangle modules/nixos/optional/nswitch-rcm.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.optional.nswitch-rcm = lib.mkEnableOption "optional nswitch-rcm settings";
config = lib.mkIf config.swarselsystems.modules.optional.nswitch-rcm {
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=";
};
};
};
}
#+end_src
**** Framework
:PROPERTIES:
:CUSTOM_ID: h:bb542fa6-b087-4c1c-838e-d9ec14e4eb85
:END:
This holds configuration that is specific to framework laptops.
#+begin_src nix :tangle modules/nixos/optional/framework.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.optional.framework = lib.mkEnableOption "optional framework machine settings";
config = lib.mkIf config.swarselsystems.modules.optional.framework {
services = {
fwupd = {
enable = true;
# framework also uses lvfs-testing, but I do not want to use it
extraRemotes = [ "lvfs" ];
};
udev.extraRules = ''
# disable Wakeup on Framework Laptop 16 Keyboard (ANSI)
ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0012", ATTR{power/wakeup}="disabled"
# disable Wakeup on Framework Laptop 16 Numpad Module
ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="32ac", ATTRS{idProduct}=="0014", ATTR{power/wakeup}="disabled"
# disable Wakeup on Framework Laptop 16 Trackpad
ACTION=="add", SUBSYSTEM=="i2c", DRIVERS=="i2c_hid_acpi", ATTRS{name}=="PIXA3854:00", ATTR{power/wakeup}="disabled"
'';
};
programs.fw-fanctrl = {
enable = true;
config = {
defaultStrategy = "lazy";
};
};
};
}
#+end_src
**** AMD CPU
:PROPERTIES:
:CUSTOM_ID: h:b64b8988-94fa-440f-8cf1-ee5568bfd75e
:END:
#+begin_src nix :tangle modules/nixos/optional/amdcpu.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.optional.amdcpu = lib.mkEnableOption "optional amd cpu settings";
config = lib.mkIf config.swarselsystems.modules.optional.amdcpu {
hardware = {
cpu.amd.updateMicrocode = true;
};
};
}
#+end_src
**** AMD GPU
:PROPERTIES:
:CUSTOM_ID: h:0dc68d4f-72b3-465a-8d73-4453d06e7853
:END:
#+begin_src nix :tangle modules/nixos/optional/amdgpu.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.optional.amdgpu = lib.mkEnableOption "optional amd gpu settings";
config = lib.mkIf config.swarselsystems.modules.optional.amdgpu {
hardware = {
amdgpu = {
opencl.enable = true;
amdvlk = {
enable = true;
support32Bit.enable = true;
};
};
};
};
}
#+end_src
**** Hibernation
:PROPERTIES:
:CUSTOM_ID: h:15b581ab-09fe-4f84-af26-2f1fbf7d726b
:END:
#+begin_src nix :tangle modules/nixos/optional/hibernation.nix
{ lib, config, ... }:
{
options.swarselsystems = {
modules.optional.hibernation = lib.mkEnableOption "optional amd gpu settings";
hibernation = {
offset = lib.mkOption {
type = lib.types.int;
default = 0;
};
resumeDevice = lib.mkOption {
type = lib.types.str;
default = "/dev/disk/by-label/nixos";
};
};
};
config = lib.mkIf config.swarselsystems.modules.optional.hibernation {
boot = {
kernelParams = [
"resume_offset=${builtins.toString config.swarselsystems.hibernation.offset}"
];
inherit (config.swarselsystems.hibernation) resumeDevice;
};
};
}
#+end_src
**** BTRFS
:PROPERTIES:
:CUSTOM_ID: h:86fb3236-9e18-43f0-8a08-3a2acd61cc98
:END:
#+begin_src nix :tangle modules/nixos/optional/btrfs.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.optional.btrfs = lib.mkEnableOption "optional btrfs settings";
config = lib.mkIf config.swarselsystems.modules.optional.btrfs {
boot = {
supportedFilesystems = [ "btrfs" ];
};
};
}
#+end_src
**** work
:PROPERTIES:
:CUSTOM_ID: h:bbf2ecb6-c8ff-4462-b5d5-d45b28604ddf
:END:
Options that I need specifically at work. There are more options at [[#h:f0b2ea93-94c8-48d8-8d47-6fe58f58e0e6][Work]] (home-manager side).
#+begin_src nix :tangle modules/nixos/optional/work.nix
{ self, lib, pkgs, config, ... }:
let
inherit (config.swarselsystems) mainUser homeDir xdgDir;
owner = mainUser;
sopsFile = self + /secrets/work/secrets.yaml;
swarselService = name: description: execStart: {
"${name}" = {
enable = true;
inherit description;
serviceConfig = {
ExecStart = execStart;
User = mainUser;
Group = "users";
Environment = [
"PATH=/run/current-system/sw/bin:/etc/profiles/per-user/${mainUser}/bin"
"XDG_RUNTIME_DIR=${xdgDir}"
"WAYLAND_DISPLAY=wayland-1"
];
Type = "oneshot";
StandardOutput = "journal";
StandardError = "journal";
};
};
};
in
{
options.swarselsystems = {
modules.optional.work = lib.mkEnableOption "optional work settings";
hostName = lib.mkOption {
type = lib.types.str;
default = "";
};
fqdn = lib.mkOption {
type = lib.types.str;
default = "";
};
};
config = lib.mkIf config.swarselsystems.modules.optional.work {
sops =
let
secretNames = [
"vcuser"
"vcpw"
"govcuser"
"govcpw"
"govcurl"
"govcdc"
"govcds"
"govchost"
"govcnetwork"
"govcpool"
];
in
{
secrets = builtins.listToAttrs (
map
(name: {
inherit name;
value = { inherit owner sopsFile; };
})
secretNames
);
};
boot.initrd = {
systemd.enable = lib.mkForce true; # make sure we are using initrd systemd even when not using Impermanence
luks = {
# disable "support" since we use systemd-cryptenroll
# make sure yubikeys are enrolled using
# sudo systemd-cryptenroll --fido2-device=auto --fido2-with-user-verification=no --fido2-with-user-presence=true --fido2-with-client-pin=no /dev/nvme0n1p2
yubikeySupport = false;
fido2Support = false;
};
};
programs = {
zsh.shellInit = ''
export VSPHERE_USER="$(cat ${config.sops.secrets.vcuser.path})"
export VSPHERE_PW="$(cat ${config.sops.secrets.vcpw.path})"
export GOVC_USERNAME="$(cat ${config.sops.secrets.govcuser.path})"
export GOVC_PASSWORD="$(cat ${config.sops.secrets.govcpw.path})"
export GOVC_URL="$(cat ${config.sops.secrets.govcurl.path})"
export GOVC_DATACENTER="$(cat ${config.sops.secrets.govcdc.path})"
export GOVC_DATASTORE="$(cat ${config.sops.secrets.govcds.path})"
export GOVC_HOST="$(cat ${config.sops.secrets.govchost.path})"
export GOVC_RESOURCE_POOL="$(cat ${config.sops.secrets.govcpool.path})"
export GOVC_NETWORK="$(cat ${config.sops.secrets.govcnetwork.path})"
'';
browserpass.enable = true;
_1password.enable = true;
_1password-gui = {
enable = true;
polkitPolicyOwners = [ "${mainUser}" ];
};
};
networking = {
inherit (config.swarselsystems) hostName fqdn;
networkmanager.wifi.scanRandMacAddress = false;
firewall = {
enable = lib.mkDefault true;
trustedInterfaces = [ "virbr0" ];
};
search = [
"vbc.ac.at"
"clip.vbc.ac.at"
"imp.univie.ac.at"
];
};
virtualisation = {
docker.enable = lib.mkIf (!config.virtualisation.podman.dockerCompat) true;
spiceUSBRedirection.enable = true;
libvirtd = {
enable = true;
qemu = {
package = pkgs.qemu_kvm;
runAsRoot = true;
swtpm.enable = true;
vhostUserPackages = with pkgs; [ virtiofsd ];
ovmf = {
enable = true;
packages = [
(pkgs.OVMFFull.override {
secureBoot = true;
tpmSupport = true;
}).fd
];
};
};
};
};
environment.systemPackages = with pkgs; [
# (python39.withPackages (ps: with ps; [
# cryptography
# ]))
# docker
stable24_11.python39
qemu
packer
gnumake
libisoburn
govc
terraform
opentofu
terragrunt
graphviz
azure-cli
# 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";
};
"moonside@oracle" = {
id = "VPCDZB6-MGVGQZD-Q6DIZW3-IZJRJTO-TCC3QUQ-2BNTL7P-AKE7FBO-N55UNQE";
};
folders = {
"Documents" = {
path = "${homeDir}/Documents";
devices = [ "magicant" "winters" "moonside@oracle" ];
id = "hgr3d-pfu3w";
};
};
};
};
udev.extraRules = ''
# share screen when dongle detected
SUBSYSTEM=="usb", ACTION=="add", ATTRS{idVendor}=="343c", ATTRS{idProduct}=="0000", TAG+="systemd", ENV{SYSTEMD_WANTS}="swarsel-screenshare.service"
# lock screen when yubikey removed
ACTION=="remove", ENV{PRODUCT}=="3/1050/407/110", RUN+="${pkgs.systemd}/bin/systemctl suspend"
'';
};
systemd.services = lib.mkMerge [
(swarselService "swarsel-screenshare" "Start screensharing after HDMI dongle is detected" "${pkgs.screenshare}/bin/screenshare -h")
];
# cgroups v1 is required for centos7 dockers
# specialisation = {
# cgroup_v1.configuration = {
# boot.kernelParams = [
# "SYSTEMD_CGROUP_ENABLE_LEGACY_FORCE=1"
# "systemd.unified_cgroup_hierarchy=0"
# ];
# };
# };
};
}
#+end_src
**** Minimal Install
:PROPERTIES:
:CUSTOM_ID: h:3fc1d301-7bae-4678-9085-d12c23eed8ac
:END:
These options are really only to be used on the iso image in order to run nixos-anywhere.
#+begin_src nix :tangle modules/iso/minimal.nix
{ 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.sudo.extraConfig = ''
Defaults env_keep+=SSH_AUTH_SOCK
Defaults lecture = never
'';
security.pam = {
sshAgentAuth.enable = true;
services = {
sudo.u2fAuth = true;
};
};
environment.systemPackages = with pkgs; [
curl
git
gnupg
rsync
ssh-to-age
sops
vim
just
sbctl
];
programs = {
git.enable = true;
};
fileSystems."/boot".options = [ "umask=0077" ];
networking.networkmanager.enable = true;
}
#+end_src
** Home-manager
:PROPERTIES:
:CUSTOM_ID: h:08ded95b-9c43-475d-a0b2-fc088a512287
:END:
The general structure is the same as in the [[#h:6da812f5-358c-49cb-aff2-0a94f20d70b3][NixOS]] section.
*** Common
:PROPERTIES:
:CUSTOM_ID: h:f0a6b5e0-2157-4522-b5e1-3f0abd91c05e
:END:
**** Imports
:PROPERTIES:
:CUSTOM_ID: h:16fd2e85-fdd4-440a-81f0-65b9b098a43a
:END:
This section sets up all the imports that are used in the home-manager section.
#+begin_src nix :tangle modules/home/common/default.nix
{ lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/home/common";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/home/common";
}
#+end_src
**** Shared Configuration Options (holds firefox & stylix config parts)
:PROPERTIES:
:CUSTOM_ID: h:79f7150f-b162-4f57-abdf-07f40dffd932
:END:
Provides settings related to nix-darwin systems. At the moment, I am only making use of a =isDarwin= flag.
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 [[#h:f0b2ea93-94c8-48d8-8d47-6fe58f58e0e6][Work]] that are all practically using the same configuration. To save screen space, I template that profile here.
Set in firefox =about:config > toolkit.legacyUserProfileCustomizations.stylesheets= to true. This should in principle be set automatically using the below config, but it seems not to be working reliably
For styling, I am using the [[https://github.com/danth/stylix][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 [[#h:e7f98ad8-74a6-4860-a368-cce154285ff0][firefox]]). The difference here was, for a long time, that this block is used in a NixOS and a home-manager-only configuration, verbatim. If I were to use an attribute set, I would have to duplicate this block once each for NixOS and home-manager. Alas, this block stays (for now). However, I learned how to use an attribute set in a custom home-manager module and pass it to both NixOS and home-manager configurations, which also removed the need for that use of it.
#+begin_src nix :noweb yes :tangle modules/home/common/sharedsetup.nix
{ self, lib, pkgs, ... }:
{
options.swarselsystems = {
isLaptop = lib.mkEnableOption "laptop host";
isNixos = lib.mkEnableOption "nixos host";
isPublic = lib.mkEnableOption "is a public machine (no secrets)";
isDarwin = lib.mkEnableOption "darwin host";
isLinux = lib.mkEnableOption "whether this is a linux machine";
isBtrfs = lib.mkEnableOption "use btrfs filesystem";
mainUser = lib.mkOption {
type = lib.types.str;
default = "swarsel";
};
homeDir = lib.mkOption {
type = lib.types.str;
default = "/home/swarsel";
};
xdgDir = lib.mkOption {
type = lib.types.str;
default = "/run/user/1000";
};
flakePath = lib.mkOption {
type = lib.types.str;
default = "/home/swarsel/.dotfiles";
};
wallpaper = lib.mkOption {
type = lib.types.path;
default = "${self}/wallpaper/lenovowp.png";
};
sharescreen = lib.mkOption {
type = lib.types.str;
default = "";
};
lowResolution = lib.mkOption {
type = lib.types.str;
default = "";
};
highResolution = lib.mkOption {
type = lib.types.str;
default = "";
};
stylix = lib.mkOption {
type = lib.types.attrs;
default = {
enable = true;
base16Scheme = "${self}/programs/stylix/swarsel.yaml";
polarity = "dark";
opacity.popups = 0.5;
cursor = {
package = pkgs.banana-cursor;
# package = pkgs.capitaine-cursors;
name = "Banana";
# name = "capitaine-cursors";
size = 16;
};
fonts = {
sizes = {
terminal = 10;
applications = 11;
};
serif = {
# package = (pkgs.nerdfonts.override { fonts = [ "FiraMono" "FiraCode"]; });
package = pkgs.cantarell-fonts;
# package = pkgs.montserrat;
name = "Cantarell";
# name = "FiraCode Nerd Font Propo";
# name = "Montserrat";
};
sansSerif = {
# package = (pkgs.nerdfonts.override { fonts = [ "FiraMono" "FiraCode"]; });
package = pkgs.cantarell-fonts;
# package = pkgs.montserrat;
name = "Cantarell";
# name = "FiraCode Nerd Font Propo";
# name = "Montserrat";
};
monospace = {
package = pkgs.nerd-fonts.fira-mono; # has overrides
name = "FiraCode Nerd Font Mono";
};
emoji = {
package = pkgs.noto-fonts-emoji;
name = "Noto Color Emoji";
};
};
};
};
stylixHomeTargets = lib.mkOption {
type = lib.types.attrs;
default = {
emacs.enable = false;
waybar.enable = false;
sway.useWallpaper = false;
firefox.profileNames = [ "default" ];
};
};
firefox = lib.mkOption {
type = lib.types.attrs;
default = {
userChrome = builtins.readFile "${self}/programs/firefox/chrome/userChrome.css";
extensions = {
packages = with pkgs.nur.repos.rycee.firefox-addons; [
tridactyl
tampermonkey
sidebery
browserpass
clearurls
darkreader
enhancer-for-youtube
istilldontcareaboutcookies
translate-web-pages
ublock-origin
reddit-enhancement-suite
sponsorblock
web-archives
onepassword-password-manager
single-file
widegithub
enhanced-github
unpaywall
don-t-fuck-with-paste
plasma-integration
noscript
# configure a shortcut 'ctrl+shift+c' with behaviour 'do nothing' in order to disable the dev console shortcut
(buildFirefoxXpiAddon {
pname = "shortkeys";
version = "4.0.2";
addonId = "Shortkeys@Shortkeys.com";
url = "https://addons.mozilla.org/firefox/downloads/file/3673761/shortkeys-4.0.2.xpi";
sha256 = "c6fe12efdd7a871787ac4526eea79ecc1acda8a99724aa2a2a55c88a9acf467c";
meta = with lib;
{
description = "Easily customizable custom keyboard shortcuts for Firefox. To configure this addon go to Addons (ctrl+shift+a) ->Shortkeys ->Options. Report issues here (please specify that the issue is found in Firefox): https://github.com/mikecrittenden/shortkeys";
mozPermissions = [
"tabs"
"downloads"
"clipboardWrite"
"browsingData"
"storage"
"bookmarks"
"sessions"
""
];
platforms = platforms.all;
};
})
];
};
settings =
{
"extensions.autoDisableScopes" = 0;
"browser.bookmarks.showMobileBookmarks" = true;
"toolkit.legacyUserProfileCustomizations.stylesheets" = true;
"browser.search.suggest.enabled" = false;
"browser.search.suggest.enabled.private" = false;
"browser.urlbar.suggest.searches" = false;
"browser.urlbar.showSearchSuggestionsFirst" = false;
"browser.topsites.contile.enabled" = false;
"browser.newtabpage.activity-stream.feeds.section.topstories" = false;
"browser.newtabpage.activity-stream.feeds.snippets" = false;
"browser.newtabpage.activity-stream.section.highlights.includePocket" = false;
"browser.newtabpage.activity-stream.section.highlights.includeBookmarks" = false;
"browser.newtabpage.activity-stream.section.highlights.includeDownloads" = false;
"browser.newtabpage.activity-stream.section.highlights.includeVisited" = false;
"browser.newtabpage.activity-stream.showSponsored" = false;
"browser.newtabpage.activity-stream.system.showSponsored" = false;
"browser.newtabpage.activity-stream.showSponsoredTopSites" = false;
};
search = {
# default = "Kagi";
default = "google";
# privateDefault = "Kagi";
privateDefault = "google";
engines = {
"Kagi" = {
urls = [{
template = "https://kagi.com/search";
params = [
{ name = "q"; value = "{searchTerms}"; }
];
}];
icon = "https://kagi.com/favicon.ico";
updateInterval = 24 * 60 * 60 * 1000; # every day
definedAliases = [ "@k" ];
};
"Nix Packages" = {
urls = [{
template = "https://search.nixos.org/packages";
params = [
{ name = "type"; value = "packages"; }
{ name = "query"; value = "{searchTerms}"; }
];
}];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = [ "@np" ];
};
"NixOS Wiki" = {
urls = [{
template = "https://nixos.wiki/index.php?search={searchTerms}";
}];
icon = "https://nixos.wiki/favicon.png";
updateInterval = 24 * 60 * 60 * 1000; # every day
definedAliases = [ "@nw" ];
};
"NixOS Options" = {
urls = [{
template = "https://search.nixos.org/options";
params = [
{ name = "query"; value = "{searchTerms}"; }
];
}];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = [ "@no" ];
};
"Home Manager Options" = {
urls = [{
template = "https://home-manager-options.extranix.com/";
params = [
{ name = "query"; value = "{searchTerms}"; }
];
}];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = [ "@hm" "@ho" "@hmo" ];
};
"Confluence search" = {
urls = [{
template = "https://vbc.atlassian.net/wiki/search";
params = [
{ name = "text"; value = "{searchTerms}"; }
];
}];
definedAliases = [ "@c" "@cf" "@confluence" ];
};
"Jira search" = {
urls = [{
template = "https://vbc.atlassian.net/issues/";
params = [
{ name = "jql"; value = "textfields ~ \"{searchTerms}*\"&wildcardFlag=true"; }
];
}];
definedAliases = [ "@j" "@jire" ];
};
"google".metaData.alias = "@g";
};
force = true; # this is required because otherwise the search.json.mozlz4 symlink gets replaced on every firefox restart
};
};
};
};
}
#+end_src
**** General home-manager-settings
:PROPERTIES:
:CUSTOM_ID: h:4af4f67f-7c48-4754-b4bd-6800e3a66664
:END:
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.
#+begin_src nix :tangle modules/home/common/settings.nix
{ lib, config, ... }:
let
inherit (config.swarselsystems) mainUser;
in
{
options.swarselsystems.modules.general = lib.mkEnableOption "general nix settings";
config = lib.mkIf config.swarselsystems.modules.general {
nix = lib.mkIf (!config.swarselsystems.isNixos) {
settings = {
experimental-features = [
"nix-command"
"flakes"
"ca-derivations"
"cgroups"
"pipe-operators"
];
trusted-users = [ "@wheel" "${mainUser}" ];
connect-timeout = 5;
bash-prompt-prefix = "[33m$SHLVL:\\w [0m";
bash-prompt = "$(if [[ $? -gt 0 ]]; then printf \"[31m\"; else printf \"[32m\"; fi)\[\e[1m\]λ\[\e[0m\] [0m";
fallback = true;
min-free = 128000000;
max-free = 1000000000;
auto-optimise-store = true;
warn-dirty = false;
max-jobs = 1;
use-cgroups = lib.mkIf config.swarselsystems.isLinux true;
};
};
nixpkgs.overlays = lib.mkIf config.swarselsystems.isNixos (lib.mkForce null);
programs.home-manager.enable = lib.mkIf (!config.swarselsystems.isNixos) true;
targets.genericLinux.enable = lib.mkIf (!config.swarselsystems.isNixos) true;
home = {
username = lib.mkDefault mainUser;
homeDirectory = lib.mkDefault "/home/${mainUser}";
stateVersion = lib.mkDefault "23.05";
keyboard.layout = "us";
sessionVariables = {
FLAKE = "/home/${mainUser}/.dotfiles";
};
};
};
}
#+end_src
**** nixGL
:PROPERTIES:
:CUSTOM_ID: h:90af1862-90b3-4c93-8730-2443bc62986a
:END:
This integrates nixGL into home-manager. NixGL provies OpenGL and Vulkan APIs to nix installed utilities. This is needed for graphical applications on non-NixOS systems.
to get the info for the secondary gpu, use `lspci -nn | grep VGA`
It can be set to either:
- a number, selecting the n-th non-default GPU
- a PCI bus id in the form =pci-XXX_YY_ZZ_U=
- a PCI id in the form =vendor_id:device_id=
#+begin_src nix :tangle modules/home/common/nixgl.nix
{ lib, config, nixgl, ... }:
{
options.swarselsystems = {
modules.nixgl = lib.mkEnableOption "nixgl settings";
isSecondaryGpu = lib.mkEnableOption "device has a secondary GPU";
SecondaryGpuCard = lib.mkOption {
type = lib.types.str;
default = "";
};
};
config = lib.mkIf config.swarselsystems.modules.nixgl {
nixGL = lib.mkIf (!config.swarselsystems.isNixos) {
inherit (nixgl) packages;
defaultWrapper = lib.mkDefault "mesa";
vulkan.enable = lib.mkDefault false;
prime = lib.mkIf config.swarselsystem.isSecondaryGpu {
card = config.swarselsystem.secondaryGpuCard;
installScript = "mesa";
};
offloadWrapper = lib.mkIf config.swarselsystem.isSecondaryGpu "mesaPrime";
installScripts = [
"mesa"
"mesaPrime"
];
};
};
}
#+end_src
**** Installed packages
:PROPERTIES:
:CUSTOM_ID: h:893a7f33-7715-415b-a895-2687ded31c18
:END:
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: [[#h:0e7e8bea-ec58-499c-9731-09dddfc39532][System Packages]]
***** Packaged
:PROPERTIES:
:CUSTOM_ID: h:6ef9bb5f-c5ee-496e-86e2-d8d271a34d75
:END:
This holds packages that I can use as provided, or with small modifications (as in the =texlive= package that needs special configuration).
#+begin_src nix :tangle modules/home/common/packages.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.packages = lib.mkEnableOption "packages settings";
config = lib.mkIf config.swarselsystems.modules.packages {
home.packages = with pkgs; [
# audio stuff
spek # spectrum analyzer
losslessaudiochecker
ffmpeg_7-full
flac
mediainfo
picard-tools
audacity
sox
stable.feishin
calibre
# printing
cups
simple-scan
cura-appimage
# dict
(aspellWithDicts (dicts: with dicts; [ de en en-computers en-science ]))
# browser
vieb
mgba
# utilities
util-linux
nmap
lsof
nvd
nix-output-monitor
hyprpicker # color picker
findutils
units
vim
sshfs
fuse
# ventoy
poppler_utils
vdhcoapp
# nix
alejandra
nixpkgs-fmt
deadnix
statix
nix-tree
nix-diff
nix-visualize
nix-init
nix-inspect
nixpkgs-review
manix
comma
# shellscripts
shfmt
# local file sharing
wormhole-rs
croc
# 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
nicotine-plus
stable.transmission_3
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.sh files
# specifically needed for anki
# mpv
anki-bin
# dirvish file previews
fd
imagemagick
# poppler
ffmpegthumbnailer
mediainfo
gnutar
unzip
#nautilus
stable.nautilus
xfce.tumbler
libgsf
# wayland stuff
wtype
wl-clipboard
stable.wl-mirror
wf-recorder
kanshi
# screenshotting tools
grim
slurp
# the following packages are used (in some way) by waybar
# playerctl
stable.pavucontrol
# stable.pamixer
# gnome.gnome-clocks
# wlogout
# jdiskreport
# monitor
#keychain
qalculate-gtk
gcr # needed for gnome-secrets to work
seahorse
# sops-related
sops
ssh-to-age
# mail related packages
mu
# latex and related packages
(texlive.combine {
inherit (pkgs.texlive) scheme-full
dvisvgm dvipng# for preview and export as html
wrapfig amsmath ulem hyperref capt-of;
})
# font stuff
nerd-fonts.fira-mono
nerd-fonts.fira-code
nerd-fonts.symbols-only
noto-fonts-emoji
font-awesome_5
noto-fonts
noto-fonts-cjk-sans
];
};
}
#+end_src
***** Self-defined
:PROPERTIES:
:CUSTOM_ID: h:96cbea91-ff13-4120-b8a9-496b2fa96e70
:END:
This is just a separate container for derivations defined in [[#h:64a5cc16-6b16-4802-b421-c67ccef853e1][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.
#+begin_src nix :tangle modules/home/common/custom-packages.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.ownpackages = lib.mkEnableOption "own packages settings";
config = lib.mkIf config.swarselsystems.modules.ownpackages {
home.packages = with pkgs; lib.mkIf (!config.swarselsystems.isPublic) [
pass-fuzzel
cdw
cdb
bak
timer
e
swarselcheck
waybarupdate
opacitytoggle
fs-diff
github-notifications
hm-specialisation
t2ts
ts2t
vershell
eontimer
project
fhs
swarsel-bootstrap
swarsel-displaypower
swarsel-deploy
swarselzellij
sshrm
rustdesk-vbc
];
};
}
#+end_src
**** sops
:PROPERTIES:
:CUSTOM_ID: h:d87d80fd-2ac7-4f29-b338-0518d06b4deb
:END:
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.
#+begin_src nix :tangle modules/home/common/sops.nix
{ config, lib, ... }:
let
inherit (config.swarselsystems) homeDir xdgDir;
in
{
options.swarselsystems.modules.sops = lib.mkEnableOption "sops settings";
config = lib.mkIf config.swarselsystems.modules.sops {
sops = lib.mkIf (!config.swarselsystems.isPublic) {
age.sshKeyPaths = [ "${homeDir}/.ssh/sops" "${homeDir}/.ssh/ssh_host_ed25519_key" ];
defaultSopsFile = lib.swarselsystems.mkIfElseList config.swarselsystems.isBtrfs "/persist/.dotfiles/secrets/general/secrets.yaml" "${homeDir}/.dotfiles/secrets/general/secrets.yaml";
validateSopsFiles = false;
secrets = {
mrswarsel = { path = "${xdgDir}/secrets/mrswarsel"; };
nautilus = { path = "${xdgDir}/secrets/nautilus"; };
leon = { path = "${xdgDir}/secrets/leon"; };
swarselmail = { path = "${xdgDir}/secrets/swarselmail"; };
github_notif = { path = "${xdgDir}/secrets/github_notif"; };
u2f_keys = { path = "${homeDir}/.config/Yubico/u2f_keys"; };
};
};
};
}
#+end_src
**** Yubikey
:PROPERTIES:
:CUSTOM_ID: h:4c850b80-56e0-437b-b564-2dd897027b2f
:END:
#+begin_src nix :tangle modules/home/common/yubikey.nix
{ lib, config, nixosConfig, ... }:
{
options.swarselsystems.modules.yubikey = lib.mkEnableOption "yubikey settings";
config = lib.mkIf config.swarselsystems.modules.yubikey {
pam.yubico.authorizedYubiKeys = {
ids = [
nixosConfig.repo.secrets.common.yubikeys.dev1
nixosConfig.repo.secrets.common.yubikeys.dev2
];
};
};
}
#+end_src
**** SSH Machines
:PROPERTIES:
:CUSTOM_ID: h:edd6720e-1f90-40bf-b6f9-30a19d4cae08
:END:
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.
#+begin_src nix :tangle modules/home/common/ssh.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.ssh = lib.mkEnableOption "ssh settings";
config = lib.mkIf config.swarselsystems.modules.ssh {
programs.ssh = {
enable = true;
forwardAgent = true;
extraConfig = ''
SetEnv TERM=xterm-256color
ServerAliveInterval 20
'';
matchBlocks = {
"pfsense" = {
hostname = "192.168.1.1";
user = "root";
};
"winters" = {
hostname = "192.168.1.2";
user = "root";
};
"minecraft" = {
hostname = "130.61.119.129";
user = "opc";
};
"sync" = {
hostname = "193.122.53.173";
user = "root";
};
"moonside" = {
hostname = "130.61.238.239";
user = "root";
};
"songdiver" = {
hostname = "89.168.100.65";
user = "ubuntu";
};
"pkv" = {
hostname = "46.232.248.161";
user = "root";
};
};
};
};
}
#+end_src
**** Theme (stylix)
:PROPERTIES:
:CUSTOM_ID: h:a92318cd-413e-4e78-a478-e63b09df019c
:END:
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 [[#h:5bc1b0c9-dc59-4c81-b5b5-e60699deda78][Theme (stylix)]].
#+begin_src nix :noweb yes :tangle modules/home/common/stylix.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.stylix = lib.mkEnableOption "stylix settings";
config = lib.mkIf config.swarselsystems.modules.stylix {
stylix = lib.mkIf (!config.swarselsystems.isNixos) (lib.recursiveUpdate
{
image = config.swarselsystems.wallpaper;
targets = config.swarselsystems.stylixHomeTargets;
}
config.swarselsystems.stylix);
};
}
#+end_src
**** Desktop Entries, MIME types (xdg)
:PROPERTIES:
:CUSTOM_ID: h:867556e6-5a24-4c43-9d47-3edca2f16488
:END:
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.
#+begin_src nix :tangle modules/home/common/desktop.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.desktop = lib.mkEnableOption "desktop settings";
config = lib.mkIf config.swarselsystems.modules.desktop {
xdg.desktopEntries = {
cura = {
name = "Ultimaker Cura";
genericName = "Cura";
exec = "cura";
terminal = false;
categories = [ "Application" ];
};
teamsNoGpu = {
name = "Microsoft Teams (no GPU)";
genericName = "Teams (no GPU)";
exec = "teams-for-linux --disableGpu=true --trayIconEnabled=true";
terminal = false;
categories = [ "Application" ];
};
rustdesk-vbc = {
name = "Rustdesk VBC";
genericName = "rustdesk-vbc";
exec = "rustdesk-vbc";
terminal = false;
categories = [ "Application" ];
};
anki = {
name = "Anki Flashcards";
genericName = "Anki";
exec = "anki";
terminal = false;
categories = [ "Application" ];
};
element = {
name = "Element Matrix Client";
genericName = "Element";
exec = "element-desktop -enable-features=UseOzonePlatform -ozone-platform=wayland --disable-gpu-driver-bug-workarounds";
terminal = false;
categories = [ "Application" ];
};
emacsclient-newframe = {
name = "Emacs (Client, New Frame)";
genericName = "Emacs (Client, New Frame)";
exec = "emacsclient -r %u";
icon = "emacs";
terminal = false;
categories = [ "Development" "TextEditor" ];
};
};
xdg.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" ];
};
};
};
};
}
#+end_src
**** Linking dotfiles (Symlinks home.file)
:PROPERTIES:
:CUSTOM_ID: h:5ef03803-e150-41bc-b603-e80d60d96efc
:END:
This section should be used in order to symlink already existing configuration files using `home.file` and setting session variables using `home.sessionVariables`.
As for the `home.sessionVariables`, it should be noted that environment variables that are needed at system start should NOT be loaded here, but instead in `programs.zsh.config.extraSessionCommands` (in the home-manager programs section). This is also where all the wayland related variables are stored.
Also, we link some files to the users XDG configuration home:
Also in firefox `about:config > toolkit.legacyUserProfileCustomizations.stylesheets` to true.
#+begin_src nix :tangle modules/home/common/symlink.nix
{ self, lib, config, ... }:
{
options.swarselsystems.modules.symlink = lib.mkEnableOption "symlink settings";
config = lib.mkIf config.swarselsystems.modules.symlink {
home.file = {
"init.el" = lib.mkDefault {
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";
};
};
xdg.configFile = {
"tridactyl/tridactylrc".source = self + /programs/firefox/tridactyl/tridactylrc;
"tridactyl/themes/base16-codeschool.css".source = self + /programs/firefox/tridactyl/themes/base16-codeschool.css;
"tridactyl/themes/swarsel.css".source = self + /programs/firefox/tridactyl/themes/swarsel.css;
"swayidle/config".source = self + /programs/swayidle/config;
};
};
}
#+end_src
**** Sourcing environment variables
:PROPERTIES:
:CUSTOM_ID: h:4486b02f-4fb8-432b-bfa2-2e786206341d
:END:
Sets environment variables. Here I am only setting the EDITOR variable, most variables are set in the [[#h:02df9dfc-d1af-4a37-a7a0-d8da0af96a20][Sway]] section.
#+begin_src nix :tangle modules/home/common/env.nix
{ lib, config, nixosConfig, ... }:
let
inherit (nixosConfig.repo.secrets.common.mail) address1 address2 address3 address4 allMailAddresses;
inherit (nixosConfig.repo.secrets.common) fullName;
in
{
options.swarselsystems.modules.env = lib.mkEnableOption "env settings";
config = lib.mkIf config.swarselsystems.modules.env {
home.sessionVariables = {
EDITOR = "e -w";
DISPLAY = ":0";
CROC_RELAY = "send.swarsel.win";
SWARSEL_LO_RES = config.swarselsystems.lowResolution;
SWARSEL_HI_RES = config.swarselsystems.highResolution;
};
systemd.user.sessionVariables = {
SWARSEL_MAIL1 = address1;
SWARSEL_MAIL2 = address2;
SWARSEL_MAIL3 = address3;
SWARSEL_MAIL4 = address4;
SWARSEL_FULLNAME = fullName;
SWARSEL_MAIL_ALL = allMailAddresses;
};
};
}
#+end_src
**** General Programs: bottom, imv, sioyek, bat, carapace, wlogout, swayr, yt-dlp, mpv, jq, nix-index, ripgrep, pandoc, fzf, zoxide
:PROPERTIES:
:CUSTOM_ID: h:f0e0b580-2e1c-4ca6-a983-f05d3ebbbcde
:END:
This section is for programs that require no further configuration. zsh Integration is enabled by default for these.
#+begin_src nix :tangle modules/home/common/programs.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.programs = lib.mkEnableOption "programs settings";
config = lib.mkIf config.swarselsystems.modules.programs {
programs = {
bottom.enable = true;
imv.enable = true;
sioyek.enable = true;
bat = {
enable = true;
extraPackages = with pkgs.bat-extras; [ batdiff batman batgrep batwatch ];
};
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;
enableZshIntegration = true;
options = [
"--cmd cd"
];
};
};
};
}
#+end_src
**** nix-index
:PROPERTIES:
:CUSTOM_ID: h:64dbbb9e-8097-4c1b-813c-8c10cf9b9748
:END:
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.
#+begin_src nix :tangle modules/home/common/nix-index.nix
{ self, lib, config, pkgs, ... }:
{
options.swarselsystems.modules.nix-index = lib.mkEnableOption "nix-index settings";
config = lib.mkIf config.swarselsystems.modules.nix-index {
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-fail @nix-locate@ ${pkgs.nix-index}/bin/nix-locate \
--replace-fail @tput@ ${pkgs.ncurses}/bin/tput
'';
in
{
enable = true;
package = pkgs.symlinkJoin {
name = "nix-index";
paths = [ commandNotFound ];
};
};
};
}
#+end_src
**** password-store
:PROPERTIES:
:CUSTOM_ID: h:ac0e5e62-0dbf-4782-9a96-9e558eae86ae
:END:
Enables password store with the =pass-otp= extension which allows me to store and generate one-time-passwords.
#+begin_src nix :tangle modules/home/common/password-store.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.passwordstore = lib.mkEnableOption "passwordstore settings";
config = lib.mkIf config.swarselsystems.modules.passwordstore {
programs.password-store = {
enable = true;
settings = {
PASSWORD_STORE_DIR = "$HOME/.local/share/password-store";
};
package = pkgs.pass.withExtensions (exts: [ exts.pass-otp ]);
};
};
}
#+end_src
**** direnv
:PROPERTIES:
:CUSTOM_ID: h:1ab84307-b3fb-4c32-9def-4b89a53a8547
:END:
Enables direnv, which I use for nearly all of my nix dev flakes.
#+begin_src nix :tangle modules/home/common/direnv.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.direnv = lib.mkEnableOption "direnv settings";
config = lib.mkIf config.swarselsystems.modules.direnv {
programs.direnv = {
enable = true;
silent = true;
nix-direnv.enable = true;
};
};
}
#+end_src
**** eza
:PROPERTIES:
:CUSTOM_ID: h:1bd6b0c7-f201-43e2-9624-6c50de00a1f6
:END:
Eza provides me with a better =ls= command and some other useful aliases.
#+begin_src nix :tangle modules/home/common/eza.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.eza = lib.mkEnableOption "eza settings";
config = lib.mkIf config.swarselsystems.modules.eza {
programs.eza = {
enable = true;
icons = "auto";
git = true;
extraOptions = [
"-l"
"--group-directories-first"
];
};
};
}
#+end_src
**** atuin
:PROPERTIES:
:CUSTOM_ID: h:38f127f3-003b-418b-85ba-a3bcf44bf16c
:END:
#+begin_src nix :tangle modules/home/common/atuin.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.atuin = lib.mkEnableOption "atuin settings";
config = lib.mkIf config.swarselsystems.modules.atuin {
programs.atuin = {
enable = true;
enableZshIntegration = true;
settings = {
auto_sync = true;
sync_frequency = "5m";
sync_address = "https://shellhistory.swarsel.win";
};
};
};
}
#+end_src
**** git
:PROPERTIES:
:CUSTOM_ID: h:419675ec-3310-438e-80ae-9eaa798a319d
:END:
Here I set up my git config, automatic signing of commits, useful aliases for my ost used commands (for when I am not using [[#h:d2c7323d-f8c6-4f23-b70a-930e3e4ecce5][Magit]]) as well as a git template defined in [[#h:5ef03803-e150-41bc-b603-e80d60d96efc][Linking dotfiles]].
#+begin_src nix :tangle modules/home/common/git.nix
{ lib, config, nixosConfig, ... }:
let
inherit (nixosConfig.repo.secrets.common.mail) address1;
inherit (nixosConfig.repo.secrets.common) fullName;
in
{
options.swarselsystems.modules.git = lib.mkEnableOption "git settings";
config = lib.mkIf config.swarselsystems.modules.git {
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 address1;
userName = fullName;
difftastic.enable = true;
lfs.enable = true;
includes = [
{
contents = {
github = {
user = "Swarsel";
};
commit = {
template = "~/.gitmessage";
};
};
}
];
};
};
}
#+end_src
**** Fuzzel
:PROPERTIES:
:CUSTOM_ID: h:069cabf3-df14-49ba-8d17-75f2bcf34fbf
:END:
Here I only need to set basic layout options - the rest is being managed by stylix.
#+begin_src nix :tangle modules/home/common/fuzzel.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.fuzzel = lib.mkEnableOption "fuzzel settings";
config = lib.mkIf config.swarselsystems.modules.fuzzel {
programs.fuzzel = {
enable = true;
settings = {
main = {
layer = "overlay";
lines = "10";
width = "40";
};
border.radius = "0";
};
};
};
}
#+end_src
**** Starship
:PROPERTIES:
:CUSTOM_ID: h:55212502-c8f6-43af-ae99-55c8377ef34e
:END:
Starship makes my =zsh= look cooler! I have symbols for most programming languages and toolchains, also I build my own powerline.
#+begin_src nix :tangle modules/home/common/starship.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.starship = lib.mkEnableOption "starship settings";
config = lib.mkIf config.swarselsystems.modules.starship {
programs.starship = {
enable = true;
enableZshIntegration = true;
settings = {
add_newline = false;
format = "$shlvl$character";
right_format = "$all";
command_timeout = 3000;
directory.substitutions = {
"Documents" = " ";
"Downloads" = " ";
"Music" = " ";
"Pictures" = " ";
};
git_status = {
style = "bg:#394260";
format = "[[($all_status$ahead_behind)](fg:#769ff0 bg:#394260)]($style) ";
};
character = {
success_symbol = "[λ](bold green)";
error_symbol = "[λ](bold red)";
};
shlvl = {
disabled = false;
symbol = "↳";
format = "[$symbol]($style) ";
repeat = true;
repeat_offset = 1;
style = "blue";
};
nix_shell = {
disabled = false;
heuristic = true;
format = "[$symbol$name]($style)";
symbol = " ";
};
aws.symbol = " ";
buf.symbol = " ";
c.symbol = " ";
conda.symbol = " ";
dart.symbol = " ";
directory.read_only = " ";
docker_context.symbol = " ";
elixir.symbol = " ";
elm.symbol = " ";
fossil_branch.symbol = " ";
git_branch.symbol = " ";
golang.symbol = " ";
guix_shell.symbol = " ";
haskell.symbol = " ";
haxe.symbol = " ";
hg_branch.symbol = " ";
hostname.ssh_symbol = " ";
java.symbol = " ";
julia.symbol = " ";
lua.symbol = " ";
memory_usage.symbol = " ";
meson.symbol = " ";
nim.symbol = " ";
nodejs.symbol = " ";
os.symbols = {
Alpaquita = " ";
Alpine = " ";
Amazon = " ";
Android = " ";
Arch = " ";
Artix = " ";
CentOS = " ";
Debian = " ";
DragonFly = " ";
Emscripten = " ";
EndeavourOS = " ";
Fedora = " ";
FreeBSD = " ";
Garuda = " ";
Gentoo = " ";
HardenedBSD = " ";
Illumos = " ";
Linux = " ";
Mabox = " ";
Macos = " ";
Manjaro = " ";
Mariner = " ";
MidnightBSD = " ";
Mint = " ";
NetBSD = " ";
NixOS = " ";
OpenBSD = " ";
openSUSE = " ";
OracleLinux = " ";
Pop = " ";
Raspbian = " ";
Redhat = " ";
RedHatEnterprise = " ";
Redox = " ";
Solus = " ";
SUSE = " ";
Ubuntu = " ";
Unknown = " ";
Windows = " ";
};
package.symbol = " ";
pijul_channel.symbol = " ";
python.symbol = " ";
rlang.symbol = " ";
ruby.symbol = " ";
rust.symbol = " ";
scala.symbol = " ";
};
};
};
}
#+end_src
**** Kitty
:PROPERTIES:
:CUSTOM_ID: h:5f1287db-d2e8-49aa-8c58-730129c7795c
:END:
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.
#+begin_src nix :tangle modules/home/common/kitty.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.kitty = lib.mkEnableOption "kitty settings";
config = lib.mkIf config.swarselsystems.modules.kitty {
programs.kitty = {
enable = true;
keybindings = { };
settings = {
scrollback_lines = 10000;
enable_audio_bell = false;
notify_on_cmd_finish = "always 20";
};
};
};
}
#+end_src
**** zsh
:PROPERTIES:
:CUSTOM_ID: h:91dd4cc4-aada-4e74-be23-0cc69ed85af5
:END:
zsh is the most convenient shell for me and it happens to be super neat to configure within home manager.
Here we set some aliases (some of them should be shellApplications instead) as well as some zsh plugins like =fzf-tab=.
Concerning the shell extensions, =zle = will run an existing widget and =zle -N = will make a function available for use. The =my-= functions all remove =.= =/= and =:= from the =WORDCHARS= so that functions will stop there. The keycodes can be found using =showkeys -a=
Regarding =initContent=:
To specify the order, use lib.mkOrder.
Common order values:
- 500 (mkBefore: Early initialization (replaces initExtraFirst
- 550: Before completion initialization (replaces initExtraBeforeCompInit
- 1000 (default: General configuration (replaces initExtra
- 1500 (mkAfter: Last to run configuration
To specify both content in Early initialization and General configuration, use lib.mkMerge:
#+begin_src nix
initContent = let
zshConfigEarlyInit = lib.mkOrder 500 "do something";
zshConfig = lib.mkOrder 1000 "do something";
in
lib.mkMerge [ zshConfigEarlyInit zshConfig ];
#+end_src
Currently I only use it as before with =initExtra= though.
#+begin_src nix :tangle modules/home/common/zsh.nix
{ config, lib, ... }:
let
inherit (config.swarselsystems) flakePath;
in
{
options.swarselsystems = {
modules.zsh = lib.mkEnableOption "zsh settings";
shellAliases = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
};
};
config = lib.mkIf config.swarselsystems.modules.zsh {
sops.secrets = {
croc-password = { };
};
programs.zsh = {
enable = true;
shellAliases = lib.recursiveUpdate
{
hg = "history | grep";
hmswitch = "home-manager --flake ${flakePath}#$(whoami)@$(hostname) switch |& nom";
# nswitch = "sudo nixos-rebuild --flake ${flakePath}#$(hostname) --show-trace --log-format internal-json -v switch |& nom --json";
nswitch = "swarsel-deploy $(hostname) switch";
# nboot = "sudo nixos-rebuild --flake ${flakePath}#$(hostname) --show-trace --log-format internal-json -v boot |& nom --json";
nboot = "swarsel-deploy $(hostname) boot";
magit = "emacsclient -nc -e \"(magit-status)\"";
config = "git --git-dir=$HOME/.cfg/ --work-tree=$HOME";
g = "git";
c = "git --git-dir=$FLAKE/.git --work-tree=$FLAKE/";
passpush = "cd ~/.local/share/password-store; git add .; git commit -m 'pass file changes'; git push; cd -;";
passpull = "cd ~/.local/share/password-store; git pull; cd -;";
hotspot = "nmcli connection up local; nmcli device wifi hotspot;";
youtube-dl = "yt-dlp";
cat-orig = "cat";
cdr = "cd \"$( (find $DOCUMENT_DIR_WORK $DOCUMENT_DIR_PRIV -maxdepth 1 && echo $FLAKE) | fzf )\"";
nix-ldd-ldd = "LD_LIBRARY_PATH=$NIX_LD_LIBRARY_PATH ldd";
nix-ldd = "LD_LIBRARY_PATH=$NIX_LD_LIBRARY_PATH ldd";
nix-ldd-locate = "nix-locate --minimal --top-level -w ";
nix-store-search = "ls /nix/store | grep";
fs-diff = "sudo mount -o subvol=/ /dev/mapper/cryptroot /mnt ; fs-diff";
lt = "eza -las modified --total-size";
boot-diff = "nix store diff-closures /run/*-system";
gen-diff = "nix profile diff-closures --profile /nix/var/nix/profiles/system";
cc = "wl-copy";
}
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 = 100000;
size = 100000;
};
historySubstringSearch = {
enable = true;
searchDownKey = "^[OB";
searchUpKey = "^[OA";
};
plugins = [
# {
# name = "fzf-tab";
# src = pkgs.zsh-fzf-tab;
# }
];
initContent = ''
my-forward-word() {
local WORDCHARS=$WORDCHARS
WORDCHARS="''${WORDCHARS//:}"
WORDCHARS="''${WORDCHARS//\/}"
WORDCHARS="''${WORDCHARS//.}"
zle forward-word
}
zle -N my-forward-word
# ctrl + right
bindkey "^[[1;5C" my-forward-word
# shift + right
bindkey "^[[1;2C" forward-word
my-backward-word() {
local WORDCHARS=$WORDCHARS
WORDCHARS="''${WORDCHARS//:}"
WORDCHARS="''${WORDCHARS//\/}"
WORDCHARS="''${WORDCHARS//.}"
zle backward-word
}
zle -N my-backward-word
# ctrl + left
bindkey "^[[1;5D" my-backward-word
# shift + left
bindkey "^[[1;2D" backward-word
my-backward-delete-word() {
local WORDCHARS=$WORDCHARS
WORDCHARS="''${WORDCHARS//:}"
WORDCHARS="''${WORDCHARS//\/}"
WORDCHARS="''${WORDCHARS//.}"
zle backward-delete-word
}
zle -N my-backward-delete-word
# ctrl + del
bindkey '^H' my-backward-delete-word
export CROC_PASS="$(cat ${config.sops.secrets.croc-password.path})"
'';
};
};
}
#+end_src
**** zellij
:PROPERTIES:
:CUSTOM_ID: h:00de4901-631c-4b4c-86ce-d9d6e62ed8c7
:END:
#+begin_src nix :tangle modules/home/common/zellij.nix
{ self, lib, config, pkgs, ... }:
{
options.swarselsystems.modules.zellij = lib.mkEnableOption "zellij settings";
config = lib.mkIf config.swarselsystems.modules.zellij {
programs.zellij = {
enable = true;
enableZshIntegration = true;
};
home.packages = with pkgs; [
zjstatus
];
xdg.configFile = {
"zellij/config.kdl".text = import "${self}/programs/zellij/config.kdl.nix" { inherit config; };
"zellij/layouts/default.kdl".text = import "${self}/programs/zellij/layouts/default.kdl.nix" { inherit config pkgs; };
};
};
}
#+end_src
**** tmux
:PROPERTIES:
:CUSTOM_ID: h:45de9430-f925-4df6-9db6-bffb5b8f1604
:END:
#+begin_src nix :tangle modules/home/common/tmux.nix
{ lib, config, pkgs, ... }:
let
tmux-super-fingers = pkgs.tmuxPlugins.mkTmuxPlugin
{
pluginName = "tmux-super-fingers";
version = "unstable-2023-01-06";
src = pkgs.fetchFromGitHub {
owner = "artemave";
repo = "tmux_super_fingers";
rev = "2c12044984124e74e21a5a87d00f844083e4bdf7";
sha256 = "sha256-cPZCV8xk9QpU49/7H8iGhQYK6JwWjviL29eWabuqruc=";
};
};
in
{
options.swarselsystems.modules.tmux = lib.mkEnableOption "tmux settings";
config = lib.mkIf config.swarselsystems.modules.tmux {
home.packages = with pkgs; [
lsof
sesh
];
programs.tmux = {
enable = true;
shell = "${pkgs.zsh}/bin/zsh";
terminal = "tmux-256color";
historyLimit = 100000;
plugins = with pkgs;
[
tmuxPlugins.tmux-thumbs
{
plugin = tmux-super-fingers;
extraConfig = "set -g @super-fingers-key f";
}
tmuxPlugins.sensible
# must be before continuum edits right status bar
{
plugin = tmuxPlugins.catppuccin;
extraConfig = ''
set -g @catppuccin_flavour 'frappe'
set -g @catppuccin_window_tabs_enabled on
set -g @catppuccin_date_time "%H:%M"
'';
}
{
plugin = tmuxPlugins.resurrect;
extraConfig = ''
set -g @resurrect-strategy-vim 'session'
set -g @resurrect-strategy-nvim 'session'
set -g @resurrect-capture-pane-contents 'on'
'';
}
{
plugin = tmuxPlugins.continuum;
extraConfig = ''
set -g @continuum-restore 'on'
set -g @continuum-boot 'on'
set -g @continuum-save-interval '10'
'';
}
tmuxPlugins.better-mouse-mode
tmuxPlugins.yank
];
extraConfig = ''
set -g default-terminal "tmux-256color"
set -ag terminal-overrides ",xterm-256color:RGB"
set-option -g prefix C-a
unbind-key C-b
bind-key C-a send-prefix
set -g mouse on
# Open new split at cwd of current split
bind | split-window -h -c "#{pane_current_path}"
bind - split-window -v -c "#{pane_current_path}"
# Use vim keybindings in copy mode
set-window-option -g mode-keys vi
# v in copy mode starts making selection
bind-key -T copy-mode-vi v send-keys -X begin-selection
bind-key -T copy-mode-vi C-v send-keys -X rectangle-toggle
bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel
# Escape turns on copy mode
bind Escape copy-mode
set-option -g status-position top
# make Prefix p paste the buffer.
unbind p
bind p paste-buffer
'';
};
};
}
#+end_src
**** Mail
:PROPERTIES:
:CUSTOM_ID: h:506d01fc-c20b-473a-ac78-bce4b53fe0e3
:END:
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.
#+begin_src nix :tangle modules/home/common/mail.nix
{ lib, config, nixosConfig, ... }:
let
inherit (nixosConfig.repo.secrets.common.mail) address1 address2 add2Name address3 add3Name address4;
inherit (nixosConfig.repo.secrets.common) fullName;
in
{
options.swarselsystems.modules.mail = lib.mkEnableOption "mail settings";
config = lib.mkIf config.swarselsystems.modules.mail {
programs = {
mbsync = {
enable = true;
};
msmtp = {
enable = true;
};
mu = {
enable = true;
};
};
services.mbsync = {
enable = true;
};
# this is needed so that mbsync can use the passwords from sops
systemd.user.services.mbsync.Unit.After = [ "sops-nix.service" ];
accounts = lib.mkIf (!config.swarselsystems.isPublic) {
email = {
maildirBasePath = "Mail";
accounts = {
leon = {
primary = true;
address = address1;
userName = address1;
realName = fullName;
passwordCommand = "cat ${config.sops.secrets.leon.path}";
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;
};
};
};
};
swarsel = {
address = address4;
userName = "8227dc594dd515ce232eda1471cb9a19";
realName = fullName;
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;
};
};
nautilus = {
primary = false;
address = address2;
userName = address2;
realName = add2Name;
passwordCommand = "cat ${config.sops.secrets.nautilus.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;
};
};
};
};
mrswarsel = {
primary = false;
address = address3;
userName = address3;
realName = add3Name;
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;
};
};
};
};
};
};
};
};
}
#+end_src
**** Home-manager: Emacs
:PROPERTIES:
:CUSTOM_ID: h:c05d1b64-7110-4151-b436-46bc447113b4
:END:
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.
#+begin_src nix :tangle modules/home/common/emacs.nix
{ self, lib, config, pkgs, ... }:
let
inherit (config.swarselsystems) homeDir isPublic;
in
{
options.swarselsystems.modules.emacs = lib.mkEnableOption "emacs settings";
config = lib.mkIf config.swarselsystems.modules.emacs {
# needed for elfeed
sops.secrets.fever = lib.mkIf (!isPublic) { path = "${homeDir}/.emacs.d/.fever"; };
# 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-git-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";
};
};
}
#+end_src
**** Waybar
:PROPERTIES:
:CUSTOM_ID: h:0bf51f63-01c0-4053-a591-7f0c5697c690
:END:
Again I am just using the first bar option here that I was able to find good understandable documentation for. Of note is that the `cpu` section's `format` is not defined here, but in section 1 (since not every machine has the same number of cores)
This section is mostly used to deliver the correct information to Waybar. AMD systems have changing hwmon paths that can be specifically set here. Also the cpu count can be set here for Waybars cpu module, but 8 is usually a good setting to show
These are explicit waybar options. Laptops do not need the battery module. However, this leads to a slight problem with theming: my waybar modules alternate their background-color between black and grey. The battery module is usually on grey background. If I were to simply delete that, I would now have two modules on black background. To avoid this, I define a pseudo-module =custom/pseudobat= that simply shows a static image and calls =wlogout= on right click. This wastes a little bit of screen space, but that is a price I am willing to pay for consistency.
The rest of the related configuration is found here:
- [[#h:a9530c81-1976-442b-b597-0b4bed6baf25][Waybar]]
- [[#h:f93f66f9-6b8b-478e-b139-b2f382c1f25e][waybarupdate]]
#+begin_src nix :tangle modules/home/common/waybar.nix
{ self, config, lib, ... }:
let
generateIcons = n: lib.concatStringsSep " " (builtins.map (x: "{icon" + toString x + "}") (lib.range 0 (n - 1)));
modulesLeft = [
"custom/outer-left-arrow-dark"
"mpris"
"custom/left-arrow-light"
"network"
"custom/vpn"
"custom/left-arrow-dark"
"pulseaudio"
"custom/left-arrow-light"
];
modulesRight = [
"custom/left-arrow-dark"
"group/hardware"
"custom/left-arrow-light"
"clock#2"
"custom/left-arrow-dark"
"clock#1"
];
in
{
options.swarselsystems = {
modules.waybar = lib.mkEnableOption "waybar settings";
cpuCount = lib.mkOption {
type = lib.types.int;
default = 8;
};
temperatureHwmon = {
isAbsolutePath = lib.mkEnableOption "absolute temperature path";
path = lib.mkOption {
type = lib.types.str;
default = "";
};
input-filename = lib.mkOption {
type = lib.types.str;
default = "";
};
};
waybarModules = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = modulesLeft ++ [
"custom/pseudobat"
] ++ modulesRight;
};
cpuString = lib.mkOption {
type = lib.types.str;
default = generateIcons config.swarselsystems.cpuCount;
description = "The generated icons string for use by Waybar.";
internal = true;
};
};
config = lib.mkIf config.swarselsystems.modules.waybar {
swarselsystems = {
waybarModules = lib.mkIf config.swarselsystems.isLaptop (modulesLeft ++ [
"battery"
] ++ modulesRight);
};
programs.waybar = {
enable = true;
systemd = {
enable = true;
target = "sway-sessions.target";
};
settings = {
mainBar = {
ipc = true;
id = "bar-0";
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 = "{}";
};
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";
};
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} [{position}/{length}]";
format-paused = "{player_icon} {title} [{position}/{length}]";
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 = "{:%Y %B}\n{calendar}";
};
"clock#2" = {
format = "{:%d. %B %Y}";
# on-click-right= "gnome-clocks";
tooltip-format = "{:%Y %B}\n{calendar}";
};
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);
};
};
}
#+end_src
**** Firefox
:PROPERTIES:
:CUSTOM_ID: h:fbec0bd4-690b-4f79-8b2b-a40263760a96
:END:
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.
#+begin_src nix :tangle modules/home/common/firefox.nix
{ config, pkgs, lib, ... }:
{
options.swarselsystems.modules.firefox = lib.mkEnableOption "firefox settings";
config = lib.mkIf config.swarselsystems.modules.firefox {
programs.firefox = {
enable = true;
package = pkgs.firefox; # uses overrides
policies = {
# CaptivePortal = false;
AppAutoUpdate = false;
BackgroundAppUpdate = false;
DisableBuiltinPDFViewer = true;
DisableFirefoxStudies = true;
DisablePocket = true;
DisableFirefoxScreenshots = true;
DisableTelemetry = true;
DisableFirefoxAccounts = false;
DisableProfileImport = true;
DisableProfileRefresh = true;
DisplayBookmarksToolbar = "always";
DontCheckDefaultBrowser = true;
NoDefaultBookmarks = true;
OfferToSaveLogins = false;
OfferToSaveLoginsDefault = false;
PasswordManagerEnabled = false;
DisableMasterPasswordCreation = true;
ExtensionUpdate = false;
EnableTrackingProtection = {
Value = true;
Locked = true;
Cryptomining = true;
Fingerprinting = true;
EmailTracking = true;
# Exceptions = ["https://example.com"]
};
PDFjs = {
Enabled = false;
EnablePermissions = false;
};
Handlers = {
mimeTypes."application/pdf".action = "saveToDisk";
};
extensions = {
pdf = {
action = "useHelperApp";
ask = true;
handlers = [
{
name = "GNOME Document Viewer";
path = "${pkgs.evince}/bin/evince";
}
];
};
};
FirefoxHome = {
Search = true;
TopSites = true;
SponsoredTopSites = false;
Highlights = true;
Pocket = false;
SponsoredPocket = false;
Snippets = false;
Locked = true;
};
FirefoxSuggest = {
WebSuggestions = false;
SponsoredSuggestions = false;
ImproveSuggest = false;
Locked = true;
};
SanitizeOnShutdown = {
Cache = true;
Cookies = false;
Downloads = true;
FormData = true;
History = false;
Sessions = false;
SiteSettings = false;
OfflineApps = true;
Locked = true;
};
SearchEngines = {
PreventInstalls = true;
Remove = [
"Bing" # Fuck you
];
};
UserMessaging = {
ExtensionRecommendations = false; # Don’t recommend extensions while the user is visiting web pages
FeatureRecommendations = false; # Don’t recommend browser features
Locked = true; # Prevent the user from changing user messaging preferences
MoreFromMozilla = false; # Don’t show the “More from Mozilla” section in Preferences
SkipOnboarding = true; # Don’t show onboarding messages on the new tab page
UrlbarInterventions = false; # Don’t offer suggestions in the URL bar
WhatsNew = false; # Remove the “What’s New” icon and menuitem
};
ExtensionSettings = {
"3rdparty".Extensions = {
# https://github.com/gorhill/uBlock/blob/master/platform/common/managed_storage.json
"uBlock0@raymondhill.net".adminSettings = {
userSettings = rec {
uiTheme = "dark";
uiAccentCustom = true;
uiAccentCustom0 = "#0C8084";
cloudStorageEnabled = lib.mkForce false;
importedLists = [
"https://filters.adtidy.org/extension/ublock/filters/3.txt"
"https://github.com/DandelionSprout/adfilt/raw/master/LegitimateURLShortener.txt"
];
externalLists = lib.concatStringsSep "\n" importedLists;
};
selectedFilterLists = [
"CZE-0"
"adguard-generic"
"adguard-annoyance"
"adguard-social"
"adguard-spyware-url"
"easylist"
"easyprivacy"
"https://github.com/DandelionSprout/adfilt/raw/master/LegitimateURLShortener.txt"
"plowe-0"
"ublock-abuse"
"ublock-badware"
"ublock-filters"
"ublock-privacy"
"ublock-quick-fixes"
"ublock-unbreak"
"urlhaus-1"
];
};
};
};
};
profiles = {
default = lib.recursiveUpdate
{
id = 0;
isDefault = true;
settings = {
"browser.startup.homepage" = "https://lobste.rs";
};
}
config.swarselsystems.firefox;
};
};
};
}
#+end_src
**** Services
:PROPERTIES:
:CUSTOM_ID: h:387c3a82-1fb1-4c0f-8051-874e2acb8804
:END:
Services that can be defined through home-manager should be defined here.
***** gnome-keyring
:PROPERTIES:
:CUSTOM_ID: h:cb812c8a-247c-4ce5-a00c-59332c2f5fb9
:END:
Used for storing sessions in e.g. Nextcloud
#+begin_src nix :tangle modules/home/common/gnome-keyring.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.gnome-keyring = lib.mkEnableOption "gnome keyring settings";
config = lib.mkIf config.swarselsystems.modules.gnome-keyring {
services.gnome-keyring = lib.mkIf (!config.swarselsystems.isNixos) {
enable = true;
};
};
}
#+end_src
***** KDE Connect
:PROPERTIES:
:CUSTOM_ID: h:be6afd89-9e1e-40b6-8542-5c07a0ab780d
:END:
This enables phone/computer communication, including sending clipboard, files etc. Sadly on Wayland many of the features are broken (like remote control).
#+begin_src nix :tangle modules/home/common/kdeconnect.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.kdeconnect = lib.mkEnableOption "kdeconnect settings";
config = lib.mkIf config.swarselsystems.modules.kdeconnect {
services.kdeconnect = {
enable = true;
indicator = true;
};
};
}
#+end_src
***** Mako
:PROPERTIES:
:CUSTOM_ID: h:99d05729-df35-4958-9940-3319d6a41359
:END:
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.
#+begin_src nix :tangle modules/home/common/mako.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.mako = lib.mkEnableOption "mako settings";
config = lib.mkIf config.swarselsystems.modules.mako {
services.mako = {
enable = true;
settings = {
border-radius = 15;
border-size = 1;
default-timeout = 5000;
ignore-timeout = 1;
icons = 1;
layer = "overlay";
sort = "-time";
height = 150;
width = 300;
"urgency=low" = {
border-color = lib.mkForce "#cccccc";
};
"urgency=normal" = {
border-color = lib.mkForce "#d08770";
};
"urgency=high" = {
border-color = lib.mkForce "#bf616a";
default-timeout = 3000;
};
"category=mpd" = {
default-timeout = 2000;
group-by = "category";
};
};
};
};
}
#+end_src
***** SwayOSD
:PROPERTIES:
:CUSTOM_ID: h:388e71be-f00a-4d45-ade1-218ce942057d
:END:
#+begin_src nix :tangle modules/home/common/swayosd.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.swayosd = lib.mkEnableOption "swayosd settings";
config = lib.mkIf config.swarselsystems.modules.swayosd {
services.swayosd = {
enable = true;
topMargin = 0.5;
};
};
}
#+end_src
***** yubikey-touch-detector
:PROPERTIES:
:CUSTOM_ID: h:1598c90b-f195-41a0-9132-94612edf3586
:END:
#+begin_src nix :tangle modules/home/common/yubikey-touch-detector.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.yubikeytouch = lib.mkEnableOption "yubikey touch detector service settings";
config = lib.mkIf config.swarselsystems.modules.yubikeytouch {
systemd.user.services.yubikey-touch-detector = {
Unit = {
Description = "Detects when your YubiKey is waiting for a touch";
Requires = [ "yubikey-touch-detector.socket" ];
};
Service = {
ExecStart = "${pkgs.yubikey-touch-detector}/bin/yubikey-touch-detector --libnotify";
EnvironmentFile = "-%E/yubikey-touch-detector/service.conf";
};
Install = {
Also = [ "yubikey-touch-detector.socket" ];
WantedBy = [ "default.target" ];
};
};
systemd.user.sockets.yubikey-touch-detector = {
Unit = {
Description = "Unix socket activation for YubiKey touch detector service";
};
Socket = {
ListenStream = "%t/yubikey-touch-detector.socket";
RemoveOnStop = true;
};
Install = {
WantedBy = [ "sockets.target" ];
};
};
};
}
#+end_src
**** Sway
:PROPERTIES:
:CUSTOM_ID: h:02df9dfc-d1af-4a37-a7a0-d8da0af96a20
:END:
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.
#+begin_src nix :tangle modules/home/common/sway.nix
{ self, config, lib, ... }:
{
options.swarselsystems = {
modules.sway = lib.mkEnableOption "sway settings";
inputs = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
default = { };
};
monitors = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
default = { };
};
keybindings = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
};
startup = lib.mkOption {
type = lib.types.listOf (lib.types.attrsOf lib.types.str);
default = [
{ command = "nextcloud --background"; }
{ command = "vesktop --start-minimized --enable-speech-dispatcher --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime"; }
{ command = "element-desktop --hidden --enable-features=UseOzonePlatform --ozone-platform=wayland --disable-gpu-driver-bug-workarounds"; }
{ command = "ANKI_WAYLAND=1 anki"; }
{ command = "OBSIDIAN_USE_WAYLAND=1 obsidian"; }
{ command = "nm-applet"; }
{ command = "feishin"; }
];
};
kyria = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
default = {
"36125:53060:splitkb.com_splitkb.com_Kyria_rev3" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
"7504:24926:Kyria_Keyboard" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
};
internal = true;
};
standardinputs = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
default = lib.recursiveUpdate (lib.recursiveUpdate config.swarselsystems.touchpad config.swarselsystems.kyria) config.swarselsystems.inputs;
internal = true;
};
touchpad = lib.mkOption {
type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
default = { };
internal = true;
};
swayfxConfig = lib.mkOption {
type = lib.types.str;
default = "
blur enable
blur_xray disable
blur_passes 1
blur_radius 1
shadows enable
corner_radius 2
titlebar_separator disable
default_dim_inactive 0.02
";
internal = true;
};
};
config = lib.mkIf config.swarselsystems.modules.sway {
swarselsystems = {
touchpad = lib.mkIf config.swarselsystems.isLaptop {
"type:touchpad" = {
dwt = "enabled";
tap = "enabled";
natural_scroll = "enabled";
middle_emulation = "enabled";
drag_lock = "disabled";
};
};
swayfxConfig = lib.mkIf (!config.swarselsystems.isNixos) " ";
};
wayland.windowManager.sway = {
enable = true;
checkConfig = false; # delete this line once SwayFX is fixed upstream
package = lib.mkIf config.swarselsystems.isNixos null;
systemd = {
enable = true;
xdgAutostart = true;
};
wrapperFeatures.gtk = true;
config = rec {
modifier = "Mod4";
# terminal = "kitty";
menu = "fuzzel";
bars = [{
command = "waybar";
mode = "hide";
hiddenState = "hide";
position = "top";
extraConfig = "modifier Mod4";
}];
keybindings =
let
inherit (config.wayland.windowManager.sway.config) modifier;
in
lib.recursiveUpdate
{
"${modifier}+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 swaymsg workspace back_and_forth";
"${modifier}+a" = "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}+Shift+v" = "exec wf-recorder -g '$(slurp -f %o -or)' -f ~/Videos/screenrecord_$(date +%Y-%m-%d-%H%M%S).mkv";
"${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}+Ctrl+Shift+r" = "exec swarsel-displaypower";
"${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}+Return" = "exec swarselzellij";
"${modifier}+Print" = "exec screenshare";
# exec swaymsg move workspace to "$(swaymsg -t get_outputs | jq '[.[] | select(.active == true)] | .[(map(.focused) | index(true) + 1) % length].name')"
# "XF86AudioRaiseVolume" = "exec pa 5%";
# "XF86AudioRaiseVolume" = "exec pamixer -i 5";
"XF86AudioRaiseVolume" = "exec swayosd-client --output-volume raise";
# "XF86AudioLowerVolume" = "exec pactl set-sink-volume @DEFAULT_SINK@ -5%";
# "XF86AudioLowerVolume" = "exec pamixer -d 5";
"XF86AudioLowerVolume" = "exec swayosd-client --output-volume lower";
# "XF86AudioMute" = "exec pactl set-sink-mute @DEFAULT_SINK@ toggle";
# "XF86AudioMute" = "exec pamixer -t";
"XF86AudioMute" = "exec swayosd-client --output-volume mute-toggle";
# "XF86MonBrightnessUp" = "exec brightnessctl set +5%";
"XF86MonBrightnessUp" = "exec swayosd-client --brightness raise";
# "XF86MonBrightnessDown" = "exec brightnessctl set 5%-";
"XF86MonBrightnessDown" = "exec swayosd-client --brightness lower";
"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' lib.swarselsystems.eachMonitor monitors;
output = {
"${config.swarselsystems.sharescreen}" = {
bg = "${self}/wallpaper/lenovowp.png ${config.stylix.imageScalingMode}";
};
"Philips Consumer Electronics Company PHL BDM3270 AU11806002320" = {
bg = "${self}/wallpaper/standwp.png ${config.stylix.imageScalingMode}";
};
};
input = config.swarselsystems.standardinputs;
workspaceOutputAssign =
let
workplaceSets = lib.mapAttrs' lib.swarselsystems.eachOutput config.swarselsystems.monitors;
workplaceOutputs = map (key: lib.getAttr key workplaceSets) (lib.attrNames workplaceSets);
in
workplaceOutputs;
startup = config.swarselsystems.startup ++ [
{ command = "kitty -T kittyterm -o confirm_os_window_close=0 zellij attach --create kittyterm"; }
{ command = "sleep 60; kitty -T spotifytui -o confirm_os_window_close=0 spotify_player"; }
];
seat = {
"*" = {
hide_cursor = "when-typing enable";
};
};
window = {
border = 1;
titlebar = false;
};
assigns = {
"15:L" = [{ app_id = "teams-for-linux"; }];
};
floating = {
border = 1;
criteria = [
{ app_id = "qalculate-gtk"; }
{ app_id = "blueman"; }
{ app_id = "pavucontrol"; }
{ app_id = "syncthingtray"; }
{ app_id = "Element"; }
{ class = "1Password"; }
{ app_id = "com.nextcloud.desktopclient.nextcloud"; }
{ title = "(?:Open|Save) (?:File|Folder|As)"; }
{ title = "^Add$"; }
{ title = "^Picture-in-Picture$"; }
{ title = "Syncthing Tray"; }
{ title = "^spotifytui$"; }
{ title = "^kittyterm$"; }
{ app_id = "vesktop"; }
{ window_role = "pop-up"; }
{ window_role = "bubble"; }
{ window_role = "dialog"; }
{ window_role = "task_dialog"; }
{ window_role = "menu"; }
{ window_role = "Preferences"; }
];
titlebar = false;
};
window = {
commands = [
{
command = "opacity 0.95";
criteria = {
class = ".*";
};
}
{
command = "opacity 1";
criteria = {
app_id = "at.yrlf.wl_mirror";
};
}
{
command = "opacity 1";
criteria = {
app_id = "Gimp-2.10";
};
}
{
command = "opacity 0.99";
criteria = {
app_id = "firefox";
};
}
{
command = "opacity 0.99";
criteria = {
app_id = "chromium-browser";
};
}
{
command = "sticky enable, shadows enable";
criteria = {
title = "^Picture-in-Picture$";
};
}
{
command = "resize set width 60 ppt height 60 ppt, opacity 0.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\"
h exec \"systemctl hibernate\", mode \"default\"
l exec \"swaylock --screenshots --clock --effect-blur 7x5 --effect-vignette 0.5:0.5 --fade-in 0.2 --daemonize\", mode \"default\
p exec \"systemctl poweroff\"
r exec \"systemctl reboot\"
u exec \"swaymsg exit\"
Return mode \"default\"
Escape mode \"default\"
${modifier}+Escape mode \"default\"
}
}
exec systemctl --user import-environment
exec swayidle -w
seat * hide_cursor 2000
exec_always kill -1 $(pidof kanshi)
bindswitch --locked lid:on exec kanshictl switch lidclosed
bindswitch --locked lid:off exec kanshictl switch lidopen
${swayfxSettings}
";
};
};
}
#+end_src
**** Kanshi
:PROPERTIES:
:CUSTOM_ID: h:eb94df98-2bcd-4555-9f88-e252f93b924f
:END:
#+begin_src nix :tangle modules/home/common/kanshi.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.kanshi = lib.mkEnableOption "kanshi settings";
config = lib.mkIf config.swarselsystems.modules.kanshi {
swarselsystems = {
monitors = {
homedesktop = {
name = "Philips Consumer Electronics Company PHL BDM3270 AU11806002320";
mode = "2560x1440";
scale = "1";
position = "0,0";
workspace = "1:一";
output = "DP-11";
};
};
};
services.kanshi = {
enable = true;
settings = [
{
# laptop screen
output = {
criteria = config.swarselsystems.sharescreen;
mode = config.swarselsystems.highResolution;
scale = 1.0;
};
}
{
# home main screen
output = {
criteria = "Philips Consumer Electronics Company PHL BDM3270 AU11806002320";
scale = 1.0;
mode = "2560x1440";
};
}
{
profile = {
name = "lidopen";
outputs = [
{
criteria = "eDP-2";
status = "enable";
scale = 1.0;
}
];
};
}
{
profile = {
name = "lidopen";
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "enable";
scale = 1.7;
position = "2560,0";
}
{
criteria = "Philips Consumer Electronics Company PHL BDM3270 AU11806002320";
scale = 1.0;
mode = "2560x1440";
position = "0,0";
}
];
};
}
{
profile = {
name = "lidclosed";
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "disable";
position = "2560,0";
}
{
criteria = "Philips Consumer Electronics Company PHL BDM3270 AU11806002320";
scale = 1.0;
mode = "2560x1440";
position = "0,0";
}
];
};
}
];
};
};
}
#+end_src
**** gpg-agent
:PROPERTIES:
:CUSTOM_ID: h:7d384e3b-1be7-4644-b304-ada4af0b692b
:END:
Settinfs that are needed for the gpg-agent. Also we are enabling emacs support for unlocking my Yubikey here.
#+begin_src nix :tangle modules/home/common/gpg-agent.nix
{ self, lib, config, pkgs, ... }:
let
inherit (config.swarselsystems) mainUser homeDir;
in
{
options.swarselsystems.modules.gpgagent = lib.mkEnableOption "gpg agent settings";
config = lib.mkIf config.swarselsystems.modules.gpgagent {
services.gpg-agent = {
enable = true;
enableZshIntegration = true;
enableScDaemon = true;
enableSshSupport = true;
enableExtraSocket = true;
pinentry.package = pkgs.pinentry.gtk2;
defaultCacheTtl = 600;
maxCacheTtl = 7200;
extraConfig = ''
allow-loopback-pinentry
allow-emacs-pinentry
'';
sshKeys = [
"4BE7925262289B476DBBC17B76FD3810215AE097"
];
};
programs.gpg = {
enable = true;
publicKeys = [
{
source = "${self}/secrets/keys/gpg/gpg-public-key-0x76FD3810215AE097.asc";
trust = 5;
}
];
};
# assure correct permissions
systemd.user.tmpfiles.rules = [
"d ${homeDir}/.gnupg 700 ${mainUser} users"
];
};
}
#+end_src
**** gammastep
:PROPERTIES:
:CUSTOM_ID: h:74e236be-a977-4d38-b8c5-0b9feef8af91
:END:
This service changes the screen hue at night. I am not sure if that really does something, but I like the color anyways.
#+begin_src nix :tangle modules/home/common/gammastep.nix
{ lib, config, nixosConfig, ... }:
let
inherit (nixosConfig.repo.secrets.common.location) latitude longitude;
in
{
options.swarselsystems.modules.gammastep = lib.mkEnableOption "gammastep settings";
config = lib.mkIf config.swarselsystems.modules.gammastep {
services.gammastep = {
enable = true;
provider = "manual";
inherit longitude latitude;
};
};
}
#+end_src
*** Server
:PROPERTIES:
:CUSTOM_ID: h:b1a00339-6e9b-4ae4-b5dc-6fd5669a2ddb
:END:
**** Imports
:PROPERTIES:
:CUSTOM_ID: h:7b4ee01a-b505-47da-8fb9-0b41285d0eab
:END:
This section sets up all the imports that are used in the home-manager section.
#+begin_src nix :tangle modules/home/server/default.nix
{ self, lib, ... }:
let
importNames = lib.swarselsystems.readNix "modules/home/server";
modulesPath = "${self}/modules";
in
{
imports = lib.swarselsystems.mkImports importNames "modules/home/server" ++ [
"${modulesPath}/home/common/settings.nix"
"${modulesPath}/home/common/sharedsetup.nix"
];
}
#+end_src
**** Symlinking dotfiles
:PROPERTIES:
:CUSTOM_ID: h:9fac0904-b615-4d9d-9bae-54a6691999c3
:END:
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.
#+begin_src nix :tangle modules/home/server/symlink.nix
{ self, lib, config, ... }:
{
options.swarselsystems.modules.server.dotfiles = lib.mkEnableOption "server dotfiles settings";
config = lib.mkIf config.swarselsystems.modules.server.dotfiles {
home.file = {
"init.el" = lib.mkForce {
source = self + /programs/emacs/server.el;
target = ".emacs.d/init.el";
};
};
};
}
#+end_src
*** Darwin
:PROPERTIES:
:CUSTOM_ID: h:e0536bff-2552-4ac4-a34a-a23937a2c30f
:END:
**** Imports
:PROPERTIES:
:CUSTOM_ID: h:cff37bdf-4f22-419a-af4e-2665ede9add0
:END:
This section sets up all the imports that are used in the home-manager section.
#+begin_src nix :tangle modules/darwin/home/default.nix
{ self, ... }:
let
modulesPath = "${self}/modules";
in
{
imports = [
"${modulesPath}/home/common/settings.nix"
"${modulesPath}/home/common/sharedsetup.nix"
];
}
#+end_src
*** Optional
:PROPERTIES:
:CUSTOM_ID: h:be623200-557e-4bb7-bb11-1ec5d76c6b8b
:END:
Akin to the optional NixOS modules.
**** Gaming
:PROPERTIES:
:CUSTOM_ID: h:84fd7029-ecb6-4131-9333-289982f24ffa
:END:
The rest of the settings is at [[#h:fb3f3e01-7df4-4b06-9e91-aa9cac61a431][gaming]].
#+begin_src nix :tangle modules/home/optional/gaming.nix
{ lib, config, pkgs, ... }:
{
options.swarselsystems.modules.optional.gaming = lib.mkEnableOption "optional gaming settings";
config = lib.mkIf config.swarselsystems.modules.optional.gaming {
# specialisation = {
# gaming.configuration = {
home.packages = with pkgs; [
lutris
wine
protonplus
winetricks
libudev-zero
dwarfs
fuse-overlayfs
# steam
steam-run
patchelf
gamescope
vulkan-tools
moonlight-qt
ns-usbloader
quark-goldleaf
# gog games installing
heroic
# minecraft
prismlauncher # has overrides
temurin-bin-17
pokefinder
retroarch
flips
];
# };
# };
};
}
#+end_src
**** Work
:PROPERTIES:
:CUSTOM_ID: h:f0b2ea93-94c8-48d8-8d47-6fe58f58e0e6
:END:
The rest of the settings is at [[#h:bbf2ecb6-c8ff-4462-b5d5-d45b28604ddf][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.
#+begin_src nix :tangle modules/home/optional/work.nix :noweb yes
{ self, config, pkgs, lib, nixosConfig, ... }:
let
inherit (config.swarselsystems) homeDir;
inherit (nixosConfig.repo.secrets.local.work) user1 user1Long user2 user2Long user3 user3Long user4 path1 loc1 loc2 site1 site2 site3 site4 site5 site6 site7 lifecycle1 lifecycle2 domain1 domain2 gitMail;
in
{
options.swarselsystems.modules.optional.work = lib.mkEnableOption "optional work settings";
config = lib.mkIf config.swarselsystems.modules.optional.work {
home.packages = with pkgs; [
stable.teams-for-linux
shellcheck
dig
docker
postman
rclone
stable24_05.awscli2
libguestfs-with-appliance
stable.prometheus.cli
tigervnc
openstackclient
];
home.sessionVariables = {
DOCUMENT_DIR_PRIV = lib.mkForce "${homeDir}/Documents/Private";
DOCUMENT_DIR_WORK = lib.mkForce "${homeDir}/Documents/Work";
};
wayland.windowManager.sway.config = {
output = {
"Applied Creative Technology Transmitter QUATTRO201811" = {
bg = "${self}/wallpaper/navidrome.png ${config.stylix.imageScalingMode}";
};
"Hewlett Packard HP Z24i CN44250RDT" = {
bg = "${self}/wallpaper/op6wp.png ${config.stylix.imageScalingMode}";
};
"HP Inc. HP 732pk CNC4080YL5" = {
bg = "${self}/wallpaper/botanicswp.png ${config.stylix.imageScalingMode}";
};
};
};
stylix.targets.firefox.profileNames = [
"${user1}"
"${user2}"
"${user3}"
"work"
];
programs = {
git.userEmail = lib.mkForce gitMail;
zsh = {
shellAliases = {
dssh = "ssh -l ${user1Long}";
cssh = "ssh -l ${user2Long}";
wssh = "ssh -l ${user3Long}";
};
cdpath = [
"~/Documents/Work"
];
dirHashes = {
d = "$HOME/.dotfiles";
w = "$HOME/Documents/Work";
s = "$HOME/.dotfiles/secrets";
pr = "$HOME/Documents/Private";
ac = path1;
};
};
ssh = {
matchBlocks = {
"${loc1}" = {
hostname = "${loc1}.${domain2}";
user = user4;
};
"${loc1}.stg" = {
hostname = "${loc1}.${lifecycle1}.${domain2}";
user = user4;
};
"${loc1}.staging" = {
hostname = "${loc1}.${lifecycle1}.${domain2}";
user = user4;
};
"${loc1}.dev" = {
hostname = "${loc1}.${lifecycle2}.${domain2}";
user = user4;
};
"${loc2}" = {
hostname = "${loc2}.${domain1}";
user = user1Long;
};
"${loc2}.stg" = {
hostname = "${loc2}.${lifecycle1}.${domain2}";
user = user1Long;
};
"${loc2}.staging" = {
hostname = "${loc2}.${lifecycle1}.${domain2}";
user = user1Long;
};
"*.${domain1}" = {
user = user1Long;
};
};
};
firefox = {
profiles =
let
isDefault = false;
in
{
"${user1}" = lib.recursiveUpdate
{
inherit isDefault;
id = 1;
settings = {
"browser.startup.homepage" = "${site1}|${site2}";
};
}
config.swarselsystems.firefox;
"${user2}" = lib.recursiveUpdate
{
inherit isDefault;
id = 2;
settings = {
"browser.startup.homepage" = "${site3}";
};
}
config.swarselsystems.firefox;
"${user3}" = lib.recursiveUpdate
{
inherit isDefault;
id = 3;
}
config.swarselsystems.firefox;
work = lib.recursiveUpdate
{
inherit isDefault;
id = 4;
settings = {
"browser.startup.homepage" = "${site4}|${site5}|${site6}|${site7}";
};
}
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"
];
};
};
services = {
kanshi = {
settings = [
{
# seminary room
output = {
criteria = "Applied Creative Technology Transmitter QUATTRO201811";
scale = 1.0;
mode = "1280x720";
};
}
{
# work main screen
output = {
criteria = "HP Inc. HP 732pk CNC4080YL5";
scale = 1.0;
mode = "3840x2160";
};
}
{
# work side screen
output = {
criteria = "Hewlett Packard HP Z24i CN44250RDT";
scale = 1.0;
mode = "1920x1200";
transform = "270";
};
}
{
profile = {
name = "lidopen";
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "enable";
scale = 1.5;
position = "1462,0";
}
{
criteria = "HP Inc. HP 732pk CNC4080YL5";
scale = 1.4;
mode = "3840x2160";
position = "-1280,0";
}
{
criteria = "Hewlett Packard HP Z24i CN44250RDT";
scale = 1.0;
mode = "1920x1200";
transform = "90";
position = "-2480,0";
}
];
};
}
{
profile = {
name = "lidopen";
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "enable";
scale = 1.7;
position = "2560,0";
}
{
criteria = "Applied Creative Technology Transmitter QUATTRO201811";
scale = 1.0;
mode = "1280x720";
position = "10000,10000";
}
];
};
}
{
profile = {
name = "lidclosed";
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "disable";
}
{
criteria = "HP Inc. HP 732pk CNC4080YL5";
scale = 1.4;
mode = "3840x2160";
position = "-1280,0";
}
{
criteria = "Hewlett Packard HP Z24i CN44250RDT";
scale = 1.0;
mode = "1920x1200";
transform = "270";
position = "-2480,0";
}
];
};
}
{
profile = {
name = "lidclosed";
outputs = [
{
criteria = config.swarselsystems.sharescreen;
status = "disable";
}
{
criteria = "Applied Creative Technology Transmitter QUATTRO201811";
scale = 1.0;
mode = "1280x720";
position = "10000,10000";
}
];
};
}
];
};
};
xdg = {
mimeApps = {
defaultApplications = {
"x-scheme-handler/msteams" = [ "teams-for-linux.desktop" ];
};
};
desktopEntries =
let
terminal = false;
categories = [ "Application" ];
icon = "firefox";
in
{
firefox_work = {
name = "Firefox (work)";
genericName = "Firefox work";
exec = "firefox -p work";
inherit terminal categories icon;
};
"firefox_${user1}" = {
name = "Firefox (${user1})";
genericName = "Firefox ${user1}";
exec = "firefox -p ${user1}";
inherit terminal categories icon;
};
"firefox_${user2}" = {
name = "Firefox (${user2})";
genericName = "Firefox ${user2}";
exec = "firefox -p ${user2}";
inherit terminal categories icon;
};
"firefox_${user3}" = {
name = "Firefox (${user3})";
genericName = "Firefox ${user3}";
exec = "firefox -p ${user3}";
inherit terminal categories icon;
};
};
};
swarselsystems = {
startup = [
{ command = "nextcloud --background"; }
{ command = "vesktop --start-minimized --enable-speech-dispatcher --ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime"; }
{ command = "element-desktop --hidden --enable-features=UseOzonePlatform --ozone-platform=wayland --disable-gpu-driver-bug-workarounds"; }
{ command = "ANKI_WAYLAND=1 anki"; }
{ command = "OBSIDIAN_USE_WAYLAND=1 obsidian"; }
{ command = "nm-applet"; }
{ command = "feishin"; }
{ command = "teams-for-linux --disableGpu=true --minimized=true --trayIconEnabled=true"; }
{ command = "1password"; }
];
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 = "14:T";
output = "DP-4";
};
};
inputs = {
"1133:45081:MX_Master_2S_Keyboard" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
# "2362:628:PIXA3854:00_093A:0274_Touchpad" = {
# dwt = "enabled";
# tap = "enabled";
# natural_scroll = "enabled";
# middle_emulation = "enabled";
# drag_lock = "disabled";
# };
"1133:50504:Logitech_USB_Receiver" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
"1133:45944:MX_KEYS_S" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
};
keybindings = {
"Mod4+Ctrl+Shift+p" = "exec screenshare";
};
};
};
}
#+end_src
**** Framework
:PROPERTIES:
:CUSTOM_ID: h:8a7b1c26-3448-42d3-932a-5d05d54b5490
:END:
This holds configuration that is specific to framework laptops.
#+begin_src nix :tangle modules/home/optional/framework.nix
{ lib, config, ... }:
{
options.swarselsystems.modules.optional.framework = lib.mkEnableOption "optional framework machine settings";
config = lib.mkIf config.swarselsystems.modules.optional.framework {
swarselsystems = {
inputs = {
"12972:18:Framework_Laptop_16_Keyboard_Module_-_ANSI_Keyboard" = {
xkb_layout = "us";
xkb_variant = "altgr-intl";
};
};
};
};
}
#+end_src
* Emacs
:PROPERTIES:
:CUSTOM_ID: h:ed4cd05c-0879-41c6-bc39-3f1246a96f04
:END:
** Initialization (early-init.el)
:PROPERTIES:
:CUSTOM_ID: h:2c331451-45ed-4592-9e00-d36b5bf31248
:END:
In this section I handle my early init file; it takes care of frame-setup for emacsclient buffers.
*** Increase startup performance
:PROPERTIES:
:CUSTOM_ID: h:38e03b65-9dfc-4547-b27d-236664d7dc15
:END:
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.
#+begin_src emacs-lisp :tangle programs/emacs/early-init.el :mkdirp yes
(defvar swarsel-file-name-handler-alist file-name-handler-alist)
(defvar swarsel-vc-handled-backends vc-handled-backends)
(setq gc-cons-threshold most-positive-fixnum
gc-cons-percentage 0.6
file-name-handler-alist nil
vc-handled-backends nil)
(add-hook 'emacs-startup-hook
(lambda ()
(progn
(setq gc-cons-threshold (* 32 1024 1024)
gc-cons-percentage 0.1
jit-lock-defer-time 0.05
read-process-output-max (* 1024 1024)
file-name-handler-alist swarsel-file-name-handler-alist
vc-handled-backends swarsel-vc-handled-backends)
(fset 'epg-wait-for-status 'ignore)
)))
#+end_src
*** Setup frames
:PROPERTIES:
:CUSTOM_ID: h:782b3632-afb2-4c67-8c46-ff94408aef5d
:END:
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.
#+begin_src emacs-lisp :tangle programs/emacs/early-init.el :mkdirp yes
(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))))
#+end_src
*** Make C-i, C-m, C-[ available in graphic sessions
:PROPERTIES:
:CUSTOM_ID: h:396c47f2-7e2f-4fad-ae71-6483bf7e3e42
:END:
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
#+begin_src emacs-lisp :tangle programs/emacs/early-init.el :mkdirp yes
(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])
))))
#+end_src
** Personal settings
:PROPERTIES:
:CUSTOM_ID: h:601ba407-b906-4869-8ef6-67a9fc285fba
:END:
This section is used to define my own functions, own variables, and own keybindings.
*** Custom functions
:PROPERTIES:
:CUSTOM_ID: h:b7b5976a-db2b-493d-8794-1924a0e12aec
:END:
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
:PROPERTIES:
:CUSTOM_ID: h:b715d7cf-da09-4e0c-95bc-8281b4f3ce9c
:END:
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.
#+begin_src emacs-lisp
(defun swarsel/toggle-evil-state ()
(interactive)
(if (or (evil-emacs-state-p) (evil-insert-state-p))
(evil-normal-state)
(evil-emacs-state)))
#+end_src
**** Switching to last used buffer
:PROPERTIES:
:CUSTOM_ID: h:1e0ee570-e509-4ecb-a3af-b75543731bb0
:END:
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.
#+begin_src emacs-lisp
(defun swarsel/last-buffer () (interactive) (switch-to-buffer nil))
#+end_src
**** mu4e functions
:PROPERTIES:
:CUSTOM_ID: h:34506761-06b9-43b5-a818-506d9b3faf28
:END:
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: [[#h:b92a18cf-eec3-4605-a8c2-37133ade3574][mu4e]]
#+begin_src emacs-lisp
(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 (getenv "SWARSEL_SWARSEL_MAIL")
user-full-name (getenv "SWARSEL_FULLNAME")))
#+end_src
**** Create non-existant directories when finding file
:PROPERTIES:
:CUSTOM_ID: h:4b9d74ef-0376-45bb-bc15-d24a04ca7e81
:END:
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.
#+begin_src emacs-lisp
(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)
#+end_src
**** [crux] Duplicate Lines
:PROPERTIES:
:CUSTOM_ID: h:91e2f45f-54fa-4b60-8758-b2ef9b439af7
:END:
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 [[https://github.com/bbatsov/crux][crux]]. I do not need the whole package, so I just extracted the three functions I needed from it.
#+begin_src emacs-lisp
(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))))
#+end_src
**** [prot] org-id-headings
:PROPERTIES:
:CUSTOM_ID: h:4819720a-9220-4c34-b903-ed4179f3ad1c
:END:
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.
#+begin_src emacs-lisp
(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))
#+end_src
**** Inhibit Messages in Echo Area
:PROPERTIES:
:CUSTOM_ID: h:285e5c0a-875d-46a8-bb9b-0222b3d73878
:END:
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:
#+begin_src emacs-lisp :tangle no :export both :results silent
(advice-add 'message :around #'who-called-me?)
#+end_src
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:
#+begin_src emacs-lisp :tangle no :results silent
(advice-remove 'message #'who-called-me?)
#+end_src
Lastly, individual messages can be reenabled using the =(advice-remove ' #'suppress-messages)= approach. Use this when you accidentally disabled a helpful message.
#+begin_src emacs-lisp
(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?)
#+end_src
**** Move up one directory for find-file
:PROPERTIES:
:CUSTOM_ID: h:de249f2a-6a2b-4114-8046-09d1014a7391
:END:
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/]]
#+begin_src emacs-lisp
(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)
#+end_src
**** org-mode: General setup
:PROPERTIES:
:CUSTOM_ID: h:06b77d28-3fd5-4554-8c7d-32c1b0ec8da5
:END:
Sets up the basic settings that I want to have active in org-mode buffers.
Used here: [[#h:877c9401-a354-4e44-a235-db1a90d19e00][General org-mode]]
#+begin_src emacs-lisp
(defun swarsel/org-mode-setup ()
(variable-pitch-mode 1)
(add-hook 'org-tab-first-hook 'org-end-of-line)
(visual-line-mode 1))
#+end_src
**** org-mode: Visual-fill column
:PROPERTIES:
:CUSTOM_ID: h:fa710375-2efe-49b4-af6a-a875aca6e4a2
:END:
This function sets the width of buffers in org-mode.
Used in: [[#h:bbcfa895-4d46-4b1d-b84e-f634e982c46e][Centered org-mode Buffers]]
#+begin_src emacs-lisp
(defun swarsel/org-mode-visual-fill ()
(setq visual-fill-column-width 150
visual-fill-column-center-text t)
(visual-fill-column-mode 1))
#+end_src
**** org-mode: Upon-save actions (Auto-tangle, export to html, formatting)
:PROPERTIES:
:CUSTOM_ID: h:59d4306e-9b73-4b2c-b039-6a6518c357fc
:END:
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.
#+begin_src emacs-lisp
(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)))
#+end_src
**** org-mode: Fold current heading
:PROPERTIES:
:CUSTOM_ID: h:dfa66e78-5748-45e3-a975-db3da104bb3a
:END:
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.
#+begin_src emacs-lisp
(defun org-fold-outer ()
(interactive)
(org-beginning-of-line)
(if (string-match "^*+" (thing-at-point 'line t))
(outline-up-heading 1))
(outline-hide-subtree)
)
#+end_src
**** corfu: Do not interrupt navigation
:PROPERTIES:
:CUSTOM_ID: h:a1802f9b-bb71-4fd5-86fa-945da18e8b81
:END:
These three functions allow me to keep using the normal navigation keys even when a corfu completion pops up.
These functions are used here: [[#h:5653d693-ecca-4c95-9633-66b9e3241070][Corfu]]
#+begin_src emacs-lisp
(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))
#+end_src
**** Disable garbace collection while minibuffer is active
:PROPERTIES:
:CUSTOM_ID: h:3c436647-71e6-441c-b452-d817ad2f8331
:END:
#+begin_src emacs-lisp
(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)
#+end_src
*** Custom Keybindings
:PROPERTIES:
:CUSTOM_ID: h:2b827c27-0de7-45ed-9d9e-6c511e2c6bb5
:END:
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.
#+begin_src emacs-lisp
;; Make ESC quit prompts
(global-set-key (kbd "") 'keyboard-escape-quit)
;; Set up general keybindings
(use-package general
:config
(general-create-definer swarsel/leader-keys
:keymaps '(normal insert visual emacs)
:prefix "SPC"
:global-prefix "C-SPC")
(swarsel/leader-keys
"e" '(:ignore e :which-key "evil")
"eo" '(evil-jump-backward :which-key "cursor jump backwards")
"eO" '(evil-jump-forward :which-key "cursor jump forwards")
"t" '(:ignore t :which-key "toggles")
"ts" '(hydra-text-scale/body :which-key "scale text")
"te" '(swarsel/toggle-evil-state :which-key "emacs/evil")
"tl" '(display-line-numbers-mode :which-key "line numbers")
"tp" '(evil-cleverparens-mode :wk "cleverparens")
"to" '(olivetti-mode :wk "olivetti")
"td" '(darkroom-tentative-mode :wk "darkroom")
"tw" '((lambda () (interactive) (toggle-truncate-lines)) :which-key "line wrapping")
"m" '(:ignore m :which-key "modes/programs")
"mm" '((lambda () (interactive) (mu4e)) :which-key "mu4e")
"mg" '((lambda () (interactive) (magit-list-repositories)) :which-key "magit-list-repos")
"mc" '((lambda () (interactive) (swarsel/open-calendar)) :which-key "calendar")
"mp" '(popper-toggle :which-key "popper")
"md" '(dirvish :which-key "dirvish")
"mr" '(bjm/elfeed-load-db-and-open :which-key "elfeed")
"o" '(:ignore o :which-key "org")
"op" '((lambda () (interactive) (org-present)) :which-key "org-present")
"oa" '((lambda () (interactive) (org-agenda)) :which-key "org-agenda")
"oa" '((lambda () (interactive) (org-refile)) :which-key "org-refile")
"ob" '((lambda () (interactive) (org-babel-mark-block)) :which-key "Mark whole src-block")
"ol" '((lambda () (interactive) (org-insert-link)) :which-key "insert link")
"oc" '((lambda () (interactive) (org-store-link)) :which-key "copy (=store) link")
"os" '(shfmt-region :which-key "format sh-block")
"od" '((lambda () (interactive) (org-babel-demarcate-block)) :which-key "demarcate (split) src-block")
"on" '(nixpkgs-fmt-region :which-key "format nix-block")
"ot" '(swarsel/org-babel-tangle-config :which-key "tangle file")
"oe" '(org-html-export-to-html :which-key "export to html")
"c" '(:ignore c :which-key "capture")
"ct" '((lambda () (interactive) (org-capture nil "tt")) :which-key "task")
"l" '(:ignore l :which-key "links")
"lc" '((lambda () (interactive) (progn (find-file swarsel-swarsel-org-filepath) (org-overview) )) :which-key "SwarselSystems.org")
"le" '((lambda () (interactive) (progn (find-file swarsel-swarsel-org-filepath) (goto-char (org-find-exact-headline-in-buffer "Emacs") ) (org-overview) (org-cycle) )) :which-key "Emacs.org")
"ln" '((lambda () (interactive) (progn (find-file swarsel-swarsel-org-filepath) (goto-char (org-find-exact-headline-in-buffer "System") ) (org-overview) (org-cycle))) :which-key "Nixos.org")
"lp" '((lambda () (interactive) (projectile-switch-project)) :which-key "switch project")
"lg" '((lambda () (interactive) (magit-list-repositories)) :which-key "list git repos")
"h" '(:ignore h :which-key "help")
"hy" '(yas-describe-tables :which-key "yas tables")
"hb" '(embark-bindings :which-key "current key bindings")
"h" '(:ignore t :which-key "describe")
"he" 'view-echo-area-messages
"hf" 'describe-function
"hF" 'describe-face
"hl" '(view-lossage :which-key "show command keypresses")
"hL" 'find-library
"hm" 'describe-mode
"ho" 'describe-symbol
"hk" 'describe-key
"hK" 'describe-keymap
"hp" 'describe-package
"hv" 'describe-variable
"hd" 'devdocs-lookup
"w" '(:ignore t :which-key "window")
"wl" 'windmove-right
"w " 'windmove-right
"wh" 'windmove-left
"w " 'windmove-left
"wk" 'windmove-up
"w " 'windmove-up
"wj" 'windmove-down
"w " '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")
"" 'up-list
"" 'down-list
))
;; General often used hotkeys
(general-define-key
"C-M-a" (lambda () (interactive) (org-capture nil "a")) ; make new anki card
"C-c d" 'crux-duplicate-current-line-or-region
"C-c D" 'crux-duplicate-and-comment-current-line-or-region
"" 'swarsel/last-buffer
"M-\\" 'indent-region
"" 'yank
"" 'kill-region
"" 'kill-ring-save
"" 'evil-undo
"" 'evil-redo
"C-S-c C-S-c" 'mc/edit-lines
"C->" 'mc/mark-next-like-this
"C-<" 'mc/mark-previous-like-this
"C-c C-<" 'mc/mark-all-like-this
)
#+end_src
*** Directory setup / File structure
:PROPERTIES:
:CUSTOM_ID: h:07951589-54ba-4e3e-bd7b-4106cd22ff6a
:END:
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.
#+begin_src emacs-lisp
;; set Nextcloud directory for journals etc.
(setq
swarsel-emacs-directory "~/.emacs.d"
swarsel-dotfiles-directory "~/.dotfiles"
swarsel-swarsel-org-filepath (expand-file-name "SwarselSystems.org" swarsel-dotfiles-directory)
swarsel-tasks-org-file "Tasks.org"
swarsel-archive-org-file "Archive.org"
swarsel-work-projects-directory "~/Documents/Work"
swarsel-private-projects-directory "~/Documents/Private"
)
#+end_src
*** Unclutter .emacs.d
:PROPERTIES:
:CUSTOM_ID: h:0cf30b76-91d9-41da-a10b-74199bc36d40
:END:
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=.
#+begin_src emacs-lisp
;; 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)
#+end_src
*** Move backup files to another location
:PROPERTIES:
:CUSTOM_ID: h:329f529a-ef9f-4787-b311-1c485e05b754
:END:
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.
#+begin_src emacs-lisp
(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
#+end_src
** General init.el setup + UI
:PROPERTIES:
:CUSTOM_ID: h:786b447d-03ad-4c1d-b114-c37caa2d591c
:END:
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
:PROPERTIES:
:CUSTOM_ID: h:76a5bd78-a20d-4068-bea8-a38fdb26428e
:END:
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.
#+begin_src emacs-lisp
;; use UTF-8 everywhere
(set-language-environment "UTF-8")
(profiler-start 'cpu)
;; set default font size
(defvar swarsel/default-font-size 130)
(setq swarsel-standard-font "FiraCode Nerd Font Mono"
swarsel-alt-font "FiraCode Nerd Font Mono")
;; (defalias 'yes-or-no-p 'y-or-n-p)
;;(setq-default show-trailing-whitespace t)
(add-hook 'before-save-hook 'delete-trailing-whitespace)
(global-hl-line-mode 1)
;; (setq redisplay-dont-pause t) ;; obsolete
(setq blink-cursor-mode nil) ;; blink-cursor is an unexpected source of slowdown
(global-subword-mode 1) ; Iterate through CamelCase words
(setq blink-matching-paren nil) ;; this makes the cursor jump around annoyingly
(delete-selection-mode 1)
(setq vc-follow-symlinks t)
(setq require-final-newline t)
(winner-mode 1)
(setq load-prefer-newer t)
(setq-default bidi-paragraph-direction 'left-to-right
bidi-display-reordering 'left-to-right
bidi-inhibit-bpa t)
(global-so-long-mode)
(setq process-adaptive-read-buffering nil) ;; not sure if this is a good idea
(setq fast-but-imprecise-scrolling t
redisplay-skip-fontification-on-input t
inhibit-compacting-font-caches t)
(setq idle-update-delay 1.0
which-func-update-delay 1.0)
(setq undo-limit 80000000
evil-want-fine-undo t
auto-save-default t
password-cache-expiry nil
)
(setq browse-url-browser-function 'browse-url-firefox)
(setenv "DISPLAY" ":0")
;; 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)
#+end_src
*** Mark all themes as safe
:PROPERTIES:
:CUSTOM_ID: h:0debe8fd-b319-4ab7-a92c-784fa7896b75
:END:
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.
#+begin_src emacs-lisp
(setq custom-safe-themes t)
#+end_src
*** Show less compilation warnings
:PROPERTIES:
:CUSTOM_ID: h:b587e869-9911-443b-bc6d-8fb3ce31908d
:END:
When Emacs compiles stuff, it often shows a bunch of warnings that I do not need to deal with. Here we silence those. Some will be disabled completely, and some only when we have native compilation available (which should be most of the time, however).
This is really not needed anymore ever since I started managing my emacs packages with nix, but I still keep this around in case I ever move away from it.
#+begin_src emacs-lisp
(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
#+end_src
*** Better garbage collection
:PROPERTIES:
:CUSTOM_ID: h:1667913c-2272-4010-bf3a-356455b97c83
:END:
This sets up automatic garbage collection when the frame is unused. There is a lot of discussion on whether it is smart to tamper with garbage collection - in my eyes it is worth running this, because I often times switch away from Emacs for a while when researching. That times can be then used to run GC.
#+begin_src emacs-lisp
(setq garbage-collection-messages nil)
(defmacro k-time (&rest body)
"Measure and return the time it takes evaluating BODY."
`(let ((time (current-time)))
,@body
(float-time (time-since time))))
;; When idle for 15sec run the GC no matter what.
(defvar k-gc-timer
(run-with-idle-timer 15 t
(lambda ()
;; (message "Garbage Collector has run for %.06fsec"
(k-time (garbage-collect)))))
;; )
#+end_src
*** Indentation
:PROPERTIES:
:CUSTOM_ID: h:6527b3ce-b76d-431a-9960-a57da7c53e1b
:END:
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.
#+begin_src emacs-lisp
(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)
#+end_src
*** Scrolling
:PROPERTIES:
:CUSTOM_ID: h:3dc9fb1d-cd16-4bd0-a9ac-55a944415a90
:END:
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.
#+begin_src emacs-lisp
(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)
#+end_src
*** Evil
:PROPERTIES:
:CUSTOM_ID: h:5bf9f014-ee96-42da-b285-7b34f04e6bb1
:END:
**** General evil
:PROPERTIES:
:CUSTOM_ID: h:218376e8-086b-46bf-91b3-78295d5d440f
:END:
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 [[#h:c3cc1c12-3ab8-42b7-be07-63f54eac397f][cape]] later.
Also, I setup initial modes for several major-modes depending on what I deem fit.
#+begin_src emacs-lisp
;; 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 "") 'evil-next-visual-line)
(define-key evil-normal-state-map (kbd "k") 'evil-previous-visual-line)
(define-key evil-normal-state-map (kbd "") '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)))
#+end_src
**** evil-collection
:PROPERTIES:
:CUSTOM_ID: h:bde208f3-01ef-4dc6-9981-65f3d2a8189b
:END:
This gives support for many different modes, and works beautifully out of the box.
#+begin_src emacs-lisp
(use-package evil-collection
:after evil
:config
(evil-collection-init)
(setq forge-add-default-bindings nil))
#+end_src
**** evil-snipe
:PROPERTIES:
:CUSTOM_ID: h:d80e3f7d-0185-4a15-832b-d756e576265c
:END:
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=.
#+begin_src emacs-lisp
;; 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))
#+end_src
**** evil-cleverparens
:PROPERTIES:
:CUSTOM_ID: h:b06a378d-5248-4451-8eee-e65a3a768b1d
:END:
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.
#+begin_src emacs-lisp
;; for parentheses-heavy languades modify evil commands to keep balance of parantheses
(use-package evil-cleverparens)
#+end_src
**** evil-surround
:PROPERTIES:
:CUSTOM_ID: h:aac82e5e-d882-4870-b644-ebdd0a2daae3
:END:
This minor-mode adds functionality for doing better surround-commands; for example =ci[= will let you change the word within square brackets.
#+begin_src emacs-lisp
;; enables surrounding text with S
(use-package evil-surround
:config
(global-evil-surround-mode 1))
#+end_src
**** evil-visual-mark-mode
:PROPERTIES:
:CUSTOM_ID: h:df6729b6-2135-4070-bcab-a6a26f0fb2c4
:END:
#+begin_src emacs-lisp
(use-package evil-visual-mark-mode
:config (evil-visual-mark-mode))
#+end_src
**** evil-textobj-tree-sitter
:PROPERTIES:
:CUSTOM_ID: h:cd9a0fb6-e287-4c3c-8013-6aad64ef89cb
:END:
This adds support for tree-sitter objects. This allows for the following chords:
- "...af" around function
- "...if" inside function
#+begin_src emacs-lisp
(use-package evil-textobj-tree-sitter)
;; bind `function.outer`(entire function block) to `f` for use in things like `vaf`, `yaf`
(define-key evil-outer-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.outer"))
;; bind `function.inner`(function block without name and args) to `f` for use in things like `vif`, `yif`
(define-key evil-inner-text-objects-map "f" (evil-textobj-tree-sitter-get-textobj "function.inner"))
;; You can also bind multiple items and we will match the first one we can find
(define-key evil-outer-text-objects-map "a" (evil-textobj-tree-sitter-get-textobj ("if_statement.outer" "conditional.outer" "loop.outer") '((python-mode . ((if_statement.outer) @if_statement.outer)) (python-ts-mode . ((if_statement.outer) @if_statement.outer)))))
#+end_src
**** evil-textobj-tree-sitter
:PROPERTIES:
:CUSTOM_ID: h:06002ad2-686a-42c5-82d7-61f1340e262d
:END:
#+begin_src emacs-lisp
(use-package evil-numbers)
#+end_src
*** ispell
:PROPERTIES:
:CUSTOM_ID: h:e888d7a7-1755-4109-af11-5358b8cf140e
:END:
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.
#+begin_src emacs-lisp
;; set the NixOS wordlist by hand
(setq ispell-alternate-dictionary (getenv "WORDLIST"))
#+end_src
*** Font Configuration
:PROPERTIES:
:CUSTOM_ID: h:60f87342-0491-4c56-8057-6f075cf35753
:END:
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.
#+begin_src emacs-lisp
(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)
#+end_src
*** Theme
:PROPERTIES:
:CUSTOM_ID: h:72a9704b-83d2-4b74-a1f6-d333203f62db
:END:
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).
#+begin_src emacs-lisp
(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))
#+end_src
*** Icons
:PROPERTIES:
:CUSTOM_ID: h:eb0ea526-a83a-4664-b3a1-2b40d3a31493
:END:
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:
- [[#h:b190d512-bfb5-42ec-adec-8d86bab726ce][Vertico and friends]]
- [[#h:5653d693-ecca-4c95-9633-66b9e3241070][IN USE Corfu]]
#+begin_src emacs-lisp
(use-package nerd-icons)
#+end_src
*** Variable Pitch Mode
:PROPERTIES:
:CUSTOM_ID: h:455ed7ac-ee7f-4f94-b857-f2c58b2282d0
:END:
This minor mode allows mixing fixed and variable pitch fonts within the same buffer.
#+begin_src emacs-lisp
(use-package mixed-pitch
:custom
(mixed-pitch-set-height nil)
(mixed-pitch-variable-pitch-cursor nil)
:hook
(text-mode . mixed-pitch-mode))
#+end_src
*** Modeline
:PROPERTIES:
:CUSTOM_ID: h:ed585848-875a-4673-910c-d2e1901dd95b
:END:
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.
I have currently disabled this in favor of [[#h:80ed2431-9c9a-4bfc-a3c0-08a2a058d208][mini-modeline]].
#+begin_src emacs-lisp
(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)))
#+end_src
*** mini-modeline
:PROPERTIES:
:CUSTOM_ID: h:80ed2431-9c9a-4bfc-a3c0-08a2a058d208
:END:
I have found that the doom-modeline, while very useful, consumes too much screen space for my liking. This modeline takes a more minimalistic approach.
#+begin_src emacs-lisp
(use-package mini-modeline
:after smart-mode-line
:config
(mini-modeline-mode t)
(setq mini-modeline-display-gui-line nil)
(setq mini-modeline-enhance-visual nil)
(setq mini-modeline-truncate-p nil)
(setq mini-modeline-l-format nil)
(setq mini-modeline-right-padding 5)
(setq window-divider-mode t)
(setq window-divider-default-places t)
(setq window-divider-default-bottom-width 1)
(setq window-divider-default-right-width 1)
(setq mini-modeline-r-format '("%e" mode-line-front-space mode-line-mule-info mode-line-client
mode-line-modified mode-line-remote mode-line-frame-identification
mode-line-buffer-identification " " mode-line-position " " mode-name evil-mode-line-tag ))
)
(use-package smart-mode-line
:config
(sml/setup)
(add-to-list 'sml/replacer-regexp-list '("^~/Documents/Work/" ":WK:"))
(add-to-list 'sml/replacer-regexp-list '("^~/Documents/Private/" ":PR:"))
(add-to-list 'sml/replacer-regexp-list '("^~/.dotfiles/" ":D:") t)
)
#+end_src
*** Helper Modes
:PROPERTIES:
:CUSTOM_ID: h:39ae01e9-8053-4f76-aa77-8cbbbcff9652
:END:
**** Vertico, Orderless, Marginalia, Consult, Embark
:PROPERTIES:
:CUSTOM_ID: h:b190d512-bfb5-42ec-adec-8d86bab726ce
:END:
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: [[#h:eb0ea526-a83a-4664-b3a1-2b40d3a31493][Icons]]
***** vertico
:PROPERTIES:
:CUSTOM_ID: h:d7c7f597-f870-4e01-8f7e-27dd31dd245d
:END:
#+begin_src emacs-lisp
(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))
#+end_src
***** vertico-directory
:PROPERTIES:
:CUSTOM_ID: h:10d4f2bd-8c72-430b-a9ed-9b5e279ec0b4
:END:
This package allows for =Ido=-like directory navigation.
#+begin_src emacs-lisp
(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))
#+end_src
***** orderless
:PROPERTIES:
:CUSTOM_ID: h:211fc0bd-0d64-4577-97d8-6abc94435f04
:END:
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.
#+begin_src emacs-lisp
(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)))
#+end_src
***** consult
:PROPERTIES:
:CUSTOM_ID: h:49ab82bf-812d-4fbe-a5b6-d3ad703fe32c
:END:
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.
#+begin_src emacs-lisp
(use-package consult
:config
(setq consult-fontify-max-size 1024)
:bind
(("C-x b" . consult-buffer)
("C-c " . 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)))
#+end_src
***** embark
:PROPERTIES:
:CUSTOM_ID: h:1c564ee5-ccd7-48be-b69a-d963400c4704
:END:
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.
#+begin_src emacs-lisp
(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)))))
#+end_src
***** embark-consult
:PROPERTIES:
:CUSTOM_ID: h:6287551c-a6f7-4870-b3f3-210d6f038b6f
:END:
Provides previews for embark.
#+begin_src emacs-lisp
(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))
#+end_src
***** marginalia
:PROPERTIES:
:CUSTOM_ID: h:f32040a4-882f-4e6b-97f1-a0105c44c034
:END:
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.
#+begin_src emacs-lisp
(use-package marginalia
:after vertico
:init
(marginalia-mode)
(setq marginalia-annotators '(marginalia-annotators-heavy marginalia-annotators-light nil)))
#+end_src
***** nerd-icons-completion
:PROPERTIES:
:CUSTOM_ID: h:d70ec2fb-da43-4523-9ee4-774ececdb80e
:END:
As stated above, this simply provides nerd-icons to the completion framework.
#+begin_src emacs-lisp
(use-package nerd-icons-completion
:after (marginalia nerd-icons)
:hook (marginalia-mode . nerd-icons-completion-marginalia-setup)
:init
(nerd-icons-completion-mode))
#+end_src
**** Helpful + which-key: Better help defaults
:PROPERTIES:
:CUSTOM_ID: h:cbf6bd48-2503-489a-89da-e3359564e989
:END:
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.
#+begin_src emacs-lisp
(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))
#+end_src
*** Ligatures
:PROPERTIES:
:CUSTOM_ID: h:bbbd9cc8-3a84-4810-a3d5-b8536a5fbda1
:END:
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.
#+begin_src emacs-lisp
(use-package ligature
:init
(global-ligature-mode t)
:config
(ligature-set-ligatures 'prog-mode
'("|||>" "<|||" "<==>" "" "---" "-<<"
"<~~" "<~>" "<*>" "<||" "<|>" "<$>" "<==" "<=>" "<=<" "<->"
"<--" "<-<" "<<=" "<<-" "<<<" "<+>" ">" "###" "#_(" "..<"
"..." "+++" "/==" "///" "_|_" "www" "&&" "^=" "~~" "~@" "~="
"~>" "~-" "**" "*>" "*/" "||" "|}" "|]" "|=" "|>" "|-" "{|"
"[|" "]#" "::" ":=" ":>" ":<" "$>" "==" "=>" "!=" "!!" ">:"
">=" ">>" ">-" "-~" "-|" "->" "--" "-<" "<~" "<*" "<|" "<:"
"<$" "<=" "<>" "<-" "<<" "<+" "" "#{" "#[" "#:" "#=" "#!"
"##" "#(" "#?" "#_" "%%" ".=" ".." ".?" "+>" "++" "?:" "?="
"?." "??" "/*" "/=" "/>" "//" "__" "~~" "(*" "*)" "\\\\"
"://" ";;")))
#+end_src
*** Popup (popper) + Shackle Buffers
:PROPERTIES:
:CUSTOM_ID: h:e9d40e63-0e1f-47df-98f7-5427992588a4
:END:
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.
#+begin_src emacs-lisp
(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))
#+end_src
*** Indicate first and last line of buffer
:PROPERTIES:
:CUSTOM_ID: h:a6d23c8c-125f-4e36-af30-ff0a1e0d5a28
:END:
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.
#+begin_src emacs-lisp
(setq-default indicate-buffer-boundaries t)
#+end_src
*** Authentication
:PROPERTIES:
:CUSTOM_ID: h:053a36bf-168f-4f63-a0c4-f0139dc6cc3b
:END:
This defines the authentication sources used by =org-calfw= ([[#h:c760f04e-622f-4b3e-8916-53ca8cce6edc][Calendar]]) and [[#h:1a8585ed-d9f2-478f-a132-440ada1cde2c][Forge]].
#+begin_src emacs-lisp
;; (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)
#+end_src
** Modules
:PROPERTIES:
:CUSTOM_ID: h:f2622fd3-7f14-47a8-8c21-33574fcbf14b
:END:
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
:PROPERTIES:
:CUSTOM_ID: h:99544398-72af-4382-b8e1-01b2221baff4
:END:
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
:PROPERTIES:
:CUSTOM_ID: h:877c9401-a354-4e44-a235-db1a90d19e00
:END:
This sets up the basic org-mode. I wrote a function to handle some of the initial org-mode behaviour in [[#h:06b77d28-3fd5-4554-8c7d-32c1b0ec8da5][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
#+begin_src emacs-lisp
(use-package org
;;:diminish (org-indent-mode)
:hook (org-mode . swarsel/org-mode-setup)
;; :mode "\\.nix\\'"
:bind
(("C-" . org-fold-outer)
("C-c s" . org-store-link))
:config
(setq org-ellipsis " ⤵"
org-link-descriptive t
org-hide-emphasis-markers t)
(setq org-startup-folded t)
(setq org-support-shift-select t)
(setq org-agenda-start-with-log-mode t)
(setq org-log-done 'time)
(setq org-log-into-drawer t)
(setq org-startup-with-inline-images t)
(setq org-export-headline-levels 6)
(setq org-image-actual-width nil)
(setq org-format-latex-options '(:foreground "White" :background default :scale 2.0 :html-foreground "Black" :html-background "Transparent" :html-scale 1.0 :matchers ("begin" "$1" "$" "$$" "\\(" "\\[")))
(setq org-agenda-files '("/home/swarsel/Nextcloud/Org/Tasks.org"
"/home/swarsel/Nextcloud/Org/Archive.org"
))
(setq org-refile-targets
'((swarsel-archive-org-file :maxlevel . 1)
(swarsel-tasks-org-file :maxlevel . 1)))
)
#+end_src
**** org-appear
:PROPERTIES:
:CUSTOM_ID: h:62829574-a069-44b8-afb3-401a268d2747
:END:
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.
#+begin_src emacs-lisp
(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))
#+end_src
**** Centered org-mode Buffers
:PROPERTIES:
:CUSTOM_ID: h:bbcfa895-4d46-4b1d-b84e-f634e982c46e
:END:
I like org-mode buffers to be centered, as I do not find that enormous lines are of big use.
Function definition in: [[#h:fa710375-2efe-49b4-af6a-a875aca6e4a2][Visual-fill column]]
#+begin_src emacs-lisp
(use-package visual-fill-column
:hook (org-mode . swarsel/org-mode-visual-fill))
#+end_src
**** Fix headings not folding sometimes
:PROPERTIES:
:CUSTOM_ID: h:c1a0adea-ca97-43d7-b5a0-b856d2ebc9a8
:END:
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.
#+begin_src emacs-lisp
(setq org-fold-core-style 'overlays)
#+end_src
**** Babel
:PROPERTIES:
:CUSTOM_ID: h:3e0b6da3-0497-4080-bb49-bab949c03bc4
:END:
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
:PROPERTIES:
:CUSTOM_ID: h:5d5ed7be-ec5f-4e17-bbb8-820ab6a9961c
:END:
- This configures the languages that babel recognizes.
#+begin_src emacs-lisp
(setq org-src-preserve-indentation nil)
(org-babel-do-load-languages
'org-babel-load-languages
'((emacs-lisp . t)
(python . t)
(js . t)
(shell . t)
))
(push '("conf-unix" . conf-unix) org-src-lang-modes)
(setq org-export-with-broken-links 'mark)
(setq org-confirm-babel-evaluate nil)
;; tangle is too slow, try to speed it up
(defadvice org-babel-tangle-single-block (around inhibit-redisplay activate protect compile)
"inhibit-redisplay and inhibit-message to avoid flicker."
(let ((inhibit-redisplay t)
(inhibit-message t))
ad-do-it))
(defadvice org-babel-tangle (around time-it activate compile)
"Display the execution time"
(let ((tim (current-time)))
ad-do-it
(message "org-tangle took %f sec" (float-time (time-subtract (current-time) tim)))))
#+end_src
***** old easy structure templates
:PROPERTIES:
:CUSTOM_ID: h:d112ed66-b2dd-45cc-8d70-9cf6631f28a9
:END:
- 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.
#+begin_src emacs-lisp
(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"))
#+end_src
**** aucTex
:PROPERTIES:
:CUSTOM_ID: h:4696e2fc-3296-47dc-8fc3-66912c329d4c
:END:
This provides several utilities for LaTeX in Emacs, including many completions and convenience functions for math-mode.
#+begin_src emacs-lisp
(use-package auctex)
(setq TeX-auto-save t)
(setq TeX-save-query nil)
(setq TeX-parse-self t)
(setq-default TeX-engine 'luatex)
(setq-default TeX-master nil)
(add-hook 'LaTeX-mode-hook 'visual-line-mode)
(add-hook 'LaTeX-mode-hook 'flyspell-mode)
(add-hook 'LaTeX-mode-hook 'LaTeX-math-mode)
(add-hook 'LaTeX-mode-hook 'reftex-mode)
(setq LaTeX-electric-left-right-brace t)
(setq font-latex-fontify-script nil)
(setq TeX-electric-sub-and-superscript t)
;; (setq reftex-plug-into-AUCTeX t)
#+end_src
**** org-download
:PROPERTIES:
:CUSTOM_ID: h:406e5ecb-66f0-49bf-85ca-8b499f73ec5b
:END:
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.
#+begin_src emacs-lisp
#+end_src
**** org-fragtog
:PROPERTIES:
:CUSTOM_ID: h:a02b1162-3e19-46f1-8efc-9f375971645c
:END:
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.
#+begin_src emacs-lisp
(use-package org-fragtog)
(add-hook 'org-mode-hook 'org-fragtog-mode)
(add-hook 'markdown-mode-hook 'org-fragtog-mode)
#+end_src
**** org-modern
:PROPERTIES:
:CUSTOM_ID: h:95b42e77-767c-4461-9ba8-b1c1cd18266c
:END:
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.
#+begin_src emacs-lisp
(use-package org-modern
:config (setq org-modern-block-name
'((t . t)
("src" "»" "∥")))
:hook (org-mode . org-modern-mode))
#+end_src
**** Presentations
:PROPERTIES:
:CUSTOM_ID: h:4e11a845-a7bb-4eb5-b4ce-5b2f52e07425
:END:
Recently I have grown fond of holding presentations using Emacs :)
#+begin_src emacs-lisp
(use-package org-present
:bind (:map org-present-mode-keymap
("q" . org-present-quit)
("" . swarsel/org-present-prev)
("" . 'ignore)
("" . 'ignore)
("" . 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)
#+end_src
*** Nix Mode
:PROPERTIES:
:CUSTOM_ID: h:406c2ecc-0e3e-4d9f-9ae3-3eb1f8b87d1b
:END:
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.
#+begin_src emacs-lisp
(use-package nix-mode)
(use-package nix-ts-mode
:mode "\\.nix\\'")
#+end_src
*** HCL Mode
:PROPERTIES:
:CUSTOM_ID: h:e8074881-3441-4abd-b25b-358a87e7984f
:END:
This adds support for Hashicorp Configuration Language. I need this at work.
#+begin_src emacs-lisp
(use-package hcl-mode
:mode "\\.hcl\\'"
:config
(setq hcl-indent-level 2))
#+end_src
*** Jenkinsfile/Groovy
:PROPERTIES:
:CUSTOM_ID: h:c9e3ffd7-4fb1-4a04-8563-92ceec4b4410
:END:
This adds support for Groovy, which I specifically need to work with Jenkinsfiles. I need this at work.
#+begin_src emacs-lisp
(use-package groovy-mode)
(use-package jenkinsfile-mode
:mode "Jenkinsfile")
#+end_src
*** Ansible
:PROPERTIES:
:CUSTOM_ID: h:77fa79d8-81d5-46f2-82f9-8e2922538d44
:END:
#+begin_src emacs-lisp
(use-package ansible)
#+end_src
*** Dockerfile
:PROPERTIES:
:CUSTOM_ID: h:534d8729-4422-4f0c-9ae6-d3737d4a6dd3
:END:
This adds support for Dockerfiles. I need this at work.
#+begin_src emacs-lisp
(use-package dockerfile-mode
:mode "Dockerfile")
#+end_src
*** Terraform Mode
:PROPERTIES:
:CUSTOM_ID: h:7834adb0-fbd3-4136-bdb7-6dbc9a083296
:END:
This adds support for Terraform configuration files. I need this at work.
#+begin_src emacs-lisp
(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)
#+end_src
*** nixpkgs-fmt
:PROPERTIES:
:CUSTOM_ID: h:5ca7484b-b9d6-4023-88d1-a1e37d5df249
:END:
Adds functions for formatting nix code.
#+begin_src emacs-lisp
(use-package nixpkgs-fmt)
#+end_src
*** shfmt
:PROPERTIES:
:CUSTOM_ID: h:489a71c4-38af-44a3-a9ef-8b1ed1ee4ac4
:END:
Adds functions for formatting shellscripts.
#+begin_src emacs-lisp
(use-package shfmt
:config
(setq shfmt-command "shfmt")
(setq shfmt-arguments '("-i" "4" "-s" "-sr")))
#+end_src
*** Markdown Mode
:PROPERTIES:
:CUSTOM_ID: h:50327461-a11b-4e81-830a-90febc720cfa
:END:
**** Mode
:PROPERTIES:
:CUSTOM_ID: h:734dc40a-a2c4-4839-b884-cb99b81aa6fe
:END:
#+begin_src emacs-lisp
(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)))
#+end_src
**** LaTeX in Markdown
:PROPERTIES:
:CUSTOM_ID: h:8d90fe51-0b32-423a-a159-4f853bc29b68
:END:
#+begin_src emacs-lisp
(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)
))
#+end_src
*** elfeed
:PROPERTIES:
:CUSTOM_ID: h:a83c5820-2016-44ae-90a0-4756bb471c01
:END:
#+begin_src emacs-lisp
(use-package elfeed)
(use-package elfeed-goodies)
(elfeed-goodies/setup)
(setq elfeed-db-directory "~/.elfeed/db/")
(use-package elfeed-protocol
:after elfeed)
(elfeed-protocol-enable)
(setq elfeed-use-curl t)
(setq elfeed-set-timeout 36000)
(setq elfeed-protocol-enabled-protocols '(fever))
(setq elfeed-protocol-fever-update-unread-only t)
(setq elfeed-protocol-fever-fetch-category-as-tag t)
(setq elfeed-protocol-feeds '(("fever+https://Swarsel@signpost.swarsel.win"
:api-url "https://signpost.swarsel.win/api/fever.php"
:password-file "~/.emacs.d/.fever")))
(define-key elfeed-show-mode-map (kbd ";") 'visual-fill-column-mode)
(define-key elfeed-show-mode-map (kbd "j") 'elfeed-goodies/split-show-next)
(define-key elfeed-show-mode-map (kbd "k") 'elfeed-goodies/split-show-prev)
(define-key elfeed-search-mode-map (kbd "j") 'next-line)
(define-key elfeed-search-mode-map (kbd "k") 'previous-line)
(define-key elfeed-show-mode-map (kbd "S-SPC") 'scroll-down-command)
#+end_src
*** Ripgrep
:PROPERTIES:
:CUSTOM_ID: h:87453f1c-8ea5-4d0a-862d-8973d5bc5405
:END:
This is the ripgrep command for Emacs.
#+begin_src emacs-lisp
(use-package rg)
#+end_src
*** Tree-sitter
:PROPERTIES:
:CUSTOM_ID: h:543641d0-02a9-459e-a2d6-96c8fcc06864
:END:
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.
#+begin_src emacs-lisp :tangle no :export both
(mapc #'treesit-install-language-grammar (mapcar #'car treesit-language-source-alist))
#+end_src
#+RESULTS:
| bash | c | cmake | cpp | css | elisp | go | html | javascript | json | julia | latex | make | markdown | R | python | typescript | rust | sql | toml | tsx | yaml |
#+begin_src emacs-lisp
;; (use-package emacs
;; :ensure nil
;; :init
;; (setq treesit-language-source-alist
;; '((bash . ("https://github.com/tree-sitter/tree-sitter-bash"))
;; (c . ("https://github.com/tree-sitter/tree-sitter-c"))
;; (cmake . ("https://github.com/uyha/tree-sitter-cmake"))
;; (cpp . ("https://github.com/tree-sitter/tree-sitter-cpp"))
;; (css . ("https://github.com/tree-sitter/tree-sitter-css"))
;; (elisp . ("https://github.com/Wilfred/tree-sitter-elisp"))
;; (go . ("https://github.com/tree-sitter/tree-sitter-go"))
;; (html . ("https://github.com/tree-sitter/tree-sitter-html"))
;; (javascript . ("https://github.com/tree-sitter/tree-sitter-javascript"))
;; (json . ("https://github.com/tree-sitter/tree-sitter-json"))
;; (julia . ("https://github.com/tree-sitter/tree-sitter-julia"))
;; (latex . ("https://github.com/latex-lsp/tree-sitter-latex"))
;; (make . ("https://github.com/alemuller/tree-sitter-make"))
;; (markdown . ("https://github.com/ikatyang/tree-sitter-markdown"))
;; (nix . ("https://github.com/nix-community/tree-sitter-nix"))
;; (R . ("https://github.com/r-lib/tree-sitter-r"))
;; (python . ("https://github.com/tree-sitter/tree-sitter-python"))
;; (typescript . ("https://github.com/tree-sitter/tree-sitter-typescript" "typescript/src" "typescript"))
;; (rust . ("https://github.com/tree-sitter/tree-sitter-rust"))
;; (sql . ("https://github.com/m-novikov/tree-sitter-sql"))
;; (toml . ("https://github.com/tree-sitter/tree-sitter-toml"))
;; (tsx . ("https://github.com/tree-sitter/tree-sitter-typescript" "master" "typescript/src"))
;; (yaml . ("https://github.com/ikatyang/tree-sitter-yaml"))))
;; )
(use-package treesit-auto
:custom
(setq treesit-auto-install t)
:config
(treesit-auto-add-to-auto-mode-alist 'all)
(global-treesit-auto-mode))
#+end_src
#+RESULTS:
: t
*** direnv (envrc)
:PROPERTIES:
:CUSTOM_ID: h:82ddeef2-99f8-465b-ba36-07c3eaad717b
:END:
In emacs, there are two packages for managing dev environments - emacs-direnv (direnv) and envrc. Direnv uses the global Emacs environment whereas envrc is buffer-local. I do not really care about this difference. What is more important to me is that emacs should not block upon handling a bigger flake.nix while setting up the dev environment. This seems to be better handled by envrc.
#+begin_src emacs-lisp
;; (use-package direnv
;; :custom (direnv-always-show-summary nil)
;; :config (direnv-mode))
(use-package envrc
:hook (after-init . envrc-global-mode))
#+end_src
*** avy
:PROPERTIES:
:CUSTOM_ID: h:efb3f0fd-e846-4df9-ba48-2e45d776f68f
:END:
=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.
#+begin_src emacs-lisp
(use-package avy
:bind
(("M-o" . avy-goto-char-timer))
:config
(setq avy-all-windows 'all-frames))
#+end_src
*** devdocs
:PROPERTIES:
:CUSTOM_ID: h:d9a6cb44-736e-4608-951f-e928e1b757c0
:END:
=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=).
#+begin_src emacs-lisp
(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)
#+end_src
*** Projectile
:PROPERTIES:
:CUSTOM_ID: h:5cde5032-251e-4cc4-9202-b4ce996f92c2
:END:
projectile is useful for keeping track of your git projects within Emacs. I mostly use it to quickly switch between projects.
#+begin_src emacs-lisp
(use-package projectile
:diminish projectile-mode
:config (projectile-mode)
:custom ((projectile-completion-system 'auto)) ;; integrate ivy into completion system
:bind-keymap
("C-c p" . projectile-command-map) ; all projectile commands under this
:init
;; NOTE: Set this to the folder where you keep your Git repos!
(when (file-directory-p swarsel-work-projects-directory)
(when (file-directory-p swarsel-private-projects-directory)
(setq projectile-project-search-path (list swarsel-work-projects-directory swarsel-private-projects-directory))))
(setq projectile-switch-project-action #'magit-status))
#+end_src
*** Magit
:PROPERTIES:
:CUSTOM_ID: h:d2c7323d-f8c6-4f23-b70a-930e3e4ecce5
:END:
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.
#+begin_src emacs-lisp
(use-package magit
:config
(setq magit-repository-directories `((,swarsel-work-projects-directory . 1)
(,swarsel-private-projects-directory . 1)
("~/.dotfiles/" . 0)))
:custom
(magit-display-buffer-function #'magit-display-buffer-same-window-except-diff-v1)) ; stay in the same window
#+end_src
*** Yubikey support
:PROPERTIES:
:CUSTOM_ID: h:d78709dd-4f79-441c-9166-76f61f90359a
:END:
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.
#+begin_src emacs-lisp
;; 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")))
#+end_src
*** Forge
:PROPERTIES:
:CUSTOM_ID: h:1a8585ed-d9f2-478f-a132-440ada1cde2c
:END:
NOTE: Make sure to configure a GitHub token before using this package!
- https://magit.vc/manual/forge/Token-Creation.html#Token-Creation
- https://magit.vc/manual/ghub/Getting-Started.html#Getting-Started
- https://magit.vc/manual/ghub/Storing-a-Token.html
- https://www.emacswiki.org/emacs/GnuPG
(1) in practice: github -<> settings -<> developer option -<>
create classic token with repo; user; read:org permissions
(2)machine api.github.com login USERNAME^forge password 012345abcdef...
#+begin_src emacs-lisp
(use-package forge
:after magit)
#+end_src
*** git-timemachine
:PROPERTIES:
:CUSTOM_ID: h:cf5b0e6b-56a5-4a93-99fb-258eb7cb2eb4
:END:
This is just a nice utility to browse different versions of a file of a git project within Emacs.
#+begin_src emacs-lisp
(use-package git-timemachine
:hook (git-time-machine-mode . evil-normalize-keymaps)
:init (setq git-timemachine-show-minibuffer-details t))
#+end_src
*** Delimiters (brackets): rainbow-delimiters, highlight-parentheses
:PROPERTIES:
:CUSTOM_ID: h:d9671ab7-a75a-47c6-a1f4-376d126c9b0a
:END:
- 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.
#+begin_src emacs-lisp
(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))))))
#+end_src
*** rainbow-mode
:PROPERTIES:
:CUSTOM_ID: h:d1a32a69-2f9a-45ef-95fe-a00e3551dc94
:END:
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.
#+begin_src emacs-lisp
(use-package rainbow-mode
:config (rainbow-mode))
#+end_src
*** Corfu
:PROPERTIES:
:CUSTOM_ID: h:5653d693-ecca-4c95-9633-66b9e3241070
:END:
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 == and ==.
Nerd icons is originally enabled here: [[#h:eb0ea526-a83a-4664-b3a1-2b40d3a31493][Icons]]
Navigation functions defined here: [[#h:a1802f9b-bb71-4fd5-86fa-945da18e8b81][corfu: Do not interrupt navigation]]
#+begin_src emacs-lisp
(use-package corfu
:init
(global-corfu-mode)
(corfu-history-mode)
(corfu-popupinfo-mode) ; Popup completion info
:custom
(corfu-auto t)
(corfu-auto-prefix 3)
(corfu-auto-delay 1)
(corfu-cycle t)
(corfu-quit-no-match 'separator)
(corfu-separator ?\s)
;; (corfu-quit-no-match t)
(corfu-popupinfo-max-height 70)
(corfu-popupinfo-delay '(0.5 . 0.2))
;; (corfu-preview-current 'insert) ; insert previewed candidate
(corfu-preselect 'prompt)
(corfu-on-exact-match nil) ; Don't auto expand tempel snippets
;; Optionally use TAB for cycling, default is `corfu-complete'.
:bind (:map corfu-map
("M-SPC" . corfu-insert-separator)
("" . swarsel/corfu-normal-return)
;; ("C-" . swarsel/corfu-complete)
("S-" . corfu-popupinfo-scroll-down)
("S-" . corfu-popupinfo-scroll-up)
("C-" . corfu-previous)
("C-" . corfu-next)
(" " . swarsel/corfu-quit-and-up)
(" " . 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)))
#+end_src
*** cape
:PROPERTIES:
:CUSTOM_ID: h:c3cc1c12-3ab8-42b7-be07-63f54eac397f
:END:
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 [[#h:218376e8-086b-46bf-91b3-78295d5d440f][General evil]].
I leave the commented out alist extensions here in case I want to try them out at some point in the future.
#+begin_src emacs-lisp
(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)
)
#+end_src
*** rust
:PROPERTIES:
:CUSTOM_ID: h:3aa20438-edf6-4b13-a90d-3d5c51239c44
:END:
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.
#+begin_src emacs-lisp
;;(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))
#+end_src
*** Tramp
:PROPERTIES:
:CUSTOM_ID: h:b9b27a88-06f3-470b-a604-a20b2079bc26
:END:
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.
#+begin_src emacs-lisp
(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'")))
#+end_src
*** diff-hl
:PROPERTIES:
:CUSTOM_ID: h:58415e95-8a7a-4517-acbb-5f1bb1028603
:END:
This is a simple highlighting utility that uses the margin to visually show the differences since the last git commit.
#+begin_src emacs-lisp
(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))
#+end_src
*** Commenting
:PROPERTIES:
:CUSTOM_ID: h:d60ce0b1-cabf-43f5-a236-a1e4b400d2f5
:END:
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.
#+begin_src emacs-lisp
(use-package evil-nerd-commenter
:bind ("M-/" . evilnc-comment-or-uncomment-lines))
#+end_src
*** eglot
:PROPERTIES:
:CUSTOM_ID: h:6cf0310b-2fdf-45f0-9845-4704649777eb
:END:
Up comes the section of lsp clients for Emacs. For a longer time, I thought that I had to choose one only, and after having started with =lsp-mode= I had tried out =lsp-booster= and then went to =eglot=. My requirements are as follow:
Must have:
- mostly unintrusive, non-blocking
- fast (configurable) completion
- xref (or similar)
Nice to have:
- Debugger
- Multi-lsp support (running two lsp's on a single project)
- Native Emacs support
=eglot= fills most items on the first list except for the non-blocking issue initially. It blocks sometimes on bigger projects as well as when entering directories using (nix-)direnv and the lsp is not yet loaded. The first issue is solved by using =eglot-booster=, which increases the parsing speed by what feels like a huge margin (but I never ran any actual tests). The second issue is solved with =eglot-sync-connect=, which avoids blocking the interface while the server is starting.
A blocking issue can still occur while entering a direnv that has a longer evaluation/build time. That issue can only be fixed by using Mic92's [[https://github.com/Mic92/emacs-direnv][emacs-direnv fork]], which calls direnv asynchronously, which in turn avoids the blocking. I am not using this on a daily basis however, since my environments are normally cached anyways and most of them (except for the LaTeX one) are not blocking for long enough for this to be worth it. However, I am considering spinning up my own fork of this at some point.
#+begin_src emacs-lisp
(use-package eglot
:hook
((python-mode
python-ts-mode
c-mode
c-ts-mode
c++-mode
c++-ts-mode
go-mode
go-ts-mode
;;rust-ts-mode
;;rustic-mode
tex-mode
LaTeX-mode
) . (lambda () (progn
(eglot-ensure)
(add-hook 'before-save-hook 'eglot-format nil 'local))))
:custom
(eldoc-echo-area-use-multiline-p nil)
(completion-category-defaults nil)
(fset #'jsonrpc--log-event #'ignore)
(eglot-events-buffer-size 0)
(eglot-sync-connect nil)
(eglot-connect-timeout nil)
(eglot-autoshutdown t)
(eglot-send-changes-idle-time 3)
(flymake-no-changes-timeout 5)
:bind (:map eglot-mode-map
("M-(" . flymake-goto-next-error)
("C-c ," . eglot-code-actions)))
(use-package eglot-booster
:ensure nil
:after eglot
:config
(eglot-booster-mode))
(defalias 'start-lsp-server #'eglot)
#+end_src
*** lsp-mode & company
:PROPERTIES:
:CUSTOM_ID: h:7b9044cf-0fab-4dfa-87fc-f8c18e433e75
:END:
#+begin_src emacs-lisp
(use-package lsp-bridge
:ensure nil)
(use-package lsp-mode
:init
;; set prefix for lsp-command-keymap (few alternatives - "C-l", "C-c l")
(setq lsp-keymap-prefix "C-c l")
:commands lsp)
(use-package company)
#+end_src
*** lsp-bridge
:PROPERTIES:
:CUSTOM_ID: h:f7bc590b-9f91-4f6a-8ffe-93e1dea90a61
:END:
#+begin_src emacs-lisp
(use-package lsp-bridge
:ensure nil)
(use-package lsp-mode
:init
;; set prefix for lsp-command-keymap (few alternatives - "C-l", "C-c l")
(setq lsp-keymap-prefix "C-c l")
:commands lsp)
(use-package company)
#+end_src
*** sideline-flymake
:PROPERTIES:
:CUSTOM_ID: h:d9cd31ea-6c8c-4f1f-83b8-7853bab53857
:END:
This brings back warnings and errors on the sideline for eglot; a feature that I have been missing from lsp-mode for a while.
#+begin_src emacs-lisp
(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)))
#+end_src
*** Prevent breaking of hardlinks
:PROPERTIES:
:CUSTOM_ID: h:e9a30d0f-423f-4e85-af4b-f8560f1c1b53
:END:
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.
#+begin_src emacs-lisp
(setq backup-by-copying-when-linked t)
#+end_src
*** Dirvish
:PROPERTIES:
:CUSTOM_ID: h:0918557a-8463-430c-b8df-6546dea9abd0
:END:
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.
#+begin_src emacs-lisp
(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
((" d" . 'dirvish)
("C-=" . 'dirvish-side)
:map dirvish-mode-map
("h" . dired-up-directory)
("" . dired-up-directory)
("l" . dired-find-file)
("" . 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)))
#+end_src
*** undo-tree
:PROPERTIES:
:CUSTOM_ID: h:1fc538d1-8c53-48b2-8652-66046f4bbbf8
:END:
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 [[#h:218376e8-086b-46bf-91b3-78295d5d440f][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.
#+begin_src emacs-lisp
(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"))))
#+end_src
*** Hydra
:PROPERTIES:
:CUSTOM_ID: h:b6c18dd0-3377-47ea-80c3-ac1486454e18
:END:
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.
#+begin_src emacs-lisp
(use-package hydra)
#+end_src
**** Text scaling
:PROPERTIES:
:CUSTOM_ID: h:c5681884-7040-4b55-ab1b-5777631a0514
:END:
I only wrote this in order to try out hydra; rarely do I really need this. However, it can be useful for [[#h:4e11a845-a7bb-4eb5-b4ce-5b2f52e07425][Presentations]]. It simply scales the text size.
#+begin_src emacs-lisp
;; 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))
#+end_src
*** Email
:PROPERTIES:
:CUSTOM_ID: h:2f333330-b19d-4f64-85ea-146ff28667e8
:END:
**** mu4e
:PROPERTIES:
:CUSTOM_ID: h:b92a18cf-eec3-4605-a8c2-37133ade3574
:END:
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: [[#h:34506761-06b9-43b5-a818-506d9b3faf28][mu4e functions]]
#+begin_src emacs-lisp
(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 (getenv "SWARSEL_MAIL4")
user-full-name (getenv "SWARSEL_FULLNAME"))
;; this does the equivalent of (setq mu4e-user-mail-address-list '(address1@about.com address2@about.com [...])))
(setq mu4e-user-mail-address-list
(mapcar #'intern (split-string (or (getenv "SWARSEL_MAIL_ALL") "") "[ ,]+" t)))
)
(add-hook 'mu4e-compose-mode-hook #'swarsel/mu4e-send-from-correct-address)
(add-hook 'mu4e-compose-post-hook #'swarsel/mu4e-restore-default)
#+end_src
**** mu4e-alert
:PROPERTIES:
:CUSTOM_ID: h:43209eeb-5d46-472e-b7c2-58a3fb465199
:END:
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=.
#+begin_src emacs-lisp
(use-package mu4e-alert
:config
(setq mu4e-alert-set-default-style 'libnotify))
(add-hook 'after-init-hook #'mu4e-alert-enable-notifications)
(mu4e t)
#+end_src
*** Calendar
:PROPERTIES:
:CUSTOM_ID: h:c760f04e-622f-4b3e-8916-53ca8cce6edc
:END:
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!
#+begin_src emacs-lisp
(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"))))
#+end_src
*** Dashboard: emacs startup screen
:PROPERTIES:
:CUSTOM_ID: h:48f5be2b-b3d2-4276-bd49-2630733f23d5
:END:
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 :)
#+begin_src emacs-lisp
(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")))
)
)))
#+end_src
*** vterm
:PROPERTIES:
:CUSTOM_ID: h:a81fb9de-6b6b-4a4a-b758-5107c6e7f0cb
:END:
#+begin_src emacs-lisp
(use-package vterm
:ensure t)
#+end_src
*** multiple cursors
:PROPERTIES:
:CUSTOM_ID: h:1f4d32a0-c1ed-4409-aec4-7b5c96aa21dd
:END:
#+begin_src emacs-lisp
(use-package multiple-cursors)
#+end_src
*** Less logging
:PROPERTIES:
:CUSTOM_ID: h:438d928f-77a8-477a-ac8b-ca54ec673f91
:END:
#+begin_src emacs-lisp
(setq mu4e--log-max-size 1000)
(setq message-log-max 30)
(setq comint-buffer-maximum-size 50)
(add-hook 'comint-output-filter-functions 'comint-truncate-buffer)
#+end_src
* Appendix B: Supplementary Files
:PROPERTIES:
:CUSTOM_ID: h:8fc9f66a-7412-4091-8dee-a06f897baf67
:END:
This section now holds some of the configuration files that cannot be defined directly within NixOS configuration. These files are usually symlinked using =home.file=.
** Server Emacs config
:PROPERTIES:
:CUSTOM_ID: h:c1e53aed-fb47-4aff-930c-dc52f3c5dcb8
:END:
On my server, I use a reduced, self-contained emacs configuration that only serves as an elfeed sync server. This is currently unused, however, I am keeping this in here for now as a reference. The big problem here was the bidirectional syncing using =bjm/elfeed-updater=. As I am using this both on a laptop client (using elfeed) as well as on a mobile phone (using elfeed-cljsrn over elfeed-web), I set up a Syncthing service to take care of the feeds as well as the db state. However, I could only either achieve changes propagating properly from the laptop to the server or from the phone to the server. Both would not work. This current state represents the state where from-laptop changes would propagate. To allow from-phone changes, change =(elfeed-db-load)= in =bjm/elfeed-updater= to =(elfeed-db-save)=.
#+begin_src emacs-lisp :tangle programs/emacs/server.el
(require 'package)
(package-initialize nil)
(setq package-enable-at-startup nil)
(add-to-list 'package-archives '("org" . "http://orgmode.org/elpa/") t)
(add-to-list 'package-archives
'("melpa" . "https://melpa.org/packages/") t)
(package-initialize)
(let ((default-directory "~/.emacs.d/elpa/"))
(normal-top-level-add-subdirs-to-load-path))
(unless (package-installed-p 'use-package)
(package-refresh-contents)
(package-install 'use-package))
(require 'use-package)
(use-package elfeed
:ensure t
:bind (:map elfeed-search-mode-map
("q" . bjm/elfeed-save-db-and-bury)))
(require 'elfeed)
(use-package elfeed-org
:ensure t
:config
(elfeed-org)
(setq rmh-elfeed-org-files (list "/var/lib/syncthing/.elfeed/elfeed.org")))
(use-package elfeed-goodies
:ensure t)
(elfeed-goodies/setup)
(use-package elfeed-web
:ensure t)
(global-set-key (kbd "C-x w") 'bjm/elfeed-load-db-and-open)
(define-key elfeed-show-mode-map (kbd "j") 'elfeed-goodies/split-show-next)
(define-key elfeed-show-mode-map (kbd "k") 'elfeed-goodies/split-show-prev)
(define-key elfeed-search-mode-map (kbd "j") 'next-line)
(define-key elfeed-search-mode-map (kbd "k") 'previous-line)
(define-key elfeed-show-mode-map (kbd "S-SPC") 'scroll-down-command)
(defun bjm/elfeed-save-db-and-bury ()
"Wrapper to save the elfeed db to disk before burying buffer"
(interactive)
(elfeed-db-save)
(quit-window))
(defun bjm/elfeed-load-db-and-open ()
"Wrapper to load the elfeed db from disk before opening"
(interactive)
(elfeed-db-load)
(elfeed)
(elfeed-search-update--force)
(elfeed-update))
(defun bjm/elfeed-updater ()
"Wrapper to load the elfeed db from disk before opening"
(interactive)
(elfeed-db-load))
(run-with-timer 0 (* 1 60) 'bjm/elfeed-updater)
(setq httpd-port 9812)
(setq httpd-host "0.0.0.0")
(setq httpd-root "/root/.emacs.d/elpa/elfeed-web-20240729.1741/")
(setq elfeed-db-directory "/var/lib/syncthing/.elfeed/db/")
(httpd-start)
(elfeed-web-start)
#+end_src
** tridactylrc
:PROPERTIES:
:CUSTOM_ID: h:fc64f42f-e7cf-4829-89f6-2d0d58e04f51
:END:
This is the configuration file for tridactyl, which provides keyboard-driven navigation in firefox. Pay attention to the warnings in this file; depending on your browsing behaviour, you might expose yourself to some vulnerabilities by copying this configuration.
The =command= command can be supplied with a =-p= flag that will take a single argmuent which is exposed as =JS_ARG=. I use this in a function that switches to an open tab if it exists and otherwise creates it.
#+begin_src config :tangle programs/firefox/tridactyl/tridactylrc :mkdirp yes
sanitise tridactyllocal tridactylsync
colourscheme swarsel
" General Settings
set update.lastchecktime 1720629386560
set update.lastnaggedversion 1.24.1
set update.nag true
set update.nagwait 7
set update.checkintervalsecs 86400
set configversion 2.0
set searchurls.no https://search.nixos.org/options?query=
set searchurls.np https://search.nixos.org/packages?query=
set searchurls.hm https://home-manager-options.extranix.com/?query=
set searchurls.@c https://vbc.atlassian.net/wiki/search?text=
set searchurls.@j https://vbc.atlassian.net/issues/?jql=textfields%20~%20%22%s*%22&wildcardFlag=true
set completions.Tab.statusstylepretty true
set hintfiltermode vimperator-reflow
set hintnames numeric
unbind --mode=hint
" Binds
bind buffer #
bind gd tabdetach
bind gD composite tabduplicate; tabdetach
bind d composite tabprev; tabclose #
bind D tabclose
bind c hint
bindurl ^http(s)?://www\.google\.com c hint -Jc [class="LC20lb MBeuO DKV0Md"],[class="YmvwI"],[class="YyVfkd"],[class="fl"]
bindurl ^http(s)?://news\.ycombinator\.com c hint -Jc [class="titleline"],[class="age"]
bindurl ^http(s)?://lobste\.rs c hint -Jc [class="u-url"],[class="comments_label"]
bindurl ^http(s)?://www\.google\.com gi composite focusinput -l ; text.end_of_line
" Work
command tab_or_tabopen jsb -p (async () => {let tabs = await browser.tabs.query({}); let tab = tabs.find(t => t.url.includes(JS_ARG)); if (tab) {browser.tabs.update(tab.id, { active: true });} else {tri.excmds.tabopen(JS_ARG);}})()
command tab_or_tabopen_local jsb -p (async () => {const currentWindow = await browser.windows.getCurrent(); const tabs = await browser.tabs.query({ windowId: currentWindow.id }); const tab = tabs.find(t => t.url.includes(JS_ARG)); if (tab) {browser.tabs.update(tab.id, { active: true });} else {tri.excmds.tabopen(JS_ARG);}})()
bind gwa tab_or_tabopen_local apic-impimba-1.m.imp.ac.at
bind gwA tab_or_tabopen_local artifactory.imp.ac.at
bind gwb tab_or_tabopen_local bitbucket.vbc.ac.at
bind gwc tab_or_tabopen_local vbc.atlassian.net/wiki
bind gwd tab_or_tabopen_local datadomain-impimba-2.imp.ac.at
bind gwe tab_or_tabopen_local exivity.vbc.ac.at
bind gwg tab_or_tabopen_local github.com
bind gwG tab_or_tabopen_local goc.egi.eu
bind gwh tab_or_tabopen_local jupyterhub.vbc.ac.at
bind gwH tab_or_tabopen_local test-jupyterhub.vbc.ac.at
bind gwj tab_or_tabopen_local jenkins.vbc.ac.at
bind gwJ tab_or_tabopen_local test-jenkins.vbc.ac.at
bind gwl tab_or_tabopen_local lucid.app
bind gwm tab_or_tabopen_local monitoring.vbc.ac.at/grafana
bind gwM tab_or_tabopen_local monitoring.vbc.ac.at/prometheus
bind gwn tab_or_tabopen_local netbox.vbc.ac.at
bind gwN tab_or_tabopen_local nap.imp.ac.at
bind gwo tab_or_tabopen_local outlook.office.com
bind gws tab_or_tabopen_local satellite.vbc.ac.at
bind gwt tab_or_tabopen_local tower.vbc.ac.at
bind gwv tab_or_tabopen_local vc-impimba-1.m.imp.ac.at/ui
bind gwx tab_or_tabopen_local xclarity.vbc.ac.at
" Search in page
set findcase smart
bind / fillcmdline find
bind ? fillcmdline find -?
bind n findnext 1
bind N findnext -1
bind j scrollline 4
bind k scrollline -4
" WARNING: This file defines and runs a command called fixamo_quiet. If you
" also have a malicious addon that operates on `` installed this
" will allow it to steal your firefox account credentials!
"
" With those credentials, an attacker can read anything in your sync account,
" publish addons to the AMO, etc, etc.
"
" Without this command a malicious addon can steal credentials from any site
" that you visit that is not in the restrictedDomains list.
"
" You should comment out the fixamo lines unless you are entirely sure that
" they are what you want.
command fixamo_quiet jsb tri.excmds.setpref("privacy.resistFingerprinting.block_mozAddonManager", "true").then(tri.excmds.setpref("extensions.webextensions.restrictedDomains", '""'))
command fixamo js tri.excmds.setpref("privacy.resistFingerprinting.block_mozAddonManager", "true").then(tri.excmds.setpref("extensions.webextensions.restrictedDomains", '""').then(tri.excmds.fillcmdline_tmp(3000, "Permissions added to user.js. Please restart Firefox to make them take affect.")))
fixamo_quiet
set allowautofocus false
" The following modification allows Tridactyl to function on more pages, e.g. raw GitHub pages.
" You may not wish to run this. Mozilla strongly feels that you shouldn't.
" Read https://wiki.mozilla.org/Security/CSP#Goals for more information.
"
" Equivalent to `set csp clobber` before it was removed.
" This weakens your defences against cross-site-scripting attacks
" and other types of code-injection by reducing the strictness
" of Content Security Policy on all sites in a couple of ways.
"
" We remove the sandbox directive
" https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/sandbox
" which allows our iframe (and anyone else's) to run on any website.
"
" We weaken the style-src directive
" https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/style-src
" to allow us to theme our elements.
" This exposes you to 'cross site styling' attacks
jsb browser.webRequest.onHeadersReceived.addListener(tri.request.clobberCSP,{urls:[""],types:["main_frame"]},["blocking","responseHeaders"])
" default is 300ms
set hintdelay 100
" Some pages like github break on the tridactyl quick search. have this as a fallback
unbind
" Subconfig Settings
seturl www.google.com followpagepatterns.next Next
seturl www.google.com followpagepatterns.prev Previous
" Autocmds
autocmd DocStart undefined mode ignore
autocmd DocStart pokerogue.net mode ignore
autocmd DocStart typelit.io mode ignore
autocmd DocStart vc-impimba-1.m.imp.ac.at/ui/webconsole mode ignore
" For syntax highlighting see https://github.com/tridactyl/vim-tridactyl
" vim: set filetype=tridactyl
#+end_src
** tridactyl theme
:PROPERTIES:
:CUSTOM_ID: h:86f1fd9b-56ee-4fd2-8b35-9ea104d83df0
:END:
#+begin_src config :tangle programs/firefox/tridactyl/themes/swarsel.css :mkdirp yes
:root {
--base00: #1D252C;
--base01: #171D23;
--base02: #5EC4FF;
--base03: #566C7D;
--base04: #5EC4FF;
--base05: #A0B3C5;
--base06: #C06ECE;
--base07: #A0B3C5;
--base08: #D95468;
--base09: #FFA880;
--base0A: #5EC4FF;
--base0B: #8BD49C;
--base0C: #008B94;
--base0D: #5EC4FF;
--base0E: #C06ECE;
--base0F: #5EC4FF;
--tridactyl-def-fg: var(--base02);
--tridactyl-cmdl-bg: var(--base00);
--tridactyl-cmdl-fg: var(--base0C);
--tridactyl-font-family: "San Francisco", sans-serif;
--tridactyl-cmdl-font-size: 1.5rem;
--tridactyl-cmdl-line-height: 1.5;
--tridactyl-cmplt-option-height: 1.4em;
--tridactyl-cmplt-font-size: var(--tridactyl-small-font-size);
--tridactyl-cmplt-border-top: unset;
--tridactyl-status-font-size: 9px;
--tridactyl-status-font-family: "Fira Code", monospace;
--tridactyl-status-border: 1px var(--tridactyl-fg) solid;
--tridactyl-header-font-size: var(--tridactyl-small-font-size);
--tridactyl-header-font-weight: 200;
--tridactyl-header-border-bottom: unset;
--tridactyl-hintspan-font-size: var(--tridactyl-font-size);
--tridactyl-hint-active-fg: none;
}
:root #command-line-holder {
order: 1;
border: 2px solid var(--tridactyl-cmdl-fg);
color: var(--tridactyl-cmdl-bg);
}
:root #tridactyl-input {
width: 90%;
padding: 1rem;
color: var(--tridactyl-def-fg);
}
:root #completions table {
font-size: 0.8rem;
font-weight: 200;
border-spacing: 0;
table-layout: fixed;
padding: 1rem;
padding-top: 0;
}
:root #completions > div {
max-height: calc(20 * var(--tridactyl-cmplt-option-height));
min-height: calc(10 * var(--tridactyl-cmplt-option-height));
}
/* COMPLETIONS */
:root #completions {
font-weight: 200;
order: 2;
color: var(--tridactyl-def-fg);
background: var(--tridactyl-cmdl-bg);
}
/* Olie doesn't know how CSS inheritance works */
:root #completions .HistoryCompletionSource {
max-height: unset;
min-height: unset;
}
:root #completions .HistoryCompletionSource table {
width: 100%;
font-size: 9pt;
border-spacing: 0;
table-layout: fixed;
}
/* redundancy 2: redundancy 2: more redundancy */
:root #completions .BmarkCompletionSource {
max-height: unset;
min-height: unset;
}
:root #completions table tr { white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:root #completions .url {
background: var(--tridactyl-cmdl-bg);
}
:root #completions .focused {
background: #44391F;
}
:root #completions .focused .url {
background: #44391F;
}
:root #completions .BufferCompletionSource table {
width: unset;
font-size: unset;
border-spacing: unset;
table-layout: unset;
}
:root #completions table tr {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
:root #completions .sectionHeader {
background: unset;
padding: 1rem !important;
padding-left: unset;
padding-bottom: 0.2rem;
}
:root #cmdline_iframe {
position: fixed !important;
bottom: unset;
top: 25% !important;
left: 10% !important;
z-index: 2147483647 !important;
width: 80% !important;
box-shadow: rgba(0, 0, 0, 0.5) 0px 0px 15px !important;
}
:root .TridactylStatusIndicator {
position: fixed !important;
bottom: 0 !important;
font-weight: 200 !important;
padding: 0.8ex !important;
}
/* #Shydactyl-normal { */
/* border-color: green !important; */
/* } */
/* #Shydactyl-insert { */
/* border-color: yellow !important; */
/* } */
#+end_src
** Waybar style.css
:PROPERTIES:
:CUSTOM_ID: h:77b1c523-5074-4610-b320-90af95e6134d
:END:
This is the stylesheet used by waybar.
#+begin_src css :tangle programs/waybar/style.css :mkdirp yes
@define-color foreground #fdf6e3;
@define-color background #1a1a1a;
@define-color background-alt #292b2e;
@define-color foreground-warning #268bd2;
@define-color background-warning @background;
@define-color foreground-error red;
@define-color background-error @background;
@define-color foreground-critical gold;
@define-color background-critical blue;
,* {
border: none;
border-radius: 0;
font-family: "FiraCode Nerd Font Propo", "Font Awesome 5 Free";
font-size: 14px;
min-height: 0;
margin: -1px 0px;
}
window#waybar {
background: transparent;
color: @foreground;
transition-duration: .5s;
}
window#waybar.hidden {
opacity: 0.2;
}
#mpris {
padding: 0 10px;
background-color: transparent;
color: #1DB954;
font-family: Monospace;
font-size: 12px;
}
#custom-right-arrow-dark,
#custom-left-arrow-dark {
color: @background;
background: @background-alt;
font-size: 24px;
}
#window {
font-size: 12px;
padding: 0 20px;
}
#mode {
background: @background-critical;
color: @foreground-critical;
padding: 0 3px;
}
#privacy,
#custom-configwarn {
color: black;
padding: 0 3px;
animation-name: configblink;
animation-duration: 0.5s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
}
#custom-nix-updates {
color: white;
padding: 0 3px;
}
#custom-outer-right-arrow-dark,
#custom-outer-left-arrow-dark {
color: @background;
font-size: 24px;
}
#custom-outer-left-arrow-dark,
#custom-left-arrow-dark,
#custom-left-arrow-light {
margin: 0 -1px;
}
#custom-right-arrow-light,
#custom-left-arrow-light {
color: @background-alt;
background: @background;
font-size: 24px;
}
#workspaces,
#clock.1,
#clock.2,
#clock.3,
#pulseaudio,
#memory,
#cpu,
#temperature,
#custom-scratchpad-indicator,
#power-profiles-daemon,
#idle_inhibitor,
#backlight-slider,
#mpris,
#tray {
background: @background;
}
#network,
#custom-vpn,
#clock.2,
#battery,
#cpu,
#custom-pseudobat,
#disk {
background: @background-alt;
}
#workspaces button {
padding: 0 2px;
color: #fdf6e3;
}
#workspaces button.focused {
color: @foreground-warning;
}
#workspaces button:hover {
background: @foreground;
color: @background;
border: @foreground;
padding: 0 2px;
box-shadow: inherit;
text-shadow: inherit;
}
#workspaces button.urgent {
color: @background-critical;
background: @foreground-critical;
}
#custom-vpn,
#network {
color: #cc99c9;
}
#temperature,
#power-profiles-daemon {
color: #9ec1cf;
}
#disk {
/*color: #b58900;*/
color: #9ee09e;
}
#custom-scratchpad-indicator {
color: #ffffff;
}
#disk.warning {
color: @foreground-error;
background-color: @background-error;
}
#disk.critical,
#temperature.critical {
color: @foreground-critical;
background-color: @background-critical;
animation-name: blink;
animation-duration: 0.5s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
}
#pulseaudio.muted {
color: @foreground-error;
}
#memory {
/*color: #2aa198;*/
color: #fdfd97;
}
#cpu {
/*color: #6c71c4;*/
color: #feb144;
}
#pulseaudio {
/*color: #268bd2;*/
color: #ff6663;
}
#battery,
#custom-pseudobat {
color: cyan;
}
#battery.discharging {
color: #859900;
}
@keyframes blink {
to {
color: @foreground-error;
background-color: @background-error;
}
}
@keyframes configblink {
to {
color: @foreground-error;
background-color: transparent;
}
}
#battery.critical:not(.charging) {
color: @foreground-critical;
background-color: @background-critical;
animation-name: blink;
animation-duration: 0.5s;
animation-timing-function: linear;
animation-iteration-count: infinite;
animation-direction: alternate;
}
#backlight-slider slider {
min-height: 0px;
min-width: 0px;
opacity: 0;
background-image: none;
border: none;
box-shadow: none;
}
#backlight-slider trough {
min-height: 5px;
min-width: 80px;
border-radius: 5px;
background-color: black;
}
#backlight-slider highlight {
min-width: 0px;
border-radius: 5px;
background-color: grey;
}
#clock.1,
#clock.2,
#clock.3 {
font-family: Monospace;
}
#clock,
#pulseaudio,
#memory,
#cpu,
#tray,
#temperature,
#power-profiles-daemon,
#network,
#custom-vpn,
#mpris,
#battery,
#custom-scratchpad-indicator,
#custom-pseudobat,
#disk {
padding: 0 3px;
}
#+end_src
** justfile
:PROPERTIES:
:CUSTOM_ID: h:788937cf-8816-466b-8e57-1b695cb50f52
:END:
This file defines a few workflows that I often need to run when working on my configuration. This works similar to =make=, but is geared towards general tasks and as such requires no extra handling (as long as there are no dependencies involved) or =.PHONY= recipes.
(In the org-src block I still call it a Makefile in order to get syntax highlighting)
#+begin_src makefile :tangle justfile
default:
@just --list
check:
nix flake check --keep-going
check-trace:
nix flake check --show-trace
update:
nix flake update
iso:
rm -rf result
nix build .#nixosConfigurations.iso.config.system.build.isoImage && ln -sf result/iso/*.iso latest.iso
iso-flake FLAKE SYSTEM="x86_64" FORMAT="iso":
nixos-generate --flake .#{{FLAKE}} -f {{FORMAT}} --system {{SYSTEM}}
iso-install DRIVE: iso
sudo dd if=$(eza --sort changed result/iso/*.iso | tail -n1) of={{DRIVE}} bs=4M status=progress oflag=sync
dd DRIVE ISO:
sudo dd if=$(eza --sort changed {{ISO}} | tail -n1) of={{DRIVE}} bs=4M status=progress oflag=sync
sync USER HOST:
rsync -av --filter=':- .gitignore' -e "ssh -l {{USER}}" . {{USER}}@{{HOST}}:.dotfiles/
#+end_src