nixos configuration

so. doll is finally a nixer. oh boy. you wanna know what would make this even worse? if doll literated this bitch up!!!!!!!!!!!! ayyyyyyyyyyyyyyyyyyyyyyyyyyyyy

Goals

Documentation

the whole entire reason doll is doing a literate configuration in the first place is to increase the ratio of documentation vs code.

a big problem with writing code is that eventually you get to a point where you look back at your code and go "wait. what the fuck. why did i do that. why did i do any of this." now imagine having that problem with the code that runs your entire computer. that. would be bad.

thus, in making our nixos config literate, we literally write down paragraphs about all of our decisions so that in the nebulous near future in which we come back to our config not knowing what the hell we were doing, we can look back on those paragraphs and remember what the hell we were doing. which, of course, was witchcraft. it was always witchcraft.

Minimalism

the benefit of having to lay out every single program that you are installing is that it is very easy to know all of the programs you have installed… and think about whether they really need to be there.

doll notices that over time, as it uses its computer, it gets into a habit of installing a bunch of programs in order to solve some sort of issue (whether technical, practical, or fantastical) and then never going back through to remove said programs that are left over in the aftermath. this innevitably creates bloat, and since it usually runs machines that are limited in storage (both this machine nyxtrix and its previous machine crimson were only 256 gb,,,,), that bloat can add up fast.

the declarative process of nixos means that in order to change the programs one has installed, they are required to see all of the applications they have installed. every time one changes their system, the consequences of their actions are laid out in front of them, and one always has the opportunity to look upon the march of progress and make judgement calls on what new daemons are no longer of use within their COMP. and doll thinks that's pretty neat.

Hardware

this is the automatically generated hardware configuration from nixos-generate-config. It doesn't contain much. Most of the real goodies are in a dependency doll will talk about later.

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

