summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--common/core.nix53
-rw-r--r--common/default.nix9
-rw-r--r--configuration.nix517
-rw-r--r--flake.lock21
-rw-r--r--flake.nix27
-rw-r--r--machines/enzo/default.nix91
-rw-r--r--machines/enzo/desktop.nix68
-rw-r--r--machines/enzo/disko.nix93
-rw-r--r--machines/enzo/hardware-configuration.nix27
-rw-r--r--machines/kusanagi/default.nix317
-rw-r--r--machines/kusanagi/hardware-configuration.nix (renamed from hardware-configuration.nix)0
-rw-r--r--profiles/apps.nix30
-rw-r--r--profiles/desktop.nix171
-rw-r--r--profiles/dev.nix85
-rw-r--r--programs.nix98
15 files changed, 987 insertions, 620 deletions
diff --git a/common/core.nix b/common/core.nix
new file mode 100644
index 0000000..5d36646
--- /dev/null
+++ b/common/core.nix
@@ -0,0 +1,53 @@
+# Boot, networking, locale, and Nix settings shared by all machines.
+{ pkgs, ... }:
+
+{
+ # Editor available on every machine (even a headless one).
+ environment.systemPackages = [ pkgs.neovim ];
+ programs.neovim = {
+ enable = true;
+ defaultEditor = true;
+ };
+
+ # Bootloader.
+ boot.loader.systemd-boot.enable = true;
+ boot.loader.efi.canTouchEfiVariables = true;
+
+ # Enable networking
+ networking.networkmanager.enable = true;
+
+ # for tailscale
+ networking.firewall.trustedInterfaces = [ "tailscale0" ];
+ networking.firewall.allowedUDPPorts = [ 41641 ];
+ services.tailscale.enable = true;
+
+ # Set your time zone.
+ time.timeZone = "America/Chicago";
+
+ # Select internationalisation properties.
+ 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";
+ };
+
+ # trusted-users is set per-machine (see machines/<host>/default.nix).
+
+ nix.settings.experimental-features = [
+ "nix-command"
+ "flakes"
+ ];
+
+ # Allow unfree packages
+ nixpkgs.config.allowUnfree = true;
+
+ environment.localBinInPath = true;
+}
diff --git a/common/default.nix b/common/default.nix
new file mode 100644
index 0000000..946b3a7
--- /dev/null
+++ b/common/default.nix
@@ -0,0 +1,9 @@
+# Configuration shared by all machines. Machine-specific settings live under
+# ../machines/<hostname>/default.nix.
+{ ... }:
+
+{
+ imports = [
+ ./core.nix
+ ];
+}
diff --git a/configuration.nix b/configuration.nix
deleted file mode 100644
index 19f11e2..0000000
--- a/configuration.nix
+++ /dev/null
@@ -1,517 +0,0 @@
-# Edit this configuration file to define what should be installed on
-# your system. Help is available in the configuration.nix(5) man page
-# and in the NixOS manual (accessible by running ‘nixos-help’).
-
-{ config, pkgs, ... }:
-
-{
- imports = [
- # Include the results of the hardware scan.
- ./hardware-configuration.nix
- ];
-
- # Bootloader.
- boot.loader.systemd-boot.enable = true;
- boot.loader.efi.canTouchEfiVariables = true;
- boot.supportedFilesystems = [ "nfs" ];
-
- # for disc drive
- boot.kernelModules = [ "sg" ];
-
- networking.hostName = "kusanagi"; # Define your hostname.
- # networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
-
- # Enable networking
- networking.networkmanager.enable = true;
-
- # for tailscale
- networking.firewall.trustedInterfaces = [ "tailscale0" ];
- networking.firewall.allowedUDPPorts = [ 41641 ];
-
- # Set your time zone.
- time.timeZone = "America/Chicago";
-
- # Select internationalisation properties.
- 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";
- };
-
- nix.settings.trusted-users = [
- "root"
- "henz"
- ];
-
- # Enable the X11 windowing system.
- services.xserver.enable = true;
-
- # Enable the GNOME Desktop Environment.
- services.displayManager.gdm.enable = false;
- services.desktopManager.gnome.enable = true;
-
- services.tailscale.enable = true;
- services.mpd.enable = false;
-
- services.kavita = {
- enable = true;
- tokenKeyFile = "/var/lib/kavita/token-key";
- settings.Port = 5000;
- };
-
- systemd.tmpfiles.rules = [
- "d /srv/library 2750 henz kavita - -"
- ];
-
- # For disc drives
- services.udev.extraRules = ''
- KERNEL=="sr[0-9]*", GROUP="cdrom", MODE="0660"
- KERNEL=="sg[0-9]*", GROUP="sg", MODE="0660"
- '';
-
- # prevent auto-mounting optical disks
- services.udisks2.enable = true;
-
- programs.dconf.enable = true;
- programs.niri.enable = true;
-
- # login screen
- services.greetd = {
- enable = true;
- settings = {
- default_session = {
- command = "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd niri-session";
- user = "greeter";
- };
- };
- };
-
- # This is required to let tuigreet write to its cache (for --remember)
- systemd.services.greetd.serviceConfig = {
- Type = "idle";
- StandardInput = "tty";
- StandardOutput = "tty";
- StandardError = "journal"; # Better for debugging
- TTYReset = true;
- TTYVHangup = true;
- TTYVTDisallocate = true;
- };
-
- systemd.user.services.mpd = {
- description = "Music Player Daemon";
- after = [
- "network.target"
- "sound.target"
- ];
- wantedBy = [ "default.target" ];
- serviceConfig = {
- ExecStart = "${pkgs.mpd}/bin/mpd --no-daemon";
- Type = "notify";
- LimitRTPRIO = 50;
- LimitRTTIME = "infinity";
- };
- };
-
- # Configure keymap in X11
- services.xserver.xkb = {
- layout = "us";
- variant = "";
- };
-
- # Enable CUPS to print documents.
- services.printing = {
- enable = true;
- drivers = with pkgs; [
- gutenprint
- ];
- };
-
- services.avahi = {
- enable = true;
- nssmdns4 = true;
- openFirewall = true;
- };
-
- services.pulseaudio.enable = false;
- security.rtkit.enable = true;
- services.pipewire = {
- enable = true;
- alsa.enable = true;
- alsa.support32Bit = true;
- pulse.enable = true;
- jack.enable = true;
- wireplumber.enable = true;
- };
- # Add this for better pro audio support
- services.pipewire.extraConfig.pipewire."10-low-latency" = {
- "context.properties" = {
- "default.clock.rate" = 48000;
- "default.clock.quantum" = 256;
- "default.clock.min-quantum" = 256;
- "default.clock.max-quantum" = 2048;
- };
- };
-
- # Pin the Scarlett 18i20 to the raw "Pro Audio" profile.
- # The 26.05 pipewire/wireplumber update began preferring the UCM "HiFi"
- # profile, which splits the interface into Direct1/Line5/SPDIF1/... sinks and
- # broke monitoring. This priority rule forces the multichannel Pro Audio
- # profile statelessly on every boot (and survives future UCM renames).
- services.pipewire.wireplumber.extraConfig."51-scarlett-pro-audio" = {
- "device.profile.priority.rules" = [
- {
- matches = [
- {
- "device.name" = "alsa_card.usb-Focusrite_Scarlett_18i20_USB_P9DCR6Y378D1E6-00";
- }
- ];
- actions = {
- update-props = {
- priorities = [ "pro-audio" ];
- };
- };
- }
- ];
- };
-
- # Real-time audio optimizations
- security.pam.loginLimits = [
- {
- domain = "@audio";
- item = "memlock";
- type = "-";
- value = "unlimited";
- }
- {
- domain = "@audio";
- item = "rtprio";
- type = "-";
- value = "99";
- }
- {
- domain = "@audio";
- item = "nofile";
- type = "soft";
- value = "99999";
- }
- {
- domain = "@audio";
- item = "nofile";
- type = "hard";
- value = "99999";
- }
- ];
-
- services.nfs.server.enable = false;
- services.rpcbind.enable = true;
-
- services.nfs.idmapd.settings = {
- General = {
- Domain = "kusanagi"; # or your actual domain name
- };
- };
-
- # NAS shares. Use the mDNS-resolvable name (bare "milgrim" doesn't resolve)
- # and automount on first access so a boot-time DNS/network race can't leave
- # them unmounted (which silently breaks MPD, whose DB points at these paths).
- fileSystems."/mnt/milgrim-share" = {
- device = "milgrim.local:/volume1/share";
- fsType = "nfs";
- options = [
- "nfsvers=4"
- "rsize=8192"
- "wsize=8192"
- "_netdev"
- "noauto"
- "x-systemd.automount"
- "x-systemd.idle-timeout=600"
- "x-systemd.mount-timeout=10s"
- ];
- };
-
- fileSystems."/mnt/milgrim-public" = {
- device = "milgrim.local:/volume1/public";
- fsType = "nfs";
- options = [
- "nfsvers=4"
- "rsize=8192"
- "wsize=8192"
- "_netdev"
- "noauto"
- "x-systemd.automount"
- "x-systemd.idle-timeout=600"
- "x-systemd.mount-timeout=10s"
- ];
- };
-
- # Kernel optimizations for audio
- boot.kernelParams = [
- "threadirqs"
- "preempt=full" # if using a PREEMPT kernel
- # Add USB audio optimizations
- "usbcore.usbfs_memory_mb=1024"
- ];
-
- fonts = {
- enableDefaultPackages = true;
-
- packages = with pkgs; [
- noto-fonts-cjk-sans
- noto-fonts-cjk-serif
- inter
- nerd-fonts.symbols-only
- noto-fonts
- ibm-plex
- public-sans
- adwaita-fonts
- ];
-
- fontconfig = {
- enable = true;
-
- defaultFonts = {
- monospace = [
- "Berkeley Mono"
- "Liberation Mono"
- "Symbols Nerd Font"
- ];
- sansSerif = [
- "Liberation Sans"
- "Noto Sans"
- ];
- serif = [ "Liberation Serif" ];
- emoji = [ "Nerd Font Symbols Only" ];
- };
-
- # Force ui-sans-serif and system-ui to resolve correctly
- localConf = ''
- <?xml version="1.0"?>
- <!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
- <fontconfig>
- <alias>
- <family>system-ui</family>
- <prefer>
- <family>Liberation Sans</family>
- </prefer>
- </alias>
- <alias>
- <family>ui-sans-serif</family>
- <prefer>
- <family>Liberation Sans</family>
- </prefer>
- </alias>
- </fontconfig>
- '';
- };
-
- fontDir.enable = true;
- };
-
- # Define a user account. Don't forget to set a password with ‘passwd’.
- users.users.henz = {
- isNormalUser = true;
- description = "Henry Webster";
- extraGroups = [
- "networkmanager"
- "wheel"
- "audio"
- "cdrom"
- "optical"
- "sg"
- "scanner"
- "lp"
- "input"
- ];
- packages = with pkgs; [
- blender
- rocmPackages.rpp
- rocmPackages.hipcc
- ardour
- gnupg
- pinentry-gnome3
- git
- tmux
- protonmail-desktop
- ghostty
- inkscape
- darktable
- nfs-utils
- imagemagick
- davinci-resolve-studio
- alsa-utils
- alsa-tools
- usbutils
- pipewire.jack
- rocmPackages.rocminfo
- discord
- vlc
- neovim
- tmux
- unzip
- calf
- gimp
- xclip
- gcc
- pkg-config
- direnv
- cargo
- ripgrep
- abcde
- guile
- autoconf
- automake
- libtool
- gnumake
- gcc
- pkg-config
-
- mpd
- mpc
- ncmpcpp
- ffmpeg
- flatpak
- nushell
-
- # for archiving
- #makemkv
- libaacs
- libbdplus
- libdvdcss
- psmisc
- mkvtoolnix
- dvdbackup
- chromaprint
- picard
- flac
-
- bc
- btop
- newsboat
- castero
-
- opencode
- claude-code
-
- # hyprland
- waybar
- networkmanagerapplet
-
- # niri
- xwayland-satellite
- fuzzel
- swaybg
-
- stow
-
- lua-language-server
- nil
- clang-tools
- marksman
- gopls
- ruff
- basedpyright
- zls
- gdb
- nixfmt
-
- gcc
- nodejs
- gnumake
-
- # scanners
- simple-scan
-
- (pkgs.makeDesktopItem {
- name = "PICO-8";
- desktopName = "PICO-8";
- exec = "pico8";
- icon = "pico8";
- comment = "Fantasy console for making, sharing and playing tiny games";
- categories = [ "Game" ];
- })
- ];
- };
-
- nixpkgs.overlays = [
- (self: super: {
- abcde = super.abcde.overrideAttrs (oldAttrs: {
- # We use postInstall to modify the script after it has been copied to the output directory
- postInstall = (oldAttrs.postInstall or "") + ''
- # Patch 1: Allow GetOptions to accept 4 arguments (instead of requiring 5+)
- sed -i 's/discinfo=i{5,}/discinfo=i{4,}/g' $out/bin/abcde-musicbrainz-tool
-
- # Patch 2: Lower the logic check from 5 to 4
- sed -i 's/$#discinfo < 5/$#discinfo < 4/g' $out/bin/abcde-musicbrainz-tool
- '';
- });
- })
- ];
-
- services.pcscd.enable = true;
-
- programs.neovim = {
- enable = true;
- defaultEditor = true;
- };
-
- # Allow unfree packages
- nixpkgs.config.allowUnfree = true;
-
- nixpkgs.config.rocmSupport = true;
-
- # List packages installed in system profile. To search, run:
- # $ nix search wget
- environment.systemPackages = with pkgs; [
- # vim # Do not forget to add an editor to edit configuration.nix! The Nano editor is also installed by default.
- # wget
- neovim
- ];
-
- environment.sessionVariables = {
- GSK_RENDERER = "gl";
- XCURSOR_THEME = "Bibata-Modern-Classic";
- XCURSOR_SIZE = "24";
- HYPRCURSOR_THEME = "Bibata-Modern-Classic";
- HYPRCURSOR_SIZE = "24";
- };
-
- environment.localBinInPath = true;
-
- # Graphics setup
- hardware.graphics = {
- enable = true;
- enable32Bit = true;
- };
- hardware.graphics.extraPackages = with pkgs; [
- rocmPackages.clr.icd
- ];
-
- # scanner
- hardware.sane.enable = true;
-
- # This value determines the NixOS release from which the default
- # settings for stateful data, like file locations and database versions
- # on your system were taken. It‘s perfectly fine and recommended to leave
- # this value at the release version of the first install of this system.
- # Before changing this value read the documentation for this option
- # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
- system.stateVersion = "24.11"; # Did you read the comment?
-
- powerManagement.cpuFreqGovernor = "performance";
-
- nix.settings.experimental-features = [
- "nix-command"
- "flakes"
- ];
-
- boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
-
-}
diff --git a/flake.lock b/flake.lock
index 09384a2..83697dc 100644
--- a/flake.lock
+++ b/flake.lock
@@ -21,6 +21,26 @@
"url": "https://git.hwebs.info/discidurl"
}
},
+ "disko": {
+ "inputs": {
+ "nixpkgs": [
+ "nixpkgs"
+ ]
+ },
+ "locked": {
+ "lastModified": 1781152676,
+ "narHash": "sha256-RxWs5ND31KzTG7wvMM+PMfUjyNpmIEr999lqNARaM5o=",
+ "owner": "nix-community",
+ "repo": "disko",
+ "rev": "ff8702b4de27f72b4c78573dfb89ec74e36abdf1",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-community",
+ "repo": "disko",
+ "type": "github"
+ }
+ },
"nixpkgs": {
"locked": {
"lastModified": 1784280462,
@@ -40,6 +60,7 @@
"root": {
"inputs": {
"discidurl": "discidurl",
+ "disko": "disko",
"nixpkgs": "nixpkgs"
}
}
diff --git a/flake.nix b/flake.nix
index 3a8765b..75732a6 100644
--- a/flake.nix
+++ b/flake.nix
@@ -4,18 +4,33 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
+ disko.url = "github:nix-community/disko";
+ disko.inputs.nixpkgs.follows = "nixpkgs";
+
discidurl.url = "git+https://git.hwebs.info/discidurl?ref=main";
discidurl.inputs.nixpkgs.follows = "nixpkgs";
};
outputs =
{ self, nixpkgs, ... }@inputs:
+ let
+ # A machine is common/ + its own machines/<name> module, plus any extra
+ # inline modules (e.g. per-machine overlays).
+ mkMachine =
+ machineModule: extraModules:
+ nixpkgs.lib.nixosSystem {
+ system = "x86_64-linux";
+ specialArgs = { inherit inputs; };
+ modules = [
+ ./common
+ machineModule
+ ]
+ ++ extraModules;
+ };
+ in
{
- nixosConfigurations.kusanagi = nixpkgs.lib.nixosSystem {
- system = "x86_64-linux";
- modules = [
- ./configuration.nix
- ./programs.nix
+ nixosConfigurations = {
+ kusanagi = mkMachine ./machines/kusanagi [
{
nixpkgs.overlays = [
(import ./overlays/haunt.nix)
@@ -23,6 +38,8 @@
];
}
];
+
+ enzo = mkMachine ./machines/enzo [ ];
};
};
}
diff --git a/machines/enzo/default.nix b/machines/enzo/default.nix
new file mode 100644
index 0000000..65adef6
--- /dev/null
+++ b/machines/enzo/default.nix
@@ -0,0 +1,91 @@
+# enzo — laptop. Gets the shared common/ base plus laptop-only settings.
+{ inputs, pkgs, ... }:
+
+{
+ imports = [
+ # enzo's own minimal desktop (not the shared profiles/desktop.nix). Add
+ # ../../profiles/dev.nix or apps.nix here, or one-off packages below, as needed.
+ ./desktop.nix
+
+ inputs.disko.nixosModules.disko
+ ./disko.nix
+ ./hardware-configuration.nix
+ ];
+
+ networking.hostName = "enzo";
+
+ # Set this to the NixOS release the laptop is first installed from, then
+ # leave it. (See the comment in machines/kusanagi/default.nix.)
+ system.stateVersion = "26.05";
+
+ # --- User account (hwebs) ---
+ # (Desktop groups audio/input come from ./desktop.nix.)
+ users.users.hwebs = {
+ isNormalUser = true;
+ description = "Henry Webster";
+ extraGroups = [
+ "networkmanager"
+ "wheel"
+ ];
+ packages = with pkgs; [
+ git
+ neovim
+ tmux
+ btop
+ ripgrep
+ unzip
+ xclip
+ bc
+ psmisc
+ usbutils
+ stow
+ nushell
+ ];
+ };
+
+ nix.settings.trusted-users = [
+ "root"
+ "hwebs"
+ ];
+
+ # --- Disk encryption / hibernation / swap ---
+ # systemd in initrd is required for TPM2 unlock and clean hibernate resume.
+ boot.initrd.systemd.enable = true;
+
+ # TPM2 auto-unlock. Enroll the key WITH A PIN post-install:
+ # sudo systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7 \
+ # --tpm2-with-pin=yes /dev/nvme0n1p2
+ # The original LUKS passphrase stays as a fallback keyslot. disko already
+ # declares boot.initrd.luks.devices."crypted".device.
+ boot.initrd.luks.devices."crypted".crypttabExtraOpts = [ "tpm2-device=auto" ];
+
+ # zram for everyday paging; the encrypted LVM swap from disko.nix is the
+ # hibernation target (boot.resumeDevice comes from disko's resumeDevice=true).
+ zramSwap.enable = true;
+
+ # Suspend-to-RAM on lid close, auto-hibernate after a delay so a dying battery
+ # doesn't lose the session. Everyday resume needs only the login password; the
+ # LUKS PIN appears only on cold boot / hibernate resume.
+ services.logind.settings.Login.HandleLidSwitch = "suspend-then-hibernate";
+ systemd.sleep.settings.Sleep.HibernateDelaySec = "60min";
+
+ # btrfs + SSD upkeep.
+ services.btrfs.autoScrub.enable = true;
+ services.fstrim.enable = true;
+
+ # --- Laptop power management ---
+ # power-profiles-daemon integrates with GNOME's power settings. If you prefer
+ # finer-grained control, disable this and enable services.tlp instead.
+ services.power-profiles-daemon.enable = true;
+ powerManagement.enable = true;
+
+ # Backlight control from the CLI / keybinds (programs.light was removed from
+ # nixpkgs; acpilight provides the udev rules and brightnessctl the CLI).
+ hardware.acpilight.enable = true;
+ environment.systemPackages = [ pkgs.brightnessctl ];
+
+ # --- GPU ---
+ # TODO: adjust for the laptop's actual GPU once known. For an Intel iGPU add
+ # intel-media-driver to hardware.graphics.extraPackages; for a discrete AMD
+ # GPU you may want ROCm as in machines/kusanagi/default.nix.
+}
diff --git a/machines/enzo/desktop.nix b/machines/enzo/desktop.nix
new file mode 100644
index 0000000..8fb68ef
--- /dev/null
+++ b/machines/enzo/desktop.nix
@@ -0,0 +1,68 @@
+# Minimal niri desktop for enzo — deliberately separate from ../../profiles/desktop.nix
+# so the laptop can grow on its own terms. Add to it (or switch to the shared
+# profile) whenever you want more.
+{ pkgs, ... }:
+
+{
+ # --- Session / login ---
+ programs.niri.enable = true;
+ programs.dconf.enable = true;
+
+ services.greetd = {
+ enable = true;
+ settings.default_session = {
+ command = "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd niri-session";
+ user = "greeter";
+ };
+ };
+ # Let tuigreet write its cache (for --remember).
+ systemd.services.greetd.serviceConfig = {
+ Type = "idle";
+ StandardInput = "tty";
+ StandardOutput = "tty";
+ StandardError = "journal";
+ TTYReset = true;
+ TTYVHangup = true;
+ TTYVTDisallocate = true;
+ };
+
+ # --- Graphics ---
+ hardware.graphics.enable = true;
+
+ # --- Audio (playback only; no rtkit/jack — enzo isn't for recording).
+ # If you ever hear dropouts under load, add: security.rtkit.enable = true;
+ services.pulseaudio.enable = false;
+ services.pipewire = {
+ enable = true;
+ alsa.enable = true;
+ pulse.enable = true;
+ };
+
+ # --- Fonts (minimal) ---
+ fonts.enableDefaultPackages = true;
+ fonts.packages = with pkgs; [
+ noto-fonts
+ nerd-fonts.symbols-only
+ ];
+
+ environment.sessionVariables = {
+ XCURSOR_THEME = "Adwaita";
+ XCURSOR_SIZE = "24";
+ };
+
+ # --- User: groups + niri essentials ---
+ users.users.hwebs.extraGroups = [
+ "audio"
+ "input"
+ ];
+
+ users.users.hwebs.packages = with pkgs; [
+ ghostty # terminal
+ fuzzel # launcher
+ swaybg # wallpaper
+ xwayland-satellite # run X11 apps under niri
+ networkmanagerapplet # wifi tray
+ mako # notifications
+ libnotify # notify-send
+ ];
+}
diff --git a/machines/enzo/disko.nix b/machines/enzo/disko.nix
new file mode 100644
index 0000000..ddfa4d3
--- /dev/null
+++ b/machines/enzo/disko.nix
@@ -0,0 +1,93 @@
+# Declarative disk layout for enzo: LVM-on-LUKS with a btrfs root and an
+# encrypted swap logical volume (>= RAM) for hibernation.
+#
+# nvme0n1
+# ├─ p1 ESP 1 GiB vfat /boot (unencrypted)
+# └─ p2 LUKS2 "crypted" ── LVM vg "vg" ──
+# ├─ lv "swap" swap (hibernation target)
+# └─ lv "root" btrfs subvolumes: @ @home @nix @log @snapshots
+#
+# Apply on the laptop with (prompts for the LUKS passphrase = fallback keyslot):
+# sudo nix run github:nix-community/disko/latest -- \
+# --mode destroy,format,mount ./machines/enzo/disko.nix
+{ ... }:
+
+{
+ disko.devices = {
+ disk.main = {
+ type = "disk";
+ device = "/dev/nvme0n1"; # TODO: confirm on the laptop with `lsblk`
+ content = {
+ type = "gpt";
+ partitions = {
+ ESP = {
+ size = "1G";
+ type = "EF00";
+ content = {
+ type = "filesystem";
+ format = "vfat";
+ mountpoint = "/boot";
+ mountOptions = [ "umask=0077" ];
+ };
+ };
+ luks = {
+ size = "100%";
+ content = {
+ type = "luks";
+ name = "crypted";
+ settings = {
+ # Allow TRIM through LUKS (minor metadata leak, fine for a laptop SSD).
+ allowDiscards = true;
+ };
+ content = {
+ type = "lvm_pv";
+ vg = "vg";
+ };
+ };
+ };
+ };
+ };
+ };
+
+ lvm_vg.vg = {
+ type = "lvm_vg";
+ lvs = {
+ swap = {
+ size = "20G"; # TODO: set >= RAM (RAM + a little) for hibernation
+ content = {
+ type = "swap";
+ resumeDevice = true; # emits swapDevices + boot.resumeDevice
+ };
+ };
+ root = {
+ size = "100%FREE";
+ content = {
+ type = "btrfs";
+ extraArgs = [ "-f" ];
+ subvolumes = {
+ "@" = {
+ mountpoint = "/";
+ mountOptions = [ "compress=zstd" "noatime" ];
+ };
+ "@home" = {
+ mountpoint = "/home";
+ mountOptions = [ "compress=zstd" "noatime" ];
+ };
+ "@nix" = {
+ mountpoint = "/nix";
+ mountOptions = [ "compress=zstd" "noatime" ];
+ };
+ "@log" = {
+ mountpoint = "/var/log";
+ mountOptions = [ "compress=zstd" "noatime" ];
+ };
+ "@snapshots" = {
+ mountpoint = "/.snapshots";
+ };
+ };
+ };
+ };
+ };
+ };
+ };
+}
diff --git a/machines/enzo/hardware-configuration.nix b/machines/enzo/hardware-configuration.nix
new file mode 100644
index 0000000..30c00ee
--- /dev/null
+++ b/machines/enzo/hardware-configuration.nix
@@ -0,0 +1,27 @@
+# PLACEHOLDER — regenerate on the laptop.
+#
+# During install run: nixos-generate-config --no-filesystems --root /mnt
+# then copy the generated hardware-configuration.nix over this file. Because the
+# disk layout is declared in ./disko.nix, this file must NOT define fileSystems
+# or swapDevices (--no-filesystems omits them). It only needs to evaluate until
+# the real one is generated; it will NOT boot as-is.
+{ config, lib, pkgs, modulesPath, ... }:
+
+{
+ imports = [
+ (modulesPath + "/installer/scan/not-detected.nix")
+ ];
+
+ # nixos-generate-config fills these in on the laptop. TPM unlock in initrd
+ # needs the TPM module (tpm_tis / tpm_crb) here — usually auto-detected.
+ boot.initrd.availableKernelModules = [ ];
+ boot.initrd.kernelModules = [ ];
+ boot.kernelModules = [ ];
+ boot.extraModulePackages = [ ];
+
+ # fileSystems / swapDevices intentionally omitted — provided by ./disko.nix.
+
+ networking.useDHCP = lib.mkDefault true;
+
+ nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
+}
diff --git a/machines/kusanagi/default.nix b/machines/kusanagi/default.nix
new file mode 100644
index 0000000..10531c2
--- /dev/null
+++ b/machines/kusanagi/default.nix
@@ -0,0 +1,317 @@
+# kusanagi — AMD workstation. Everything here is specific to this machine:
+# ROCm/GPU, the Focusrite Scarlett audio interface, optical-disc archiving, the
+# NAS mounts, and the Kavita/Immich/Ollama servers.
+{ pkgs, ... }:
+
+{
+ imports = [
+ ../../profiles/desktop.nix
+ ../../profiles/dev.nix
+ ../../profiles/apps.nix
+ ./hardware-configuration.nix
+ ];
+
+ networking.hostName = "kusanagi";
+
+ # This value determines the NixOS release from which the default
+ # settings for stateful data, like file locations and database versions
+ # on your system were taken. It‘s perfectly fine and recommended to leave
+ # this value at the release version of the first install of this system.
+ system.stateVersion = "24.11"; # Did you read the comment?
+
+ powerManagement.cpuFreqGovernor = "performance";
+
+ boot.binfmt.emulatedSystems = [ "aarch64-linux" ];
+
+ # --- GPU (ROCm) ---
+ nixpkgs.config.rocmSupport = true;
+ hardware.graphics.extraPackages = with pkgs; [
+ rocmPackages.clr.icd
+ ];
+
+ # --- Optical disc drives / archiving ---
+ boot.kernelModules = [ "sg" ]; # for disc drive
+
+ # For disc drives
+ services.udev.extraRules = ''
+ KERNEL=="sr[0-9]*", GROUP="cdrom", MODE="0660"
+ KERNEL=="sg[0-9]*", GROUP="sg", MODE="0660"
+ '';
+
+ services.pcscd.enable = true;
+
+ # abcde: patch its musicbrainz tool to accept discs with 4 args.
+ nixpkgs.overlays = [
+ (self: super: {
+ abcde = super.abcde.overrideAttrs (oldAttrs: {
+ # We use postInstall to modify the script after it has been copied to the output directory
+ postInstall = (oldAttrs.postInstall or "") + ''
+ # Patch 1: Allow GetOptions to accept 4 arguments (instead of requiring 5+)
+ sed -i 's/discinfo=i{5,}/discinfo=i{4,}/g' $out/bin/abcde-musicbrainz-tool
+
+ # Patch 2: Lower the logic check from 5 to 4
+ sed -i 's/$#discinfo < 5/$#discinfo < 4/g' $out/bin/abcde-musicbrainz-tool
+ '';
+ });
+ })
+ ];
+
+ # --- Scanner ---
+ hardware.sane.enable = true;
+
+ # --- Audio interface (Focusrite Scarlett 18i20) ---
+ # Add this for better pro audio support
+ services.pipewire.extraConfig.pipewire."10-low-latency" = {
+ "context.properties" = {
+ "default.clock.rate" = 48000;
+ "default.clock.quantum" = 256;
+ "default.clock.min-quantum" = 256;
+ "default.clock.max-quantum" = 2048;
+ };
+ };
+
+ # Pin the Scarlett 18i20 to the raw "Pro Audio" profile.
+ # The 26.05 pipewire/wireplumber update began preferring the UCM "HiFi"
+ # profile, which splits the interface into Direct1/Line5/SPDIF1/... sinks and
+ # broke monitoring. This priority rule forces the multichannel Pro Audio
+ # profile statelessly on every boot (and survives future UCM renames).
+ services.pipewire.wireplumber.extraConfig."51-scarlett-pro-audio" = {
+ "device.profile.priority.rules" = [
+ {
+ matches = [
+ {
+ "device.name" = "alsa_card.usb-Focusrite_Scarlett_18i20_USB_P9DCR6Y378D1E6-00";
+ }
+ ];
+ actions = {
+ update-props = {
+ priorities = [ "pro-audio" ];
+ };
+ };
+ }
+ ];
+ };
+
+ # Real-time audio optimizations
+ security.pam.loginLimits = [
+ {
+ domain = "@audio";
+ item = "memlock";
+ type = "-";
+ value = "unlimited";
+ }
+ {
+ domain = "@audio";
+ item = "rtprio";
+ type = "-";
+ value = "99";
+ }
+ {
+ domain = "@audio";
+ item = "nofile";
+ type = "soft";
+ value = "99999";
+ }
+ {
+ domain = "@audio";
+ item = "nofile";
+ type = "hard";
+ value = "99999";
+ }
+ ];
+
+ # Kernel optimizations for audio
+ boot.kernelParams = [
+ "threadirqs"
+ "preempt=full" # if using a PREEMPT kernel
+ # Add USB audio optimizations
+ "usbcore.usbfs_memory_mb=1024"
+ ];
+
+ # --- MPD (music) ---
+ services.mpd.enable = false;
+
+ systemd.user.services.mpd = {
+ description = "Music Player Daemon";
+ after = [
+ "network.target"
+ "sound.target"
+ ];
+ wantedBy = [ "default.target" ];
+ serviceConfig = {
+ ExecStart = "${pkgs.mpd}/bin/mpd --no-daemon";
+ Type = "notify";
+ LimitRTPRIO = 50;
+ LimitRTTIME = "infinity";
+ };
+ };
+
+ # --- NAS (NFS) ---
+ boot.supportedFilesystems = [ "nfs" ];
+
+ services.nfs.server.enable = false;
+ services.rpcbind.enable = true;
+
+ services.nfs.idmapd.settings = {
+ General = {
+ Domain = "kusanagi"; # or your actual domain name
+ };
+ };
+
+ # NAS shares. Use the mDNS-resolvable name (bare "milgrim" doesn't resolve)
+ # and automount on first access so a boot-time DNS/network race can't leave
+ # them unmounted (which silently breaks MPD, whose DB points at these paths).
+ fileSystems."/mnt/milgrim-share" = {
+ device = "milgrim.local:/volume1/share";
+ fsType = "nfs";
+ options = [
+ "nfsvers=4"
+ "rsize=8192"
+ "wsize=8192"
+ "_netdev"
+ "noauto"
+ "x-systemd.automount"
+ "x-systemd.idle-timeout=600"
+ "x-systemd.mount-timeout=10s"
+ ];
+ };
+
+ fileSystems."/mnt/milgrim-public" = {
+ device = "milgrim.local:/volume1/public";
+ fsType = "nfs";
+ options = [
+ "nfsvers=4"
+ "rsize=8192"
+ "wsize=8192"
+ "_netdev"
+ "noauto"
+ "x-systemd.automount"
+ "x-systemd.idle-timeout=600"
+ "x-systemd.mount-timeout=10s"
+ ];
+ };
+
+ # --- Servers ---
+ services.kavita = {
+ enable = true;
+ tokenKeyFile = "/var/lib/kavita/token-key";
+ settings.Port = 5000;
+ };
+
+ systemd.tmpfiles.rules = [
+ "d /srv/library 2750 henz kavita - -"
+ ];
+
+ services.ollama = {
+ enable = true;
+ package = pkgs.ollama-rocm;
+ environmentVariables = {
+ HIP_VISIBLE_DEVICES = "0";
+ OLLAMA_CONTEXT_LENGTH = "32768";
+ OLLAMA_KV_CACHE_TYPE = "q8_0";
+ };
+ };
+
+ services.immich = {
+ enable = true;
+ machine-learning.enable = false;
+ };
+
+ # --- discidurl / haunt tooling (needs the per-machine overlays in flake.nix) ---
+ environment.systemPackages =
+ let
+ # open the MusicBrainz submission URL for the disc in the drive
+ discid = pkgs.writeShellApplication {
+ name = "discid";
+ runtimeInputs = [
+ pkgs.discidurl
+ pkgs.xdg-utils
+ ];
+ text = "discidurl | xargs -r xdg-open";
+ };
+ in
+ with pkgs;
+ [
+ discidurl
+ discid
+ haunt
+ ];
+
+ # --- User account (henz) ---
+ # (Desktop groups audio/lp/input come from profiles/desktop.nix.)
+ users.users.henz.isNormalUser = true;
+ users.users.henz.description = "Henry Webster";
+ users.users.henz.extraGroups = [
+ # base
+ "networkmanager"
+ "wheel"
+ # kusanagi hardware
+ "cdrom"
+ "optical"
+ "sg"
+ "scanner"
+ ];
+
+ nix.settings.trusted-users = [
+ "root"
+ "henz"
+ ];
+
+ users.users.henz.packages = with pkgs; [
+ # base CLI
+ git
+ neovim
+ tmux
+ btop
+ ripgrep
+ unzip
+ xclip
+ bc
+ psmisc
+ usbutils
+ stow
+ nushell
+
+ # kusanagi
+ blender
+ rocmPackages.rpp
+ rocmPackages.hipcc
+ rocmPackages.rocminfo
+ ardour
+ davinci-resolve-studio
+ calf
+ alsa-utils
+ alsa-tools
+ pipewire.jack
+ nfs-utils
+
+ # music
+ mpd
+ mpc
+ ncmpcpp
+
+ # for archiving
+ #makemkv
+ abcde
+ libaacs
+ libbdplus
+ libdvdcss
+ mkvtoolnix
+ dvdbackup
+ chromaprint
+ picard
+ flac
+
+ # scanners
+ simple-scan
+
+ (pkgs.makeDesktopItem {
+ name = "PICO-8";
+ desktopName = "PICO-8";
+ exec = "pico8";
+ icon = "pico8";
+ comment = "Fantasy console for making, sharing and playing tiny games";
+ categories = [ "Game" ];
+ })
+ ];
+}
diff --git a/hardware-configuration.nix b/machines/kusanagi/hardware-configuration.nix
index 9c601e9..9c601e9 100644
--- a/hardware-configuration.nix
+++ b/machines/kusanagi/hardware-configuration.nix
diff --git a/profiles/apps.nix b/profiles/apps.nix
new file mode 100644
index 0000000..279ef03
--- /dev/null
+++ b/profiles/apps.nix
@@ -0,0 +1,30 @@
+# Apps profile: GUI and personal applications, plus gaming. Opt-in per machine.
+{ pkgs, ... }:
+
+{
+ programs.steam.enable = true;
+ programs.firefox.enable = true;
+
+ environment.systemPackages = with pkgs; [
+ itch
+ ];
+
+ users.users.henz.packages = with pkgs; [
+ discord
+ vlc
+ gimp
+ inkscape
+ darktable
+ protonmail-desktop
+
+ # media tooling
+ imagemagick
+ ffmpeg
+
+ flatpak
+
+ # TUI apps
+ newsboat
+ castero
+ ];
+}
diff --git a/profiles/desktop.nix b/profiles/desktop.nix
new file mode 100644
index 0000000..b56ac68
--- /dev/null
+++ b/profiles/desktop.nix
@@ -0,0 +1,171 @@
+# Desktop profile: the GNOME+niri graphical shell, login manager, audio, fonts,
+# graphics, printing, a terminal, and the gpg agent. Opt-in per machine.
+{ pkgs, ... }:
+
+{
+ # Enable the X11 windowing system.
+ services.xserver.enable = true;
+
+ # Configure keymap in X11
+ services.xserver.xkb = {
+ layout = "us";
+ variant = "";
+ };
+
+ # Enable the GNOME Desktop Environment.
+ services.displayManager.gdm.enable = false;
+ services.desktopManager.gnome.enable = true;
+
+ programs.dconf.enable = true;
+ programs.niri.enable = true;
+
+ # login screen
+ services.greetd = {
+ enable = true;
+ settings = {
+ default_session = {
+ command = "${pkgs.tuigreet}/bin/tuigreet --time --remember --cmd niri-session";
+ user = "greeter";
+ };
+ };
+ };
+
+ # This is required to let tuigreet write to its cache (for --remember)
+ systemd.services.greetd.serviceConfig = {
+ Type = "idle";
+ StandardInput = "tty";
+ StandardOutput = "tty";
+ StandardError = "journal"; # Better for debugging
+ TTYReset = true;
+ TTYVHangup = true;
+ TTYVTDisallocate = true;
+ };
+
+ # prevent auto-mounting optical disks
+ services.udisks2.enable = true;
+
+ # Enable CUPS to print documents.
+ services.printing = {
+ enable = true;
+ drivers = with pkgs; [
+ gutenprint
+ ];
+ };
+
+ # mDNS: printer discovery, and resolves kusanagi's *.local NAS names.
+ services.avahi = {
+ enable = true;
+ nssmdns4 = true;
+ openFirewall = true;
+ };
+
+ services.pulseaudio.enable = false;
+ security.rtkit.enable = true;
+ services.pipewire = {
+ enable = true;
+ alsa.enable = true;
+ alsa.support32Bit = true;
+ pulse.enable = true;
+ jack.enable = true;
+ wireplumber.enable = true;
+ };
+
+ programs.gnupg.agent = {
+ enable = true;
+ enableSSHSupport = true;
+ };
+
+ fonts = {
+ enableDefaultPackages = true;
+
+ packages = with pkgs; [
+ noto-fonts-cjk-sans
+ noto-fonts-cjk-serif
+ inter
+ nerd-fonts.symbols-only
+ noto-fonts
+ ibm-plex
+ public-sans
+ adwaita-fonts
+ ];
+
+ fontconfig = {
+ enable = true;
+
+ defaultFonts = {
+ monospace = [
+ "Berkeley Mono"
+ "Liberation Mono"
+ "Symbols Nerd Font"
+ ];
+ sansSerif = [
+ "Liberation Sans"
+ "Noto Sans"
+ ];
+ serif = [ "Liberation Serif" ];
+ emoji = [ "Nerd Font Symbols Only" ];
+ };
+
+ # Force ui-sans-serif and system-ui to resolve correctly
+ localConf = ''
+ <?xml version="1.0"?>
+ <!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd">
+ <fontconfig>
+ <alias>
+ <family>system-ui</family>
+ <prefer>
+ <family>Liberation Sans</family>
+ </prefer>
+ </alias>
+ <alias>
+ <family>ui-sans-serif</family>
+ <prefer>
+ <family>Liberation Sans</family>
+ </prefer>
+ </alias>
+ </fontconfig>
+ '';
+ };
+
+ fontDir.enable = true;
+ };
+
+ environment.sessionVariables = {
+ GSK_RENDERER = "gl";
+ XCURSOR_THEME = "Bibata-Modern-Classic";
+ XCURSOR_SIZE = "24";
+ HYPRCURSOR_THEME = "Bibata-Modern-Classic";
+ HYPRCURSOR_SIZE = "24";
+ };
+
+ # Graphics setup
+ hardware.graphics = {
+ enable = true;
+ enable32Bit = true;
+ };
+
+ users.users.henz.extraGroups = [
+ "audio"
+ "lp"
+ "input"
+ ];
+
+ environment.systemPackages = with pkgs; [
+ # notifications (mako = wayland notification daemon, libnotify = notify-send)
+ mako
+ libnotify
+ ];
+
+ users.users.henz.packages = with pkgs; [
+ ghostty # terminal — required to actually use the GUI
+ gnupg
+ pinentry-gnome3
+
+ # niri support tools
+ waybar
+ networkmanagerapplet
+ xwayland-satellite
+ fuzzel
+ swaybg
+ ];
+}
diff --git a/profiles/dev.nix b/profiles/dev.nix
new file mode 100644
index 0000000..e20fe5a
--- /dev/null
+++ b/profiles/dev.nix
@@ -0,0 +1,85 @@
+# Dev profile: compilers, language servers, dev CLIs, and Emacs. Opt-in per machine.
+{ pkgs, ... }:
+
+{
+ environment.systemPackages = with pkgs; [
+ zig
+ prettier
+
+ (aspellWithDicts (
+ dicts: with dicts; [
+ en
+ en-computers
+ en-science
+ ]
+ ))
+
+ pi-coding-agent
+
+ (buildNpmPackage rec {
+ pname = "pi-acp";
+ version = "0.0.31";
+ src = fetchFromGitHub {
+ owner = "svkozak";
+ repo = "pi-acp";
+ rev = "v${version}";
+ hash = "sha256-bM3V/3fxkY2Ib+OyfT82StIIRSLXGDuYUbt1CZKpTuo=";
+ };
+ npmDepsHash = "sha256-qN+b/tMbnJLkWjotl3XrA0nfZ3KT/mT6gM+n3Qiz8Wk=";
+ })
+ ];
+
+ users.users.henz.packages = with pkgs; [
+ # toolchains / build
+ gcc
+ pkg-config
+ gnumake
+ autoconf
+ automake
+ libtool
+ cargo
+ nodejs
+ guile
+ direnv
+ gdb
+
+ # language servers / formatters
+ lua-language-server
+ nil
+ clang-tools
+ marksman
+ gopls
+ ruff
+ basedpyright
+ zls
+ nixfmt
+
+ # coding agents
+ opencode
+ claude-code
+ ];
+
+ services.emacs = {
+ enable = true;
+ package =
+ let
+ emacs = pkgs.emacs-pgtk.overrideAttrs (old: {
+ postInstall = (old.postInstall or "") + ''
+ # I only want the client version
+ rm -f $out/share/applications/emacs.desktop
+ '';
+ });
+ in
+ (pkgs.emacsPackagesFor emacs).withPackages (epkgs: [
+ # NB: no g.tree-sitter-zig -- nixpkgs builds it from maxxnino/tree-sitter-zig,
+ # but zig-ts-mode requires the tree-sitter-grammars/tree-sitter-zig variant
+ # (different node names). The zig grammar is installed instead via
+ # `C-u M-x zig-ts-install-grammars' into ~/.config/emacs/tree-sitter/.
+ (epkgs.treesit-grammars.with-grammars (g: [
+ g.tree-sitter-nix
+ g.tree-sitter-javascript
+ ]))
+ epkgs.vterm
+ ]);
+ };
+}
diff --git a/programs.nix b/programs.nix
deleted file mode 100644
index 2872cb4..0000000
--- a/programs.nix
+++ /dev/null
@@ -1,98 +0,0 @@
-{ pkgs, ... }:
-
-let
- # open the MusicBrainz submission URL for the disc in the drive
- discid = pkgs.writeShellApplication {
- name = "discid";
- runtimeInputs = [
- pkgs.discidurl
- pkgs.xdg-utils
- ];
- text = "discidurl | xargs -r xdg-open";
- };
-in
-{
- environment.systemPackages = with pkgs; [
- discidurl
- discid
-
- itch
- haunt
-
- # --- wayland
- mako
-
- # provides notify-send
- libnotify
-
- # website is down :(
- # makemkv
-
- (aspellWithDicts (
- dicts: with dicts; [
- en
- en-computers
- en-science
- ]
- ))
-
- pi-coding-agent
-
- (buildNpmPackage rec {
- pname = "pi-acp";
- version = "0.0.31";
- src = fetchFromGitHub {
- owner = "svkozak";
- repo = "pi-acp";
- rev = "v${version}";
- hash = "sha256-bM3V/3fxkY2Ib+OyfT82StIIRSLXGDuYUbt1CZKpTuo=";
- };
- npmDepsHash = "sha256-qN+b/tMbnJLkWjotl3XrA0nfZ3KT/mT6gM+n3Qiz8Wk=";
- })
-
- ];
-
- programs = {
- steam.enable = true;
- firefox.enable = true;
-
- gnupg.agent = {
- enable = true;
- enableSSHSupport = true;
- };
- };
-
- services = {
- emacs = {
- enable = true;
- package =
- let
- emacs = pkgs.emacs-pgtk.overrideAttrs (old: {
- postInstall = (old.postInstall or "") + ''
- # I only want the client version
- rm -f $out/share/applications/emacs.desktop
- '';
- });
- in
- (pkgs.emacsPackagesFor emacs).withPackages (epkgs: [
- (epkgs.treesit-grammars.with-grammars (g: [ g.tree-sitter-nix ]))
- epkgs.vterm
- ]);
- };
-
- ollama = {
- enable = true;
- package = pkgs.ollama-rocm;
- environmentVariables = {
- HIP_VISIBLE_DEVICES = "0";
- OLLAMA_CONTEXT_LENGTH = "32768";
- OLLAMA_KV_CACHE_TYPE = "q8_0";
- };
- };
- };
-
- services.immich = {
- enable = true;
- machine-learning.enable = false;
- };
-}