Files
nixos/henrovnix_ok

f#+OPTIONS: toc:nil broken-links:t

NixOS Emacs

Henrov's Literate System Configuration

Introduction

Screenshot

This repository contains a literate NixOS configuration built using Emacs Org mode. The primary source of truth is this document itself, which embeds configuration blocks that are tangled into individual files. These generated files are evaluated as a NixOS flake, resulting in an immutable, reproducible system configuration.

This work is based on the foundational efforts of Sandeep Nambiar (https://github.com/gamedolphin). All credit for the original structure, methodology, and guidance belongs to him. His work provided the architectural basis and practical direction upon which this repository is built. This project would not have been possible without his prior contributions, and much of the instructional approach preserved here originates from his documentation. The purpose of this repository is to offer a structured, minimal starting point for deploying a functional NixOS system. It is intentionally not a complete desktop environment, nor is it a “batteries-included” distribution. Instead, it provides a clean and extensible foundation that can be adapted and expanded after installation. Customization is designed to occur primarily through modular .conf files, allowing the system to evolve incrementally while maintaining clarity and separation of concerns. The goal is to enable users to build their own tailored NixOS setup on top of a coherent and reproducible base. Before proceeding with installation, it is strongly recommended to read this documentation carefully. Understanding the structure and design philosophy will help ensure a smooth setup and provide the necessary context for extending the system effectively.

What do you get?

This repository delivers a reproducible foundation built on NixOS, Home-Manager, and Flakes. It assumes a clean NixOS installation as a starting point, preferably minimal or headless, onto which the configuration is applied. The system provides a predefined baseline configuration that installs and enables the essential components required for a functional and extensible environment. Rather than prescribing a complete desktop experience, it establishes the structural framework upon which such an environment can be composed. Core packages are installed as part of the base configuration. Additional software can be incorporated in a controlled and modular manner by extending configuration files.

What you do not get

This repository does not provide a fully polished, bug-free desktop system with every default preconfigured and validated across all hardware combinations. It is a structured foundation, not a turnkey end-user distribution. You should not expect graphical configuration tools, wizard-driven setup screens, or extensive GUI-based system management. Configuration is performed declaratively through Nix modules and supporting configuration files. Familiarity with reading logs, adjusting modules, and rebuilding the system is assumed. Certain subsystems may require manual tuning depending on hardware, desktop environment, or portal backend selection. For example, XDG desktop portals can exhibit inconsistent behavior across compositors and applications, particularly in Wayland-based environments. File chooser dialogs, screen sharing, or drag-and-drop functionality may require additional configuration or troubleshooting. This project favors clarity, reproducibility, and modular structure over convenience abstractions. As a result, some integration details are intentionally left explicit rather than hidden behind automated defaults. In short, this repository provides a coherent and extensible base, not a finished consumer product. It is a work in progress.

What Is a Literate System in the Context of NixOS?

A literate system combines documentation and implementation into a single, coherent source. In this repository, that source is:

README.org

Everything originates from this file:

  • Architectural explanations
  • Design decisions
  • NixOS modules
  • Home-Manager modules
  • Generated documentation

There is no separation between “docs” and “code”. The documentation explains the intent. The source blocks define the system. Org-mode turns that narrative into both executable configuration and readable documentation.

The README is not describing the system. The README is the system.

Two Types of Code Blocks

This literate system uses two different kinds of source blocks.

1. Documentation Blocks These blocks exist purely for documentation purposes. They generate visible code blocks in the exported documentation, but they do not create files. Example:

  #+begin_src bash :tangle no
  <tekst>
  #+end_src

These are used to show commands, examples, or explanatory snippets in the generated documentation. They are never tangled into the filesystem.

2. File-Generating Blocks These blocks generate real .nix files and insert the same code into the documentation. Example:

  ** install_packages.nix
  <tekst>
  #+begin_src nix :tangle configuration/apps/install_packages.nix :noweb tangle :mkdirp yes
  <nixos code>
  #+end_src

Explanation:

  • The headline install_packages.nix becomes a documentation chapter.
  • The paragraph <tekst> explains what the module does.
  • <nixos code> is exactly what will be written into the .nix module configuration/apps/install_packages.nix
  • The same source block is rendered as a code block in the documentation.

This means:

  • The explanation and the implementation live side-by-side.
  • The documentation cannot drift away from the code.
  • The generated .nix file is always derived from the canonical explanation.

The Two Core Commands

There are exactly two commands that matter.

1. Generate all .nix files

emacs README.org --batch -f org-babel-tangle

This command:

  • Regenerates ./configuration
  • Regenerates ./home
  • Overwrites previously generated modules
  • Ensures the system matches the README

2. Generate documentation

emacs --batch -l org -l ox-html README.org -f org-html-export-to-html --kill

This command exports the same README into HTML documentation. In practice you usually combine them:

emacs README.org --batch -f org-babel-tangle && emacs --batch -l org -l ox-html README.org -f org-html-export-to-html --kill

First the system is generated. Then the documentation is generated. Both come from the same source.

Editing Generated Files

The directories:

  • ./configuration
  • ./home

are fully generated by:

emacs README.org --batch -f org-babel-tangle

Editing these files directly is allowed only temporarily, for experimentation or testing. Structural or permanent changes must always be implemented in the corresponding section inside README.org. If you change a file in ./configuration or ./home without updating the README, your changes will disappear on the next tangle. Generated directories are output, not source.

Recommended Workflow

This workflow allows safe experimentation while preserving literate structure.

  1. Change any existing file in ./assets, ./home or ./configuration
  2. Commit your experimental change
  3. Test the configuration
  4. If satisfied, migrate the change into README.org
  5. Regenerate system and documentation
  6. Commit again
  7. Test again

Commands:

git add .
git commit -m "experiment: local change"
sudo nixos-rebuild test --flake .#YOUR_HOSTNAME

After confirming the change:

emacs README.org --batch -f org-babel-tangle && emacs --batch -l org -l ox-html README.org -f org-html-export-to-html --kill
git add .
git commit -m "literate: structural update"
sudo nixos-rebuild test --flake .#YOUR_HOSTNAME

If you are confident about your changes, you may skip steps 13 and edit README.org directly.

Folder Structure Explained

The repository separates generated system code from non-generated supporting files.

./assets

Contains non-generated assisting files such as:

  • Icons
  • Themes
  • Static configuration files

These files are safe to edit directly.

./assets/conf

Contains non-generated assisting configuration files that influence several aspects of builds. Users are expected to modify these when needed.

./configuration

Fully (re)generated by README.org.

Contains:

  • All NixOS modules
  • Service definitions
  • System-level configuration

This directory is output.

./hardware

Contains non-generated hardware.nix files detailing hardware-specific details. This directory will likely be deprecated in the future.

./home

Fully (re)generated by README.org.

Contains:

  • All Home-Manager modules
  • User-level configuration
  • Shell and desktop configuration

./machines

Contains one folder per machine you want to configure. Each machine folder contains non-generated files detailing specifics for that machine:

  • Host-specific overrides
  • Hardware references
  • Host definitions

These determine how shared modules apply to each system.

Final Principle

A literate NixOS system guarantees:

  • Single source of truth
  • No divergence between documentation and configuration
  • Reproducible system builds
  • Clear architectural reasoning
  • Controlled experimentation

You are not maintaining configuration files. You are maintaining a structured narrative that builds an operating system.

Base packages

The baseline package set is defined explicitly within the repository to ensure reproducibility:

- alsa-utils
- avy
- bibata-cursors
- brightnessctl
- cape
- catppuccin-gtk
- catppuccin-theme
- consult
- coreutils
- corfu
- crux
- dash
- diminish
- doom-modeline
- eat
- eldoc
- eldoc-box
- elephant
- emacs-pgtk
- envrc
- exec-path-from-shell
- expreg
- flatpak
- gnugrep
- gnused
- gptel
- hcl-mode
- hypridle
- hyprlandPlugins
- hyprlock
- hyprshell
- librsvg
- linuxPackages_latest
- magit
- magnetic-catppuccin-gtk
- marginalia
- nerd-icons
- nerd-icons-corfu
- nix-mode
- nixpkgs-fmt
- nushell
- ollama-vulkan
- orderless
- papirus-icon-theme
- pulsar
- puni
- rg
- rust-mode
- rustic
- shell-pop
- sideline
- sideline-eglot
- sideline-flymake
- tuigreet
- vertico
- vundo
- walker
- which-key
- wpaperd
- xdg-desktop-portal-gtk
- yasnippet
- yasnippet-snippets
- zsh

Additional packages

Additional software can be installed by extending the dedicated configuration files that define system and Flatpak packages:

├── assets
│   ├── conf
│   │   ├── apps
│   │   │   ├── flatpaks.conf
│   │   │   └── packages.conf

System packages are declared in packages.conf using their attribute names from Nixpkgs. The correct package names can be located through the official NixOS package search at https://search.nixos.org/packages.

Available Flatpak identifiers can be discovered using:

flatpak search <application-name>

or by browsing: https://flathub.org/.

The behavior and integration of Flatpak installation within the system are defined in install_flatpaks.nix, which reads the corresponding configuration files and ensures declarative installation.

This separation maintains clarity between system-level packages and user-facing Flatpak applications while preserving reproducibility and modular structure.

Setting up your system manually

Pre-requisites to build this setup

- a NIXOS system installed with a user with sudo rights.
- an internet connection
- the folder henrovnix_ok as you find it here

Setup when Emacs not (yet) available

  1. Copy the folder henrovnix_ok to your machine
  2. First setup an internet connection

    # Check if NetworkManager service is running
    systemctl status NetworkManager
    # If not running, start it temporarily
    sudo systemctl start NetworkManager
    # Enable Wi-Fi radio (in case it is disabled)
    nmcli radio wifi on
    # List available Wi-Fi networks
    nmcli device wifi list
    # Connect to a Wi-Fi network (replace SSID and PASSWORD)
    nmcli device wifi connect "SSID_NAME" password "YOUR_PASSWORD"
    # Verify that the connection is active
    nmcli connection show --active
    # Optional: show device status
    nmcli device status
  3. Replace <defaultUser> in all files with YOUR_USER

    find ~/Repos/nixos/henrovnix_ok \
    -type d -name ".*" -prune -o \
    -type f -print0 \
    | xargs -0 sed -i 's/=<defaultUser>=/YOUR_USER/g'
  4. Replace machine1 in all files with YOUR_HOSTNAME

    find ~/Repos/nixos/henrovnix_ok \
    -type d -name ".*" -prune -o \
    -type f -print0 \
    | xargs -0 sed -i 's/machine1/YOUR_HOSTNAME/g'
  5. Rename the folder ./machines/machine1 to your hostname

    mv ./machines/machine1 ./machines/YOUR_HOSTNAME
  6. Create a hardware-configuration.nix and copy it into the folder ./machines/YOUR_HOSTNAME overwriting any existing file

    nixos-generate-config
  7. Run the build command to set up the system for the first time

    sudo nixos-rebuild switch --flake .#YOUR_HOSTNAME

Testing and generating builds

At this stage, you should have a functional and reproducible system that can be edited, rebuilt, and extended according to your needs. The foundational structure is now in place, and further customization can occur incrementally through the modular configuration files.

From this point onward, development becomes iterative: modify configuration, rebuild the system, verify behavior, and refine. Because the system is declarative, every change remains explicit, reviewable, and reversible.

The following sections provide practical guidance for editing and rebuilding, along with an explanation of the repository structure. They describe how the various files relate to one another, how the modular layers are composed, and how the configuration evolved into its current form. Understanding this structure will make future modifications more predictable and easier to maintain.

Below are several useful commands for testing configurations, generating builds, and managing system generations. These commands support safe experimentation by allowing you to evaluate changes before switching to them permanently, and to roll back if necessary.

To generate the Nix files:

emacs README.org --batch -f org-babel-tangle

To generate this documentation:

emacs --batch -l org -l ox-html README.org -f org-html-export-to-html --kill

Test the build while being able to reboot to previous version

sudo nixos-rebuild test --flake .#machine1

Build and switch to this version on the next reboot

sudo nixos-rebuild switch --flake .#machine1

Build and run in a virtual machine (qemu must be installed)

sudo nixos-rebuild build-vm --flake .#machine1
# running the vm:
./result/bin/run-nixos-vm

Other important files:

flake.lock for pinning input versions.

assets/* for non-Nix-managed artifacts such as images and wallpapers.

Generated outputs should not be edited directly. A CI workflow can tangle and commit generated outputs when they differ.

Emacs + Org + Tangle

[https://www.gnu.org/software/emacs/ is used as the editor and execution environment for this literate configuration.

[https://orgmode.org/ provides the document structure and the source block execution model used here.

Tangling exports source blocks from this Org document into the corresponding configuration files.

  • References of the form <<code-id>> are noweb placeholders that are expanded from other blocks during tangling.

Nix & NixOS

[https://nix.dev/ is used to define packages, environments, and configuration as pure expressions.

[https://nixos.org/ evaluates Nix expressions into a complete system configuration that can be applied by rebuild operations.

Repository layout and folder conventions

<p> This repository contains system modules, user modules, and configuration fragments. The following directories are treated as separate layers: <code>./configuration</code> (NixOS modules), <code>./home</code> (Home Manager modules), and <code>./assets/conf</code> (configuration fragments referenced or deployed by the modules). </p> <p> To keep navigation consistent, the same internal substructure is used in all three locations. Each layer keeps its role; only the internal grouping is standardized. </p>

Shared domain folders

<ul> <li><code>core/</code> base settings and common infrastructure</li> <li><code>desktop/</code> graphical session, compositor, UI components, and integration</li> <li><code>apps/</code> application enablement and application-level configuration</li> <li><code>services/</code> background services and daemons</li> <li><code>security/</code> secrets handling and security-related configuration</li> <li><code>dev/</code> developer tooling and editor configuration</li> </ul>

Full tree (including unchanged parts)

The tree below shows the full repository layout, with the standardized internal structure applied only inside

├── assets
│   ├── conf
│   │   ├── app.
├── assets
│   ├── conf
│   │   ├── apps
│   │   ├── ollama.nix
│   │   │   ├── flatpaks.conf
│   │   │   └── packages.conf
│   │   ├── core
│   │   │   ├── lightdm.conf
│   │   │   └── lightdm-gtk-greeter.conf
│   │   ├── desktop
│   │   │   ├── hypr
│   │   │   │   ├── bindings.conf
│   │   │   │   ├── hypridle.conf
│   │   │   │   ├── hyprland.conf
│   │   │   │   ├── hyprlock.conf
│   │   │   │   ├── hyprscrolling.conf
│   │   │   │   ├── hyprshell
│   │   │   │   │   ├── config.ron
│   │   │   │   │   └── styles.css
│   │   │   │   └── scripts
│   │   │   │     ├── hyprscrolling-per-monitor.sh
│   │   │   │     ├── hyprscroll-overflow.sh
│   │   │   │     ├── lid-lock.sh
│   │   │   │     ├── lid-restore.sh
│   │   │   │     └── powermenu.sh
│   │   │   ├── wallpaper
│   │   │   │   ├── gif
│   │   │   │   ├── pictures
│   │   │   │   │   ├── 1.jpg
│   │   │   │   │   ├── 2.jpg
│   │   │   │   │   ├── 3.jpg
│   │   │   │   │   ├── 4.png
│   │   │   │   │   ├── 5.jpg
│   │   │   │   │   ├── 6.jpg
│   │   │   │   │   ├── 7.jpg
│   │   │   │   │   ├── 8.jpg
│   │   │   │   │   └── 9.jpg
│   │   │   │   ├── videos
│   │   │   │   │   ├── dark_water_large.mp4
│   │   │   │   │   └── white_blobs_small.mp4
│   │   │   │   └── wallpaper.conf
│   │   │   └── waybar
│   │   │     ├── config.jsonc
│   │   │     └── style.css
│   │   ├── dev
│   │   │   └── terminal
│   │   │     ├── alacritty.toml
│   │   │     ├── aliases.conf
│   │   │     ├── Catppuccin-Mocha.conf
│   │   │     ├── enabled_shells.conf
│   │   │     ├── kitty.conf
│   │   │     ├── starship.toml
│   │   │     └── zsh.conf
│   │   ├── security
│   │   │   └── ssh
│   │   │     └── ssh-client.conf
│   │   └── services
│   ├── lock.png
│   └── scripts
├── configuration
│   ├── apps
│   │   ├── install_flatpaks.nix
│   │   └── install_packages.nix
│   ├── core
│   │   ├── boot.nix
│   │   ├── files.nix
│   │   ├── locale.nix
│   │   ├── login-lightdm.nix
│   │   ├── login-tuigreeter.nix
│   │   ├── networking.nix
│   │   └── nix-settings.nix
│   ├── default.nix
│   ├── desktop
│   │   ├── audio.nix
│   │   └── hyprland.nix
│   ├── dev
│   │   └── terminal.nix
│   └── services
│     └── services.nix
├── flake.lock
├── flake.nix
├── hardware
│   └── hardware.nix
├── home
│   ├── apps
│   │   ├── ollama.nix
│   │   ├── defaults-apps.nix
│   │   └── theme.nix
│   ├── default.nix
│   ├── desktop
│   │   ├── animated_wallpaper.nix
│   │   ├── hyprexpo.nix
│   │   ├── hypridle.nix
│   │   ├── hyprland.nix
│   │   ├── hyprlock.nix
│   │   ├── hyprscrolling.nix
│   │   ├── hyprshell.nix
│   │   ├── powermenu.nix
│   │   ├── rotating_wallpaper.nix
│   │   ├── walker.nix
│   │   ├── waybar.nix
│   │   └── workspace_wallpaper.nix
│   └── dev
│     ├── alacritty.nix
│     ├── dev.nix
│     ├── emacs
│     │   ├── default.nix
│     │   ├── early-init.el
│     │   └── init.el
│     ├── kitty.nix
│     ├── shells.nix
│     ├── starship.nix
│     └── zsh.nix
├── LICENSE
├── machines
│   └── traveldroid
│     ├── configuration.nix
│     ├── hardware-configuration.nix
│     └── home.nix
├── README.html
├── README.org
└── user.nix

Notes

<ul> <li>Only the internal layout of <code>configuration/</code>, <code>home/</code>, and <code>assets/conf/</code> is standardized; all other paths remain as currently organized.</li> <li>The <code>services/</code> and <code>security/</code> folders are included for completeness even if initially empty in some layers.</li> </ul>

YourNixCode(Input) -> System Configuration

I use nix flakes which means that the entry point for the nix evaluation is a file called flake.nix which has two parts (among other things)

  {
  inputs: # describes the function input, consisting mainly of package sources
  outputs: # what the function outputs, a nixos configuration in our case
  }

Nix flakes is still behind an experimental flag, but it is de facto the standard by most of the community. Flakes allow us to pin the input package versions using a flake.lock file. This prevents unwanted and surprise updates when rebuilding without changing the configuration.

TLDR App List

Window Manager Hyprland
Bar Waybar
Application Launcher Walker
Terminal Emulator Alacritty
Shell Zsh and Starship
Text Editor Emacs
File Manager Thunar
Fonts Aporeti
Colors Catppuccin
Icons Catppuccin Nix
Lock Screen Hyprlock
Wallpapers Hyprpaper

Configuration Variables

I have a bunch of constant strings that I would rather put in a file. Thats what user.nix is. The values are imported at the beginning and are available to almost all the functions being called to configure the system.

{
  system = "x86_64-linux";
  username = "henrov";
  stateVersion = "25.11";
  locale = "nl_NL.UTF-8";
}

Flake Inputs

The inputs for my system's configuration are very simple

  1. nixpkgs - the main nix repository of packages. Its huge and growing. Pinned to the unstable release channel.

Sometimes pinned to a specific commit because unstable broke something and the fix hasn't made it into the release yet.

  1. home-manager - a nix module that helps keep track of user specific dotfiles and configurations as part of my nix config.
  2. emacs-overlay - this has more configuration options and generally a newer emacs available provided by the community.
  3. catppuccin - nix module that allows everything to be catppuccin themed.
  {
  description = "Henrov's nixos configuration";
  inputs = {
  nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
  home-manager = {
    url = "github:nix-community/home-manager";
    inputs.nixpkgs.follows = "nixpkgs";
  };
  emacs-overlay = {
    url = "github:nix-community/emacs-overlay";
    inputs.nixpkgs.follows = "nixpkgs";
    };
  catppuccin = {
    url = "github:catppuccin/nix";
    inputs.nixpkgs.follows = "nixpkgs";
  };
  zen-browser = {
    url = "github:youwen5/zen-browser-flake";
    inputs.nixpkgs.follows = "nixpkgs";
  };
  };
  <<flake-outputs>>
  }

Flake Output

Now that the inputs are ready, the outputs define what the system will actually look like. I also define the machines that this configuration specifies early on. Finally, I iterate over the machines list and pull files from /.machines/${name} subdirectory. This allows me to have configuration that has machine specific configuration limited to those files while also keeping a modular reusable base. We also add a devshell that makes editing this repository easier in emacs.

  outputs = inputs@{
  nixpkgs,
  home-manager,
  emacs-overlay,
  catppuccin,
  ...
  }:
  let
  user = import ./user.nix;
  lib = nixpkgs.lib;
  machines = [
    "traveldroid"
  ];
  pkgs = import nixpkgs {
    inherit (user) system;
  };
  in
  {
    nixosConfigurations = builtins.listToAttrs (
    builtins.map (machine: {
    name = machine;
    value = lib.nixosSystem {
      modules = [
      <<flake-emacs-module>>
      <<flake-config-module>>
      <<flake-home-module>>
      catppuccin.nixosModules.catppuccin # theme
      ];

      specialArgs = {
      hostname = machine;
      inherit user;
      inherit inputs;
      flakeRoot = inputs.self;
      };
    };
    }) machines
    );

    devShells.${user.system}.default = pkgs.mkShell {
    buildInputs = with pkgs; [
    nil
    nixfmt-rfc-style
    ];
    };
  };

Lets look at the individual modules

  1. Emacs

The first is the emacs overlay so that it uses the nix-community emacs overlay from the inputs instead of the nixpkgs one. Overlays are a special nix way to override existing packages within a repository.

  ({ ... }: {
  nixpkgs.overlays = [ emacs-overlay.overlays.default ];
  })
  1. Then the machine specific configuration, in this case, just "traveldroid".
  ./machines/${machine}/configuration.nix
  1. And finally the home-manager module.

This can be initialized and managed on its own but I'd rather use the nixos-rebuild command to build everything instead of managing userland dotfiles separately.

 home-manager.nixosModules.home-manager
 {
 home-manager.useGlobalPkgs = true;
 home-manager.useUserPackages = true;

 home-manager.extraSpecialArgs = {
   inherit user inputs;
   flakeRoot = inputs.self;
 };

 <<flake-home-backup>>
 <<flake-home-config>>
 }
  • Home-Manager will not overwrite existing configuration files and that is good in most cases, but when everything is declarative like it is here, I'd rather that home-manager create a .backup and replace the file. #+name: flake-home-backup
  home-manager.backupFileExtension = "backup";
  • Finally I pull in the machine specific home configuration. Along with the overrides from catppuccin. #+name: flake-home-config
  home-manager.users.${user.username} = {
  imports =  [
    ./machines/${machine}/home.nix
    catppuccin.homeModules.catppuccin
  ];
  };

Envrc + Direnv

Editing this file will be much nicer if we have the dev environment configured. That is done in the devshells section. But to auto load this dev shell, we need a .envrc file. This tells direnv to load the devshell in the flake. #Finally, we also look for a .envrc-private file and try to load that. That contains devshell specific secrets.

  use flake

  watch_file .envrc.private
  if [[ -f .envrc.private ]]; then
  source_env .envrc.private
  fi

Machines

The individual machines subdirectory is configured as follows :-

  +--machine
  |  +--configuration.nix
  |  +--home.nix
  |  +--hardware-configuration.nix
  • configuration.nix has the system configuration.
  • home.nix has the user level configuration.
  • hardware-configuration.nix has the unique hardware configuration.
  • Note about imports imports = [] in a nix file will pull in the function/object from the list of files provided. This imported object (or function result) is just trivially merged into a common object.

We can take a look at that the common hardware options I have for all my machines.

Other Utils

Updates

To update the computer, I just need to update the flake.lock file to have references to the latest repository. This is done with :-

  nix flake update

Hardware

I'll let the code comments explain the file here.

  { pkgs, lib, user, config, ...} :
  {
  nixpkgs.hostPlatform = lib.mkDefault user.system;    # x86_64-linux
  powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; # enable power saving on the cpu

  # update cpu microcode with firmware that allows redistribution
  hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;

  hardware = {
  # always enable bluetooth
  bluetooth.enable = true;

  # always enable graphics drivers and enable a bunch of layers for it (including vulkan validation)
  graphics = {
    enable = true;
    extraPackages = with pkgs; [
    vulkan-validation-layers # helps catch and debug vulkan crashes
    ];
  };
  };

  hardware.enableAllFirmware = true; # enable all firmware regardless of license
  }

Configuration

This section describes the main system configuration for the computers that I have. Nix will look for a default.nix file if you give it a path to a folder to import. And default.nix looks as follows :-

  { pkgs, user, ... } :
{
  imports = [
  ./apps/install_flatpaks.nix
  ./apps/install_packages.nix
  ./core/files.nix
  ./core/locale.nix
  ./core/networking.nix
  ./core/nix-settings.nix
  #./core/login-tuigreeter.nix
  ./core/login-lightdm.nix
  ./desktop/audio.nix
  ./desktop/hyprland.nix
  ./dev/terminal.nix
  ./core/boot.nix
  ./services/services.nix
  ];

  <<config-system-packages>>

  <<config-user>>

  <<config-programs>>

  <<config-fonts>>

  # enable the catppuccin theme for everything with mocha + blue accents
  catppuccin.enable = true;
  catppuccin.flavor = "mocha";
  catppuccin.accent = "blue";

  system.stateVersion = user.stateVersion;
}

Apps section

This section describes a way of installing packages, either through nixpkgs orr flatpak. What apps to instal is decided in the files ./assets/conf/apps/packages.conf and flatpaks.conf

install_packages.nix

{ config, lib, pkgs, flakeRoot, inputs, ... }:
let
  packagesConfPath = flakeRoot + "/assets/conf/apps/packages.conf";
  raw = builtins.readFile packagesConfPath;
  # IMPORTANT: explicit "\n" so we never accidentally split into characters
  rawLines = lib.splitString "\n" raw;
  # Guard: if we accidentally split into characters, rawLines length ~= stringLength raw
  _guard = assert !(
  builtins.stringLength raw > 1 &&
  builtins.length rawLines == builtins.stringLength raw
  ); true;
  cleanLine = l:
  let
  noCR = lib.replaceStrings [ "\r" ] [ "" ] l;
  noInlineComment = lib.head (lib.splitString "#" noCR);
  in
  lib.strings.trim noInlineComment;
  entries =
  builtins.filter (l: l != "")
  (map cleanLine rawLines);
  resolvePkg = name:
  let
  parts = lib.splitString "." name;
  found = lib.attrByPath parts null pkgs;
  in
  if found == null then
    throw ''
    install_packages.nix: package not found in pkgs
    Token    : ${builtins.toJSON name}
    packages.conf   : ${toString packagesConfPath}
    Hint     : check the attribute name on search.nixos.org/packages
    ''
  else
    found;
  packages = builtins.seq _guard (map resolvePkg entries);
  zenBrowser =
  inputs.zen-browser.packages.${pkgs.stdenv.hostPlatform.system}.default;
in
{
  environment.systemPackages =
  packages
  ++ [ zenBrowser ];
}

install_flatpaks.nix

{ config, pkgs, lib, flakeRoot, ... }:
let
  moduleName = "install-flatpaks";
  flatpakConfPath = flakeRoot + "/assets/conf/apps/flatpaks.conf";
  raw = builtins.readFile flatpakConfPath;
  # Explicit "\n" so we never accidentally split into characters
  rawLines = lib.splitString "\n" raw;

  # Guard: if we accidentally split into characters, rawLines length ~= stringLength raw
  _guard = assert !(
  builtins.stringLength raw > 1 &&
  builtins.length rawLines == builtins.stringLength raw
  ); true;

  cleanLine = l:
  let
  noCR = lib.replaceStrings [ "\r" ] [ "" ] l;
  noInlineComment = lib.head (lib.splitString "#" noCR);
  in
  lib.strings.trim noInlineComment;

  entries =
  builtins.filter (l: l != "")
  (map cleanLine rawLines);

  # Flatpak app IDs are reverse-DNS style like org.example.App (at least 2 dots).
  # We'll validate and fail early with a clear message.
  dotCount = s: builtins.length (lib.splitString "." s) - 1;

  isValidId = s:
  (dotCount s) >= 2; # matches the error you're seeing: "at least 2 periods"

  _validate =
  builtins.seq _guard (
  builtins.map (id:
    if isValidId id then true else
    throw ''
    ${moduleName}: invalid Flatpak ID in flatpaks.conf (needs reverse-DNS with at least 2 dots)

    Token    : ${builtins.toJSON id}
    flatpaks.conf : ${toString flatpakConfPath}

    Fix: remove stray tokens/headers, or comment them out with '#'.
    ''
  ) entries
  );

  # Use validated entries
  flatpakApps = builtins.seq _validate entries;

  syncFlatpaks = pkgs.writeShellScript "sync-flatpaks" ''
  set -euo pipefail

  # Use the deployed config path (matches environment.etc below)
  CONF="/etc/flatpak/flatpaks.conf"
  if [[ -f "$CONF" ]]; then
  echo "flatpak-sync: using $CONF"
  else
  echo "flatpak-sync: WARNING: $CONF not found, using embedded list"
  fi

  if ! flatpak remotes --system --columns=name | grep -qx flathub; then
  flatpak remote-add --system --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
  fi

  desired_apps=(
  ${lib.concatStringsSep "\n" (map (a: ''"${a}"'') flatpakApps)}
  )

  for app in "''${desired_apps[@]}"; do
  if ! flatpak info --system "$app" >/dev/null 2>&1; then
    flatpak install --system -y --noninteractive flathub "$app"
  fi
  done
  '';
in
{
  services.flatpak.enable = true;

  xdg.portal = {
  enable = true;
  extraPortals = with pkgs; [
  xdg-desktop-portal-hyprland
  xdg-desktop-portal-gtk
  ];
  };

  # Deploy the config file for runtime visibility/debugging
  environment.etc."flatpak/flatpaks.conf".source = lib.mkForce flatpakConfPath;

  systemd.services.flatpak-sync = {
  description = "Install Flatpak apps listed in flatpaks.conf";
  wantedBy = [ "multi-user.target" ];
  wants = [ "network-online.target" ];
  after = [ "network-online.target" ];

  serviceConfig = {
  Type = "oneshot";
  ExecStart = syncFlatpaks;
  };

  restartTriggers = [ flatpakConfPath ];
  path = [ pkgs.flatpak pkgs.coreutils pkgs.gnugrep pkgs.gnused ];
  };
}

Nix Settings

These are global nix settings that configure the settings for the actual tool.

  { pkgs, user, ... } :
  {
  nix.settings = {
  # enable flakes
  experimental-features = ["nix-command" "flakes"];

  # add a cache that speed up new applications by downloading binaries
  # from the trusted cache instead of compiling from sourcer
  substituters = [
    "https://nix-community.cachix.org"
  ];
  # trust the cache public key
  trusted-public-keys = [
    "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs="
  ];
  };

  # allow proprietary software on this machine. I'm not a purist.
  nixpkgs.config.allowUnfree = true;
  # unityhub depends on this... for now
  nixpkgs.config.permittedInsecurePackages = [ "libxml2-2.13.8" ];

  # this declares how often old configurations are cleared up.
  # i cleanup anything older than a week, every week.
  nix.gc = {
  automatic = true;
  options = "--delete-older-than 7d";
  dates = "weekly";
  };

  programs = {
  # command line utility that makes applying changes easy and pretty
  nh = {
    enable = true;
    flake = "/home/${user.username}/system";
  };
  };
  }

Boot

This file has most of the settings the control how the computer boots up.

{ pkgs, ... } :
{
  boot = {
  initrd = {
  verbose = false;   # its a lot of logs. dont need it, unless we do.
  kernelModules = [ ]; # no kernel modules on boot
  };

  extraModulePackages = [ ];       # no extra packages on boot either
  kernelPackages = pkgs.linuxPackages_latest;  # latest greatest linux kernel
  kernelParams = [ "silent" ];       # quiet those logs

  consoleLogLevel = 0;         # quiten more logs
  plymouth.enable = true;        # graphical boot animation instead

  supportedFilesystems = [ "ntfs" ];     # should see the ntfs (windows)

  loader = {
  systemd-boot.enable = true;      # systemd-boot
  systemd-boot.configurationLimit = 10;
  efi.canTouchEfiVariables = true;     # allow editing efi to edit the boot loader


  timeout = 5;           # grub timeout to make a selection
  };
  };
}

Login

Here we control what the login screen would look like. In configuration/default.nix you can choose whether to use tuigreet (very minimalistic) or LightDM (nicer, themeable)

Tuigreet

Doesn't match the rest of the aesthetic of the system (with hyprland), but I like its simplicity.

  { pkgs, user, ... } :
  {
  environment.systemPackages = with pkgs; [
  tuigreet
  ];
  services.greetd = {
  enable = true;
  settings = {
    default_session = {
    command = pkgs.lib.mkForce "${pkgs.tuigreet}/bin/tuigreet --remember --time --time-format '%I:%M %p | %a • %h | %F'";
    };
  };
  };
  }

LightDM

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

let
  lightdmConf = builtins.readFile ../../assets/conf/core/lightdm.conf;
  lockPng   = ../../assets/lock.png;

  greeterConfPath = ../../assets/conf/core/lightdm-gtk-greeter.conf;
  greeterRaw  = builtins.readFile greeterConfPath;

  # Extract "key = value" from the greeter conf.
  # Returns null if not found.
  getIniValue = key:
  let
  lines = lib.splitString "\n" greeterRaw;

  # Captures the value part (group 0) from a single line.
  # We match line-by-line because Nix regex does NOT support PCRE flags like (?s).
  m =
    let
    ms = builtins.filter (x: x != null) (map (line:
    builtins.match
      ("^[[:space:]]*" + key + "[[:space:]]*=[[:space:]]*([^#;]+).*$")
      line
    ) lines);
    in
    if ms == [] then null else builtins.elemAt ms 0;
  in
  if m == null then null else lib.strings.trim (builtins.elemAt m 0);

  # In your greeter.conf these are *package keys*, not theme names.
  themePkgKey  = getIniValue "theme-name";
  iconPkgKey = getIniValue "icon-theme-name";
  cursorPkgKey = getIniValue "cursor-theme-name";

  cursorSizeStr = getIniValue "cursor-theme-size";
  cursorSize =
  if cursorSizeStr == null then null
  else lib.toInt (lib.strings.trim cursorSizeStr);

  # Map package-keys (from greeter.conf) -> { package, name }
  #
  # IMPORTANT:
  # - "name" must be the real theme/icon/cursor NAME as seen under share/themes or share/icons.
  # - "package" is the Nixpkgs derivation providing it.
  pkgMap = {
  catppuccinThemePkg = {
  package = pkgs.catppuccin-gtk.override {
    accents = [ "blue" ];
    variant = "mocha";
    size  = "standard";
    tweaks  = [ ];
  };
  name = "Catppuccin-Mocha-Standard-Blue-Dark";
  };

  papirus-icon-theme = {
  package = pkgs.papirus-icon-theme;
  name  = "Papirus-Dark";
  };

  bibata-cursors = {
  package = pkgs.bibata-cursors;
  name  = "Bibata-Modern-Ice";
  };
  };

  pick = key:
  if key == null then
  throw "lightdm: missing required key in ${toString greeterConfPath}"
  else if !(pkgMap ? "${key}") then
  throw "lightdm: unknown package key '${key}' in ${toString greeterConfPath}. Known keys: ${lib.concatStringsSep ", " (builtins.attrNames pkgMap)}"
  else
  pkgMap."${key}";

  themeSel  = pick themePkgKey;
  iconSel = pick iconPkgKey;
  cursorSel = pick cursorPkgKey;

  # Rewrite greeter.conf so LightDM sees REAL names, not package keys.
  # Also force background to lockPng.
  greeterFixed =
  ''
  [greeter]
  theme-name = ${themeSel.name}
  icon-theme-name = ${iconSel.name}
  cursor-theme-name = ${cursorSel.name}
  ${lib.optionalString (cursorSize != null) "cursor-theme-size = ${toString cursorSize}"}
  ''
  + "\n"
  + greeterRaw;
in
{
  services.greetd.enable = false;

  services.xserver = {
  enable = true;
  desktopManager.xterm.enable = false;

  displayManager.lightdm = {
  enable = true;
  background = lockPng;

  greeters.gtk = {
    enable = true;

    theme = {
    name  = themeSel.name;
    package = themeSel.package;
    };

    iconTheme = {
    name  = iconSel.name;
    package = iconSel.package;
    };

    cursorTheme = {
    name  = cursorSel.name;
    package = cursorSel.package;
    } // lib.optionalAttrs (cursorSize != null) {
    size = cursorSize;
    };

    # This includes your (rewritten) greeter config.
    extraConfig = greeterFixed;
  };

  extraConfig = lightdmConf;
  };
  };

  programs.hyprland.enable = true;

  # Optional: make them available system-wide as well
  environment.systemPackages = [
  themeSel.package
  iconSel.package
  cursorSel.package
  ];
}

Terminal (default system)

This is the initial system level configuration for the terminal that I use on this machine. Its just zsh.

{ pkgs, user, ... }:
{
  console.useXkbConfig = true;
  users.users.${user.username}.shell = pkgs.zsh;
  programs.zsh.enable = true;
  environment.shells = [ pkgs.zsh ];
  environment.pathsToLink = [ "/share/zsh" ];
}

Files

I use Thunar as the file explorer. Also setup a few plugins for Thunar in this config. Along with that, a few other utilities like zip and enabling services to automount usb drives.

{ pkgs, user, config, ... }:
{
  environment.systemPackages = with pkgs; [
  zip
  unzip
  p7zip
  usbutils
  udiskie
  file-roller
  ];

  programs.thunar = {
  enable = true;
  plugins = with pkgs; [
  thunar-archive-plugin
  thunar-media-tags-plugin
  thunar-volman
  thunar-vcs-plugin
  ];
  };

  programs.xfconf.enable = true; # to save thunar settings

  services = {
  gvfs.enable = true;  # Mount, trash, and other functionalities
  tumbler.enable = true; # Thumbnail support for images
  udisks2.enable = true; # Auto mount usb drives
  };
}

Locale

I live in Netherlands and would like all my locale and timezone settings to match. Except my default locale.

  { user, ... } :
  let
  locale = user.locale;
  defaultLocale = "nl_NL.UTF-8";
  in
  {
  # Set your time zone.
  time.timeZone = "Europe/Amsterdam";

  # Select internationalisation properties.
  i18n.defaultLocale = defaultLocale;

  i18n.extraLocaleSettings = {
  LC_ADDRESS = locale;
  LC_IDENTIFICATION = locale;
  LC_MEASUREMENT = locale;
  LC_MONETARY = locale;
  LC_NAME = locale;
  LC_NUMERIC = locale;
  LC_PAPER = locale;
  LC_TELEPHONE = locale;
  LC_TIME = defaultLocale;
  };
  }

Networking

Not much to see here. I want networking to be enabled. I want firewall as well.

{ pkgs, lib, ... }:
{
  networking = {
  useDHCP = lib.mkDefault true;
  networkmanager.enable = true;
  networkmanager.wifi.backend = "iwd";
  wireless.iwd.enable = true;
  wireless.userControlled.enable = true;
  firewall = {
  enable = true;
  # KDE Connect: discovery + encrypted connections
  allowedTCPPortRanges = [
    { from = 1714; to = 1764; }
  ];
  allowedUDPPortRanges = [
    { from = 1714; to = 1764; }
  ];
  };
  };
  environment.systemPackages = with pkgs; [ impala ];
}

Hyprland

This is a big one because the DE needs so much configuration. This section mostly installs Hyprland. The configuration is done in the home manager section.

{ pkgs, ... }:
{
  nix.settings = {
  substituters = [ "https://hyprland.cachix.org" ];
  trusted-public-keys = [
  "hyprland.cachix.org-1:a7pgxzMz7+chwVL3/pzj6jIBMioiJM7ypFP8PwtkuGc="
  ];
  };
  services.dbus.enable = true;
  security.polkit.enable = true;
  services.flatpak.enable = true;
  services.pipewire = {
  enable = true;
  alsa.enable = true;
  alsa.support32Bit = true;
  pulse.enable = true;
  wireplumber.enable = true;
  };
  services.gvfs.enable = true;
  xdg.portal = {
  enable = true;
  extraPortals = with pkgs; [
  xdg-desktop-portal-hyprland
  xdg-desktop-portal-gtk
  ];
  config.common.default = [ "hyprland" "gtk" ];
  };
  environment.systemPackages = with pkgs; [
  walker
  uwsm
  hyprland-qtutils
  hyprpolkitagent
  grimblast
  ];
  programs = {
  uwsm.enable = true;
  uwsm.waylandCompositors.hyprland = {
  prettyName = "Hyprland";
  comment = "Hyprland compositor managed by UWSM";
  binPath = "/run/current-system/sw/bin/Hyprland";
  };
  hyprland = {
  withUWSM = true;
  enable = true;
  xwayland.enable = true;
  };
  };
  environment.sessionVariables = {
  XDG_SESSION_TYPE = "wayland";
  XDG_CURRENT_DESKTOP = "Hyprland";
  XDG_SESSION_DESKTOP = "Hyprland";
  NIXOS_OZONE_WL = "1";
  XCURSOR_SIZE = "24";
  };
  security.pam.services.hyprlock = { };
  # Optional; GNOME-specific (keep only if you really use gnome-keyring integration)
  security.pam.services.gdm.enableGnomeKeyring = true;
}

Services

These are some of the services that I enable at the system level. Explanation in the comments.

  { user, ...} :
  {
  services = {
  blueman.enable = true;      # bluetooth manager
  fwupd.enable = true;      # firmware updating service
  fstrim.enable = true;       # ssd maintenance service
  thermald.enable = true;     # thermal regulation service
  printing.enable = true;     # printing services, cups
  gnome.gnome-keyring.enable = true;  # keyring
  flatpak.enable = true;      # allow installing things from flatpaks
  #flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo

  # printer discovery
  avahi = {
    enable = true;
    nssmdns4 = true;
    openFirewall = true;
  };
  };

  virtualisation.docker.enable = true;  # enable docker
  users.users.${user.username}.extraGroups = [ "docker" ]; # add self to docker user group
  }

Audio

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

{
  environment.systemPackages = with pkgs; [
  pipewire
  wireplumber
  alsa-utils
  pulseaudio
  pamixer
  pavucontrol
  ];

  services.pipewire = {
  enable = true;
  alsa.enable = true;
  alsa.support32Bit = true;
  pulse.enable = true;
  jack.enable = true;
  wireplumber.enable = true;
  };

  security.rtkit.enable = true;

  # Helps on many laptops (Intel SOF etc.)
  hardware.enableRedistributableFirmware = true;

  # Prefer analog over HDMI/DP in a machine-agnostic way
  services.pipewire.wireplumber.extraConfig."51-audio-priorities" = {
  "monitor.alsa.rules" = [
  # De-prioritize HDMI / DisplayPort sinks
  {
    matches = [
    { "node.name" = "~alsa_output\\..*HDMI.*"; }
    { "node.name" = "~alsa_output\\..*DisplayPort.*"; }
    ];
    actions.update-props = {
    "priority.session" = 100;
    "priority.driver" = 100;
    };
  }

  # Prefer analog sinks (speakers/headphones)
  {
    matches = [
    { "node.name" = "~alsa_output\\..*analog.*"; }
    { "node.name" = "~alsa_output\\..*Headphones.*"; }
    { "node.name" = "~alsa_output\\..*Speaker.*"; }
    ];
    actions.update-props = {
    "priority.session" = 2000;
    "priority.driver" = 2000;
    };
  }
  ];
  };

  # Optional: clear "sticky" user-selected defaults so priority rules win
  systemd.user.services.wireplumber-clear-default-nodes = {
  description = "Clear WirePlumber saved default nodes (avoid HDMI becoming sticky)";

  after = [ "wireplumber.service" ];
  partOf = [ "wireplumber.service" ];
  wantedBy = [ "default.target" ];

  serviceConfig = {
  Type = "oneshot";
  ExecStart = "${pkgs.coreutils}/bin/rm -f %h/.local/state/wireplumber/default-nodes";
  };
  };
}

Fonts

Nothing much to see here. I love Aporetic, and I use it everywhere.

  fonts.packages = with pkgs; [
  aporetic
  nerd-fonts.iosevka
  ];

User Config

This creates the user profile that I login with. Initially created during install.

  users.users.${user.username} = {
  isNormalUser = true;
  description = "henrov";
  extraGroups = [
  "networkmanager" # allow editing network connections
  "wheel"    # can do sudo
  "scanner"    # access to the network scanner
  "lp"     # access to the printer
  ];
  };

Home

I use home-manager to manage my user level dotfiles and configurations. Most of the "theme" of the system is decided here. I also use it to install programs that are okay with being installed at the user level instead of the system.

default.nix

This module will import all necessities.

{ pkgs, user, ... } :
  {
  imports = [
  ./apps/ollama.nix
  #./apps/default-apps.nix
  ./apps/theme.nix
  ./desktop/hypridle.nix
  ./desktop/hyprland.nix
  ./desktop/hyprexpo.nix
  ./desktop/hyprlock.nix
  ./desktop/hyprscrolling.nix
  ./desktop/hyprshell.nix
  ./desktop/ncsway.nix
  ./desktop/powermenu.nix
  #./desktop/animated_wallpaper.nix
  #./desktop/rotating_wallpaper.nix
  ./desktop/workspace_wallpaper.nix
  ./desktop/waybar.nix
  ./desktop/walker.nix
  ./dev/dev.nix
  ./dev/kitty.nix
  ./dev/shells.nix
  ./dev/starship.nix
  ./dev/zsh.nix
  ./dev/emacs
  ];

  <<home-user>>

  <<home-packages>>

  programs.home-manager.enable =  true;
  }

Ollama

This Home Manager Nix module (ai.nix) installs the Ollama package and configures it by reading a simple key-value configuration file (ollama.conf) for settings like the server host and default model. It sets environment variables (OLLAMA_HOST and OLAMA_DEFAULT_MODEL) for easy access in your shell or applications, with fallback defaults if the config file is missing or empty. Optionally, it also defines a user-level systemd service to automatically start the Ollama server on NixOS or systems with Home Managers systemd support enabled.

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

let
  ollamaConfPath = flakeRoot + "/assets/conf/apps/ai/ollama/ollama.conf";
  envVars = builtins.fromJSON (builtins.readFile ollamaConfPath);
in

{
  services.ollama = {
  enable = true;
  package = pkgs.ollama;
  environmentVariables = envVars;
  };
}

Powermenu

Creates a script for a powermenu

{ config, lib, pkgs, flakeRoot, ... }:
let
  repoScript =
  flakeRoot + "/assets/conf/desktop/hypr/scripts/powermenu.sh";
  targetRel = "hypr/scripts/powermenu.sh";
in
{
  # Ensure script exists in ~/.config/hypr/scripts/
  xdg.configFile."${targetRel}" = {
  source = lib.mkForce repoScript;
  backup = lib.mkForce false;
  executable = true;
  };
}

Animated Wallpaper

userRelRoot = "nixos_conf/wallpaperstuff"; animated_wallpaper.nix installs mpvpaper and deploys your wallpaper files from the repo (./assets/conf/desktop/wallpaper) into ~/conf/desktop/wallpaper/pictures.

{ config, pkgs, lib, flakeRoot, ... }:
let
  repoWallpaperDir = flakeRoot + "/assets/conf/desktop/wallpaper";
  userRelRoot = "nixos_conf/wallpaperstuff";
  userAbsRoot = "${config.home.homeDirectory}/${userRelRoot}";
  # The video file you want as wallpaper (must exist under the synced dir)
  userVideoPath = "${userAbsRoot}/videos/myWallpaper.mp4";
  # (keep your existing approach: sync the repo wallpaper dir to the user dir)
  repoWallpapersOnly = lib.cleanSourceWith {
  src = repoWallpaperDir;
  filter = path: type: true;
  };
in
{
  home.packages = [
  pkgs.mpvpaper
  pkgs.mpv
  ];
  # Sync repo wallpapers (including videos/) into ~/nixos_conf/wallpaperstuff
  home.file."${userRelRoot}" = {
  source = repoWallpapersOnly;
  recursive = true;
  };
  systemd.user.services.mpvpaper-wallpaper = {
  Unit = {
  Description = "Video wallpaper (mpvpaper)";
  After = [ "graphical-session.target" ];
  PartOf = [ "graphical-session.target" ];
  };
  Service = {
  Type = "simple";
  # -p auto-pause saves resources when the wallpaper surface is hidden.
  # '*' applies to all outputs.
  # Stretch-to-fill (cover) behavior:
  # --panscan=1.0 fills the entire output by cropping (no letterboxing).
  # If you literally want distortion-stretch (ignore aspect ratio), use --keepaspect=no instead.
  ExecStart = ''
    ${pkgs.mpvpaper}/bin/mpvpaper \
    -p \
    -o "no-audio --loop-file=inf --no-terminal --really-quiet --panscan=1.0 --keepaspect=yes" \
    '*' "${userVideoPath}"
  '';
  Restart = "on-failure";
  RestartSec = 1;
  };
  Install = {
  WantedBy = [ "graphical-session.target" ];
  };
  };
}

Rotating Wallpaper

rotating_wallpaper.nix installs wpaperd and deploys your wallpaper files from the repo (./assets/conf/desktop/wallpaper/pictures/) into ~/conf/desktop/wallpaper/pictures. It also deploys the default wallpaper configuration from assets/conf/desktop/wallpaper/wallpaper.conf into ~/conf/desktop/wallpaper/wallpaper.conf, which is the file you can edit as a user override. Finally, it creates a systemd user service (wpaperd.service) that automatically starts wpaperd at login and keeps it running, using your override config so wallpapers rotate according to your settings.

#+begin_src nix :tangle home/desktop/wallpaper.nix :noweb tangle :mkdirp yes
{ config, pkgs, lib, flakeRoot, ... }:
let
  repoWallpaperDir  = flakeRoot + "/assets/conf/desktop/wallpaper";
  repoWallpaperConf = flakeRoot + "/assets/conf/desktop/wallpaper/wallpaper.conf";
  userRelRoot  = "nixos_conf/wallpaperstuff";
  userAbsRoot  = "${config.home.homeDirectory}/${userRelRoot}";
  userConfPath = "${userAbsRoot}/wallpaper.conf";
  # Exclude wallpaper.conf so HM does NOT manage it (avoids backup collisions)
  repoWallpapersOnly = lib.cleanSourceWith {
  src = repoWallpaperDir;
  filter = path: type:
  (builtins.baseNameOf path) != "wallpaper.conf";
  };
in
{
  home.packages = [ pkgs.wpaperd ];
  # Sync everything *except* wallpaper.conf into ~/nixos_conf/wallpaperstuff
  home.file."${userRelRoot}" = {
  source = repoWallpapersOnly;
  recursive = true;
  };
  # Now safely overwrite the config every activation (no HM collision)
  home.activation.wallpaperConfForce =
  lib.hm.dag.entryAfter [ "writeBoundary" ] ''
  set -euo pipefail
  mkdir -p "${userAbsRoot}"
  install -m 0644 "${repoWallpaperConf}" "${userConfPath}"
  '';
  systemd.user.services.wpaperd = {
  Unit = {
  Description = "wpaperd wallpaper daemon";
  After = [ "default.target" ];
  };
  Service = {
  Type = "simple";
  ExecStart = "${pkgs.wpaperd}/bin/wpaperd --config ${userConfPath}";
  Restart = "on-failure";
  RestartSec = 1;
  };
  Install.WantedBy = [ "default.target" ];
  ;

Workspace Wallpaper

{ config, lib, pkgs, flakeRoot, ... }:
let
  # Where your numbered wallpapers live (1.*, 2.*, ... 9.*)
  userRelRoot = "nixos_conf/wallpaperstuff";
  userAbsRoot = "${config.home.homeDirectory}/${userRelRoot}";
  picturesDir = "${userAbsRoot}/pictures";
  # (Optional) still sync your repo wallpapers/scripts into ~/nixos_conf/wallpaperstuff
  repoWallpaperDir = flakeRoot + "/assets/conf/desktop/wallpaper";
  repoWallpapersOnly = lib.cleanSourceWith {
  src = repoWallpaperDir;
  filter = path: type: true;
  };
  daemonRel = "hypr/scripts/hyprpaper-ws-daemon.sh";
  setRel  = "hypr/scripts/set-wallpaper.sh";
in
{
  home.packages = [
  pkgs.hyprpaper
  pkgs.socat
  pkgs.jq
  pkgs.findutils
  pkgs.coreutils
  pkgs.gnused
  pkgs.gawk
  ];
  # Keep your existing "sync wallpapers into a writable dir" pattern
  home.file."${userRelRoot}" = {
  source = repoWallpapersOnly;
  recursive = true;
  };
  # Hyprpaper config (hyprpaper reads this; it does NOT need to write it)
  # `ipc = true` enables `hyprctl hyprpaper ...` commands. :contentReference[oaicite:0]{index=0}
  xdg.configFile."hypr/hyprpaper.conf".text = lib.mkForce ''
  ipc = true
  splash = false
  '';
  xdg.configFile."hypr/hyprpaper.conf".backup = lib.mkForce false;
  # Workspace wallpaper daemon: listens to socket2, applies w-<id>=... mapping
  # Uses workspacev2 to get numeric workspace id. :contentReference[oaicite:1]{index=1}
  xdg.configFile."${daemonRel}" = {
  executable = true;
  text = lib.mkForce ''
  #!/usr/bin/env bash
  set -euo pipefail
  : "''${XDG_RUNTIME_DIR:?XDG_RUNTIME_DIR not set}"
  : "''${HYPRLAND_INSTANCE_SIGNATURE:?HYPRLAND_INSTANCE_SIGNATURE not set}"
  SOCK="''${XDG_RUNTIME_DIR}/hypr/''${HYPRLAND_INSTANCE_SIGNATURE}/.socket2.sock"
  [[ -S "$SOCK" ]] || { echo "Hyprland socket not found: $SOCK" >&2; exit 1; }
  PICTURES_DIR="''${1:-${picturesDir}}"
  FIT_MODE="fill" # hyprpaper fit_mode: contain|cover|tile|fill :contentReference[oaicite:2]{index=2}
  HYPR_DIR="''${XDG_CONFIG_HOME:-$HOME/.config}/hypr"
  MAP_ROOT="''${HYPR_DIR}/hyprpaper/config"
  focused_monitor() {
    hyprctl -j monitors | jq -r '.[] | select(.focused==true) | .name' | head -n 1
  }
  map_file_for_monitor() {
    local mon="''${1}"
    echo "''${MAP_ROOT}/''${mon}/defaults.conf"
  }
  ensure_map_file() {
    local mon="''${1}"
    local f
    f="$(map_file_for_monitor "''${mon}")"
    mkdir -p "$(dirname "''${f}")"
    if [[ ! -f "''${f}" ]]; then
    # Seed with 1..9 from picturesDir if present, else empty entries
    {
    for i in 1 2 3 4 5 6 7 8 9; do
      seed="$(ls -1 "''${PICTURES_DIR}/''${i}."* 2>/dev/null | head -n 1 || true)"
      echo "w-''${i}= ''${seed}"
    done
    } > "''${f}"
    fi
    echo "''${f}"
  }
  get_wall_for_ws() {
    local mon="''${1}"
    local wsid="''${2}"
    local f key val
    f="$(ensure_map_file "''${mon}")"
    key="w-''${wsid}"
    # accept "w-1=/path" or "w-1= /path"
    val="$(awk -F= -v k="''${key}" '$1==k {sub(/^[[:space:]]+/, "", $2); print $2; exit}' "''${f}" || true)"
    echo "''${val}"
  }
  apply_wallpaper() {
    local mon="''${1}"
    local wsid="''${2}"
    local file
    file="$(get_wall_for_ws "''${mon}" "''${wsid}")"
    [[ -n "''${file}" ]] || return 0
    [[ -f "''${file}" ]] || return 0
    # Apply via IPC
    # hyprpaper “wallpaper { monitor path fit_mode }” model is per monitor. :contentReference[oaicite:3]{index=3}
    hyprctl hyprpaper wallpaper "''${mon}, ''${file}, ''${FIT_MODE}" >/dev/null
  }
  # Initial apply on startup
  mon="$(focused_monitor || true)"
  wsid="$(hyprctl -j activeworkspace | jq -r '.id' | head -n 1 || true)"
  [[ -n "''${mon}" && -n "''${wsid}" ]] && apply_wallpaper "''${mon}" "''${wsid}"
  handle() {
    local line="''${1}"
    case "''${line}" in
    workspacev2* )
    # workspacev2>>ID,NAME  :contentReference[oaicite:4]{index=4}
    local payload wsid
    payload="''${line#*>>}"
    wsid="''${payload%%,*}"
    mon="$(focused_monitor || true)"
    [[ -n "''${mon}" && -n "''${wsid}" ]] && apply_wallpaper "''${mon}" "''${wsid}"
    ;;
    focusedmon* )
    # focusedmon>>MON,WORKSPACENAME :contentReference[oaicite:5]{index=5}
    # When monitor focus changes, re-apply for the active workspace id.
    mon="$(focused_monitor || true)"
    wsid="$(hyprctl -j activeworkspace | jq -r '.id' | head -n 1 || true)"
    [[ -n "''${mon}" && -n "''${wsid}" ]] && apply_wallpaper "''${mon}" "''${wsid}"
    ;;
    esac
  }
  socat -U - UNIX-CONNECT:"''${SOCK}" | while read -r line; do
    handle "''${line}" || true
  done
  '';
  backup = lib.mkForce false;
  };

  # CLI setter in the style of your inspiration script.
  # Usage: set-wallpaper.sh <workspace_id> <monitor> [wallpaper]
  xdg.configFile."${setRel}" = {
  executable = true;
  text = lib.mkForce ''
  #!/usr/bin/env bash
  set -euo pipefail

  HYPR_DIR="''${XDG_CONFIG_HOME:-$HOME/.config}/hypr"
  MAP_ROOT="''${HYPR_DIR}/hyprpaper/config"

  usage() {
    echo "Usage: set-wallpaper.sh <workspace_id> <monitor> [wallpaper_path]"
  }

  wsid="''${1:-}"
  mon="''${2:-}"
  wp="''${3:-}"

  [[ -n "''${wsid}" ]] || { usage; exit 1; }
  [[ -n "''${mon}"  ]] || { usage; exit 1; }

  cfg="''${MAP_ROOT}/''${mon}/defaults.conf"
  mkdir -p "$(dirname "''${cfg}")"
  [[ -f "''${cfg}" ]] || touch "''${cfg}"

  if [[ -z "''${wp}" ]]; then
    # Random pick from your defaults folder if you want; adjust path if needed:
    wp="$(find "$HOME/.config/wallpapers/defaults" -type f 2>/dev/null | shuf -n 1 || true)"
    [[ -n "''${wp}" ]] || { echo "No wallpaper found (random). Provide a path as arg 3."; exit 1; }
  fi

  # Ensure key exists; if not, append it
  key="w-''${wsid}"
  if ! grep -q "^''${key}=" "''${cfg}"; then
    echo "''${key}=" >> "''${cfg}"
  fi

  # Set mapping
  ${pkgs.gnused}/bin/sed -i "s|^''${key}=.*|''${key}= ''${wp}|g" "''${cfg}"

  # If this monitor is currently showing that workspace id, apply immediately
  curws="$(hyprctl -j monitors | jq -r --arg m "''${mon}" '.[] | select(.name==$m) | .activeWorkspace.id' | head -n 1 || true)"
  if [[ "''${curws}" == "''${wsid}" ]]; then
    hyprctl hyprpaper wallpaper "''${mon}, ''${wp}, fill" >/dev/null
  fi
  '';
  backup = lib.mkForce false;
  };

  # Services
  systemd.user.services.hyprpaper = {
  Unit = {
  Description = "hyprpaper wallpaper daemon";
  PartOf = [ "graphical-session.target" ];
  After  = [ "graphical-session.target" ];
  };
  Service = {
  ExecStart = "${pkgs.hyprpaper}/bin/hyprpaper";
  Restart = "on-failure";
  RestartSec = 1;
  };
  Install = { WantedBy = [ "graphical-session.target" ]; };
  };

  systemd.user.services.hyprpaper-ws-daemon = {
  Unit = {
  Description = "Workspace->wallpaper mapping daemon (hyprpaper + socket2)";
  PartOf = [ "graphical-session.target" ];
  After  = [ "graphical-session.target" "hyprpaper.service" ];
  };
  Service = {
  ExecStart = "${pkgs.bash}/bin/bash ${config.xdg.configHome}/${daemonRel} ${picturesDir}";
  Restart = "on-failure";
  RestartSec = 1;
  };
  Install = { WantedBy = [ "graphical-session.target" ]; };
  };
}

Waybar

./.github/images/waybar.png

Mostly styling and enabling modules in the top bar.

{ config, lib, pkgs, flakeRoot, ... }:
let
  repoWaybarDir = flakeRoot + "/assets/conf/desktop/waybar";
in
{
  programs.waybar.enable = true;
  # Ensure config matches repo (HM-managed symlink, not user-editable)
  xdg.configFile."waybar/config" = {
  source = lib.mkForce repoWaybarDir + "/config.jsonc";
  backup = lib.mkForce false;
  force = true;
  };
  # Override HM's internally-generated waybar-style.css derivation
  # and use your repo file instead.
  xdg.configFile."waybar/style.css" = {
  source = lib.mkForce (repoWaybarDir + "/style.css");
  backup = lib.mkForce false;
  force = true;
  };
  # Prevent HM from also trying to generate style content via programs.waybar.style
  # (not strictly required once mkForce is in place, but keeps intent clear)
  programs.waybar.style = "";
}

Lock Screen

The lock screen configured using hyprlock. I use hypridle to detect idle time and use wlogout to show a logout menu. They are configured below.

{config, lib, pkgs, flakeRoot, ... }:
let
  lockPngSrc = flakeRoot + "/assets/lock.png";
  hyprlockConf = flakeRoot + "/assets/conf/desktop/hypr/hyprlock.conf";
in
{
  home.packages = [ pkgs.hyprlock ];
  xdg.configFile."hypr/lock.png".source = lib.mkForce lockPngSrc;
  xdg.configFile."hypr/lock.png".backup = lib.mkForce  false;
  xdg.configFile."hypr/hyprlock.conf".source = lib.mkForce hyprlockConf;
  xdg.configFile."hypr/hyprlock.conf".backup = lib.mkForce  false;
}

Idle Screen

<henro: needs instruction>

{ config, lib, pkgs, flakeRoot, ... }:
let
  hypridleConf = flakeRoot + "/assets/conf/desktop/hypr/hypridle.conf";
in
{
  home.packages = [ pkgs.hypridle ];
  xdg.configFile."hypr/hypridle.conf".source = lib.mkForce hypridleConf;
  xdg.configFile."hypr/hypridle.conf".backup = lib.mkForce false ;
}

hyprscrolling

This Nix module integrates the hyprscrolling plugin into a Home-Manager managed Hyprland setup in a declarative and reproducible way. It ensures the plugin is installed, optionally switches Hyprland to the scrolling layout, and renders user-defined plugin settings directly into the Hyprland configuration. The goal is to manage the scrolling workspace behavior entirely from Nix instead of maintaining manual edits inside hyprland.conf.

{ config, lib, pkgs, flakeRoot,...}:
let
  # Hyprscrolling drop-in config (repo -> ~/.config)
  repoConf = flakeRoot + "/assets/conf/desktop/hypr/hyprscrolling.conf";
  targetRel = "hypr/conf.d/90-hyprscrolling.conf";
  # Overflow indicator script (repo -> ~/.config)
  repoOverflowScript = flakeRoot + "/assets/conf/desktop/hypr/scripts/hyprscroll-overflow.sh";
  targetOverflowRel = "hypr/scripts/hyprscroll-overflow.sh";
  # Adapt columnsize to monitor
  repoPerMonitorScript = flakeRoot + "/assets/conf/desktop/hypr/scripts/hyprscrolling-per-monitor.sh";
  targetPerMonitor = "hypr/scripts/hyprscrolling-per-monitor.sh";
  # Facilitate switching between scrolling and dwindle
  repoSwitchScript =
  flakeRoot + "/assets/conf/desktop/hypr/scripts/toggle-layout-scrolling-dwindle.sh";
  targetSwitchScript = "hypr/scripts/toggle-layout-scrolling-dwindle.sh";
in
{
  # Ensure deps for the script exist at runtime
  # (hyprctl comes with Hyprland; jq is often not installed by default)
  home.packages = with pkgs; [
  jq
  ];
  wayland.windowManager.hyprland = {
  enable = true;
  plugins = [
  pkgs.hyprlandPlugins.hyprscrolling
  ];
  extraConfig = ''
  source = ~/.config/${targetRel}
  '';
  };
  # Copy repo configs/scripts into ~/.config
  xdg.configFile."${targetRel}" = {
    source = lib.mkForce repoConf;
    backup = lib.mkForce false;
  };
  xdg.configFile."${targetOverflowRel}" = {
  source = lib.mkForce repoOverflowScript;
  backup = lib.mkForce false;
  executable = true; # makes it chmod +x
  };
  xdg.configFile."${targetPerMonitor}" = {
  source = lib.mkForce repoPerMonitorScript;
  backup = lib.mkForce false;
  executable = true; # makes it chmod +x
  };
  xdg.configFile."${targetSwitchScript}" = {
  source = lib.mkForce repoSwitchScript;
  backup = lib.mkForce false;
  executable = true; # makes it chmod +x
  };
}

Hyprshell

For nice task-starting and -switching

# home/desktop/hyprshell.nix  (Home-Manager module)
{ config, pkgs, lib, flakeRoot, ... }:
let
  repoDir = flakeRoot + "/assets/conf/desktop/hypr/hyprshell";
  cfgRon  = repoDir + "/config.ron";
  cssFile = repoDir + "/styles.css";
in
{
  xdg.enable = true;
  home.packages = [ pkgs.hyprshell ];
  # Link repo -> ~/.config/hyprshell/...
  xdg.configFile."hyprshell/config.ron".source = lib.mkForce cfgRon;
  xdg.configFile."hyprshell/config.ron".backup = lib.mkForce false;
  xdg.configFile."hyprshell/styles.css".source = lib.mkForce cssFile;
  xdg.configFile."hyprshell/styles.css".backup = lib.mkForce false;
  # Autostart (systemd user service)
  systemd.user.services.hyprshell = {
  Unit = {
  Description = "Hyprshell (window switcher / launcher)";
  PartOf = [ "graphical-session.target" ];
  After  = [ "graphical-session.target" ];
  };
  Service = {
  ExecStart = "${pkgs.hyprshell}/bin/hyprshell";
  Restart = "on-failure";
  RestartSec = 1;
  };
  Install = {
  WantedBy = [ "graphical-session.target" ];
  };
  };
}

Hyprland

This configures the desktop environment along with the peripherals. The comments should explain whats happening.

{ config, lib, pkgs, flakeRoot, ... }:
let
  hyprConf   = flakeRoot + "/assets/conf/desktop/hypr/hyprland.conf";
  bindingsConf = flakeRoot + "/assets/conf/desktop/hypr/bindings.conf";
in
{
  wayland.windowManager.hyprland = {
  enable = true;
  # Load base config + bindings from repo files
  extraConfig =
  (builtins.readFile hyprConf)
  + "\n\n# --- Repo keybindings ---\n"
  + (builtins.readFile bindingsConf)
  + "\n";
  settings = {
    windowrule = [
    "match:class nm-connection-editor, float 1, center 1, size 900 700"
    ];
  };

  };
  xdg.configFile."hypr/scripts/lid-lock.sh" = {
  source = lib.mkForce flakeRoot + "/assets/conf/desktop/hypr/scripts/lid-lock.sh";
  backup = lib.mkForce false;
  executable = true;
  };
  xdg.portal = {
  enable = true;
  extraPortals = with pkgs; [
  xdg-desktop-portal-gtk
  xdg-desktop-portal-hyprland
  ];
  # GTK als algemene backend (OpenURI is daar betrouwbaar)
  config.common.default = [ "gtk" ];
  # Hyprland alleen voor screensharing / remote desktop
  config.hyprland = {
  "org.freedesktop.impl.portal.Screencast" = [ "hyprland" ];
  "org.freedesktop.impl.portal.RemoteDesktop" = [ "hyprland" ];
  };
  };
}

Walker

This is how I launch applications. It is bound to Win+Space in the ./asstes/conf/desktop/hypr/bindings.conf.

{ config, pkgs, lib, flakeRoot, inputs ? null, ... }:
let
  walkerPkg =
  if inputs != null && inputs ? walker
  then inputs.walker.packages.${pkgs.system}.default
  else pkgs.walker;
  elephantPkg =
  if inputs != null && inputs ? elephant
  then inputs.elephant.packages.${pkgs.system}.default
  else pkgs.elephant;
  sessionTarget = "graphical-session.target";
  # All theme files now live here
  repoThemesDir = flakeRoot + "/assets/conf/desktop/walker";
in
{
  xdg.enable = true;
  home.packages = [ walkerPkg elephantPkg ];
  # ~/.config/walker/themes/*
  xdg.configFile."walker/themes/frosted/default.css".source = lib.mkForce repoThemesDir + "/themes/frosted/default.css";
  xdg.configFile."walker/themes/frosted/default.css".backup = lib.mkForce false;
  xdg.configFile."walker/themes/frosted/style.css".source  = repoThemesDir + "/themes/frosted/style.css";
  xdg.configFile."walker/themes/frosted/style.css".backup = lib.mkForce false;
  xdg.configFile."walker/config.toml".source  = repoThemesDir + "/config.toml";
  xdg.configFile."walker/config.toml".backup = lib.mkForce false;
  # xdg.configFile."walker/themes/default.html".source = lib.mkForce repoThemesDir + "/default.html";
  # xdg.configFile."walker/themes/default.html".backup = lib.mkForce false;
  # (services unchanged)
  systemd.user.services.elephant = { /* ... your existing service ... */ };
  systemd.user.services.walker = { /* ... your existing service ... */ };
}

