Files
nevaforgetandClaude Opus 5 9dce00ff8a feat(desktop): declare the real disk paths
hosts/desktop carried two SET-VIA-disko-install placeholders, so the file
did not say which disks the machine has. The invalid paths only ever
caught a missing --disk flag, never a wrong one, and paid for it by
leaving the hardware undocumented.

The system disk is the 512G XPG, /home goes on the 1T Crucial;
disko-install --disk main|home still overrides both. The 1T currently
holds an ext4 Data partition, which the install erases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 16:50:27 +02:00

305 lines
12 KiB
Markdown

# moonix
NixOS configuration for a scrollable-tiling Wayland desktop: niri as the
compositor, Quickshell as the bar and launcher, regreet as the login greeter,
swaylock as the screen locker, Catppuccin Mocha throughout.
Ships Nix packages for the components that are not in nixpkgs — stasis,
sweet-cursors, the moonarch helper scripts — and NixOS modules that assemble them
into a working session. Greeter, locker and power menu are no longer own programs:
`regreet` and `swaylock` come from nixpkgs, the power menu is a Quickshell popout
inside the moonarch config.
The XDG configuration files (niri, Quickshell, foot, GTK, Qt) come from the
`moonarch` repository, which is consumed as a flake input. One set of config
files therefore stays valid on both NixOS and Arch.
## Layout
```
flake.nix Inputs and outputs
pkgs/ Build recipes
stasis.nix idle manager
sweet-cursors.nix cursor theme
moonarch-scripts.nix battery and camera helpers
modules/
user.nix moonarch.user option, set per host
disk.nix partitioning: GPT, optional LUKS2, btrfs subvolumes
desktop.nix /etc deployment, package set, theme
greetd.nix greetd + regreet, autologin per host
services.nix systemd system and user services
hosts/
testvm/ QEMU test VM
thinkpad/ ThinkPad T14 Gen 3 (AMD)
desktop/ self-built AMD desktop, second disk for /home
```
`nixosModules.moonarch` is the entry point: it imports disko's NixOS module
alongside the five above and hands them the packages and the config source.
## Usage
Build a single package:
```bash
nix build .#stasis
```
Build a whole system without activating it, which is also how a host is checked
for evaluation errors without touching anything:
```bash
nix build .#nixosConfigurations.testvm.config.system.build.toplevel
```
Activate. This runs on the machine being changed, out of a checkout that sits
there — see "Keeping a machine up to date" for how that checkout gets there:
```bash
sudo nixos-rebuild switch --flake .#testvm
```
## Installing on a machine
The disk layout is declared in `modules/disk.nix`, so nothing has to be
generated on the target machine and no file travels back into this repo. Boot
the NixOS ISO and run one command.
> **This erases the disk given to `--disk`.** Check the path against `lsblk` on
> the machine you are standing in front of. Do not copy the path below without
> looking — it is this ThinkPad's SSD.
1. Partition and install in one go:
```bash
sudo nix --experimental-features "nix-command flakes" run \
'github:nix-community/disko/latest#disko-install' -- \
--write-efi-boot-entries \
--flake 'git+https://gitea.moonarch.de/nevaforget/moonix.git#thinkpad' \
--disk main /dev/disk/by-id/nvme-SKHynix_HFS001TEJ9X102N_SDB7N7740101A8708
```
The LUKS passphrase is asked for twice while formatting. The repo is fetched
from the flake reference, so the ISO needs no `git` and nothing is cloned.
`--write-efi-boot-entries` is not optional here. Without it `disko-install`
forces `boot.loader.efi.canTouchEfiVariables` off, overriding the `true` in
the host file, and the install finishes without an NVRAM boot entry. Leave it
out only when the disk is installed in one machine and then moved to another.
2. Reboot.
The account comes up with the initial password declared in the host file
(`users.users.<name>.initialPassword`, currently `moonarch`). Autologin gets
past the greeter either way, but swaylock authenticates through pam_unix and
cannot unlock a screen for an account without a password, so one has to exist
from the first boot. Change it with `passwd` afterwards — `mutableUsers` is at
its default, so that sticks and the declared value is only ever used to create
the account.
`hardware-configuration.nix` is not used and not needed. NixOS puts `nvme`,
`ahci`, `sd_mod` and the USB HID modules into the initrd by default
(`boot.initrd.includeDefaultModules`), `nixpkgs.hostPlatform` comes from the
flake, and the `nixos-hardware` profile for the T14 Gen 3 (AMD) covers
firmware, microcode, power management and graphics quirks.
Only the partitioning step reads `moonarch.disk.device`. Nothing the booted
system uses depends on it: the ESP entry and the generated
`boot.initrd.luks.devices` point at `/dev/disk/by-partlabel/`, and the
filesystems inside the container come up as `/dev/mapper/crypted`. Either way
the disk may enumerate differently next time.
### Adding another machine
1. New directory under `hosts/`, with a `default.nix` holding the machine
specific settings — hostname, users, locale, bootloader.
2. Point the shared layout at its disk:
```nix
moonarch.disk = {
enable = true;
device = "/dev/disk/by-id/..."; # check against lsblk
};
```
`encrypt = false` drops the LUKS layer, `swapSize = ""` the swapfile, and
`espSize` resizes the ESP. Everything else is shared.
A machine with a second disk for `/home` adds `home`, which moves the
subvolume off the first disk:
```nix
moonarch.disk.home = {
enable = true;
device = "/dev/disk/by-id/...";
};
```
Set the login name in the same file. `moonarch.user` has no default and is
read by `users.users`, the autologin session and snapper, so the name is
declared once per machine.
3. Add a `nixosConfigurations.<name>` entry in `flake.nix`.
4. Install with the same `disko-install` command, with `#<name>` and that
machine's disk. A second disk is another flag:
```bash
--disk main /dev/disk/by-id/... --disk home /dev/disk/by-id/...
```
Both disks are erased. `disko-install` overrides whatever paths the host
file declares, so the flags are what decides which disks are written —
compare them against `lsblk` and `ls -l /dev/disk/by-id/` first.
Without a matching `nixos-hardware` profile, set
`hardware.cpu.intel.updateMicrocode` or `hardware.cpu.amd.updateMicrocode` in
the host file — the profile is what provides it for the ThinkPad.
### Disk layout
| Partition / subvolume | Mountpoint | Notes |
|---|---|---|
| ESP, `espSize` (1G) | `/boot` | vfat, `umask=0077` |
| `root` | `/` | btrfs, `compress=zstd`, `noatime` |
| `home` | `/home` | same; on the second disk where `home.enable` is set |
| `nix` | `/nix` | same; separate to keep the store out of snapshots |
| `swap` | `/.swapvol` | swapfile, `swapSize` (8G) |
With `encrypt = true` everything below the ESP sits in a LUKS2 container named
`crypted`. Hibernation is not set up — see `DECISIONS.md`.
A second disk gets its own container, `crypted-home`, and no ESP. Both are
formatted separately, so `disko-install` asks for a passphrase four times
instead of two. Give them the same one: both are unlocked from the initrd,
where systemd caches the first passphrase and retries it on the second
container, so booting prompts once.
Worth doing once the disks are formatted, though nothing here depends on it: back
up the LUKS headers. They hold the encrypted keyslots, and a damaged header loses
the data whatever the passphrase is. A header only changes when its keyslots do,
so once after the install and again after a passphrase change is enough:
```bash
sudo cryptsetup luksHeaderBackup /dev/disk/by-partlabel/disk-main-root \
--header-backup-file /run/media/<medium>/<host>-main.header
```
A second disk carries its own header on `disk-home-home`. Whoever holds such a
file can attack the passphrase offline, so it belongs on a medium that gets put
away — not in this repository and not in a synced directory.
## Keeping a machine up to date
`disko-install` built the system from the flake reference and left no checkout
behind, so a freshly installed machine runs its configuration without holding
it. The checkout goes to `~/flakes/moonix` and belongs to the account, so commits
and pushes carry the account's identity. It is not read as configuration by
anything on the machine — it is the source a system is built from, which is why
it sits outside `~/.config`:
```bash
git clone git@gitea.moonarch.de:nevaforget/moonix.git ~/flakes/moonix
sudo ln -s ~/flakes/moonix/flake.nix /etc/nixos/flake.nix
```
`nixos-rebuild` treats `/etc/nixos/flake.nix` as if `--flake` had been given and
resolves the flake to the directory the symlink points into, so `/etc/nixos`
itself is neither a repository nor a flake. Without an attribute the
configuration is picked by hostname, which is why the `nixosConfigurations` are
named after the machines.
Changing the machine is then an edit, a build and a commit:
```bash
cd ~/flakes/moonix
$EDITOR hosts/thinkpad/default.nix
sudo nixos-rebuild switch
git commit -am 'thinkpad: ...'
git push
```
The build reads the working tree, so a change is testable before it is
committed. Nix only sees tracked files though — a new file under `hosts/` or
`modules/` has to be `git add`ed or evaluation fails on the missing path.
A machine that is never edited locally can skip the checkout and build from the
reference, which is what `disko-install` did:
```bash
sudo nixos-rebuild switch \
--flake 'git+https://gitea.moonarch.de/nevaforget/moonix.git#thinkpad'
```
Every `switch` leaves a generation behind. A bad one is left through the boot
menu, where the previous generation is still listed, or with `sudo
nixos-rebuild switch --rollback` from a system that still comes up.
## Version bumps
`stasis` is pinned to a release tag. After a new upstream release:
```bash
# adjust the tag in flake.nix, then
nix flake update stasis
```
`moonarch` tracks `main`, so its config changes arrive with
`nix flake update moonarch`.
## Configuration files
Config files are deployed unchanged wherever possible. These are generated
instead, because they contain a path that does not exist under Nix, or because a
NixOS module owns the destination:
| File | Reason |
|---|---|
| `greetd/regreet.toml`, `regreet.css` | written by `services.displayManager.regreet` from module options; the wallpaper points at the store path |
| `swaylock/config` | wallpaper path, rewritten and seeded into `$HOME` — swaylock's sysconfdir is inside the store, so a system-wide copy is never read |
| `waypaper/config.ini` | wallpaper path, rewritten and seeded into `$HOME` |
| fontconfig defaults | `/etc/fonts/conf.d` is owned by the NixOS module |
`kanshi/config` is not deployed: the file in the config source is empty, and
monitor profiles are machine specific — they belong in
`~/.config/kanshi/config`. A missing config file is what kanshi treats as an
error (`failed to parse config file`), which with `Restart=on-failure` would
spin, so the service starts only once that file exists.
Programs that read only from `$HOME` and have no system-wide fallback get a
copy seeded through `systemd.user.tmpfiles` with the `C` directive, which never
overwrites later edits.
## Test VM
`hosts/testvm` targets a QEMU guest. The launcher script lives outside this
repo at `~/VMs/moonix-vm.sh`.
It uses the same layout module as real hardware, pointed at `/dev/vda` — the
launcher attaches the qcow2 as `if=virtio` and there is no `by-id` path for it.
LUKS stays enabled and the swapfile shrinks to 2G. Installing the guest is
therefore also the test for `modules/disk.nix`.
Recreating the disk from scratch:
```bash
qemu-img create -f qcow2 ~/VMs/moonix.qcow2 60G
cp /usr/share/edk2/x64/OVMF_VARS.4m.fd ~/VMs/moonix-vars.fd
~/VMs/moonix-vm.sh ~/Downloads/nixos-graphical-*.iso
```
Then run the `disko-install` command from above inside the live session, with
`#testvm` and `--disk main /dev/vda`.
`sudo` asks for a password here like on every other host. SSH accepts one:
`services.openssh.settings.PasswordAuthentication` is at its NixOS default of
`true`, and the account's password is the `initialPassword` from the host file,
which is in this repository. The guest is reachable on the launcher's forward on
`127.0.0.1:2222` with it. Anything a test needs beyond that belongs at runtime —
a drop-in under `/run/systemd/system/`, a key in `~/.ssh/authorized_keys` — so
it does not end up in version control.
The guest needs a 3D capable virtio GPU: niri's TTY backend fails every buffer
import with `Error::DeviceMissing` otherwise, and software rendering is not a
workaround. On the current host that path has twice triggered an amdgpu hard
recovery, killing the QEMU process — shut the VM down when not testing.