Files
Nomarchy/modules/nixos/default.nix
Bernardo Magri 5ea4f0c9ac
Some checks failed
Check / eval (push) Has been cancelled
feat(stability): default-on memory-pressure protection (earlyoom)
modules/nixos/oom.nix — running out of memory now kills the offending
process (with a desktop notification via systembus-notify) instead of
freezing the desktop for minutes until the kernel OOM killer fires.

earlyoom over systemd-oomd, deliberately: oomd kills whole cgroups,
and a Hyprland session runs as ONE scope (nothing here spawns per-app
systemd scopes, unlike GNOME) — under pressure oomd would take out the
entire desktop to save it. earlyoom kills the single largest process
before the thrash point. nixpkgs default-enables oomd in an inert
state (no slices monitored); it is disabled outright so there is one
owner. Session plumbing is --avoid-listed (unanchored — NixOS wrappers
rename comm to .foo-wrapped); no --prefer tuning, largest-RSS
selection already finds the hog. All mkDefault; opt out with
services.earlyoom.enable = false (README note added).

Verified: V0 (flake check) + V2 — new checks.oom-protection
runNixOSTest, executed: a chunked allocator (686 MB peak, 1 GB VM) is
SIGTERM'd by earlyoom in 0.1 s, a bystander unit survives (the
process-level granularity the module exists for), and systemd-oomd is
asserted inactive. 25 s runtime.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 09:45:21 +01:00