Theme

I use the Catppuccin almost everywhere. The nix module integrates almost automatically everywhere (except gtk). You'll notice the color values in multiple places outside this as well.

  { pkgs, ...}:
  {
  gtk = {
  enable = true;
  colorScheme = "dark";
  theme = {
    name = "Catppuccin-GTK-Grey-Dark-Compact";
    package = (pkgs.magnetic-catppuccin-gtk.override {
    accent = [ "grey" ];
    shade = "dark";
    tweaks = [ "black" ];
    size = "compact";
    });
  };
  iconTheme.name = "Papirus-Dark";
  };
  catppuccin.enable = true;
  catppuccin.flavor = "mocha";
  catppuccin.accent = "blue";
  catppuccin.gtk.icon.enable = true;
  catppuccin.cursors.enable = true;
  }

Default-apps

This is where you can set defaults

{ config, pkgs, lib, ... }:
{
  xdg.mimeApps.enable = true;
  xdg.mimeApps.defaultApplications = {
  "x-scheme-handler/http"  = [ "app.zen_browser.zen.desktop" ];
  "x-scheme-handler/https" = [ "app.zen_browser.zen.desktop" ];
  "text/html"      = [ "app.zen_browser.zen.desktop" ];
  };
}

Hyperexpo

hyprexpo gets installed and configured

