Merge branch 'main' of gitea.moonarch.de:nevaforget/moonix

# Conflicts:
#	DECISIONS.md
#	modules/services.nix
This commit is contained in:
nevaforget committed 2026-09-10 13:38:34 +02:00
commit e205ea6358
10 files changed
+438 -127

No files matched your search

+35
View File
@@ -1,5 +1,40 @@
# Decisions
## 2026-09-08 – nwg-displays replaces wdisplays; kanshi dropped for niri's own outputs
- **Who**: Dominik, ClaudeCode
- **Why**: Monitors could not be set persistently at all. moonarch pairs
`wdisplays-persistent` (AUR fork `sfs-pra/wdisplays`, see moonarch `README.md:25` and
its `DECISIONS.md:178`) with kanshi: the fork saves straight into the kanshi config.
The port took the nixpkgs `wdisplays` instead — upstream, with no save function — and
never recorded the difference. The result was a GUI that applies a layout and forgets
it, an always-empty `~/.config/kanshi/config`, and a `kanshi.service` whose
`ConditionPathExists` therefore skipped it on every boot.
- **Tradeoffs**: Packaging the fork would match moonarch exactly and keeps kanshi's
per-combination profiles, but it is unmaintained and would be ours to carry.
`nwg-displays` is in nixpkgs, actively developed, and supports niri as a first-class
target alongside sway and Hyprland; it writes `~/.config/niri/monitor.kdl` and adds the
`include` for it. That makes kanshi redundant — niri applies `output` blocks itself, on
hotplug and with live reload — so it goes, together with the hardening set it had just
been given. What is given up is kanshi's profiles *per attached combination*: niri has
one setting per output. Fixed monitors do not care; several docks with different
arrangements would, and then kanshi has to come back.
The seed is the same mechanism moonarch chose for stasis (its `DECISIONS.md`,
2026-04-22): niri has no config hierarchy either, so system-default-plus-user-override
cannot be modelled — the home file is seeded once and then belongs to the user.
- **How**: `modules/desktop.nix` — `wdisplays` → `nwg-displays`, `kanshi` dropped from
`environment.systemPackages`, new `niriUserConfig` in the `let` block holding
`include "/etc/niri/config.kdl"`, seeded through the existing
`systemd.user.tmpfiles.rules` block with `C` so later edits survive. The stale comment
about the undeployed kanshi config is gone from `environment.etc`, the paragraph in
`README.md` rewritten. `modules/services.nix` — `systemd.user.services.kanshi` removed
and `wlsunset`'s `after` reduced to `graphical-session.target`.
Verified: niri 26.04 does load `include` (an include pointing at a deliberately broken
file makes `niri validate` fail on that file, so it is parsed, not ignored);
`nwg-displays` 0.4.4 logs `Running on niri` and sees both outputs; without
`~/.config/niri/` it exits 0 with no window, and it creates `monitor.kdl` but no
`config.kdl` — which is what the seed is for.
## 2026-09-07 – A timer reports the age of the nixpkgs lock
- **Who**: Dominik, ClaudeCode
+8 -6
View File
@@ -12,7 +12,7 @@ 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.
files therefore serves every system that pulls it in.
## Layout
@@ -272,11 +272,13 @@ NixOS module owns the destination:
| `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.
`kanshi/config` is not deployed and kanshi is not installed: niri configures its
outputs itself, and `nwg-displays` writes the layout it is given to
`~/.config/niri/monitor.kdl`. niri prefers a home config over the deployed one
and does not merge them, so `~/.config/niri/config.kdl` is seeded with an
`include` of `/etc/niri/config.kdl` — the home config then adds the monitor
layout instead of replacing everything. nwg-displays also refuses to start
without that file.
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
Generated
+10 -10
View File
@@ -24,11 +24,11 @@
"moonarch": {
"flake": false,
"locked": {
"lastModified": 1787827834,
"narHash": "sha256-3yGZz3/JAtCmAQrh8jwTIAd6xCbE3O7e2Iar6AVnbLk=",
"lastModified": 1788961983,
"narHash": "sha256-ndUyq3X3E5vVdUflbCxdd2c+CBlXcNgn0rc5qiovCUM=",
"ref": "refs/heads/main",
"rev": "a1e06bf380b8c79a646930881d76cbe7137b4b77",
"revCount": 169,
"rev": "ba3745603cd01508669a70e56527e3d471a0d5f6",
"revCount": 179,
"type": "git",
"url": "https://gitea.moonarch.de/nevaforget/moonarch.git"
},
@@ -42,11 +42,11 @@
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1787144466,
"narHash": "sha256-HHfv2/HkNSKbbSyU9iD/g8lbP6r4tl33sSw1W4rXCk0=",
"lastModified": 1788335262,
"narHash": "sha256-3jBEq8avfzlrsY4UpW4d5TOmm7ocseZfnitEJhqauyc=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "0471accf8d0a8210b31d947497d179ecc99e0021",
"rev": "44d95795ee2d475b3d687325e26dcf4ca9104557",
"type": "github"
},
"original": {
@@ -70,11 +70,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1787736819,
"narHash": "sha256-cV5xEJJK3BvhU8rEd4mC9UsmDi5qscv/kzGPhBRC5WA=",
"lastModified": 1788316716,
"narHash": "sha256-bc7rSpXIdn9QWGNqfWcPZWOhEVF8NoeAZkWq0XWnf/k=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9fbb54b33e91ee4ca368e35a78e0613c720600b3",
"rev": "3ed67ec0a4d3c7ab4ae1f04f8ee8df07bfa506a2",
"type": "github"
},
"original": {
+1 -1
View File
@@ -15,7 +15,7 @@
};
# Project sources. These repos are not flakes themselves, they are
# consumed as plain source trees pinned to a release tag.
# consumed as plain source trees following their default branch.
moonarch = {
url = "git+https://gitea.moonarch.de/nevaforget/moonarch.git";
flake = false;
+22 -27
View File
@@ -5,14 +5,22 @@
{
# Read off the machine with `ls -l /dev/disk/by-id/`: the 512G XPG is the
# system disk, the 1T Crucial carries /home. disko-install still overrides
# both with `--disk main <path>` and `--disk home <path>`, so check them
# against lsblk before installing — by-id names follow the drive, not the
# slot, and a replaced disk changes them.
# system disk and carries /home as well. disko-install still overrides it with
# `--disk main <path>`, so check that against lsblk before installing — by-id
# names follow the drive, not the slot, and a replaced disk changes them.
#
# The 1T Crucial P1 carried /home until 09.09.2026 and now takes the storage
# role instead, which is the only one it can still be trusted with. Its
# controller reaches 90 °C under pure read load while the rest of the drive
# stays at 45, throttles from 1.5 GB/s down to under a megabyte, and it fell
# off the PCIe bus four times in four days (`controller is down,
# CSTS=0xffffffff`), forcing /home read-only. SMART is clean and P3CR013 is the
# final firmware, so there is nothing to repair — but as storage its failures
# cost a download, not a workday, and it can no longer delay a boot.
moonarch.disk = {
enable = true;
device = "/dev/disk/by-id/nvme-XPG_GAMMIX_S11_Pro_2K322LAES7JG";
home = {
storage = {
enable = true;
device = "/dev/disk/by-id/nvme-CT1000P1SSD8_2030E2BAC109";
};
@@ -67,28 +75,15 @@
user = config.moonarch.user;
};
# Snapshots of root and home. snap-pac has no counterpart here: NixOS keeps
# its own generations, so pre/post package snapshots are redundant. /home is
# its own btrfs on the second disk, which snapper does not care about — a
# subvolume is a subvolume.
services.snapper = {
configs = {
root = {
SUBVOLUME = "/";
ALLOW_USERS = [ config.moonarch.user ];
TIMELINE_CREATE = true;
TIMELINE_CLEANUP = true;
};
home = {
SUBVOLUME = "/home";
ALLOW_USERS = [ config.moonarch.user ];
TIMELINE_CREATE = true;
TIMELINE_CLEANUP = true;
};
};
snapshotInterval = "hourly";
cleanupInterval = "1d";
};
# No snapper here. It was configured for / and /home and never took a single
# snapshot: the module writes the configs and the timers, but nobody creates
# the .snapshots subvolumes they need, so snapper-timeline failed every hour
# with errno:2 for months without anyone noticing. Rather than paper over that,
# note what it would and would not have bought: rollbacks of / are already
# covered by NixOS generations, and snapshots of /home share a disk with /home,
# so they answer "I deleted the wrong file" but not "the disk is gone" — which
# is the failure that actually happened here on 09.09.2026. The second question
# needs a copy off this machine and is still unanswered.
# No nixos-hardware profile applies to a self-built desktop, so the firmware
# this machine needs is enabled here instead.
+93 -33
View File
@@ -8,7 +8,7 @@ let
etc = "${moonarchSrc}/defaults/etc";
# waypaper calls `swww`/`swww-daemon`, but the maintained successor ships as
# `awww`. Same compatibility symlinks the Arch package creates.
# `awww`. The symlinks keep the shared config working unchanged.
swww-compat = pkgs.runCommand "swww-compat" { } ''
mkdir -p "$out/bin"
ln -s ${pkgs.awww}/bin/awww "$out/bin/swww"
@@ -28,14 +28,28 @@ let
# Same treatment for swaylock's wallpaper path. swaylock searches
# ~/.swaylock/config, $XDG_CONFIG_HOME/swaylock/config and
# SYSCONFDIR/swaylock/config — and its sysconfdir is inside the store here,
# so the home copy seeded below is the only one it ever reads. On Arch the
# same file ships to /etc/swaylock/config, which is why it lives in
# so the home copy seeded below is the only one it ever reads. The file is a
# system config rather than an XDG one, which is why moonarch keeps it under
# defaults/etc/ rather than defaults/xdg/.
swaylockConfig = pkgs.runCommand "swaylock-config" { } ''
substitute ${etc}/swaylock/config "$out" \
--replace-fail /usr/share/moonarch/wallpaper-blur.jpg ${wallpaperBlur}
'';
# niri prefers ~/.config/niri/config.kdl over /etc/niri/config.kdl and does not
# merge the two, so a home config would otherwise replace the whole deployed
# one. This seed pulls the system config back in through `include`, which niri
# 26.04 really does load (verified by pointing an include at a broken file and
# watching `niri validate` fail on it).
#
# It exists for nwg-displays, which writes the output layout to
# ~/.config/niri/monitor.kdl and adds an include for it here. Without a
# config.kdl in the home directory nwg-displays refuses to start at all
# ("Couldn't find niri config directory", exit 0, no window).
niriUserConfig = pkgs.writeText "niri-config.kdl" ''
include "/etc/niri/config.kdl"
'';
# PipeWire searches $PIPEWIRE_CONFIG_DIR, then $XDG_CONFIG_HOME/pipewire, then
# its compiled-in /etc/pipewire — never /etc/xdg (verified in PipeWire's
# src/pipewire/conf.c). The shared file therefore cannot be deployed the way
@@ -43,7 +57,7 @@ let
# which is what its own option documentation shows for a conf.d file.
#
# The file names the plugin as `librnnoise_ladspa`, resolved through
# LADSPA_PATH — /usr/lib/ladspa on Arch, nothing here. Substituted to the
# LADSPA_PATH, which points at nothing here. Substituted to the
# store path, like the wallpaper paths above. LADSPA has no counterpart to
# services.pipewire.extraLv2Packages, so the path belongs in the file.
denoisingConfig = pkgs.runCommand "moonarch-pipewire-denoising" { } ''
@@ -54,12 +68,22 @@ let
"plugin = ${pkgs.rnnoise-plugin}/lib/ladspa/librnnoise_ladspa.so"
'';
# The theme deployed Kvantum/kvantum.kvconfig selects. Of the four places
# Kvantum searches (style/Kvantum.cpp), three are under $HOME and the fourth
# is a DATADIR compiled into the style plugin — which under Nix is the
# plugin's own store path. A separate theme package is therefore unreachable
# system-wide, and the directory is seeded into the home directory below.
kvantumTheme = pkgs.catppuccin-kvantum.override {
accent = "lavender";
variant = "mocha";
};
# zsh/newuser runs its setup wizard on every interactive login for as long as
# the user has none of .zshenv, .zprofile, .zshrc or .zlogin — the whole
# configuration living in /etc/zshrc does not count. Seeded below to stop
# that, and deliberately without configuration: on Arch this file carries
# `source /etc/zsh/zshrc.moonarch`, which here would source the shared zshrc
# a second time, on top of the one /etc/zshrc already did.
# that, and deliberately without configuration: moonarch's own copy of this
# file sources the shared zshrc, which here would run it a second time, on
# top of the one /etc/zshrc already did.
userZshrc = pkgs.writeText "zshrc" ''
# The moonarch zsh configuration is system-wide, in /etc/zshrc.
#
@@ -77,17 +101,12 @@ in
environment.etc = {
# niri reads /etc/niri, not /etc/xdg/niri. Both are provided: the former
# is what niri actually loads, the latter keeps the layout consistent with
# the Arch package and with any user config that includes it.
# moonarch's tree and with any user config that includes it.
"niri/config.kdl".source = "${xdg}/niri/config.kdl";
"xdg/niri/config.kdl".source = "${xdg}/niri/config.kdl";
"xdg/quickshell/moonarch".source = "${xdg}/quickshell/moonarch";
"xdg/foot/foot.ini".source = "${xdg}/foot/foot.ini";
"xdg/fastfetch/config.jsonc".source = "${xdg}/fastfetch/config.jsonc";
# defaults/xdg/kanshi/config is an empty placeholder — on Arch too. Not
# deployed: monitor profiles are machine specific and belong in
# ~/.config/kanshi/config. An empty file parses fine; a missing one is what
# kanshi reports as "failed to parse config file", which is why the unit in
# modules/services.nix waits for the home config to exist.
"xdg/Kvantum/kvantum.kvconfig".source = "${xdg}/Kvantum/kvantum.kvconfig";
"xdg/qt6ct/qt6ct.conf".source = "${xdg}/qt6ct/qt6ct.conf";
"xdg/gtk-3.0/settings.ini".source = "${xdg}/gtk-3.0/settings.ini";
@@ -95,15 +114,14 @@ in
# No xdg/pipewire/ entry: PipeWire does not read /etc/xdg, so the file was
# deployed and inert. It goes through services.pipewire.configPackages
# below instead.
# Kept for layout parity with the Arch package; waypaper itself only reads
# Kept for layout parity with moonarch's tree; waypaper itself only reads
# the copy in the home directory, seeded below.
"xdg/waypaper/config.ini".source = waypaperConfig;
# stasis reads /etc/stasis/ as its system-wide fallback, never /etc/xdg/.
"stasis/stasis.rune".source = "${xdg}/stasis/stasis.rune";
# No stasis entry: /etc/stasis/ is only its fallback for a missing user
# config, and the seed below means there never is one.
# Configs outside /etc/xdg/ in the Arch package as well.
"mpv/mpv.conf".source = "${etc}/mpv/mpv.conf";
# Configs that live outside defaults/xdg/ in moonarch as well.
"xdg-desktop-portal/niri-portals.conf".source =
"${etc}/xdg-desktop-portal/niri-portals.conf";
};
@@ -140,13 +158,12 @@ in
programs.niri.enable = true;
# niri searches $XDG_CONFIG_HOME/niri/config.kdl, then /etc/niri/config.kdl —
# not /etc/xdg. On Arch a one-line ~/.config/niri/config.kdl bridges there
# via `include`; here the system path is used directly, so nothing has to be
# placed in the home directory.
# not /etc/xdg. The system path is used directly, so nothing has to be placed
# in the home directory to bridge there.
# config.kdl spawns the polkit agent by its FHS path
# (/usr/lib/polkit-gnome/...). Provide that path rather than diverging the
# config, which is shared with Arch.
# config, which moonarch owns.
systemd.tmpfiles.rules = [
"d /usr/lib 0755 root root -"
"L+ /usr/lib/polkit-gnome - - - - ${pkgs.polkit_gnome}/libexec"
@@ -155,7 +172,7 @@ in
# --- Shell ---
programs.zsh.enable = true;
# Same file the Arch package installs as /etc/zsh/zshrc.moonarch. zsh reads
# moonarch's shared zshrc, which it installs as /etc/zsh/zshrc.moonarch. zsh reads
# /etc/zshrc before ~/.zshrc, so a user file still wins over it.
programs.zsh.interactiveShellInit = ''
source ${moonarchSrc}/defaults/shell/zshrc
@@ -167,10 +184,9 @@ in
# The zshrc owns the prompt, so nothing else has to run here.
programs.zsh.promptInit = "";
# The zshrc sources both plugins from /usr/share/zsh/plugins, the path the
# Arch packages use; the lookups are guarded and silently find nothing here.
# These modules source the same plugins from the store instead. The strategy
# is the one the zshrc sets on Arch.
# The zshrc sources both plugins from /usr/share/zsh/plugins; the lookups are
# guarded and silently find nothing here. These modules source the same
# plugins from the store instead, with the strategy the zshrc sets.
programs.zsh.syntaxHighlighting.enable = true;
programs.zsh.autosuggestions = {
enable = true;
@@ -210,6 +226,14 @@ in
}];
};
# Qt applications are wrapped, so each one looks for plugins in its own
# closure and never finds a style installed alongside it — which is why VLC
# kept coming up in the stock Qt style. Enabling this puts the profile's Qt5
# and Qt6 plugin directories on QT_PLUGIN_PATH, which the wrappers append to.
# platformTheme and style stay null: it would add packages that are already
# listed below, and niri's environment block sets both variables already.
qt.enable = true;
# --- Portals ---
xdg.portal = {
enable = true;
@@ -268,7 +292,7 @@ in
moonarchPkgs.sweet-cursors
moonarchPkgs.moonarch-scripts
# Browser. Arch runs waterfox-bin, which nixpkgs does not carry; see
# Browser. moonarch runs waterfox-bin, which nixpkgs does not carry; see
# DECISIONS.md for why this is not packaged here.
librewolf
@@ -277,17 +301,27 @@ in
# Compositor extras
xwayland-satellite
kanshi
# Bar, notifications, clipboard.
# Neither waybar nor swaync are installed: the Quickshell bar owns both
# roles. The "kept as a reserve" notes in the Arch config date from the
# roles. The "kept as a reserve" notes in moonarch's config date from the
# migration and do not apply to a fresh setup.
quickshell
libnotify
cliphist
wl-clipboard
# The launcher's start prefix, and why the launcher needs one: Quickshell
# spawns applications as its own children, and a child inherits its
# parent's cgroup, so everything started from the launcher lands inside
# quickshell.service — which systemd kills whole on every restart of the
# unit (KillMode=control-group is the default). A crash of the bar took the
# open browser with it, and Librewolf counted each one as a crash until it
# stopped restoring the session at all. app2unit wraps systemd-run to put
# each application in its own transient app-*.scope, the same shape niri
# gives the windows it spawns itself.
app2unit
# Display and wallpaper
wlsunset
nwg-look
@@ -349,10 +383,9 @@ in
git
# System
fwupd
upower
polkit_gnome
wdisplays
nwg-displays
wl-color-picker
stasis
@@ -372,15 +405,42 @@ in
# Set by PAM at session start, so it takes a re-login, not just a rebuild.
environment.sessionVariables.NIXPKGS_ALLOW_UNFREE = "1";
# niri sets both in its own environment block, but that only reaches what niri
# itself spawns. Anything under the systemd user manager — quickshell, and so
# everything started from its launcher — never sees them, and Qt applications
# come up unstyled. Set here they travel through /etc/profile into
# niri-session's import-environment, and from there into the user manager.
environment.sessionVariables.QT_QPA_PLATFORMTHEME = "qt6ct";
environment.sessionVariables.QT_STYLE_OVERRIDE = "kvantum";
# Same gap, cursors: niri's cursor block reaches xwayland-satellite, so X11
# clients have a themed pointer available, but everything under the user
# manager arrives with XCURSOR_PATH alone and has no theme name to look up.
# The name matches modules/greetd.nix, so greeter and session agree.
environment.sessionVariables.XCURSOR_THEME = "Sweet-cursors";
environment.sessionVariables.XCURSOR_SIZE = "24";
# Seed configs for programs that read only from the home directory and have
# no system-wide fallback. 'C' copies once and never overwrites, so user
# edits survive. This is what post-install.sh does imperatively on Arch.
# edits survive. This is what moonarch's post-install.sh does imperatively.
systemd.user.tmpfiles.rules = [
"C %h/.config/Kvantum/catppuccin-mocha-lavender 0755 - - - ${kvantumTheme}/share/Kvantum/catppuccin-mocha-lavender"
"C %h/.config/waypaper/config.ini 0644 - - - ${waypaperConfig}"
# stasis prefers the user config over /etc and generates its own if none
# exists, so the moonarch config has to be in place before it starts.
"C %h/.config/stasis/stasis.rune 0644 - - - ${xdg}/stasis/stasis.rune"
"C %h/.config/swaylock/config 0644 - - - ${swaylockConfig}"
# nwg-displays needs this file to exist before it will start, and niri would
# otherwise ignore the monitor.kdl it writes next to it.
"C %h/.config/niri/config.kdl 0644 - - - ${niriUserConfig}"
"C %h/.zshrc 0644 - - - ${userZshrc}"
# Steam gets XCURSOR_THEME from the block above — verified in its own
# environment — and still drew the core pointer, so parts of it resolve the
# theme literally named 'default' rather than the variable. Xcursor looks
# that name up along XCURSOR_PATH like any other, and ~/.icons is first,
# so pointing it at the theme covers those lookups too. 'L+' replaces the
# link on every activation, unlike the 'C' rules above, which keeps it
# following the theme instead of freezing at today's one.
"L+ %h/.icons/default - - - - /run/current-system/sw/share/icons/Sweet-cursors"
];
}
+101
View File
@@ -30,6 +30,30 @@ let
"/home" = { mountpoint = "/home"; inherit mountOptions; };
};
# A disk holding nothing anyone waits for is allowed to be absent. These
# options make that a non-event: the mount happens on first access rather than
# at boot, systemd stops waiting for the device after five seconds, and the
# filesystem is unmounted again once nothing has touched it for ten minutes,
# which leaves the disk idle and cool most of the time.
scratchMountOptions = mountOptions ++ [
"noauto"
"nofail"
"x-systemd.automount"
"x-systemd.device-timeout=5"
"x-systemd.mount-timeout=10"
"x-systemd.idle-timeout=600"
# Without this the disk is reachable only by typing its path: file managers
# list removable media and gvfs entries, not plain fstab lines. Since the
# entry is noauto, it shows up as a drive that is not mounted yet, which is
# what it usually is.
"x-gvfs-show"
"x-gvfs-name=Storage"
];
storageSubvolumes = {
"/storage" = { mountpoint = "/storage"; mountOptions = scratchMountOptions; };
};
btrfs = subvolumes: {
type = "btrfs";
# Overwrite an existing signature instead of asking. The device is erased
@@ -56,6 +80,13 @@ let
inherit content;
};
# Same container, opened after stage 1 instead of inside the initrd. A disk
# that stage 1 waits for can stop the machine from booting when it is missing
# or has died, which is exactly what must not happen for expendable storage;
# initrdUnlock = false keeps it out of there. Nothing then opens it on its own,
# so the caller has to supply a crypttab entry — see storageCrypttab below.
luksAfterInitrd = name: content: (luks name content) // { initrdUnlock = false; };
# Wraps a filesystem in LUKS unless the host opted out of encryption.
maybeEncrypted = name: subvolumes:
if cfg.encrypt then luks name (btrfs subvolumes) else btrfs subvolumes;
@@ -100,6 +131,45 @@ in
};
};
storage = {
enable = lib.mkEnableOption "a disk for data whose loss costs nothing";
device = lib.mkOption {
type = lib.types.str;
example = "/dev/disk/by-id/nvme-eui.0f1e2d3c4b5a6978";
description = ''
Whole disk for expendable data, mounted at /storage — a game library,
downloads, images. Nothing that would be missed and nothing anyone
waits for, because this disk is allowed to be slow, absent or dead.
Unlike the other two it is never unlocked in the initrd, so a failure
cannot hold up the boot. It is mounted on first access instead and
unmounted again when idle. That is what makes it usable for a drive
that is not entirely trustworthy.
disko-install addresses it as `--disk storage <path>`.
'';
};
keyFile = lib.mkOption {
type = lib.types.str;
default = "/var/keys/storage.key";
description = ''
Key file that unlocks the container without a prompt. It has to live on
an already-decrypted filesystem — the default sits on the root disk, so
it is covered by that disk's encryption and unreadable without the boot
passphrase.
Create it once and add it as a second key slot; the existing passphrase
keeps working:
dd bs=512 count=4 if=/dev/random of=<keyFile> iflag=fullblock
chmod 400 <keyFile>
cryptsetup luksAddKey <partition> <keyFile>
'';
};
};
encrypt = lib.mkOption {
type = lib.types.bool;
default = true;
@@ -178,5 +248,36 @@ in
};
};
})
# Expendable storage, same shape as the disk above with one difference that
# changes everything about it: its container never enters the initrd, so a
# drive that is slow, missing or dead cannot stop the machine from booting.
(lib.mkIf cfg.storage.enable {
disko.devices.disk.storage = {
type = "disk";
device = cfg.storage.device;
content = {
type = "gpt";
partitions.storage = {
size = "100%";
content =
if cfg.encrypt
then luksAfterInitrd "storage" (btrfs storageSubvolumes)
else btrfs storageSubvolumes;
};
};
};
})
# Keeping it out of the initrd means nothing opens it any more, so this is
# what does. The partition is addressed by its partlabel, which disko derives
# from the disk and partition names above — no UUID has to be pasted in here,
# and a replaced disk needs no edit. noauto leaves the opening to the mount
# in storageSubvolumes, nofail keeps a failure from becoming a boot problem.
(lib.mkIf (cfg.storage.enable && cfg.encrypt) {
environment.etc.crypttab.text = ''
storage /dev/disk/by-partlabel/disk-storage-storage ${cfg.storage.keyFile} noauto,nofail
'';
})
]);
}
+12 -1
View File
@@ -1,7 +1,7 @@
# ABOUTME: Sets up greetd with regreet as the login greeter.
# ABOUTME: Counterpart to defaults/etc/greetd/ in the moonarch repo.
{ pkgs, lib, moonarchSrc, moonarchPkgs, moonarchThemes, ... }:
{ config, pkgs, lib, moonarchSrc, moonarchPkgs, moonarchThemes, ... }:
let
wallpaper = "${moonarchSrc}/defaults/backgrounds/wallpaper.jpg";
@@ -11,6 +11,10 @@ in
{
environment.etc."greetd/niri-greeter.kdl".source = greeterNiriConfig;
# niri-greeter.kdl spawns `regreet` by name, and the module only installs it
# through the default_session command that is overridden below.
environment.systemPackages = [ config.services.displayManager.regreet.package ];
# /etc/greetd/regreet.toml and regreet.css are written by the module, so
# defaults/etc/greetd/regreet.toml is not deployed verbatim: its wallpaper
# path points at /usr/share/moonarch/, which does not exist here. Every other
@@ -63,6 +67,13 @@ in
services.greetd = {
enable = true;
# The option defaults to false as soon as a host sets initial_session, on
# the grounds that a restart would autologin again. greetd(5) says it does
# not: the initial session runs only on the first launch since boot, marked
# by a runfile under /run. Left at the default, logging out ends greetd and
# leaves a dead VT with no way back in.
restart = true;
settings.default_session = {
# regreet needs a Wayland compositor. niri instead of the module's cage
# default: niri-greeter.kdl carries the German keyboard layout, numlock
+154 -47
View File
@@ -1,7 +1,7 @@
# ABOUTME: systemd system and user services for the desktop session.
# ABOUTME: Mostly rebuilt from moonarch/defaults/etc/systemd/, whose units use FHS paths.
{ pkgs, lib, moonarchPkgs, ... }:
{ config, pkgs, lib, moonarchPkgs, ... }:
let
# Days after which the nixpkgs input counts as stale. nixpkgs carries no
@@ -73,6 +73,88 @@ let
read -r -p '[enter to close] ' _"
'';
};
# Shared by the two root oneshots below. Both read one state file and write a
# single sysfs attribute, so everything else is taken away. Scored with
# `systemd-analyze security`: 7.5 EXPOSED before, 0.9 SAFE after.
#
# Deliberately absent:
# ProtectKernelTunables - remounts /sys read-only, which is exactly the
# write both units exist for. ReadWritePaths is no fix, /sys/class/… is a
# symlink into /sys/devices/….
# PrivateUsers - capabilities inside a user namespace are namespace-local,
# while the kernel's sysfs store handlers test against the initial one.
#
# ProtectSystem=strict is safe here: it leaves /dev, /proc and /sys writable,
# and the state file is only ever read.
sysfsWriterHardening = {
CapabilityBoundingSet = [ "" ];
AmbientCapabilities = [ "" ];
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateNetwork = true;
IPAddressDeny = "any";
RestrictAddressFamilies = [ "AF_UNIX" ];
ProtectClock = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
KeyringMode = "private";
UMask = "0077";
};
# Shared by the Wayland session clients below: socket to the compositor,
# nothing else. 9.4 UNSAFE before, 0.9 SAFE after, each one started under the
# full set with `systemd-run --user` first.
#
# Deliberately absent:
# ProtectHome - hides /home, /root *and /run/user*, where the Wayland
# socket lives. Read-only fails too: connect() needs write on the inode.
# PrivateUsers - maps the session uid to nobody, the socket is owned by the
# real one.
waylandClientHardening = {
CapabilityBoundingSet = [ "" ];
AmbientCapabilities = [ "" ];
NoNewPrivileges = true;
ProtectSystem = "strict";
ReadWritePaths = [ "%t" ];
PrivateTmp = true;
PrivateDevices = true;
PrivateNetwork = true;
IPAddressDeny = "any";
RestrictAddressFamilies = [ "AF_UNIX" ];
RemoveIPC = true;
ProtectClock = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
SystemCallErrorNumber = "EPERM";
KeyringMode = "private";
};
in
{
# --- Audio ---
@@ -100,27 +182,6 @@ in
# --- Firewall, replaces the ufw calls in post-install.sh ---
networking.firewall.enable = true;
# --- Session services ---
#
# Rebuilt rather than copied: the upstream units reference /usr/bin/,
# which does not exist under Nix.
systemd.user.services.kanshi = {
description = "Dynamic display configuration";
partOf = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
# Without a config kanshi exits with a parse error and, with
# Restart=on-failure, spins. Monitor profiles are per machine.
unitConfig.ConditionPathExists = "%h/.config/kanshi/config";
serviceConfig = {
Type = "simple";
ExecStart = lib.getExe pkgs.kanshi;
Restart = "on-failure";
RestartSec = 3;
};
};
# --- System services from defaults/etc/systemd/system/ ---
#
# Rebuilt rather than copied, same reason as the user units: the originals
@@ -137,15 +198,7 @@ in
serviceConfig = {
Type = "oneshot";
ExecStart = "${moonarchPkgs.moonarch-scripts}/bin/moonarch-batsaver-restore";
NoNewPrivileges = true;
ProtectHome = true;
PrivateTmp = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictNamespaces = true;
RestrictRealtime = true;
LockPersonality = true;
};
} // sysfsWriterHardening;
};
systemd.services.moonarch-camera = {
@@ -156,14 +209,43 @@ in
serviceConfig = {
Type = "oneshot";
ExecStart = "${moonarchPkgs.moonarch-scripts}/bin/moonarch-camera-boot";
NoNewPrivileges = true;
ProtectHome = true;
PrivateTmp = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
RestrictNamespaces = true;
RestrictRealtime = true;
LockPersonality = true;
} // sysfsWriterHardening;
};
# --- Session services ---
#
# Rebuilt rather than copied: the upstream units reference /usr/bin/,
# which does not exist under Nix.
# The bar, launcher and every popout. A unit rather than a spawn-at-startup so
# a crash does not leave the session without a bar until the next login.
systemd.user.services.quickshell = {
description = "Quickshell bar and shell";
partOf = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
# The widgets shell out to niri, nmcli, powerprofilesctl, the moonarch
# scripts and a dozen more, and which ones is a property of the QML, not of
# this file. As a spawn-at-startup it inherited the session PATH; the unit
# gets the same set rather than a list that goes stale on the next widget.
path = [ config.system.path ];
# Restart=on-failure alone spun 32 times through a dead Wayland socket after
# a rebuild had sent niri SIGTERM: graphical-session-pre.target went down,
# graphical-session.target stayed up, so the unit was still wanted and every
# attempt died on "no Qt platform plugin could be initialized". Quickshell
# refuses a relaunch itself ("crashed within 10 seconds"), but systemd never
# reached the default limit of 5 starts per 10s at a 3s delay. Three tries
# per 30s ends it after ~9s. The window slides, so a single crash after a
# normal run still gets its restart — the point of running this as a unit.
unitConfig = {
StartLimitIntervalSec = 30;
StartLimitBurst = 3;
};
serviceConfig = {
Type = "simple";
ExecStart = "${lib.getExe pkgs.quickshell} -c moonarch";
Restart = "on-failure";
RestartSec = 3;
};
};
@@ -174,16 +256,41 @@ in
partOf = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
# stasis runs its actions as `sh -lc <command>` and spawns niri and pactl
# directly, all resolved against this PATH. pulseaudio is here for pactl
# alone; it stays off systemPackages, see modules/desktop.nix.
path = with pkgs; [ bash swaylock brightnessctl niri pulseaudio ];
# Every binary a configured action names has to be listed here. stasis does
# not run the actions through a login shell, so /etc/profile never applies
# and nothing resolves via /run/current-system/sw/bin. A missing binary is
# silent: the spawn fails, stasis logs the action as started, sees the
# immediate exit, treats the step as finished and stops there — which is how
# swaylock was absent for the whole idle chain while `niri msg` worked.
# pactl is why pulseaudio appears at all — it stays off systemPackages, see
# modules/desktop.nix. brightnessctl is for the laptop's brightness step.
path = with pkgs; [
bashNonInteractive
niri
pulseaudio
libnotify
swaylock
brightnessctl
systemd
];
# No waylandClientHardening: swaylock is a child of this unit and
# authenticates through the setuid-root unix_chkpwd. `systemd --user` builds
# mount namespaces through an unprivileged user namespace, whose mounts are
# nosuid — the wrapper loses setuid and the session is no longer unlockable.
# Measured per directive with `echo wrong | unix_chkpwd $USER nonull`: exit
# 7 means shadow was read, 9 means no access. Only these five return 7, so
# only they are set. 9.4 → 9.0; there is no more to get here.
serviceConfig = {
Type = "simple";
ExecStart = "${lib.getExe pkgs.stasis}";
ExecReload = "${lib.getExe pkgs.stasis} reload";
Restart = "on-failure";
RestartSec = 2;
IPAddressDeny = "any";
ProtectProc = "invisible";
ProcSubset = "pid";
RemoveIPC = true;
KeyringMode = "private";
};
};
@@ -197,7 +304,7 @@ in
ExecStart = "${pkgs.wl-clipboard}/bin/wl-paste --watch ${pkgs.cliphist}/bin/cliphist -db-path %t/cliphist/db store";
Restart = "on-failure";
RestartSec = 3;
};
} // waylandClientHardening;
};
systemd.user.services.cliphist-image = {
@@ -210,7 +317,7 @@ in
ExecStart = "${pkgs.wl-clipboard}/bin/wl-paste --type image --watch ${pkgs.cliphist}/bin/cliphist -db-path %t/cliphist/db store";
Restart = "on-failure";
RestartSec = 3;
};
} // waylandClientHardening;
};
# Night light stays disabled by default: it is a user toggle. Enabling it
@@ -219,7 +326,7 @@ in
systemd.user.services.wlsunset = {
description = "Wlsunset night light (blue light filter)";
partOf = [ "graphical-session.target" ];
after = [ "graphical-session.target" "kanshi.service" ];
after = [ "graphical-session.target" ];
wantedBy = [ ];
serviceConfig = {
Type = "simple";
@@ -227,7 +334,7 @@ in
ExecStart = "${lib.getExe pkgs.wlsunset} -T 6500 -t 5000 -S 00:00 -s 00:01";
Restart = "on-failure";
RestartSec = 3;
};
} // waylandClientHardening;
};
# --- Flake age ---
+2 -2
View File
@@ -25,11 +25,11 @@ let
"base_keymap": "SublimeText",
"cursor_shape": "block",
"ui_font_size": 14,
"ui_font_family": "Hack Nerd Font",
"ui_font_family": "UbuntuSans Nerd Font",
"ui_font_features": {
"calt": true
},
"buffer_font_family": "Hack Nerd Font",
"buffer_font_family": "UbuntuSansMono Nerd Font",
"buffer_font_size": 14,
"theme": {
"mode": "system",