Introduction
Hello! Welcome to my literate file that describes (and generates) the configuration for all my computers.
For people who are confused, this file has a bunch of source blocks that are then put into individual files using emacs+org’s tangle mode.
Once the files are generated, they are converted into an immutable system configuration using nixos.
This repository consists of configuration for my main machine
smallbox– my wfh computer and main laptophomelab– personal homelab server for experiments etc
To generate the actual nix files, you need to open this file in emacs and then execute M-x org-babel-tangle.
Or run the following from the command line
emacs README.org --batch -f org-babel-tangle
Once the nix files are ready, you can deploy using
nixos-rebuild switch --flake .#<machine>To update the homelab machine
nix run github:serokell/deploy-rs -- .#homelabOther files in this repo are :-
flake.lockso as to keep my versions intact. More on that later.assets/*contains images like the wallpaper that cannot be part of this.secrets/secrets.yamlcontains encrypted keys and is edited usingsops.
Everything else in this repository is generated. They ideally shouldn’t be edited directly. There’s also a github action that generates and commits these files if they do differ.
Emacs + Org + Tangle
- Emacs is the text editor that I use. Some people might take offense at me calling it a text editor.
- Org mode is an inbuilt plugin for emacs that helps with managing org files like this one. Org files are similar to markdown but with superpowers.
- Tangle is an org mode option that lets us export snippets to other files. In this case, the configuration snippets you see are written to individual files.
- Anything that appears in
<<code-id>>is like a variable that gets filled in later. You will see them in the snippets below where they are filled in by other snippets later in the file.
- Anything that appears in
Nix & Nixos
- Nix is a bespoke programming language, used mainly to configure environments and dependencies.
- Nixos is a linux distro where you define your operating system and other things using nix.
The expectation is that the output of evaluating the nix files is a “configuration” that can be applied.
This way your operating system is always defined by configuration files. You almost never install anything.
Make a change to the configuration, reapply and repeat. You need vim? Add it to the config, and rebuild.
YourNixCode(Input) -> System ConfigurationI use nix flakes which means that the entry point for the nix evaluation is a file called
flake.nixwhich 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
experimentalflag, but it is considered the standard by most of the community. Flakes allow us to pin the input package versions using aflake.lockfile. This prevents unwanted and surprise updates when rebuilding without changing the configuration.
TLDR App List
| Window Manager | Niri |
| Desktop Shell | Noctalia |
| Login Greeter | tuigreet |
| Terminal Emulator | Alacritty |
| Shell | Zsh |
| Text Editor | Emacs |
| File Manager | Thunar |
| Fonts | Aporeti |
| Colors + Icons | Catppuccin |
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 = "nambiar"; stateVersion = "26.05"; locale = "sv_SE.UTF-8"; sshPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIPeSRZJzhHgam3p9UOuJfdRZSOWPZ991ksZIjqLl7WSo sandeep@wavefunk.io"; }
Flake Inputs
The inputs for my system’s configuration are very simple
- 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.
- home-manager - a nix module that helps keep track of user specific dotfiles and configurations as part of my nix config.
- emacs-overlay - this has more configuration options and generally a newer emacs available provided by the community.
- sops-nix - adds latest sops for secrets management.
- catppuccin - nix module that allows everything to be catppuccin themed.
- deploy-rs - tool that allows deploying to remote machines (like the homelab)
- noctalia - the desktop shell: bar, launcher, notifications, control center, lock screen and wallpapers in one.
- disko - manage the disk configuration declaratively.
- llm-agents - latest greatest AI tooling versions.
- niri - desktop window manager.
- noctalia - desktop shell that manages everything else. stays out of the way.
- kache - rust specific cache for cross worktree shared cache on the machine.
{ description = "Sandeep'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"; }; sops-nix = { url = "github:Mic92/sops-nix"; inputs.nixpkgs.follows = "nixpkgs"; }; catppuccin = { url = "github:catppuccin/nix"; inputs.nixpkgs.follows = "nixpkgs"; }; deploy-rs.url = "github:serokell/deploy-rs"; disko = { url = "github:nix-community/disko"; inputs.nixpkgs.follows = "nixpkgs"; }; llm-agents.url = "github:numtide/llm-agents.nix"; niri = { url = "github:sodiboo/niri-flake"; inputs.nixpkgs.follows = "nixpkgs"; }; noctalia.url = "github:noctalia-dev/noctalia/cachix"; noctalia-greeter = { url = "github:noctalia-dev/noctalia-greeter"; inputs.nixpkgs.follows = "nixpkgs"; }; kache.url = "github:kunobi-ninja/kache"; }; <<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 = { self, nixpkgs, home-manager, emacs-overlay, sops-nix, catppuccin, deploy-rs, disko, llm-agents, niri, noctalia, noctalia-greeter, kache, ... }@inputs: let user = import ./user.nix; lib = nixpkgs.lib; workstations = [ "smallbox" ]; pkgs = import nixpkgs { inherit (user) system; }; deployPkgs = import nixpkgs { inherit (user) system; overlays = [ deploy-rs.overlays.default (self: super: { deploy-rs = { inherit (pkgs) deploy-rs; lib = super.deploy-rs.lib; }; }) ]; }; in { nixosConfigurations = builtins.listToAttrs ( builtins.map (machine: { name = machine; value = lib.nixosSystem { modules = [ <<flake-emacs-module>> <<flake-kache-module>> <<flake-config-module>> <<flake-home-module>> niri.nixosModules.niri # niri-flake disko.nixosModules.disko # disks sops-nix.nixosModules.sops # sops catppuccin.nixosModules.catppuccin # theme kache.nixosModules.default # kache noctalia-greeter.nixosModules.default # login ]; specialArgs = { hostname = machine; inherit user llm-agents; }; }; }) workstations ) // { homelab = lib.nixosSystem { modules = [ ./machines/homelab/configuration.nix disko.nixosModules.disko sops-nix.nixosModules.sops ]; specialArgs = { inherit user; }; }; }; <<homelab-module>> devShells.${user.system}.default = pkgs.mkShell { buildInputs = with pkgs; [ nil # nix lsp server nixfmt # nix formatter sops # used to edit secrets ]; }; };
Lets look at the individual modules
- Emacs + Kache
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 ]; })
({ ... }: { nixpkgs.overlays = [ kache.overlays.default ]; })
- Then the workstation-specific configuration.
./machines/${machine}/configuration.nix - And finally the home-manager module.
This can be initialized and managed on its own but I’d rather use the
nixos-rebuildcommand 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; }; <<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
.backupand replace the file.home-manager.backupFileExtension = "backup";
- Finally I pull in the machine specific home configuration. Along with the overrides from catppuccin.
home-manager.users.${user.username} = { imports = [ ./machines/${machine}/home.nix catppuccin.homeModules.catppuccin noctalia.homeModules.default ]; };
- 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
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 # has the system configuration.
| +--home.nix # has the user level configuration.
| +--hardware-configuration.nix # has the unique hardware configuration.
nixos-rebuild switch --flake .#smallbox looks for the smallbox configuration (in /machines/smallbox) and returns the configuration that is specific to that computer.
- 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 :-
Editing secrets
The age key needs to exist. Then I can edit the secrets using :-
sops edit secrets/secrets.yaml
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 = [ ./boot.nix ./login.nix ./cli.nix ./files.nix ./locale.nix ./nix-settings.nix ./networking.nix ./niri.nix ./services.nix ./audio.nix ./steam.nix ./sops.nix ./ai.nix ]; <<config-system-packages>> <<config-ai>> <<config-user>> <<config-programs>> <<config-fonts>> # enable the catppuccin theme for everything with mocha + blue accents catppuccin.enable = true; catppuccin.autoEnable = true; catppuccin.flavor = "mocha"; catppuccin.accent = "blue"; system.stateVersion = user.stateVersion; }
Whoa. Thats a lot of imports. Lets go through them one by one.
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"]; # allow the configured primary user to use flake-provided binary caches trusted-users = [ user.username ]; # 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" "https://noctalia.cachix.org" ]; # trust the cache public key trusted-public-keys = [ "nix-community.cachix.org-1:mB9FSh9qf2dCimDSUo8Zy7bkq5CX+/rkCWyvRCYg3Fs=" "noctalia.cachix.org-1:pCOR47nnMEo5thcxNDtzWpOxNFQsBRglJzxWPp3dkU4=" ]; }; # allow proprietary software on this machine. I'm not a purist. nixpkgs.config.allowUnfree = true; # 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 = 5; efi.canTouchEfiVariables = true; # allow editing efi to edit the boot loader timeout = 5; # grub timeout to make a selection }; }; }
Login
{ programs.noctalia-greeter = { enable = true; settings = { session = { default = "Niri"; }; user = { default = "nambiar"; }; }; }; }
CLI
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; environment.shells = with pkgs; [ zsh ]; programs.zsh.enable = true; 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 uv ]; programs.thunar = { enable = true; plugins = with pkgs; [ thunar-archive-plugin thunar-media-tags-plugin thunar-volman ]; }; 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 Sweden and would like all my locale and timezone settings to match. Except my default locale.
{ user, ... } : let locale = user.locale; defaultLocale = "en_GB.UTF-8"; in { # Set your time zone. time.timeZone = "Europe/Stockholm"; # 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, ... } : { networking = { # allow automatic ip assignment when connecting to a network useDHCP = pkgs.lib.mkDefault true; networkmanager.enable = true; firewall.enable = true; # let wifi info be NOT declarative, allowing user to configure wifi. wireless.userControlled = true; }; }
Niri
This is a big one because the DE needs so much configuration. This section mostly installs Niri. The key bindings and monitor configuration are done in the home manager section.
{ config, lib, pkgs, ... }: { programs.niri = { enable = true; package = pkgs.niri; }; niri-flake.cache.enable = false; # Keep using the existing polkit-gnome user agent instead of niri-flake's KDE agent. systemd.user.services.niri-flake-polkit.enable = false; # Niri's default portal preference tries GNOME first. GNOME's FileChooser # backend delegates to a service that is not activatable in this session, so # route file dialogs directly to GTK while keeping the rest of the defaults. xdg.portal = { extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; config.niri = { default = [ "gnome" "gtk" ]; "org.freedesktop.impl.portal.Access" = [ "gtk" ]; "org.freedesktop.impl.portal.FileChooser" = [ "gtk" ]; "org.freedesktop.impl.portal.Notification" = [ "gtk" ]; "org.freedesktop.impl.portal.Secret" = [ "gnome-keyring" ]; }; }; environment.sessionVariables = { XDG_SESSION_TYPE = "wayland"; XDG_CURRENT_DESKTOP = "niri"; XDG_SESSION_DESKTOP = "niri"; NIXOS_OZONE_WL = "1"; ELECTRON_OZONE_PLATFORM_HINT = "auto"; XCURSOR_SIZE = "24"; GDK_SCALE = "1.5"; RUSTICL_ENABLE = "radeonsi"; # for davinci resolve }; security.polkit.enable = true; 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 upower.enable = true; # battery/power reporting, used by noctalia power-profiles-daemon.enable = true; # power profile switching from noctalia's control center # printer discovery avahi = { enable = true; nssmdns4 = true; openFirewall = true; }; }; virtualisation = { containers.enable = true; docker.enable = true; podman = { enable = true; defaultNetwork.settings.dns_enabled = true; # Required for containers under podman-compose to be able to talk to each other. }; }; users.users.${user.username}.extraGroups = [ "podman" "docker" ]; # add self to docker and podman user group }
Audio
This is still a work in progress, but it almost works. I enable all the audio related services, hoping everything is going to be okay.
{ pkgs, ...}: { environment.systemPackages = with pkgs; [ pamixer ]; services.pipewire = { enable = true; alsa.enable = true; alsa.support32Bit = true; pulse.enable = true; jack.enable = true; }; # pipewire needs realtime scheduling access security.rtkit.enable = true; }
Steam
Finally, I have steam installed and it requires a few extra things as well.
{ pkgs, ... } : { environment.systemPackages = with pkgs; [ steam-run # also used for random executables that expect fhs ]; programs.steam = { enable = true; # Open ports in the firewall for Steam Remote Play remotePlay.openFirewall = true; # Open ports in the firewall for Source Dedicated Server dedicatedServer.openFirewall = true; }; }
Sops
We use sops to manage secrets on this machine.
{ user, ...} : { sops.defaultSopsFile = ../secrets/secrets.yaml; sops.defaultSopsFormat = "yaml"; sops.age.keyFile = "/home/${user.username}/.config/sops/age/keys.txt"; }
Sops requires a public .sops.yaml that dictates how the secrets are encrypted.
This contains the public key and path to the secrets file.
keys: - &primary age1yq35g6mmlem0rhr47u6ewh8dctlwp9hj0s0ac60e4hrw9hjzlqms6crf7n creation_rules: - path_regex: secrets/secrets.yaml$ key_groups: - age: - *primary
AI + LLMs
The workstation is increasingly being automated with agents at this point. Adding it to the global packages makes it easier to launch in arbitrary locations.
{ pkgs, lib, config, user, llm-agents, ... } : { environment.systemPackages = (with llm-agents.packages.${pkgs.stdenv.hostPlatform.system}; [ claude-code pi agent-browser codex codegraph ]); nix.settings = { substituters = [ "https://cache.numtide.com" ]; trusted-public-keys = [ "niks3.numtide.com-1:DTx8wZduET09hRmMtKdQDxNNthLQETkc/yaX7M4qK0g=" ]; }; }
Miscellaneous Packages and Programs
environment.systemPackages = with pkgs; [ wget # fetch utility curl # more fetch utility binutils # executable utilities, like ld dmidecode # tool for dumping system info libnotify # notification daemon python3 # nice to have this ready for quick things cacert # certificate authority remmina # remote desktop app jaq # rust based jq alternative comma # nix-run helper tmux # seems useful for a bunch of things kache # rust cache element-desktop ]; # to enable svg icons in gtk apps # https://github.com/catppuccin/nix/issues/584 programs.gdk-pixbuf.modulePackages = [ pkgs.librsvg ]; # sharing folders to the tv over local network services.minidlna = { enable = true; openFirewall = true; settings = { media_dir = [ "V,/home/nambiar/videos" ]; friendly_name = "smallbox"; inotify = "yes"; }; }; users.users.minidlna = { extraGroups = [ "users" ]; }; # enabling the kache service for faster rust builds services.kache = { enable = true; daemon.enable = true; settings.cache = { local_max_size = "40GB"; }; };
programs = { nix-ld.enable = true; # helps with linking troubles with dynamic libraries appimage.enable = true; # allow appimage installations dconf.enable = true; # to save user settings gnupg.agent = { # pgp client enable = true; enableSSHSupport = true; }; firefox.enable = true; # browser wireshark.enable = true; # vpn };
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 = "Sandeep Nambiar"; 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.
{ pkgs, user, ... } : { imports = [ ./noctalia.nix ./lock.nix ./unity.nix ./niri.nix ./theme.nix ./terminal.nix ./dev.nix ./emacs ]; <<home-user>> <<home-packages>> programs.home-manager.enable = true; }
Oof! Again with all the imports! We can go through them one at a time!
Noctalia
Noctalia is the desktop shell. A single Wayland client provides the bar, the application launcher, the notification daemon, the control center, the session menu, the lock screen and the wallpaper.
The settings below are generated into a TOML file that noctalia validates at build time.
I stay close to the defaults, with the catppuccin palette to match the rest of the system.
The shell runs as a systemd user service (the method the v5 NixOS docs recommend): it is bound to graphical-session.target, restarts on crash and on config changes.
Niri talks to it over IPC (noctalia msg ...) — the keybindings are in the niri section below.
Per the docs, launch_apps_as_systemd_services keeps apps launched from noctalia alive across shell restarts.
{ config, ... }: { programs.noctalia = { enable = true; systemd.enable = true; settings = { shell = { font_family = "Aporetic Sans Mono"; launch_apps_as_systemd_services = true; }; bar = { margin_edge = 0; margin_ends = 0; radius = 0; }; theme = { mode = "dark"; source = "builtin"; builtin = "Catppuccin"; }; wallpaper.directory = "${config.home.homeDirectory}/wallpapers"; }; }; home.file = { background = { source = ../assets/background.png; target = "wallpapers/background.png"; }; me = { source = ../assets/me.jpg; target = ".me.jpg"; }; }; }
Lock Screen
The lock screen and the session (logout) menu are provided by noctalia, configured in the section above. I use swayidle for explicit session lock/sleep events. Idle timeouts are deliberately disabled so this machine never locks, turns monitors off, or sleeps unless I ask it to. They are configured below.
{ config, lib, pkgs, ... } : let lockCommand = "${lib.getExe config.programs.noctalia.package} msg session lock"; in { services = { swayidle = { enable = true; events = { lock = lockCommand; # noctalia's lock screen. before-sleep = "${pkgs.systemd}/bin/loginctl lock-session"; # lock before suspend. }; timeouts = []; }; }; }
Unity
I work with the Unity Game Engine and I have the unity hub installed globally, instead of in a project flake. Unity also ships an experimental standalone Unity CLI for installing editors, managing modules, and scripting Hub workflows from the terminal. Often I work with dotnet directly inside of unity and so we set that up too.
{ config, lib, pkgs, ... }: let unityCliBin = pkgs.stdenvNoCC.mkDerivation rec { pname = "unity-cli-bin"; version = "0.1.0-beta.7"; src = pkgs.fetchurl { url = "https://public-cdn.cloud.unity3d.com/hub/prod/cli/${version}/unity-linux-x64"; hash = "sha256-5BmKjJTP0PQ2tDSvjPBTm9Ktfvx380yDkrmfUKEOXhU="; }; dontUnpack = true; installPhase = '' runHook preInstall install -Dm755 $src $out/bin/unity-cli runHook postInstall ''; }; unityhubWithCli = let unityhub = pkgs.unityhub.override { extraPkgs = pkgs: with pkgs; [ dotnet-sdk_10 python314 unityCliBin ]; }; in unityhub.overrideAttrs (old: { installPhase = old.installPhase + '' makeWrapper ${unityhub.fhsEnv}/bin/unityhub-fhs-env $out/bin/unity \ --add-flags ${unityCliBin}/bin/unity-cli \ --argv0 unity ''; }); in { home.packages = [ unityhubWithCli ]; xdg.desktopEntries.unityhub = { name = "Unity Hub"; exec = "${unityhubWithCli}/bin/unityhub %U"; terminal = false; type = "Application"; icon = "unityhub"; settings.StartupWMClass = "unityhub"; comment = "The Official Unity Hub"; categories = [ "Development" ]; mimeType = [ "x-scheme-handler/unityhub" ]; }; xdg.mimeApps = { enable = true; defaultApplications."x-scheme-handler/unityhub" = "unityhub.desktop"; }; }
Niri
This configures the desktop environment along with the peripherals. The comments should explain whats happening.
{ config, lib, pkgs, ... }: let inherit (config.lib.niri) actions; noctalia = lib.getExe config.programs.noctalia.package; in { config = { # required for the default Niri config programs.kitty.enable = true; services.polkit-gnome.enable = true; programs.niri.settings = with actions; { input = { keyboard = { xkb = { layout = "us,se"; options = "ctrl:nocaps"; }; track-layout = "global"; }; touchpad.scroll-factor = 0.5; mouse = { accel-speed = -0.5; accel-profile = "adaptive"; scroll-factor = 0.5; }; focus-follows-mouse.enable = true; mod-key = "Super"; }; layout = { gaps = 4; default-column-width.proportion = 0.5; focus-ring = { enable = true; width = 2; active.gradient = { from = "#89b4fa"; to = "#a6e3a1"; angle = 45; }; inactive.color = "#1e1e2e"; }; border.enable = false; }; screenshot-path = "~/screenshots/Screenshot from %Y-%m-%d %H-%M-%S.png"; prefer-no-csd = true; xwayland-satellite.path = lib.getExe pkgs.xwayland-satellite; hotkey-overlay.skip-at-startup = true; window-rules = [ { geometry-corner-radius = { top-left = 5.0; top-right = 5.0; bottom-right = 5.0; bottom-left = 5.0; }; clip-to-geometry = true; } { matches = [ { app-id = "firefox$"; title = "^Picture-in-Picture$"; } ]; open-floating = true; } { # the noctalia settings window floats instead of tiling. matches = [ { app-id = "dev.noctalia.Noctalia.Settings"; } ]; open-floating = true; } { matches = [ { is-window-cast-target = true; } ]; focus-ring = { active.color = "#f38ba8"; inactive.color = "#7d0d2d"; }; } ]; binds = { "Mod+Return".action = spawn "ghostty" "+new-window"; "Mod+I".action = spawn "firefox"; "Mod+E".action = spawn "emacs"; "Mod+Shift+Q".action = close-window; "Mod+Shift+M".action = quit; "Mod+F".action = spawn "thunar"; "Mod+Shift+Space".action = toggle-window-floating; "Mod+Shift+K".action = switch-layout "next"; "Mod+M".action = maximize-window-to-edges; "Mod+Shift+F".action = fullscreen-window; # noctalia's application launcher, clipboard history and control center "Mod+Space".action = spawn noctalia "msg" "panel-toggle" "launcher"; "Mod+V".action = spawn noctalia "msg" "panel-toggle" "clipboard"; "Mod+N".action = spawn noctalia "msg" "panel-toggle" "control-center"; "Alt+Tab".action = focus-window-previous; "Mod+Shift+Left".action = move-column-left; "Mod+Shift+Right".action = move-column-right; "Mod+Shift+Up".action = move-window-up; "Mod+Shift+Down".action = move-window-down; "Mod+Ctrl+Left".action = move-workspace-to-monitor-left; "Mod+Ctrl+Right".action = move-workspace-to-monitor-right; "Mod+Left".action = focus-column-or-monitor-left; "Mod+A".action = focus-column-or-monitor-left; "Mod+Right".action = focus-column-or-monitor-right; "Mod+D".action = focus-column-or-monitor-right; "Mod+Up".action = focus-window-or-monitor-up; "Mod+W".action = focus-window-or-monitor-up; "Mod+Down".action = focus-window-or-monitor-down; "Mod+S".action = focus-window-or-monitor-down; "Mod+WheelScrollDown" = { cooldown-ms = 150; action = focus-workspace-down; }; "Mod+WheelScrollUp" = { cooldown-ms = 150; action = focus-workspace-up; }; "Mod+Shift+P".action = { screenshot-window = [ ]; }; "Mod+Shift+A".action = { screenshot = [ ]; }; "Mod+Alt+P".action = { screenshot-screen = [ ]; }; "Mod+Ctrl+P".action = { screenshot-screen = [ ]; }; "Mod+Shift+S".action = spawn noctalia "msg" "panel-toggle" "session"; "Mod+Alt+L" = { allow-when-locked = true; action = spawn noctalia "msg" "session" "lock"; }; "Mod+Ctrl+Shift+F".action = toggle-windowed-fullscreen; "Mod+Alt+C".action = set-dynamic-cast-window; "Mod+Shift+C".action = set-dynamic-cast-monitor; "Mod+Ctrl+C".action = clear-dynamic-cast-target; "Mod+1".action = focus-workspace "1"; "Mod+2".action = focus-workspace "2"; "Mod+3".action = focus-workspace "3"; "Mod+4".action = focus-workspace "4"; "Mod+5".action = focus-workspace "5"; "Mod+6".action = focus-workspace "6"; "Mod+7".action = focus-workspace "7"; "Mod+8".action = focus-workspace "8"; "Mod+9".action = focus-workspace "9"; "Mod+Shift+1".action = { move-window-to-workspace = "1"; }; "Mod+Shift+2".action = { move-window-to-workspace = "2"; }; "Mod+Shift+3".action = { move-window-to-workspace = "3"; }; "Mod+Shift+4".action = { move-window-to-workspace = "4"; }; "Mod+Shift+5".action = { move-window-to-workspace = "5"; }; "Mod+Shift+6".action = { move-window-to-workspace = "6"; }; "Mod+Shift+7".action = { move-window-to-workspace = "7"; }; "Mod+Shift+8".action = { move-window-to-workspace = "8"; }; "Mod+Shift+9".action = { move-window-to-workspace = "9"; }; "Mod+Escape" = { allow-inhibiting = false; action = toggle-keyboard-shortcuts-inhibit; }; }; }; }; }
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"; gtk4.extraConfig.gtk-interface-color-scheme = "dark"; }; catppuccin.enable = true; catppuccin.autoEnable = true; catppuccin.flavor = "mocha"; catppuccin.accent = "blue"; catppuccin.gtk.icon.enable = true; catppuccin.cursors.enable = true; catppuccin.cache.enable = true; }
Terminal
Alacritty is my terminal program. The snippet below configures how it looks.
{ programs = { ghostty = { enable = true; enableZshIntegration = true; systemd.enable = true; settings = { font-family = "Aporetic Sans Mono"; font-size = 12; command = "zsh"; keybind = [ "ctrl+x>3=new_split:right" "ctrl+x>2=new_split:down" "ctrl+x>o=goto_split:next" "ctrl+x>k=close_surface" ]; window-padding-x = 4; window-padding-y = 4; cursor-style = "block"; }; }; }; catppuccin.ghostty.enable = true; catppuccin.ghostty.flavor = "mocha"; }
Dev Tools
All the miscellaneous dev tools on this computer.
{ user, pkgs, ... }: { programs = { vscode.enable = true; # yes, sometimes i like to dabble ripgrep.enable = true; # fast text search across projects btop.enable = true; # even better task manager # fuzzy finder fzf = { enable = true; enableZshIntegration = true; enableBashIntegration = true; }; # better cd zoxide = { enable = true; enableZshIntegration = true; enableBashIntegration = true; }; # better ls eza = { enable = true; enableZshIntegration = true; enableBashIntegration = true; }; # this is mainly for integration with nix flakes in individual projects direnv = { enable = true; enableZshIntegration = true; enableBashIntegration = true; nix-direnv.enable = true; }; # zsh everywhere with oh-my-zsh zsh = { enable = true; oh-my-zsh = { enable = true; plugins = [ "git" ]; theme = "robbyrussell"; }; shellAliases = { cd = "z"; # zoxide jq = "jaq"; bd = "br"; # beads }; }; # git with lfs git = { lfs.enable = true; enable = true; }; }; }
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;
Home Packages
A bunch of programs that I use.
home.packages = with pkgs; [ audacity # audio recording zoom-us # meetings handbrake # video transcoding xdg-utils # utils, for screensharing vlc # media player discord # other chat slack # work chat pavucontrol # audio control spotify # music player simple-scan # scanner software pinta # image editor mpv # media player ]; programs.obs-studio.enable = true; # screen recording tool
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-git-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.ghostel # better shell epkgs.nix-mode # nix lang epkgs.hcl-mode # hashicorp file mode epkgs.shell-pop # quick shell popup epkgs.nixpkgs-fmt # format nix files epkgs.f # string + file utilities epkgs.gptel epkgs.agent-shell # acp-powered agent shell (uses claude-agent-acp) 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.ben # support for loading envrc, async ]; }; home.sessionVariables = { EDITOR = "emacs"; XDG_SCREENSHOTS_DIR = "~/screenshots"; # zoxide's doctor check false-positives in non-interactive shells # (agents, scripts): the chpwd hook never fires without a prompt _ZO_DOCTOR = "0"; }; 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 "Sandeep Nambiar" ; 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) (display-buffer-base-action '((display-buffer-reuse-window display-buffer-use-some-window))) :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 (add-to-list 'default-frame-alist '(font . "Aporetic Sans Mono-12")) :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)) (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 Catppuccin 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) :map project-prefix-map ("m" . magit-project-status)) :config (add-to-list 'project-switch-commands '(magit-project-status "Magit") t)) (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 ghostel :bind (("C-x m" . ghostel) :map ghostel-semi-char-mode-map ("C-s" . consult-line) ("M-<backspace>" . ghostel-backward-kill-word) ("M-p" . (lambda () (interactive) (ghostel-send-key "p" "ctrl"))) ("M-n" . (lambda () (interactive) (ghostel-send-key "n" "ctrl"))) :map project-prefix-map ("t" . ghostel-project) ("T" . ghostel-project-list-buffers)) :config (defun ghostel-send-C-k-and-kill () "Send `C-k' to ghostel. Like normal Emacs `C-k'. Kill to end of line and put content in kill-ring." (interactive) (kill-ring-save (point) (line-end-position)) (ghostel-send-key "k" "ctrl")) (add-to-list 'project-switch-commands '(ghostel-project "Ghostel") t) (add-to-list 'project-switch-commands '(ghostel-project-list-buffers "Ghostel buffers") t) (add-to-list 'ghostel-eval-cmds '("magit-status-setup-buffer" magit-status-setup-buffer))) (use-package ghostel-compile :hook (after-init . ghostel-compile-global-mode)) (use-package ghostel-comint :hook (after-init . ghostel-comint-global-mode)) (use-package f :demand t) (use-package ben :diminish ben-global-mode :bind (:map ben-mode-map ("C-c b" . ben-command-map)) :hook (after-init . ben-global-mode)) (use-package gptel) (use-package sideline-flymake) (use-package sideline :custom (sideline-backends-right '(sideline-flymake)) :hook (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 . (lambda () (add-hook 'before-save-hook #'eglot-format-buffer nil t))) :bind (:map eglot-mode-map ("C-c l a" . eglot-code-actions) ("C-c l b" . flymake-show-buffer-diagnostics) ("C-c l r" . eglot-rename) ("C-c l h" . eldoc) ("C-c l g" . xref-find-references) ("C-c l p" . flymake-show-project-diagnostics) ("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)) ;; 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 rust-ts-mode :ensure nil :mode "\\.rs\\'" :hook (rust-ts-mode . eglot-ensure)) (provide 'init) ;;; init.el ends here
Machines
Only a few more things left. Specifically the machine level extra settings.
Smallbox
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, config, ... } : { imports = [ ./hardware-configuration.nix ./network-configuration.nix ../../configuration ]; <<smallbox-secrets>> }
Wireguard
Both smallbox and homelab use wireguard to connect to the VPN. The shared peer public key and common settings (mtu, dns, keepalive) are extracted into mkWireguard so each machine only specifies what differs.
mkWireguard = { address, privateKeyFile, peerPublicKeyFile, presharedKeyFile, endpointFile, allowedIPs, listenPort ? null }: let ips = builtins.concatStringsSep "," allowedIPs; routes = builtins.concatStringsSep "\n" (map (ip: "ip route add ${ip} dev wg0 2>/dev/null || true") allowedIPs); in { inherit address privateKeyFile; mtu = 1300; dns = [ "1.1.1.1" "8.8.8.8" ]; postUp = '' wg set wg0 peer "$(cat ${peerPublicKeyFile})" \ preshared-key ${presharedKeyFile} \ endpoint "$(cat ${endpointFile})" \ allowed-ips ${ips} \ persistent-keepalive 15 ${routes} ''; preDown = '' wg set wg0 peer "$(cat ${peerPublicKeyFile})" remove 2>/dev/null || true ''; } // (if listenPort != null then { inherit listenPort; } else {});
Network Configuration
{ config, ... }: let <<wireguard-config>> in { networking.wg-quick.interfaces.wg0 = mkWireguard { address = [ "172.30.0.2/32" "2a01:4f9:c014:e0de:ac1e::2/128" ]; privateKeyFile = config.sops.secrets.wg_private_key.path; peerPublicKeyFile = config.sops.secrets.wg_peer_public_key.path; presharedKeyFile = config.sops.secrets.wg_preshared_key.path; endpointFile = config.sops.secrets.vpn_endpoint.path; allowedIPs = [ "172.30.0.1/32" "172.30.0.4/32" ]; }; networking.wg-quick.interfaces.wg1 = { address = [ "10.0.1.10/32" ]; privateKeyFile = config.sops.secrets.wg_wavefunk_private_key.path; peers = [ { publicKey = "a2wrqGLi/aw3mODD0Yal3V2ha6c6isN6HfDI1RLF83M="; endpoint = "vpn.wavefunk.io:51820"; allowedIPs = [ "10.0.1.0/24" ]; persistentKeepalive = 25; } ]; }; }
Hardware
This is the most different. Mostly taken from hardware-configuration.nix setup at first install. As you might notice, there are a lot more kernel modules. This device has thunderbolt for example.
{ hostname, pkgs, lib, modulesPath, user, ... }: { imports = [ (modulesPath + "/installer/scan/not-detected.nix") ../../hardware/hardware.nix ]; boot.initrd.availableKernelModules = [ "xhci_pci" # usb wake up (usb host controller) "thunderbolt" # :/ "nvme" # support for the nvme disk in here "usb_storage" # :/ "sd_mod" # hard drive controller ]; boot.kernelParams = [ "amd_pstate=active" "acpi.ec_no_wakeup=1" # Force use of the thinkpad_acpi driver for backlight control. # This allows the backlight save/load systemd service to work. "acpi_backlight=native" # Needed for touchpad to work properly (click doesn't register by pushing down the touchpad). "psmouse.synaptics_intertouch=0" ]; boot.kernelModules = [ "kvm-amd" ]; boot.loader.grub.efiSupport = true; boot.loader.grub.useOSProber = true; # detect windows since thats on a partition here boot.loader.grub.devices = [ "/dev/nvme0n1" ]; fileSystems."/" = { device = "/dev/disk/by-uuid/0bda9355-76f4-4b55-9012-0a14a73ac6b9"; fsType = "ext4"; }; boot.initrd.luks.devices."luks-f400c0ed-57e0-4b5a-b701-c1a10c19480f".device = "/dev/disk/by-uuid/f400c0ed-57e0-4b5a-b701-c1a10c19480f"; fileSystems."/boot" = { device = "/dev/disk/by-uuid/5A5A-DEFE"; fsType = "vfat"; options = [ "fmask=0077" "dmask=0077" ]; }; # external disk - manual mount so missing travel disks don't block shell startup fileSystems."/home/${user.username}/external" = { device = "/dev/disk/by-uuid/18818348-1ee4-4fa5-9984-e4e01b9fa304"; fsType = "ext4"; options = [ "noauto" "nofail" ]; }; swapDevices = []; hardware.graphics = { enable = lib.mkDefault true; enable32Bit = lib.mkDefault true; extraPackages = with pkgs; [ mesa.opencl # Enables Rusticl (OpenCL) support rocmPackages.clr.icd ]; }; hardware.amdgpu.initrd.enable = lib.mkDefault true; networking.hostName = hostname; # enalbe fingerprinting services services.fprintd.enable = true; }
Home
This is mostly about configuring the monitor and key bindings. And laptop specific utilities.
{ config, pkgs, ... } : let mainOutput = "PNP(AOC) Q3279WG5B 0x00000161"; laptopOutput = "eDP-1"; in { imports = [ ../../home ]; home.packages = with pkgs; [ brightnessctl davinci-resolve ]; programs.niri.settings = with config.lib.niri.actions; { outputs = { main = { name = mainOutput; scale = 1; position = { x = 0; y = 0; }; focus-at-startup = true; }; laptop = { name = laptopOutput; mode = { width = 2880; height = 1800; refresh = 60.000; }; scale = 1.5; position = { x = 2560; y = 0; }; }; }; workspaces = { "1".open-on-output = mainOutput; "2".open-on-output = mainOutput; "3".open-on-output = mainOutput; "4".open-on-output = mainOutput; "5".open-on-output = laptopOutput; "6".open-on-output = laptopOutput; }; spawn-at-startup = [ { command = [ "firefox" ]; } { command = [ "emacs" ]; } { command = [ "slack" ]; } ]; window-rules = [ { matches = [ { at-startup = true; app-id = "firefox$"; } ]; open-on-workspace = "1"; } { matches = [ { at-startup = true; app-id = "emacs"; } ]; open-on-workspace = "2"; } { matches = [ { at-startup = true; app-id = "Slack"; } { at-startup = true; app-id = "slack"; } ]; open-on-workspace = "5"; open-maximized = true; } ]; binds = { "XF86MonBrightnessUp" = { allow-when-locked = true; action = spawn "brightnessctl" "set" "5%+"; }; "XF86MonBrightnessDown" = { allow-when-locked = true; action = spawn "brightnessctl" "set" "5%-"; }; "XF86AudioRaiseVolume" = { allow-when-locked = true; action = spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%+"; }; "XF86AudioLowerVolume" = { allow-when-locked = true; action = spawn "wpctl" "set-volume" "@DEFAULT_AUDIO_SINK@" "5%-"; }; "XF86AudioMute" = { allow-when-locked = true; action = spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SINK@" "toggle"; }; "XF86AudioMicMute" = { allow-when-locked = true; action = spawn "wpctl" "set-mute" "@DEFAULT_AUDIO_SOURCE@" "toggle"; }; }; }; }
Secrets
Helper to reduce boilerplate for SSH key pairs managed by sops. Each call to mkSshKeyPair declares both the private and public key secrets with correct ownership and permissions.
sops.secrets = let mkSshKeyPair = name: localName: { "ssh/${name}/private" = { owner = "${user.username}"; mode = "600"; path = "/home/${user.username}/.ssh/${localName}"; }; "ssh/${name}/public" = { owner = "${user.username}"; mode = "644"; path = "/home/${user.username}/.ssh/${localName}.pub"; }; }; in (mkSshKeyPair "smallbox" "id_ed25519") // (mkSshKeyPair "wavefunk" "wavefunk") // (mkSshKeyPair "wavefunk_dev" "wavefunk_dev") // { wg_private_key = { # wireguard private key mode = "600"; path = "/home/${user.username}/.ssh/wg_private_key"; }; wg_preshared_key = { # wireguard preshared key mode = "600"; path = "/home/${user.username}/.ssh/wg_preshared_key"; }; wg_peer_public_key = {}; # wireguard VPN server public key vpn_endpoint = {}; # wireguard VPN server endpoint homelab_domain = {}; # homelab base domain wg_wavefunk_private_key = { mode = "600"; path = "/home/${user.username}/.ssh/wg_wavefunk_private_key"; }; }; sops.templates."ssh-hosts" = { owner = "${user.username}"; content = '' Host homelab HostName ${config.sops.placeholder.homelab_domain} User root IdentityFile ${config.sops.secrets."ssh/smallbox/private".path} ''; }; programs._1password.enable = true; programs._1password-gui = { enable = true; polkitPolicyOwners = [ "${user.username}" ]; };
Homelab
System Level
{ config, lib, pkgs, user, ... }: { imports = [ ./hardware-configuration.nix ./disko-config.nix ./sops.nix ./security.nix ./networking.nix ./caddy.nix ./matrix.nix ]; boot.loader.grub = { efiSupport = true; efiInstallAsRemovable = true; }; services.openssh.enable = true; users.users.root.openssh.authorizedKeys.keys = [ user.sshPublicKey ]; users.users.nambiar = { isNormalUser = true; description = "Sandeep Nambiar"; extraGroups = [ "wheel" # can do sudo ]; }; environment.systemPackages = map lib.lowPrio [ pkgs.curl pkgs.gitMinimal ]; system.stateVersion = "26.05"; }
Homelab Hardware
{ config, lib, pkgs, modulesPath, ... }: { imports = [ (modulesPath + "/installer/scan/not-detected.nix") (modulesPath + "/profiles/qemu-guest.nix") ]; networking.useDHCP = lib.mkDefault true; nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; }
Homelab Security
{ config, pkgs, ... }: { services.fail2ban = { enable = true; # Ban IPs for 1 hour after 5 failures within 10 minutes ignoreIP = [ "172.30.0.0/8" "192.168.0.0/16" ]; # Whitelist your local subnet bantime = "1h"; bantime-increment.enable = true; # Increase ban time for repeat offenders jails = { sshd.settings = { maxretry = 3; findtime = "10m"; }; }; }; services.openssh.settings = { PasswordAuthentication = false; KbdInteractiveAuthentication = false; # Disables challenge-response PermitRootLogin = "prohibit-password"; # Allow root only via key }; }
Homelab Sops
{ config, pkgs, ... }: { sops = { defaultSopsFile = ../../secrets/secrets.yaml; age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ]; }; sops.secrets.wg_homelab_key = { # wireguard private key mode = "600"; path = "/root/.ssh/wg_private_key"; }; sops.secrets.wg_homelab_preshared_key = { # wireguard preshared key mode = "600"; path = "/root/.ssh/wg_preshared_key"; }; sops.secrets.wg_peer_public_key = {}; # wireguard VPN server public key sops.secrets.homelab_peer_endpoint = {}; # wireguard peer endpoint for homelab sops.secrets.homelab_domain = {}; # homelab base domain }
Networking
{ config, ... }: let wg_port = 51821; <<wireguard-config>> in { networking.wg-quick.interfaces.wg0 = mkWireguard { address = [ "2a01:4f9:c014:e0de:ac1e::3/128" "172.30.0.4/32" ]; privateKeyFile = config.sops.secrets.wg_homelab_key.path; peerPublicKeyFile = config.sops.secrets.wg_peer_public_key.path; presharedKeyFile = config.sops.secrets.wg_homelab_preshared_key.path; endpointFile = config.sops.secrets.homelab_peer_endpoint.path; allowedIPs = [ "172.30.0.1/32" "172.30.0.2/32" "172.30.0.3/32" ]; listenPort = wg_port; }; boot.kernel.sysctl."net.ipv4.ip_forward" = true; networking.firewall.allowedUDPPorts = [ wg_port ]; }
Caddy
Caddy configuration uses sops.templates to generate the Caddyfile at activation time, keeping domains out of the public repo.
{ config, pkgs, ... } : let domain = config.sops.placeholder.homelab_domain; in { sops.secrets.cloudflare_token = {}; sops.templates."caddy.env".content = '' CF_TOKEN=${config.sops.placeholder.cloudflare_token} ''; sops.templates."Caddyfile" = { owner = "caddy"; group = "caddy"; content = '' { email admin@${domain} } (hardening) { header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" header Referrer-Policy same-origin header X-Content-Type-Options nosniff } (csp) { header Content-Security-Policy "default-src 'self'; img-src 'self' https://*.${domain}/; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; frame-ancestors 'self'; frame-src 'self'; form-action 'self'; media-src 'self' https://*.${domain}/; sandbox allow-downloads allow-forms allow-same-origin allow-scripts" } *.${domain} { tls { dns cloudflare {$CF_TOKEN} } } health.${domain} { respond "Hi!" } <<caddy-matrix-host>> ''; }; services.caddy = { enable = true; package = pkgs.caddy.withPlugins { plugins = [ "github.com/mholt/caddy-ratelimit@v0.1.0" "github.com/caddy-dns/cloudflare@v0.2.2" ]; hash = "sha256-SdgeHilPgV9dNDnjuz8ULQ1CexbyToSbZONvJkrzUWg="; }; environmentFile = "${config.sops.templates."caddy.env".path}"; configFile = config.sops.templates."Caddyfile".path; }; networking.firewall.allowedTCPPorts = [ 443 80 ]; }
Matrix
The matrix domain is derived from the homelab domain sops secret. The synapse server_name and public_baseurl are injected via a sops template extra config file since NixOS module options are evaluated at build time.
The caddy reverse proxy block is a noweb reference (caddy-matrix-host) included in the Caddyfile sops template in the caddy section above.
chat.${domain} { reverse_proxy /_matrix/* localhost:8008 reverse_proxy /_synapse/client/* localhost:8008 reverse_proxy localhost:8008 respond /.well-known/matrix/server "{\"m.server\": \"chat.${domain}:443\"}" 200 respond /.well-known/matrix/client "{\"m.homeserver\": {\"base_url\": \"chat.${domain}\"}, \"m.identity_server\": {\"base_url\": \"https://vector.im\"}}" 200 }
{ config, pkgs, ... }: let domain = config.sops.placeholder.homelab_domain; chatDomain = "chat.${domain}"; in { services.postgresql.enable = true; services.postgresql.initialScript = pkgs.writeText "synapse-init.sql" '' CREATE DATABASE "matrix-synapse" WITH OWNER "matrix-synapse" TEMPLATE template0 LC_COLLATE = "C" LC_CTYPE = "C"; ''; sops.secrets.matrix_shared_secret = { owner = config.systemd.services.matrix-synapse.serviceConfig.User; mode = "644"; }; sops.templates."synapse-domain.yaml" = { owner = config.systemd.services.matrix-synapse.serviceConfig.User; content = '' server_name: "${chatDomain}" public_baseurl: "https://${chatDomain}" ''; }; services.matrix-synapse = { enable = true; settings = { listeners = [{ port = 8008; bind_addresses = [ "0.0.0.0" ]; type = "http"; tls = false; x_forwarded = true; resources = [{ names = [ "client" "federation" ]; compress = false; }]; }]; database = { name = "psycopg2"; args = { user = "matrix-synapse"; database = "matrix-synapse"; host = "/run/postgresql"; }; }; experimental_features = { msc3202_device_masquerading = true; msc3202_transaction_extensions = true; msc2409_to_device_messages_enabled = true; }; }; extraConfigFiles = [ "/run/secrets/matrix_shared_secret" config.sops.templates."synapse-domain.yaml".path ]; enableRegistrationScript = true; }; }
Homelab Disks
{ disko.devices = { disk = { main = { type = "disk"; device = "/dev/sda"; content = { type = "gpt"; partitions = { boot = { size = "1M"; type = "EF02"; priority = 1; }; ESP = { size = "512M"; type = "EF00"; content = { type = "filesystem"; format = "vfat"; mountpoint = "/boot"; }; }; root = { size = "100%"; content = { type = "filesystem"; format = "ext4"; mountpoint = "/"; }; }; }; }; }; }; }; }
Homelab Home
{ pkgs, user, ...} : { home.username = "${user.username}"; home.homeDirectory = pkgs.lib.mkDefault "/home/${user.username}"; home.stateVersion = user.stateVersion; programs.home-manager.enable = true; }
Homelab deploy-rs module
deploy.nodes.homelab = { hostname = "homelab"; profiles.system = { user = "root"; sshUser = "root"; path = deployPkgs.deploy-rs.lib.activate.nixos self.nixosConfigurations.homelab; }; };
Extras
# AGENTS.md This file provides guidance to AI Agents when working with code in this repository. ## Critical Rule **Never read or edit `.nix` files directly.** All `.nix` files are generated from `README.org` via org-babel-tangle. `README.org` is the single source of truth — every change must be made there. Edits to `.nix` files will be silently overwritten on next tangle. ## Commands ```bash # Tangle README.org into nix files (the CI workflow runs a more complete version with a startup block) emacs README.org --batch -f org-babel-tangle # Deploy to local machine nixos-rebuild switch --flake .#smallbox # Deploy to the remote machine nix run github:serokell/deploy-rs -- .#homelab # Edit encrypted secrets sops secrets/secrets.yaml # Update flake inputs nix flake update # Format nix code nixfmt <file> # Enter dev shell (provides nil, nixfmt, sops) nix develop # or: direnv is configured via .envrc ``` There are no tests or linters — validation happens at `nixos-rebuild` time. ## Architecture This is a literate NixOS configuration. `README.org` contains org-mode source blocks that tangle into all `.nix` files. Noweb references (``) compose fragments across blocks — understand how they assemble before changing structure. ### Machines - **smallbox** — primary workstation (x86_64-linux, NixOS, deployed locally) - **homelab** — home server (NixOS, deployed via deploy-rs) ### Flake structure `flake.nix` dynamically generates `nixosConfigurations` by mapping over a machines list. Each machine imports `machines/<name>/configuration.nix` and `machines/<name>/home.nix`. Shared modules live in `configuration/` (system-level) and `home/` (home-manager). Key inputs: nixpkgs-unstable, home-manager, emacs-overlay, sops-nix, catppuccin, deploy-rs, disko. ### Secrets Managed with sops-nix using age encryption. Key defined in `.sops.yaml`. Edit with `sops secrets/secrets.yaml`. ### CI GitHub Actions workflow (`.github/workflows/tangle.yml`) auto-tangles on push to `README.org` and commits the generated files.
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") (delete-trailing-whitespace) (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))