{ config, lib, pkgs, ... }:
{
  wayland.windowManager.hyprland = {
  # Load the Hyprexpo plugin (from nixpkgs)
  plugins = [
  pkgs.hyprlandPlugins.hyprexpo
  ];

  # Append plugin config + keybind after your existing hyprland.conf
  extraConfig = lib.mkAfter ''
  ############################
  # Hyprexpo (workspace/window overview)
  ############################

  # Basic plugin config (tweak as you like)
  plugin {
    hyprexpo {
    columns = 3
    gaps_in = 5
    gaps_out = 20

    # Optional; comment out if you don't want it
    # workspace_method = center current
    }
  }
  '';
  };
}

Alacritty

Alacritty gets installed and configured

{ config, pkgs, lib, flakeRoot, ... }:
let
  repoAlacrittyConf = flakeRoot + "/assets/conf/dev/alacritty.toml";
in
{
  xdg.enable = true;
  programs.alacritty.enable = true;
  # Override the config generated by programs.alacritty
  xdg.configFile."alacritty/alacritty.toml".source = lib.mkForce repoAlacrittyConf;
  xdg.configFile."alacritty/alacritty.toml".backup = lib.mkForce false
  catppuccin.alacritty.enable = true;
  catppuccin.alacritty.flavor = "mocha";
}