438 lines
21 KiB
Nix
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Nomarchy — reusable system layer (NixOS 26.05).
#
# This module is the distro: import it from any host (see
# nixosModules.nomarchy in flake.nix) and layer your machine specifics
# (bootloader, hostname, users, hardware) on top. Host concerns are
# deliberately NOT set here. Everything user-facing (Hyprland config,
# Waybar, Ghostty, theming) lives in modules/home.
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.system;
# Reused by the greeter banner and MOTD so the brand name lives in one
# place (system.nixos.distroName, set below).
distroName = config.system.nixos.distroName;
# The Nomarchy glyph font: a single-glyph face (the logo at U+F000) used
# by the Waybar menu button in the summer themes. Pinned via
# `font-family: Nomarchy` in their CSS, since Nerd Fonts also occupy
# U+F000 (a glass icon) and would otherwise win.
nomarchyLogoFont = pkgs.runCommandLocal "nomarchy-logo-font" { } ''
install -Dm444 ${./branding/Nomarchy.ttf} $out/share/fonts/truetype/Nomarchy.ttf
'';
in
{
imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ./services.nix ./hardware.nix ./timezone.nix ./oom.nix ];
config = {
# Distro branding. distroName flows into /etc/os-release PRETTY_NAME,
# systemd-boot entry titles and the ISO boot-menu label; distroId is the
# machine-readable ID (DEFAULT_HOSTNAME, lsb DISTRIB_ID, CPE name).
# distroId = "nomarchy" makes os-release honest — ID=nomarchy with
# ID_LIKE=nixos, the standard derivative-distro lineage marker (cf.
# Ubuntu→debian) — and is safe: switch-to-configuration builds its
# "is this NixOS?" guard from the *configured* distroId (and /etc/NIXOS
# still exists as the fallback), so rebuilds keep working; the nixos-*
# CLI tools are package names, untouched. The one side effect is that
# isNixos goes false and blanks the upstream nixos.org URLs, so we
# restore them pointing at the project instead.
system.nixos.distroName = lib.mkDefault "Nomarchy";
system.nixos.distroId = lib.mkDefault "nomarchy";
system.nixos.extraOSReleaseArgs = lib.mkDefault {
HOME_URL = "https://git.bemagri.xyz/bernardo/Nomarchy";
DOCUMENTATION_URL = "https://git.bemagri.xyz/bernardo/Nomarchy";
SUPPORT_URL = "https://git.bemagri.xyz/bernardo/Nomarchy";
BUG_REPORT_URL = "https://git.bemagri.xyz/bernardo/Nomarchy/issues";
};
# MOTD on TTY/SSH login (the desktop auto-logs into Hyprland, so this
# is mostly seen over SSH or on a bare console). Branded, and doubles
# as a cheat sheet for the distro's own helpers.
users.motd = lib.mkDefault ''
${distroName} a NixOS desktop, themed from one JSON.
sys-update update inputs + rebuild the system
home-update apply home/theme changes (no sudo)
nomarchy-theme-sync apply <theme> switch the whole palette
SUPER+? keybindings cheatsheet
'';
# Unfree allowed distro-wide: pragmatic-desktop territory (vendor
# GPU/wifi drivers, firmware, fonts, …). The custom nixpkgs-config
# type can't carry a nested mkDefault; disagree with
# `nixpkgs.config = lib.mkForce { allowUnfree = false; }`.
nixpkgs.config.allowUnfree = true;
# ── One keyboard layout everywhere, incl. the LUKS prompt ────────
# services.xserver.xkb.layout is the single source of truth for the
# layout (the installer writes it; downstream sets it in system.nix).
# These bridge that one knob through to the encrypted-disk prompt —
# without them, setting only xkb.layout moves the graphical session
# but leaves the console keymap unset, so the initrd LUKS passphrase
# prompt falls back to US:
# useXkbConfig the virtual-console keymap follows xkb (TTYs)
# earlySetup bakes that keymap into the initrd
# systemd initrd loads it before cryptsetup asks for the passphrase
# (the legacy scripted initrd cannot; also what
# Plymouth needs — defaulted here so it no longer
# hinges on the splash being enabled).
# All mkDefault, so an explicit console.keyMap or a scripted initrd
# still wins.
console.useXkbConfig = lib.mkDefault true;
console.earlySetup = lib.mkDefault true;
boot.initrd.systemd.enable = lib.mkDefault true;
# Nomarchy roots are BTRFS, not ZFS, so adopt the 26.11 default early and
# silence the eval warning the old `true` default emits. mkDefault, so a
# genuine ZFS-root downstream can still force it back on.
boot.zfs.forceImportRoot = lib.mkDefault false;
# ── Wayland session: Hyprland ────────────────────────────────────
# Installs the binary, registers the session, wires up
# xdg-desktop-portal-hyprland. Configuration is Home Manager's job.
programs.hyprland.enable = lib.mkDefault true;
xdg.portal = {
enable = lib.mkDefault true;
extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; # file pickers, etc.
};
services.greetd = lib.mkIf cfg.greeter.enable {
enable = lib.mkDefault true;
settings = {
default_session = {
# start-hyprland is Hyprland 0.55's watchdog launcher; running
# the bare binary makes every session print a warning.
command = lib.mkDefault "${pkgs.tuigreet}/bin/tuigreet --time --remember --greeting 'Welcome to ${distroName}' --cmd start-hyprland";
user = "greeter";
};
# Boot straight into the session once; logout → normal greeter.
initial_session = lib.mkIf (cfg.greeter.autoLogin != null) {
command = "start-hyprland";
user = cfg.greeter.autoLogin;
};
};
};
# ── Audio: Pipewire ──────────────────────────────────────────────
security.rtkit.enable = lib.mkDefault cfg.audio.enable;
services.pulseaudio.enable = lib.mkDefault false;
services.pipewire = lib.mkIf cfg.audio.enable {
enable = lib.mkDefault true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
wireplumber.enable = true;
};
# ── Desktop services ─────────────────────────────────────────────
security.polkit.enable = lib.mkDefault true;
services.gnome.gnome-keyring.enable = lib.mkDefault true;
services.dbus.enable = lib.mkDefault true;
services.upower.enable = lib.mkDefault true;
# Firmware updates via LVFS. Ships the daemon + its metadata-refresh
# timer only — it never flashes anything on its own; applying an update
# is an explicit `fwupdmgr update`. On by default as desktop security
# hygiene (BIOS/UEFI/Thunderbolt/SSD fixes land here); a clean no-op
# where LVFS sees no devices (VMs/headless). Disable natively with
# `services.fwupd.enable = false`.
services.fwupd.enable = lib.mkDefault true;
networking.networkmanager.enable = lib.mkDefault true;
# OpenVPN support for the VPN menu's import/connect flow (nomarchy-vpn).
# WireGuard needs no plugin (NetworkManager imports wg .conf natively);
# this adds the openvpn type so `nmcli connection import type openvpn` works.
# mkDefault so a downstream can drop it to slim the closure.
networking.networkmanager.plugins = lib.mkDefault [ pkgs.networkmanager-openvpn ];
# No double-unlock on hibernate. Locking the session before sleep is
# right for suspend (resumes from RAM, no other gate), but an encrypted
# hibernate already resumes through the LUKS passphrase — a hyprlock on
# top is a second password. And we can't just drop the lock after the
# resume: a Wayland session-lock whose client dies without releasing
# trips hyprlock's "go to a tty" crash failsafe (the compositor keeps the
# screen locked for safety). So don't lock before an encrypted hibernate
# in the first place. hypridle can't tell suspend from hibernate, hence a
# system unit hooked to the sleep targets: it locks on the RAM-resume
# sleeps always, and on hibernate only when the disk is unencrypted (no
# LUKS gate to rely on). Replaces hypridle's old before_sleep_cmd.
systemd.services.nomarchy-lock-before-sleep =
let
encrypted = builtins.attrNames config.boot.initrd.luks.devices != [ ];
# Sleeps whose normal resume is from RAM — always lock these.
ramTargets = [ "suspend.target" "hybrid-sleep.target" "suspend-then-hibernate.target" ];
ramServices = [ "systemd-suspend.service" "systemd-hybrid-sleep.service" "systemd-suspend-then-hibernate.service" ];
in
{
description = "Lock the session before sleep (skipped for an encrypted hibernate)";
before = ramServices ++ lib.optional (!encrypted) "systemd-hibernate.service";
wantedBy = ramTargets ++ lib.optional (!encrypted) "hibernate.target";
serviceConfig = {
Type = "oneshot";
# Plural: a system unit has no session of its own to lock.
ExecStart = "${config.systemd.package}/bin/loginctl lock-sessions";
};
};
# zsh as the default login shell (the desktop's shell experience —
# starship/bat/eza/zoxide — is configured home-side in shell.nix).
# programs.zsh.enable wires /etc/zshrc, completion and /etc/shells;
# defaultUserShell applies to normal users that don't set their own.
programs.zsh.enable = lib.mkDefault true;
# mkOverride 500: beats nixpkgs' bash.nix `mkDefault bashInteractive`
# (equal mkDefault priority would conflict) while staying below a
# plain downstream assignment. A per-user `users.users.<n>.shell`
# overrides this regardless.
users.defaultUserShell = lib.mkOverride 500 pkgs.zsh;
hardware.bluetooth.enable = lib.mkDefault cfg.bluetooth.enable;
services.blueman.enable = lib.mkDefault cfg.bluetooth.enable;
# ── Foreign binaries: nix-ld ─────────────────────────────────────
# An ld.so shim so dynamically-linked binaries not built for NixOS
# (downloaded tools, language servers, pip/npm-installed ELFs, the
# npx-fetched claude-code) run without manual patchelf. On by default —
# a pragmatic-desktop expectation.
programs.nix-ld.enable = lib.mkDefault true;
# ── Firmware ─────────────────────────────────────────────────────
# Blobs for in-kernel drivers: wifi (iwlwifi/ath/rtw/brcm), SOF
# audio, Bluetooth. Drivers ship with the kernel, but without these
# a real machine can boot with no wifi — QEMU never catches it.
# nixos-generate-config also keys CPU microcode off this flag.
hardware.enableRedistributableFirmware = lib.mkDefault true;
# ── BTRFS timeline snapshots (ported from the previous iteration) ─
# Guarded on the actual filesystem so enabling it on an ext4 machine
# is a clean no-op rather than a failing timer.
services.snapper.configs = lib.mkIf
(cfg.snapper.enable && (config.fileSystems."/".fsType or "") == "btrfs")
({
root = {
SUBVOLUME = "/";
TIMELINE_CREATE = true;
TIMELINE_CLEANUP = true;
TIMELINE_LIMIT_HOURLY = "5";
TIMELINE_LIMIT_DAILY = "7";
TIMELINE_LIMIT_WEEKLY = "0";
TIMELINE_LIMIT_MONTHLY = "0";
TIMELINE_LIMIT_YEARLY = "0";
};
}
# /home is user data — snapshot it too when it's its own BTRFS subvolume
# (the installer's @home). Keep the recent hourlies for oops-recovery
# plus dailies, and a month of weeklies; "at least daily" by default.
// lib.optionalAttrs ((config.fileSystems."/home".fsType or "") == "btrfs") {
home = {
SUBVOLUME = "/home";
TIMELINE_CREATE = true;
TIMELINE_CLEANUP = true;
TIMELINE_LIMIT_HOURLY = "5";
TIMELINE_LIMIT_DAILY = "7";
TIMELINE_LIMIT_WEEKLY = "4";
TIMELINE_LIMIT_MONTHLY = "0";
TIMELINE_LIMIT_YEARLY = "0";
};
});
# snapper needs a `.snapshots` subvolume inside each tracked subvolume.
# The installer made one for / (@snapshots → /.snapshots) but not /home,
# so create a nested one there if missing (idempotent via the condition;
# excluded from /home snapshots since BTRFS snapshots are non-recursive).
# Runs at boot so it also fixes already-installed machines — no disko
# change needed.
systemd.services.nomarchy-home-snapshots-subvol = lib.mkIf
(cfg.snapper.enable && (config.fileSystems."/home".fsType or "") == "btrfs") {
description = "Create /home/.snapshots for snapper if missing";
wantedBy = [ "multi-user.target" ];
before = [ "snapper-timeline.service" "snapper-cleanup.service" ];
unitConfig.ConditionPathExists = "!/home/.snapshots";
serviceConfig = {
Type = "oneshot";
ExecStart = "${pkgs.btrfs-progs}/bin/btrfs subvolume create /home/.snapshots";
};
};
# ── Fonts ────────────────────────────────────────────────────────
# The ten most popular Nerd Fonts ship by default, so any of them
# can be named in the theme state's fonts.mono and actually resolve
# (nomarchy-theme-sync warns when a configured font is missing).
fonts = {
packages = with pkgs; [
nerd-fonts.jetbrains-mono
nerd-fonts.fira-code
nerd-fonts.caskaydia-cove
nerd-fonts.hack
nerd-fonts.sauce-code-pro
nerd-fonts.meslo-lg
nerd-fonts.roboto-mono
nerd-fonts.ubuntu-mono
# iosevka would be next by popularity, but its package is 1.1 GB
# (every weight × variant) — too heavy for the ISO and closures.
nerd-fonts.mononoki
nerd-fonts.inconsolata
inter
noto-fonts
noto-fonts-color-emoji
# The legacy-ported waybar identities (summer-day/night) use
# FontAwesome codepoints in their layouts and list it in css.
font-awesome
# The Nomarchy logo glyph (U+F000) for the summer menu button.
nomarchyLogoFont
];
fontconfig.defaultFonts = {
monospace = lib.mkDefault [ "JetBrainsMono Nerd Font" ];
sansSerif = lib.mkDefault [ "Inter" ];
emoji = lib.mkDefault [ "Noto Color Emoji" ];
};
};
# ── Essential packages ───────────────────────────────────────────
environment.systemPackages = with pkgs; [
nomarchy-theme-sync # provided by overlays.default
# Friendly wrappers for the two rebuild paths (README §3). Run as
# your user: `nix flake update` must NOT run as root (libgit2
# refuses the user-owned flake repo) — sudo happens inside, only
# for the system switch.
(pkgs.writeShellScriptBin "sys-update" ''
set -e
if [ "$(id -u)" -eq 0 ]; then
echo "sys-update: run as your normal user (it sudos the rebuild itself)" >&2
exit 1
fi
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
echo "sys-update: updating flake inputs in $flake"
nix flake update --flake "$flake"
if command -v nixos-rebuild-snap >/dev/null 2>&1; then
sudo nixos-rebuild-snap "$@" # BTRFS snapshot first
else
sudo nixos-rebuild switch --flake "$flake#default" "$@"
fi
'')
(pkgs.writeShellScriptBin "home-update" ''
set -e
exec home-manager switch --flake "''${NOMARCHY_PATH:-$HOME/.nomarchy}" "$@"
'')
git
vim
wget
curl
jq
brightnessctl
playerctl
pamixer
wl-clipboard
grim
slurp
] ++ lib.optional (cfg.snapper.enable && (config.fileSystems."/".fsType or "") == "btrfs")
# Snapshot, then rebuild — rollback material for system changes
# (theme changes don't need it; HM generations already roll back).
(pkgs.writeShellScriptBin "nixos-rebuild-snap" ''
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root (use sudo)" >&2
exit 1
fi
echo "Creating pre-rebuild snapshot..."
${pkgs.snapper}/bin/snapper -c root create \
-d "Pre-rebuild $(date +'%Y-%m-%d %H:%M:%S')" \
--cleanup-algorithm number
echo "Rebuilding..."
nixos-rebuild switch --flake /etc/nixos#default "$@"
'')
# The desktop snapshot manager (browse / diff / restore / rollback over
# snapper, elevating via polkit). Kept for when nixpkgs fixes its crash.
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant
# Keyboard-driven snapper browser/restore — the fallback `nomarchy-menu
# snapshot` actually launches while btrfs-assistant 2.2 segfaults on 26.05
# (libbtrfsutil ABI). Runs as root (snapper is root-only here; the menu
# opens it in a terminal via sudo, one password prompt), fzf to pick, with
# browse/diff (read-only) and typed-`yes` confirmation before any write.
++ lib.optional cfg.snapper.enable (pkgs.writeShellApplication {
name = "nomarchy-snapshots";
runtimeInputs = with pkgs; [ snapper fzf gawk gnugrep less coreutils systemd ];
text = ''
if [ "$(id -u)" -ne 0 ]; then
echo "nomarchy-snapshots must run as root (snapper needs it) use sudo." >&2
exit 1
fi
mapfile -t configs < <(snapper list-configs | awk 'NR>2 {print $1}' | grep .)
if [ "''${#configs[@]}" -eq 0 ]; then
echo "No snapper configs found." >&2; exit 1
elif [ "''${#configs[@]}" -eq 1 ]; then
config="''${configs[0]}"
else
config=$(printf '%s\n' "''${configs[@]}" | fzf --prompt="snapper config> ") || exit 0
fi
while :; do
snap=$(snapper -c "$config" list \
| fzf --header-lines=2 --prompt="[$config] pick a snapshot (Esc quits)> ") || exit 0
num=$(awk '{print $1}' <<<"$snap")
case "$num" in ""|*[!0-9]*) continue ;; esac
action=$(printf '%s\n' \
"Browse changes since #$num (read-only)" \
"Restore changed files to #$num (undochange)" \
"Roll the system back to #$num (reboot)" \
" Back to the snapshot list" \
| fzf --prompt="snapshot #$num> ") || exit 0
case "$action" in
Browse*)
snapper -c "$config" status "$num..0" | less -R || true ;;
Restore*)
read -rp "Revert files in '$config' to snapshot #$num? Type yes to confirm: " ans || continue
if [ "$ans" = yes ]; then
snapper -c "$config" undochange "$num..0"
echo "Files restored. Press enter."; read -r _ || true
fi ;;
Roll*)
if [ "$config" != root ]; then
echo "Rollback applies to the 'root' config only; use Restore for '$config'. Press enter."
read -r _ || true
else
read -rp "Roll the SYSTEM back to #$num and REBOOT now? Type yes to confirm: " ans || continue
if [ "$ans" = yes ]; then
snapper -c root rollback "$num"
echo "Rolled back rebooting"; systemctl reboot
fi
fi ;;
*) continue ;;
esac
done
'';
});
# Don't let boot entries fill the ESP over the years.
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;
# Backlight udev rule (chgrp video + g+w on /sys/class/backlight) so
# the desktop's swayosd brightness control works for video-group
# users without root. Harmless baseline even if the OSD is disabled.
services.udev.packages = [ pkgs.swayosd ];
# ── Nix itself ───────────────────────────────────────────────────
nix = {
settings = {
experimental-features = [ "nix-command" "flakes" ];
auto-optimise-store = lib.mkDefault true;
# The downstream flake (~/.nomarchy) is meant to be a live working
# tree: nomarchy-theme-sync rewrites theme-state.json on every
# switch (and you needn't commit each tweak), so the "Git tree is
# dirty" warning fires on every rebuild and is pure noise here.
warn-dirty = lib.mkDefault false;
};
gc = {
automatic = lib.mkDefault true;
dates = lib.mkDefault "weekly";
options = lib.mkDefault "--delete-older-than 14d";
};
};
};
}