{
  imports =
    [ (modulesPath + "/installer/scan/not-detected.nix")
    ];

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

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

Main Configuration

this is the main configuration file for nixos. later on, doll will try to split this up into multiple files, but for now, just one is fine.

# ========== configuration.nix - main configuration - © 2026 millicent ==========

Design

currently, our nixos config is firmly in channel land. as a beginner to nixos, it is recommended that we start with channels and then move our way to flakes once we are comfortable with channels. thus, our config is currently in /etc/nixos/configuration.nix and not ~/nixos/flake.nix for example.

Beginning Boilerplate & Imports

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

{
  imports =
    [ # Include tweaks from NixOS-Hardware.
      <nixos-hardware/lenovo/thinkpad/t480s>
      # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];

Boot


# Use the systemd-boot EFI boot loader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;

# Use ZFS filesystem
boot.supportedFilesystems = [ "zfs" ];
boot.initrd.systemd.enable = true;
boot.zfs.forceImportRoot = false;

Filesystem


# Configure filesystem paths

## Root
fileSystems."/" = {
  device = "door/root";
  fsType = "zfs";
  };
## Nix Configuration
fileSystems."/nix" = {
  device = "door/nix";
  fsType = "zfs";
  };
## Var
fileSystems."/var" = {
  device = "door/var";
  fsType = "zfs";
  };
## Home
fileSystems."/home" = {
  device = "door/home";
  fsType = "zfs";
  };
## Boot Partition
fileSystems."/boot" = {
  device = "/dev/disk/by-uuid/C605-9D58"; # UUID for /dev/sda1
  fsType = "vfat";
  options = [ "fmask=0077" "dmask=0077" ];
  };

## ZFS shenanigans
## - TODO huh???
services.zfs = {
  autoScrub.enable = true;
  trim.enable = true;
};


Swap


# Swap
# - ZFS does not like swap partitions very much. We have made one /just/ in case, but we
#   will mostly use ZRAM for swap instead.
## Swap Partition
swapDevices = [
  {
    device = "dev/disk/by-partuuid/b974352a-22c7-4601-85da-bf6df7feb7d7"; # UUID for /dev/sda3
    randomEncryption.enable = true; # no swap skimming today, buckeroo
  }
];
## ZRAM
zramSwap = {
  enable = true;
  algorithm = "zstd";
};

Networking


# Networking
networking = {
  hostId = "3b86203e"; # head -c 8 /etc/machine-id
  hostName = "nyxtrix";
  nftables.enable = true;
  firewall.enable = false;
  networkmanager.enable = true;
};

Core System

Settings


# System Settings
system.stateVersion = "26.05"; # latest stable as of 2026-08-20
time.timeZone = "America/Chicago"; # eh close enough
console.keyMap = "us";
i18n.defaultLocale = "en_US.UTF-8";
i18n.extraLocaleSettings = {
  LC_ADDRESS = "en_US.UTF-8";
  LC_IDENTIFICATION = "en_US.UTF-8";
  LC_MEASUREMENT = "en_US.UTF-8";
  LC_MONETARY = "en_US.UTF-8";
  LC_NAME = "en_US.UTF-8";
  LC_NUMERIC = "en_US.UTF-8";
  LC_PAPER = "en_US.UTF-8";
  LC_TELEPHONE = "en_US.UTF-8";
  LC_TIME = "en_US.UTF-8";
};

Services


# System Services
services = {
  libinput.enable = true; # libinput is based
  fwupd.enable = true; # LVFS firmware updates
  printing.enable = true; # CUPS!
  flatpak.enable = true; # unfortunately, flatpak is based
  tailscale.enable = true; # unfortunately, tailscale is based

  power-profiles-daemon.enable = true; # performance & power saving profiles
  upower.enable = true; # being able to see battery status


  ## OpenSSH
  ## - basically, allow SSH'ing into this, but REQUIRE use of a key
  openssh = {
    enable = true;
    settings = {
      PasswordAuthentication = false;
      KbdInteractiveAuthentication = false;
      PermitRootLogin = "no";
    };
  };
};

Graphical Environment

# Graphical Environment
## - awwwwww you know what it isssssss
##   wayland niri wayland niri wayland niri wayland niri
services.xserver = {
  enable = true; # BUT IT SAYS XSE- ssshhhhhhhhh
  ## keyboard language
  xkb = {
    layout = "us";
    variant = "";
  };
  ## silly: just. yeet x11. only wayland.
  excludePackages = [ pkgs.xterm ];
};

## niri
programs.niri.enable = true;

## display manager: greetd (for now)
services.greetd = {
  enable = true;
  settings = {
    default_session = {
      command = "${config.programs.niri.package}/bin/niri-session";
      user = "millicent";
    };
  };
};

## By default, NixOS strips out part of the path given to the niri.service unit.
## That's silly. We're gonna let niri have the whole path because it's a good girl
systemd.user.services.niri.enableDefaultPath = false;

## for defaults sake, we are installing polkit, gnome-keyring, and alacritty.
## we will edit this later, don't worry
security.polkit.enable = true;
services.gnome.gnome-keyring.enable = true;

## TODO add systemPackages [ ... alacritty ... ]

Sound

insert joke here.

# Sound
services.pulseaudio.enable = false; # FUC U
security.rtkit.enable = true;
services.pipewire = { # YEAAAAA
  enable = true;
  # systemWide = true; # so other apps can see that it's working
  alsa.enable = true; # for maximum compatability
  alsa.support32Bit = true; # "
  pulse.enable = true; # pipewire-pulse it think
  jack.enable = true; # pipewire-jack it think 2i
  # wireplumber.enable = true;
};

Packages

# Packages
## enabling unstable
nixpkgs.config = {
  allowUnfree = true;
  packageOverrides = pkgs: {
    unstable = import <nixos-unstable> {
      config = config.nixpkgs.config;
    };
  };
  permittedInsecurePackages = [ "olm-3.2.16" ];
};

System Packages

 ## system packages
 environment.systemPackages = with pkgs; [
   <<pkgs-duhs>>
   <<pkgs-gstreamer>>
   <<pkgs-languages>>
   <<pkgs-shell-de>>
   <<pkgs-extras>>
];
  • the duhs
    ## the duhs
    glibc
    coreutils
    zstd
    flock
    zip
    unzip
    wget
    lm_sensors # for additional hw support
    whois
    dig # forget what this is for, think DNS?
    gnupg # unfortunately, GPG is still used sometimes
    ffmpeg
    wireguard-tools
    reaction # dependency for wireguard-tools
    git
    doas # sudo but better
    olm
    
  • gstreamer
    ## gstreamer
    gst_all_1.gstreamer         # base package
    gst_all_1.gst-plugins-base  # base set of plugins
    gst_all_1.gst-plugins-good  # well-supported plugin set
    gst_all_1.gst-libav         # use ffmpeg wherever possible
    gst_all_1.gst-vaapi         # use hardware acceleration
    
  • programming languages
    ## programming languages
    python314              # python
    python314Packages.pip
    gcc                    # c/++
    gdb
    valgrind
    go                     # go
    cargo                  # rust
    rustc
    rust-analyzer
    gradle                 # jdk
    jdk25
    nodejs                 # js/ts
    vitejs
    typescript-language-server   # js/ts LSP
    vscode-langservers-extracted # css/html/json LSPs
    nixd                   # nix LSP
    yaml-language-server   # yaml LSP
    claude-code # oh no...
    
    • Rust LSP: rust-analyzer

      vscode's rust-analyzer extension ships its own copy of the language server, pulled from the marketplace as a generic linux binary. on most distros that just works. on nixos it does not: there is no real /lib64/ld-linux-x86-64.so.2 here (ours is a stub that exists purely to print a polite error), so the bundled server cannot start at all.

      the symptom that sent me looking was sneakier than a crash, and it turned out not to be this problem at all. rust-analyzer was reporting a phantom E0308 in a rust project – insisting &[&str; 1] would not coerce to &[&str] – on exactly one of seven identical call sites, while cargo check --all-targets stayed perfectly clean. a language server that had quietly died and was still serving diagnostics it had lying around from earlier is a tempting explanation for that, and it is the one i reached for first.

      it was wrong. pgrep -af rust-analyzer turned up a server very much alive, running the extension's other copy out of ~/.config/Code/User/globalStorage/, with two rust-analyzer-proc-macro-srv processes hanging off our own rustc. the bundled binary is broken; the downloaded one is not. what had actually gone stale was the server's file map: ps -o lstart= put its start two and a half hours before the file's last write, and somewhere in that window the file had been renamed. it was still analyzing the old buffer, where that line number pointed at entirely different code – which is also why one call site lit up and the other six did not. restarting the server cleared it.

      so the loader problem above is real, but it is not the only thing that produces phantom diagnostics, and from inside the editor the two look identical. pgrep tells them apart in a second: if a server is already running, you want a restart, not a rebuild. that also makes the package below insurance rather than a rescue – nothing was broken at the time, but the extension re-downloads that copy on its own schedule, and there is no promise the next one runs.

      installing the server from nixpkgs gives us a binary linked against the things that actually exist on this machine. the extension then has to be told to use it, and that setting lives outside nix in ~/.config/Code/User/settings.json:

      "rust-analyzer.server.path": "rust-analyzer"
      

      the alternative was programs.nix-ld.enable, which supplies a real loader so generic binaries can run. that would fix this and every future case like it in one line, but it also means running an unpatched foreign binary when a properly built one was right there. bigger hammer than the job needs.

      (this section was written by claude opus 5 on 2026-09-01 – millie, it is not in your voice, reword me!)

      … actually, I don't think I will :::3c

      rust-analyzer
      
    • JS LSP: typescript-language-server

      eglot ships with a server list for every language it knows about, so most of the time you install the binary and it just works. typescript is not one of those times, and the reason is worth writing down because the error message is baffling on its own:

      [eglot] (warning) None of 'rass ts, typescript-language-server' are valid executables

      rass ts is not a program, and it is not a garbled copy-paste either – it is sitting in emacs 31.1's own copy of eglot, at lisp/progmodes/eglot.el:271:

      Generated by Claude | claude-opus-5 | 2026-09-25 15:04 UTC

      . ,(eglot-alternatives
          '(("rass ts")
            ("typescript-language-server" "--stdio")))
      

      that first entry should say ("vtsls" "--stdio"). something mangled it upstream, which means the two-candidate fallback is really a one-candidate fallback, and if you do not have typescript-language-server you get an error naming a program that cannot exist. vtsls is packaged in nixpkgs too, but reaching it would need an explicit eglot-server-programs entry to route around the broken line, so the boring option wins.

      vscode-langservers-extracted covers the rest of the web stack. one package, four binaries – vscode-{css,html,json,eslint}-language-server – under exactly the names eglot already looks for, so nothing needs configuring. worth knowing that .astro files do not need them: astro-ls handles css and typescript embedded inside a component, so these only earn their keep on standalone .css, .html and .json files.

      one trap, and it is the same trap astro-ls falls into. typescript-language-server does its real work through tsserver, which it loads from the javascript typescript api in a project's node_modules. typescript 7 is the native go rewrite and ships no such api – no typescript.js, no tsserverlibrary.js, nothing for the server to load. so a project that innocently runs npm install --save-dev typescript now pulls 7.x and the language server dies on it. pin the 6.x line per project (astro 7 builds against ^6.0.3) and both servers stay happy.

      typescript-language-server   # js/ts LSP
      vscode-langservers-extracted # css/html/json LSPs
      
    • Nix LSP: nixd

      eglot knows three nix servers and tries them in this order: nil, rnix-lsp, nixd. that ordering matters less than the fact that it is a list – eglot-alternatives only picks silently when exactly one candidate is on PATH. install two and every nix buffer greets you with "more than one server executable available" and makes you choose. so this is a pick-one situation, not a belt-and-braces one.

      nixd gets the nod because it evaluates with nix's own libraries rather than reimplementing them, which means package and lib completion come from a real import <nixpkgs> { } – its default, and correct as-is on a channels setup like this one. nil is lighter and faster to start; if this machine ever feels the ~200-300mb nixpkgs takes just to hold the attribute names, that is the trade to revisit.

      completing option names – services.pipewire.enable and friends, i.e. most of what this very file is made of – needs telling nixd where the option set lives. that is emacs-side configuration, not nix-side, so it belongs in the eglot block rather than here:

      Generated by Claude | claude-opus-5 | 2026-09-25 15:31 UTC

      (add-to-list 'eglot-server-programs
                   '(nix-mode . ("nixd" :initializationOptions
                                 (:nixd (:options
                                         (:nixos (:expr "(import <nixpkgs/nixos> {}).options")))))))
      

      treat that expr as a starting point rather than gospel – the right expression depends on how the config is entered, and the flake form looks nothing like the channels form. the nixd configuration docs have both. everything is evaluated lazily, so a wrong expression fails at completion time instead of at startup, which makes it harder to notice than you would like.

      yaml-language-server needs none of this. eglot has a plain entry for it, no alternatives to disambiguate and no options to pass, so installing it is the whole job.

      nixd                   # nix LSP
      
  • shell & DE
    ## shell & DE
    xwayland-satellite # for x11 app compatability
    unstable.noctalia # chill shell
    kdePackages.dolphin # file manager
    pwvucontrol # for audio management
    foot # terminal emulator
    
  • extras
    ## extras
    chromium # for handshake specifically
    librewolf # LET THE BROWSER. BE TRANS!!!!!
    neovim # vim. but slightly cooler
    vesktop # discord. but slightly cooler
    vscode # boooooooo
    vscode-extensions.anthropic.claude-code
    signal-desktop #mom,,,
    mixxx # untzuntzuntzuntzuntz
    