Dev Tools

All the miscellaneous dev tools on this computer.

{ config, pkgs, lib, ... }:
{
  programs = {
  vscode.enable = true;
  vim.enable = true;
  ripgrep.enable = true;
  btop.enable = true;
  fzf = {
  enable = true;
  enableZshIntegration = true;
  enableBashIntegration = true;
  };
  zoxide = {
  enable = true;
  enableZshIntegration = true;
  enableBashIntegration = true;
  };
  eza = {
  enable = true;
  enableZshIntegration = true;
  enableBashIntegration = true;
  };
  direnv = {
  enable = true;
  enableZshIntegration = true;
  enableBashIntegration = true;
  nix-direnv.enable = true;
  };
  # Zsh-specific config belongs here
  zsh = {
 # for emacs-eat package
  initContent = lib.mkOrder 1200 ''
    [ -n "$EAT_SHELL_INTEGRATION_DIR" ] && \
    source "$EAT_SHELL_INTEGRATION_DIR/zsh"
  '';
  };
  git = {
  enable = true;
  lfs.enable = true;
  };
  };
}

Kitty

Kitty gets installed and configured

{ config, pkgs, lib, flakeRoot, ... }:
let
  catppuccinMochaConf =
  builtins.readFile (flakeRoot + "/assets/conf/dev/terminal/Catppuccin-Mocha.conf");
  # Your own keymaps / other settings (but we will NOT rely on it for opacity)
  repoKittyConfText =
  builtins.readFile (flakeRoot + "/assets/conf/dev/terminal/kitty.conf");
