From 11ba75e40728e8b5735c9c7cefc7af1631c942af Mon Sep 17 00:00:00 2001 From: "Henry J. Webster" Date: Thu, 30 Jul 2026 10:38:54 -0500 Subject: Restructure into a multi-machine flake and add the enzo laptop Split the single-host config into a shared minimal base plus opt-in profiles, and add a second machine (enzo laptop) alongside kusanagi. - flake: add mkMachine helper; declare nixosConfigurations.kusanagi and .enzo; add the disko input; move overlays per-machine. - common/: minimal base only (boot, networking, tailscale, locale, nix settings, neovim editor). Old configuration.nix/programs.nix removed. - profiles/: coarse opt-in bundles (desktop, dev, apps). kusanagi imports all three and stays byte-for-byte identical to the running system. - machines/kusanagi/: all workstation-only config (ROCm, Scarlett audio, optical-disc archiving, NAS mounts, kavita/immich/ollama); hardware config moved here unchanged. User stays 'henz'. - machines/enzo/: new laptop. Declarative LUKS+btrfs via disko (LVM-on-LUKS, TPM2+PIN unlock, zram + encrypted swap for suspend-then-hibernate); its own minimal niri desktop with playback-only audio (no rtkit/jack); user 'hwebs'. Assisted-by: Claude:claude-opus-4-8 --- common/core.nix | 53 +++ common/default.nix | 9 + configuration.nix | 517 --------------------------- flake.lock | 21 ++ flake.nix | 27 +- hardware-configuration.nix | 39 -- machines/enzo/default.nix | 91 +++++ machines/enzo/desktop.nix | 68 ++++ machines/enzo/disko.nix | 93 +++++ machines/enzo/hardware-configuration.nix | 27 ++ machines/kusanagi/default.nix | 317 ++++++++++++++++ machines/kusanagi/hardware-configuration.nix | 39 ++ profiles/apps.nix | 30 ++ profiles/desktop.nix | 171 +++++++++ profiles/dev.nix | 85 +++++ programs.nix | 98 ----- 16 files changed, 1026 insertions(+), 659 deletions(-) create mode 100644 common/core.nix create mode 100644 common/default.nix delete mode 100644 configuration.nix delete mode 100644 hardware-configuration.nix create mode 100644 machines/enzo/default.nix create mode 100644 machines/enzo/desktop.nix create mode 100644 machines/enzo/disko.nix create mode 100644 machines/enzo/hardware-configuration.nix create mode 100644 machines/kusanagi/default.nix create mode 100644 machines/kusanagi/hardware-configuration.nix create mode 100644 profiles/apps.nix create mode 100644 profiles/desktop.nix create mode 100644 profiles/dev.nix delete mode 100644 programs.nix 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//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//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 = '' - - - - - system-ui - - Liberation Sans - - - - ui-sans-serif - - Liberation Sans - - - - ''; - }; - - 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/ 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/hardware-configuration.nix b/hardware-configuration.nix deleted file mode 100644 index 9c601e9..0000000 --- a/hardware-configuration.nix +++ /dev/null @@ -1,39 +0,0 @@ -# Do not modify this file! It was generated by ‘nixos-generate-config’ -# and may be overwritten by future invocations. Please make changes -# to /etc/nixos/configuration.nix instead. -{ config, lib, pkgs, modulesPath, ... }: - -{ - imports = - [ (modulesPath + "/installer/scan/not-detected.nix") - ]; - - boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ]; - boot.initrd.kernelModules = [ ]; - boot.kernelModules = [ "kvm-amd" ]; - boot.extraModulePackages = [ ]; - - fileSystems."/" = - { device = "/dev/disk/by-uuid/ad74db0c-c909-4503-84f6-cc08ca0b1e4b"; - fsType = "ext4"; - }; - - fileSystems."/boot" = - { device = "/dev/disk/by-uuid/9499-40FE"; - fsType = "vfat"; - options = [ "fmask=0077" "dmask=0077" ]; - }; - - swapDevices = [ ]; - - # Enables DHCP on each ethernet and wireless interface. In case of scripted networking - # (the default) this is the recommended approach. When using systemd-networkd it's - # still possible to use this option, but it's recommended to use it in conjunction - # with explicit per-interface declarations with `networking.interfaces..useDHCP`. - networking.useDHCP = lib.mkDefault true; - # networking.interfaces.enp9s0.useDHCP = lib.mkDefault true; - # networking.interfaces.wlp8s0.useDHCP = lib.mkDefault true; - - nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; - hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; -} 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/machines/kusanagi/hardware-configuration.nix b/machines/kusanagi/hardware-configuration.nix new file mode 100644 index 0000000..9c601e9 --- /dev/null +++ b/machines/kusanagi/hardware-configuration.nix @@ -0,0 +1,39 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ config, lib, pkgs, modulesPath, ... }: + +{ + imports = + [ (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ "nvme" "xhci_pci" "ahci" "usb_storage" "usbhid" "sd_mod" ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-amd" ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = + { device = "/dev/disk/by-uuid/ad74db0c-c909-4503-84f6-cc08ca0b1e4b"; + fsType = "ext4"; + }; + + fileSystems."/boot" = + { device = "/dev/disk/by-uuid/9499-40FE"; + fsType = "vfat"; + options = [ "fmask=0077" "dmask=0077" ]; + }; + + swapDevices = [ ]; + + # Enables DHCP on each ethernet and wireless interface. In case of scripted networking + # (the default) this is the recommended approach. When using systemd-networkd it's + # still possible to use this option, but it's recommended to use it in conjunction + # with explicit per-interface declarations with `networking.interfaces..useDHCP`. + networking.useDHCP = lib.mkDefault true; + # networking.interfaces.enp9s0.useDHCP = lib.mkDefault true; + # networking.interfaces.wlp8s0.useDHCP = lib.mkDefault true; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} 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 = '' + + + + + system-ui + + Liberation Sans + + + + ui-sans-serif + + Liberation Sans + + + + ''; + }; + + 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; - }; -} -- cgit v1.3