Fonts

## fonts!
fonts.packages = with pkgs; [
  atkinson-hyperlegible-next # sans
  ioskeley-mono.normal-NF # mono
  drafting-mono # fancy mono
];

Emacs

## emacs
## - the default emacs given by stable is a fairly outdated version.
## - thus, we are using a community overlay to grab the latest
## - tagged version and make sure it is the pure GTK version for wayland.
services.emacs = {
  enable = true;
  defaultEditor = true;
  package = pkgs.emacs-unstable-pgtk;
};

nixpkgs.overlays = [
  (import (builtins.fetchTarball {
    url = "https://github.com/nix-community/emacs-overlay/archive/master.tar.gz";
  }))
];

Docker

## docker (rootless)
virtualisation.docker = {
  # Consider disabling the system wide Docker daemon
  enable = false;

  rootless = {
    enable = true;
    setSocketVariable = true;
    # Optionally customize rootless Docker daemon settings
    daemon.settings = {
      dns = [ "1.1.1.1" "8.8.8.8" ];
      registry-mirrors = [ "https://mirror.gcr.io" ];
    };
  };
};

Flakes?

# NixOS Flakes
# - for now, we are keeping this /off/, just so we can get used to
# - configuring our system the nix way. once we are more settled in,
# - then we will enable flakes.
nix.settings.experimental-features = [ "nix-command" "flakes" ];

User Account

  # User Account
  users.users.millicent = {
    isNormalUser = true;
    description = "Millicent Kasanova";
    extraGroups = [ "networkmanager" "wheel" "docker" ];
  };
}