in
{
  xdg.enable = true;
  # Stable theme file so kitty.conf can include it without /nix/store paths
  xdg.configFile."kitty/themes/Catppuccin-Mocha.conf".text =  lib.mkForce catppuccinMochaConf;
  xdg.configFile."kitty/themes/Catppuccin-Mocha.conf".backup =  lib.mkForce false;
  programs.kitty = {
  enable = true;
  # Home Manager generates ~/.config/kitty/kitty.conf; we append in-order:
  # 1) include theme
  # 2) your repo config (keymaps etc.)
  # 3) force opacity LAST so it always wins
  extraConfig = ''
  # 1) Theme first (stable path)
  include themes/Catppuccin-Mocha.conf
  # 2) Your repo config (may also include theme; harmless if duplicated)
  ${repoKittyConfText}
  # 3) Force transparency last (wins)
  #background_opacity 0.60
  #dynamic_background_opacity yes
  '';
  };
}

Shells

The aliases mentioned in ./assets/conf/dev/terminal/shells.conf will be added to enabled shells

# shells.nix — Home-Manager module
#
# Reads:
# ${flakeRoot}/assets/conf/shells.nixdev/terminal/enabled_shells.conf
# ${flakeRoot}/assets/conf/dev/terminal/aliases.conf
#
# For each enabled shell in [enabled_shells]:
# - installs/enables shell (where HM has an enable option)
# - if ${flakeRoot}/assets/conf/dev/terminal/<shell>.conf exists, sources it
# - ensures a *user-editable* aliases file exists in the shells default location
# - if a shell is disabled, its aliases file is removed
# .
# Notes on “editable”:
# - We do NOT manage the aliases file with xdg.configFile/home.file (those would be overwritten).
# - Instead, we create/remove files via home.activation (create only if missing).
{ config, pkgs, lib, flakeRoot, ... }:
let
  terminalDir = flakeRoot + "/assets/conf/dev/terminal";
  enabledFile = terminalDir + "/enabled_shells.conf";
  aliasesFile = terminalDir + "/aliases.conf";
  trim = lib.strings.trim;
  # ---------- minimal INI-ish parser (sections + raw lines) ----------
  readMaybe = p: if builtins.pathExists p then builtins.readFile p else "";
  normalizeLine = l: trim (lib.replaceStrings [ "\r" ] [ "" ] l);
  parseSections = text:
  let
  lines = map normalizeLine (lib.splitString "\n" text);
    isHeader = l:
    let s = l;
    in lib.hasPrefix "[" s
    && lib.hasSuffix "]" s
    && builtins.stringLength s >= 3;
  nameOf = l: lib.removeSuffix "]" (lib.removePrefix "[" l);
  folded =
    builtins.foldl'
    (st: l:
    if l == "" then st else
    if isHeader l then st // { current = nameOf l; }
    else
      let
      cur = st.current;
      prev = st.sections.${cur} or [];
      in
      st // { sections = st.sections // { ${cur} = prev ++ [ l ]; }; }
    )
    { current = "__root__"; sections = {}; }
    lines;
  in
  folded.sections;
  enabledSections = parseSections (readMaybe enabledFile);
  aliasSections = parseSections (readMaybe aliasesFile);
  # [enabled_shells] lines: key = yes/no
  enabledShells =
  let
  raw = enabledSections.enabled_shells or [];
  parseKV = l:
    let m = builtins.match ''^([A-Za-z0-9_-]+)[[:space:]]*=[[:space:]]*(.*)$'' l;
    in if m == null then null else {
    k = trim (builtins.elemAt m 0);
    v = lib.toLower (trim (builtins.elemAt m 1));
    };
  kvs = builtins.filter (x: x != null) (map parseKV raw);
  in
  map (x: x.k) (builtins.filter (x: x.v == "yes" || x.v == "true" || x.v == "1") kvs);
  shellEnabled = shell: builtins.elem shell enabledShells;
  # ---------- per-shell repo config file (<shell>.conf) ----------
  shellConfPath = shell: terminalDir + "/${shell}.conf";
  shellConfExists = shell: builtins.pathExists (shellConfPath shell);
  sourceIfExistsSh = p: ''
  if [ -f "${toString p}" ]; then
  source "${toString p}"
  fi
  '';
 # ---------- aliases section helpers ----------
  secLines = name: aliasSections.${name} or [];
  secText  = name: lib.concatStringsSep "\n" (secLines name);
  # Default alias-file locations
  bashAliasesPath = "${config.home.homeDirectory}/.bash_aliases";
  zshAliasesPath  = "${config.home.homeDirectory}/.zsh_aliases";
  fishAliasesPath = "${config.xdg.configHome}/fish/conf.d/aliases.fish";
  # Seeds (created once; user can edit afterwards)
  bashSeed = ''
  # Created once from: ${toString aliasesFile}
  # Edit freely; Home Manager will not overwrite this file.
  #
  ${secText "bash_zsh"}
  ${secText "bash_specific"}
  '';
  zshSeed = ''
  # Created once from: ${toString aliasesFile}
  # Edit freely; Home Manager will not overwrite this file.
  ${secText "bash_zsh"}
  ${secText "zsh_specific"}
  '';
  # Fish: translate [bash_zsh] POSIX alias lines + append [fish_specific] as-is
  parsePosixAlias = l:
  let
  m = builtins.match ''^[[:space:]]*alias[[:space:]]+([A-Za-z0-9_+-]+)=(.*)$'' l;
  in
  if m == null then null else
    let
    name = trim (builtins.elemAt m 0);
    rhs0 = trim (builtins.elemAt m 1);
    unquote =
    if lib.hasPrefix "'" rhs0 && lib.hasSuffix "'" rhs0 then
      lib.removeSuffix "'" (lib.removePrefix "'" rhs0)
    else if lib.hasPrefix "\"" rhs0 && lib.hasSuffix "\"" rhs0 then
      lib.removeSuffix "\"" (lib.removePrefix "\"" rhs0)
    else
      rhs0;
    in
    { inherit name; cmd = unquote; };
  escapeForFish = s:
  lib.replaceStrings
  [ "\\"  "\""  "$" "`" ]
  [ "\\\\" "\\\"" "\\$" "\\`" ]
  s;
  fishTranslated =
  let
  parsed = builtins.filter (x: x != null) (map parsePosixAlias (secLines "bash_zsh"));
  in
  lib.concatStringsSep "\n" (map (a: ''alias ${a.name} "${escapeForFish a.cmd}"'') parsed);
  fishSeed = ''
  # Created once from: ${toString aliasesFile}
  # Edit freely; Home Manager will not overwrite this file.
  status is-interactive; or exit
  # Translated from [bash_zsh]:
  ${fishTranslated}
  # From [fish_specific]:
  ${secText "fish_specific"}
  '';
in
{
  xdg.enable = true;
  # Install/enable shells (no login-shell changes)
  programs.bash.enable = shellEnabled "bash";
  programs.zsh.enable  = shellEnabled "zsh";
  programs.fish.enable = shellEnabled "fish";
  home.packages =
  (lib.optionals (shellEnabled "dash")  [ pkgs.dash ]) ++
  (lib.optionals (shellEnabled "nushell") [ pkgs.nushell ]);
  # Source per-shell repo config (if present) AND source the user alias file (if it exists).
  # Important: define each option only ONCE.
  programs.bash.bashrcExtra = lib.mkIf (shellEnabled "bash") (lib.mkAfter ''
  ${lib.optionalString (shellConfExists "bash") (sourceIfExistsSh (shellConfPath "bash"))}
  if [ -f "${bashAliasesPath}" ]; then
  source "${bashAliasesPath}"
  fi
  '');
  programs.zsh.initContent = lib.mkIf (shellEnabled "zsh") (lib.mkAfter ''
  ${lib.optionalString (shellConfExists "zsh") (sourceIfExistsSh (shellConfPath "zsh"))}
  if [ -f "${zshAliasesPath}" ]; then
  source "${zshAliasesPath}"
  fi
  '');
  programs.fish.interactiveShellInit = lib.mkIf (shellEnabled "fish") (lib.mkAfter ''
  ${lib.optionalString (shellConfExists "fish") ''
  if test -f "${toString (shellConfPath "fish")}"
    source "${toString (shellConfPath "fish")}"
  end
  ''}
  if test -f "${fishAliasesPath}"
  source "${fishAliasesPath}"
  end
  '');
# Create/remove alias files based on enabled shells
home.activation.shellAliasesFiles = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
  set -euo pipefail
  # bash -------------------------------------------------------
  if ${if shellEnabled "bash" then "true" else "false"}; then
  cat > "${bashAliasesPath}" <<'EOF'
${bashSeed}
EOF
  else
  rm -f "${bashAliasesPath}"
  fi
  # zsh -------------------------------------------------------
 if ${if shellEnabled "zsh" then "true" else "false"}; then
  cat > "${zshAliasesPath}" <<'EOF'
${zshSeed}
EOF
  else
  rm -f "${zshAliasesPath}"
  fi
  # fish -------------------------------------------------------
  if ${if shellEnabled "fish" then "true" else "false"}; then
  mkdir -p "$(dirname "${fishAliasesPath}")"
  cat > "${fishAliasesPath}" <<'EOF'
${fishSeed}
EOF
  else
  rm -f "${fishAliasesPath}"
  fi
  # fish
  if ${if shellEnabled "fish" then "true" else "false"}; then
  mkdir -p "$(dirname "${fishAliasesPath}")"
  if [ ! -f "${fishAliasesPath}" ]; then
    cat > "${fishAliasesPath}" <<'EOF'
${fishSeed}
EOF
  fi
  else
  rm -f "${fishAliasesPath}"
  fi
  '';
}

Zsh

Zsh gets installed and configured

{  config,  pkgs,  lib, flakeRoot,  ... }:
{
  programs.zsh = {
  enable = true;
  enableCompletion = true;
  autocd = true;
  # Optional but recommended: keep zsh config in one dir (relative to $HOME)
  dotDir = ".config/zsh";
  oh-my-zsh = {
  enable = true;
  theme = "";
  plugins = [
    "git"
    "sudo"
    "extract"
    "colored-man-pages"
    "command-not-found"
    "history"
    "docker"
    "kubectl"
  ];
  };
  autosuggestion.enable = true;
  syntaxHighlighting.enable = true;
  };
}

Starship

The configuration mentioned in ./assets/conf/dev/terminal/starship.toml will be added to enabled shells

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

let
  repoStarshipToml = flakeRoot + "/assets/conf/dev/terminal/starship.toml";

  # The exact key that appears in the error:
  targetKey = "${config.home.homeDirectory}/.config/starship.toml";
in
{
  xdg.enable = true;

  programs.starship = {
  enable = true;
  enableZshIntegration  = true;
  enableBashIntegration = true;
  enableFishIntegration = true;
  };

  # Force the *actual conflicting option* (home.file."<abs path>".source)
  home.file."${targetKey}".source = lib.mkForce repoStarshipToml;
}

Other Settings

Some repeated info from the configuration.

Home User

  home.username = "${user.username}";
  home.homeDirectory = pkgs.lib.mkDefault "/home/${user.username}";
  home.stateVersion = user.stateVersion;

Emacs

I practically live inside emacs. The configuration for it is a mix between init.el and the nix configuration. Nix allows me to install emacs packages as part of the configuration which is most of the following file. I install the nix community provided emacs overlay that lets me have the latest emacs with pgtk ui (for wayland). Comments describe the emacs package and what it does.

  { pkgs, ... }:
  {
  programs.emacs = {
  enable = true;
  # install with tree sitter enabled
  package = (pkgs.emacs-pgtk.override { withTreeSitter = true; });
  extraPackages = epkgs: [
    # also install all tree sitter grammars
    epkgs.manualPackages.treesit-grammars.with-all-grammars
    epkgs.nerd-icons     # nerd fonts support
    epkgs.doom-modeline    # model line
    epkgs.diminish       # hides modes from modeline
    epkgs.eldoc        # doc support
    epkgs.pulsar       # pulses the cursor when jumping about
    epkgs.which-key      # help porcelain
    epkgs.expreg       # expand region
    epkgs.vundo        # undo tree
    epkgs.puni       # structured editing
    epkgs.avy        # jumping utility
    epkgs.consult      # emacs right click
    epkgs.vertico      # minibuffer completion
    epkgs.marginalia     # annotations for completions
    epkgs.crux       # utilities
    epkgs.magit        # git porcelain
    epkgs.nerd-icons-corfu   # nerd icons for completion
    epkgs.corfu        # completion
    epkgs.cape       # completion extensions
    epkgs.orderless      # search paradigm
    epkgs.yasnippet      # snippets support
    epkgs.yasnippet-snippets   # commonly used snippets
    epkgs.rg         # ripgrep
    epkgs.exec-path-from-shell   # load env and path
    epkgs.eat        # better shell
    epkgs.rust-mode      # rust mode (when rust-ts doesn't cut it)
    epkgs.rustic       # more rust things
    epkgs.nix-mode       # nix lang
    epkgs.hcl-mode       # hashicorp file mode
    epkgs.shell-pop      # quick shell popup
    epkgs.envrc        # support for loading .envrc
    epkgs.nixpkgs-fmt      # format nix files
    epkgs.f        # string + file utilities
    epkgs.gptel        # llm chat (mainly claude)
    epkgs.catppuccin-theme   # catppuccin theme
    epkgs.eldoc-box      # docs in a box
    epkgs.sideline       # mainly for flymake errors on the side
    epkgs.sideline-flymake   # mainly for flymake errors on the side
    epkgs.sideline-eglot     # mainly for flymake errors on the side
  ];
  };
  home.sessionVariables = {
  EDITOR = "emacs";
  XDG_SCREENSHOTS_DIR = "~/screenshots";
  };
  home.file = {
  emacs-init = {
    source = ./early-init.el;
    target = ".emacs.d/early-init.el";
  };
  emacs = {
    source = ./init.el;
    target = ".emacs.d/init.el";
  };
  };
  services.nextcloud-client = {
  enable = true;
  };
  }

Early Initialization

There are some emacs settings that can be configured before the gui shows up. And some of them help increase performance and let the gui show up that much faster. These are listed here.

  ;;; package --- early init -*- lexical-binding: t -*-
  ;;; Commentary:
  ;;; Prevents white flash and better Emacs defaults
  ;;; Code:
  (set-language-environment "UTF-8")
  (setq-default
 default-frame-alist
 '((background-color . "#1e1e2e")
   (bottom-divider-width . 1)    ; Thin horizontal window divider
   (foreground-color . "#bac2de")    ; Default foreground color
   (fullscreen . maximized)      ; Maximize the window by default
   (horizontal-scroll-bars . nil)    ; No horizontal scroll-bars
   (left-fringe . 8)       ; Thin left fringe
   (menu-bar-lines . 0)      ; No menu bar
   (right-divider-width . 1)     ; Thin vertical window divider
   (right-fringe . 8)        ; Thin right fringe
   (tool-bar-lines . 0)      ; No tool bar
   (undecorated . t)       ; Remove extraneous X decorations
   (vertical-scroll-bars . nil))   ; No vertical scroll-bars
 user-full-name "Henrov henrov"      ; ME!
 ;; memory configuration
 ;; Higher garbage collection threshold, prevents frequent gc locks, reset later
 gc-cons-threshold most-positive-fixnum
 ;; Ignore warnings for (obsolete) elisp compilations
 byte-compile-warnings '(not obsolete)
 ;; And other log types completely
 warning-suppress-log-types '((comp) (bytecomp))
 ;; Large files are okay in the new millenium.
 large-file-warning-threshold 100000000
 ;; dont show garbage collection messages at startup, will reset later
 garbage-collection-messages nil
 ;; native compilation
 package-native-compile t
 native-comp-warning-on-missing-source nil
 native-comp-async-report-warnings-errors 'silent
 ;; Read more based on system pipe capacity
 read-process-output-max (max (* 10240 10240) read-process-output-max)
 ;; scroll configuration
 scroll-margin 0           ; Lets scroll to the end of the margin
 scroll-conservatively 100000        ; Never recenter the window
 scroll-preserve-screen-position 1     ; Scrolling back and forth
 ;; frame config
 ;; Improve emacs startup time by not resizing to adjust for custom settings
 frame-inhibit-implied-resize t
 ;; Dont resize based on character height / width but to exact pixels
 frame-resize-pixelwise t
 ;; backups & files
 backup-directory-alist '(("." . "~/.backups/")) ; Don't clutter
 backup-by-copying t           ; Don't clobber symlinks
 create-lockfiles nil          ; Don't have temp files
 delete-old-versions t         ; Cleanup automatically
 kept-new-versions 6           ; Update every few times
 kept-old-versions 2           ; And cleanup even more
 version-control t           ; Version them backups
 delete-by-moving-to-trash t       ; Dont delete, send to trash instead
 ;; startup
 inhibit-startup-screen t        ; I have already done the tutorial. Twice
 inhibit-startup-message t         ; I know I am ready
 inhibit-startup-echo-area-message t     ; Yep, still know it
 initial-scratch-message nil       ; I know it is the scratch buffer!
 initial-buffer-choice nil
 inhibit-startup-buffer-menu t
 inhibit-x-resources t
 initial-major-mode 'fundamental-mode
 pgtk-wait-for-event-timeout 0.001     ; faster child frames
 ad-redefinition-action 'accept      ; dont care about legacy things being redefined
 inhibit-compacting-font-caches t
 ;; tabs
 tab-width 4             ; Always tab 4 spaces.
 indent-tabs-mode nil          ; Never use actual tabs.
 ;; rendering
 cursor-in-non-selected-windows nil      ; dont render cursors other windows
 ;; packages
 use-package-always-defer t
 load-prefer-newer t
 default-input-method nil
 use-dialog-box nil
 use-file-dialog nil
 use-package-expand-minimally t
 package-enable-at-startup nil
 use-package-enable-imenu-support t
 auto-mode-case-fold nil  ; No second pass of case-insensitive search over auto-mode-alist.
 package-archives '(("melpa"    . "https://melpa.org/packages/")
         ("gnu"    . "https://elpa.gnu.org/packages/")
         ("nongnu"   . "https://elpa.nongnu.org/nongnu/")
         ("melpa-stable" . "https://stable.melpa.org/packages/"))
 package-archive-priorities '(("gnu"  . 99)
             ("nongnu" . 80)
             ("melpa"  . 70)
             ("melpa-stable" . 50))
 )
 ;;; early-init.el ends here

Initialization

Now starts the main emacs configuration.

  ;;; package --- Summary - My minimal Emacs init file -*- lexical-binding: t -*-

  ;;; Commentary:
  ;;; Simple Emacs setup I carry everywhere

  ;;; Code:
  (setq custom-file (locate-user-emacs-file "custom.el"))
  (load custom-file 'noerror)    ;; no error on missing custom file

  (require 'package)
  (package-initialize)

  (defun reset-custom-vars ()
  "Resets the custom variables that were set to crazy numbers"
  (setopt gc-cons-threshold (* 1024 1024 100))
  (setopt garbage-collection-messages t))

  (use-package emacs
  :custom
  (native-comp-async-query-on-exit t)
  (read-answer-short t)
  (use-short-answers t)
  (enable-recursive-minibuffers t)
  (which-func-update-delay 1.0)
  (visible-bell nil)
  (custom-buffer-done-kill t)
  (whitespace-line-column nil)
  (x-underline-at-descent-line t)
  (imenu-auto-rescan t)
  (uniquify-buffer-name-style 'forward)
  (confirm-nonexistent-file-or-buffer nil)
  (create-lockfiles nil)
  (make-backup-files nil)
  (kill-do-not-save-duplicates t)
  (sentence-end-double-space nil)
  (treesit-enabled-modes t)
  :init
  ;; base visual
  (menu-bar-mode -1)       ;; no menu bar
  (toggle-scroll-bar -1)     ;; no scroll bar
  (tool-bar-mode -1)       ;; no tool bar either
  (blink-cursor-mode -1)     ;; stop blinking

  ;; font of the century
  (set-frame-font "Aporetic Sans Mono 12" nil t)

  :bind
  (("C-<wheel-up>" . pixel-scroll-precision) ; dont zoom in please, just scroll
   ("C-<wheel-down>" . pixel-scroll-precision) ; dont zoom in either, just scroll
   ("C-x k"    . kill-current-buffer)) ; kill the buffer, dont ask
  :hook
  (text-mode . delete-trailing-whitespace-mode)
  (prog-mode . delete-trailing-whitespace-mode)
  (after-init . global-display-line-numbers-mode) ;; always show line numbers
  (after-init . column-number-mode)     ;; column number in the mode line
  (after-init . size-indication-mode)     ;; file size in the mode line
  (after-init . pixel-scroll-precision-mode)  ;; smooth mouse scroll
  (after-init . electric-pair-mode)     ;; i mean ... parens should auto create
  (after-init . reset-custom-vars)
  )

  (use-package autorevert
  :ensure nil
  :custom
  (auto-revert-interval 3)
  (auto-revert-remote-files nil)
  (auto-revert-use-notify t)
  (auto-revert-avoid-polling nil)
  (auto-revert-verbose t)
  :hook
  (after-init . global-auto-revert-mode))

  (use-package recentf
  :ensure nil
  :commands (recentf-mode recentf-cleanup)
  :hook
  (after-init . recentf-mode)
  :custom
  (recentf-auto-cleanup 'never)
  (recentf-exclude
   (list "\\.tar$" "\\.tbz2$" "\\.tbz$" "\\.tgz$" "\\.bz2$"
     "\\.bz$" "\\.gz$" "\\.gzip$" "\\.xz$" "\\.zip$"
     "\\.7z$" "\\.rar$"
     "COMMIT_EDITMSG\\'"
     "\\.\\(?:gz\\|gif\\|svg\\|png\\|jpe?g\\|bmp\\|xpm\\)$"
     "-autoloads\\.el$" "autoload\\.el$"))

  :config
  ;; A cleanup depth of -90 ensures that `recentf-cleanup' runs before
  ;; `recentf-save-list', allowing stale entries to be removed before the list
  ;; is saved by `recentf-save-list', which is automatically added to
  ;; `kill-emacs-hook' by `recentf-mode'.
  (add-hook 'kill-emacs-hook #'recentf-cleanup -90))

  (use-package savehist
  :ensure nil
  :commands (savehist-mode savehist-save)
  :hook
  (after-init . savehist-mode)
  :custom
  (savehist-autosave-interval 600)
  (savehist-additional-variables
   '(kill-ring        ; clipboard
   register-alist       ; macros
   mark-ring global-mark-ring   ; marks
   search-ring regexp-search-ring)))

  (use-package hl-line
  :ensure nil
  :custom
  (hl-line-sticky-flag nil)
  (global-hl-line-sticky-flag nil)
  :hook
  (after-init . global-hl-line-mode))

  (use-package saveplace
  :ensure nil
  :commands (save-place-mode save-place-local-mode)
  :hook
  (after-init . save-place-mode)
  :custom
  (save-place-limit 400))

  (use-package nerd-icons
  :custom
  ;; disable bright icon colors
  (nerd-icons-color-icons nil))hells.nix

  (use-package doom-modeline
  :custom
  (inhibit-compacting-font-caches t)  ;; speed
  (doom-modeline-buffer-file-name-style 'relative-from-project)
  (doom-modeline-major-mode-icon nil) ;; distracting icons, no thank you
  (doom-modeline-buffer-encoding nil) ;; everything is utf-8 anyway
  (doom-modeline-buffer-state-icon nil) ;; the filename already shows me
  (doom-modeline-lsp nil)     ;; lsp state is too distracting, too often
  :hook (after-init . doom-modeline-mode))

  (load-theme 'catppuccin :no-confirm)

  (use-package diminish :demand t)   ;; declutter the modeline
  (use-package eldoc
  :diminish eldoc-mode
  :custom
  (eldoc-echo-area-use-multiline-p nil)) ;; docs for everything

  (use-package eldoc-box
  :defer t
  :config
  (set-face-background 'eldoc-box-border (catppuccin-color 'green))
  (set-face-background 'eldoc-box-body (catppuccin-color 'base))
  :bind
  (("M-h" . eldoc-box-help-at-point)))

  (use-package pulsar
  :commands pulsar-global-mode pulsar-recenter-top pulsar-reveal-entry
  :init
  (defface pulsar-catppuccin
  `((default :extend t)
    (((class color) (min-colors 88) (background light))
   :background ,(catppuccin-color 'sapphire))
    (((class color) (min-colors 88) (background dark))
   :background ,(catppuccin-color 'sapphire))
    (t :inverse-video t))
  "Alternative nord face for `pulsar-face'."
  :group 'pulsar-faces)
  :custom
  (pulsar-face 'pulsar-catppuccin)
  :hook
  (after-init . pulsar-global-mode))

  (use-package which-key
  :commands which-key-mode
  :diminish which-key-mode
  :hook
  (after-init . which-key-mode))

  (use-package expreg
  :bind ("M-m" . expreg-expand))

  (use-package vundo) ;; undo tree

  ;; better structured editing
  (use-package puni
  :commands puni-global-mode
  :hook
  (after-init . puni-global-mode))

  (use-package avy
  :bind
  ("M-i" . avy-goto-char-2)
  :custom
  (avy-background t))

  (use-package consult
  :bind
  ("C-x b" . consult-buffer)   ;; orig. switch-to-buffer
  ("M-y"   . consult-yank-pop) ;; orig. yank-pop
  ("M-g M-g" . consult-goto-line)  ;; orig. goto-line
  ("M-g i" . consult-imenu)  ;; consult version is interactive
  ("M-g r" . consult-ripgrep)  ;; find in project also works
  :custom
  (consult-narrow-key "<"))

  (use-package vertico
  :commands vertico-mode
  :custom
  (read-file-name-completion-ignore-case t)
  (read-buffer-completion-ignore-case t)
  (completion-ignore-case t)
  (enable-recursive-minibuffers t)
  (minibuffer-prompt-properties '(read-only t cursor-intangible t face minibuffer-prompt))
  :init
  (vertico-mode)
  :hook
  (minibuffer-setup-hook . cursor-intangible-mode))

  (use-package marginalia
  :commands marginalia-mode
  :hook (after-init . marginalia-mode))

  (use-package crux
  :bind
  ("C-c M-e" . crux-find-user-init-file)
  ("C-c C-w" . crux-transpose-windows)
  ("C-c M-d" . crux-find-current-directory-dir-locals-file)
  ("C-a"   . crux-move-beginning-of-line))

  (use-package magit
  :bind (("C-M-g" . magit-status)))

  (use-package nerd-icons-corfu
  :commands nerd-icons-corfu-formatter
  :defines corfu-margin-formatters)

  (use-package corfu
  :commands global-corfu-mode
  :custom
  (corfu-cycle t)
  (corfu-auto t)
  (corfu-auto-delay  1)
  (corfu-auto-prefix 3)
  (corfu-separator ?_)
  :hook
  (after-init . global-corfu-mode)
  :config
  (add-to-list 'corfu-margin-formatters #'nerd-icons-corfu-formatter))

  (use-package cape)

  (use-package orderless
  :custom
  (completion-styles '(orderless partial-completion basic))
  (completion-category-defaults nil)
  (completion-category-overrides nil))

  (use-package yasnippet
  :commands yas-global-mode
  :diminish yas-minor-mode
  :hook
  (after-init . yas-global-mode))

  (use-package yasnippet-snippets :after yasnippet)

  (use-package exec-path-from-shell
  :commands exec-path-from-shell-initialize
  :custom
  (exec-path-from-shell-arguments nil)
  :hook
  (after-init . exec-path-from-shell-initialize))

  (use-package nixpkgs-fmt
  :custom
  (nixpkgs-fmt-command "nixfmt"))

  (use-package eat
  :bind
  (("C-c e p" . eat-project)
   ("C-c e t" . eat)))

  (use-package f :demand t)

  (use-package envrc
  :commands envrc-global-mode
  :hook
  (after-init . envrc-global-mode))

  (use-package gptel
  :commands gptel-make-anthropic f-read-text
  :config
  (gptel-make-anthropic "Claude"
  :stream t :key (f-read-text "/run/secrets/claude_key")))

  (use-package sideline-flymake)
  (use-package sideline-eglot)
  (use-package sideline
  :custom
  (sideline-backends-right '(sideline-flymake sideline-eglot))
  :hook
  (eglot-managed-mode . sideline-mode)
  (flymake-mode . sideline-mode))

  (use-package eglot
  :custom
  (eglot-extend-to-xref t)
  (eglot-ignored-server-capabilities '(:inlayHintProvider))
  (jsonrpc-event-hook nil)
  :hook
  (eglot-managed-mode . eldoc-box-hover-mode)
  (before-save . eldoc-format-buffer)
  :bind
  (:map eglot-mode-map
    ("C-c l a" . eglot-code-actions)
    ("C-c l r" . eglot-rename)
    ("C-c l h" . eldoc)
    ("C-c l g" . xref-find-references)
    ("C-c l w" . eglot-reconnect)))

  (use-package proced
  :custom
  (proced-auto-update-flag t)
  (proced-auto-update-interval 3)
  (proced-enable-color-flag t)
  (proced-show-remote-processes t))

  (use-package org
  :ensure t
  :defer t
  :commands (org-mode org-capture org-agenda)
  :init
  (defvar org-journal-file "~/nextcloud/org/journal.org")
  (defvar org-archive-file "~/nextcloud/org/archive.org")
  (defvar org-notes-file "~/nextcloud/org/notes.org")
  (defvar org-inbox-file "~/nextcloud/org/inbox.org")
  (defvar org-work-file "~/nextcloud/org/work.org")
  (defun my/org-capture-project-target-heading ()
  "Determine Org target headings from the current file's project path.

  This function assumes a directory structure like '~/projects/COMPANY/PROJECT/'.
  It extracts 'COMPANY' and 'PROJECT' to use as nested headlines
  for an Org capture template.

  If the current buffer is not visi
  ting a file within such a
  project structure, it returns nil, causing capture to default to
  the top of the file."
  (when-let* ((path (buffer-file-name))) ; Ensure we are in a file-visiting buffer
    (let ((path-parts (split-string path "/" t " ")))
    (when-let* ((projects-pos (cl-position "projects" path-parts :test #'string=))
        (company  (nth (+ 1 projects-pos) path-parts))
        (project  (nth (+ 2 projects-pos) path-parts)))
    ;; Return a list of headlines for Org to find or create.
    (list company project)))))
  :bind
  (("C-c c" . org-capture)
   ("C-c i" . org-store-link)
   ("C-c a" . org-agenda)
   :map org-mode-map
   ("C-c t" . org-toggle-inline-images)
   ("C-c l" . org-toggle-link-display))
  :custom
  (org-agenda-files (list org-inbox-file org-journal-file))
  (org-directory "~/nextcloud/org")
  (org-default-notes-file org-inbox-file)
  (org-archive-location (concat org-archive-file "::* From %s"))
  (org-log-done 'time)
  (org-log-into-drawer t)
  (org-hide-emphasis-markers t)
  (org-src-fontify-natively t)
  (org-src-tab-acts-natively t)
  (org-capture-templates '(("t" "Todo" entry (file org-inbox-file)
          "* TODO %?\n:PROPERTIES:\n:CREATED: %U\n:END:\n\n%a\n\n)")
           ("j" "Journal" entry (file+olp+datetree org-journal-file)
          "* %?\n:PROPERTIES:\n:CREATED: %U\n:END:\n\n%a\n\n")
           ("n" "Note" entry (file org-notes-file)
          "* %?\n:PROPERTIES:\n:CREATED: %U\n:END:\n\n%a\n\n")
           ("p" "Project Task" item
          (file+function org-work-file my/org-capture-project-target-heading)
          "* TODO %? \n  CLOCK: %U"
          ))
         )
  :config
  ;; Enable syntax highlighting in code blocks
  (add-hook 'org-mode-hook 'turn-on-font-lock)
  (add-hook 'org-mode-hook 'org-indent-mode))

  ;; extras
  (use-package comp-run
  :ensure nil
  :config
  (push "tramp-loaddefs.el.gz" native-comp-jit-compilation-deny-list)
  (push "cl-loaddefs.el.gz" native-comp-jit-compilation-deny-list))

  (use-package rustic
  :custom
  (rustic-lsp-client 'eglot))

  (provide 'init)

  ;;; init.el ends here

Machines

Only a few more things left. Specifically the machine level extra settings.

Traveldroid

The configuration for the laptop does not change much. Most changes are because the hardware is different.

System Level

Nothing specific for the laptop.

{ user, ... } : {
  imports =
  [
  ./hardware-configuration.nix
  ../../configuration
  ];
}

Hardware

This is the most different. Mostly taken from hardware-configuration.nix setup at first install.

  {
  hostname,
  pkgs,
  lib,
  modulesPath,
  user,
  config,
  ...
  }:
  {
  imports = [
  (modulesPath + "/installer/scan/not-detected.nix")
  ../../hardware/hardware.nix
  ];

  boot.initrd.availableKernelModules = [ "xhci_pci" "nvme" "usb_storage" "sd_mod" "rtsx_usb_sdmmc" ];
  boot.initrd.kernelModules = [ ];
  boot.kernelModules = [ "kvm-intel" ];
  boot.extraModulePackages = [ ];

  fileSystems."/" =
  { device = "/dev/disk/by-uuid/69433a14-fbaf-401b-af85-cd1bbf02b4e2";
  fsType = "ext4";
  };

  fileSystems."/boot" =
  { device = "/dev/disk/by-uuid/811D-0676";
  fsType = "vfat";
  options = [ "fmask=0077" "dmask=0077" ];
  };

  swapDevices =
  [ { device = "/dev/disk/by-uuid/b6c557c2-7682-460b-a5e7-8f6f2f429a3a"; }
  ];

  nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
  hardware.cpu.intel.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware;

  }

Home

This is mostly about configuring the monitor. And laptop specific utilities.

{ pkgs, ... }:
{
  imports = [
  ../../home
  ];
  home.packages = with pkgs; [
  brightnessctl
  ];
  wayland.windowManager.hyprland = {
  extraConfig = ''
  # Default portable monitor rule
  monitor=DP-1,3840x1080@144,1920x0,1
  '';
  };
}

README Utils

Headers

This script adds a DO NOT MODIFY header to all the generated nix files.

  (progn
  (defun add-tangle-headers ()
  (message "running in %s" (buffer-file-name))
  (when (string= (file-name-extension (buffer-file-name)) "nix")
    (goto-char (point-min))
    (insert "# WARNING : This file was generated by README.org\n# DO NOT MODIFY THIS FILE!\n# Any changes made here will be overwritten.\n")
    (save-buffer))
  (save-buffer))
  (add-hook 'org-babel-post-tangle-hook 'add-tangle-headers))