501 lines
24 KiB
Nix
501 lines
24 KiB
Nix
# 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 ./greeter.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
|
||
sys-rebuild rebuild the system, no input update
|
||
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;
|
||
|
||
# Magic SysRq Keys: safety net for Wayland lockups. Alt+SysRq+REISUB allows
|
||
# safe reboot without data loss when the compositor hangs.
|
||
boot.kernel.sysctl."kernel.sysrq" = lib.mkDefault 1;
|
||
|
||
# ── 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.
|
||
};
|
||
|
||
# The greetd/tuigreet login screen lives in ./greeter.nix — themed
|
||
# from the state JSON (console.colors + --theme) at system rebuild.
|
||
|
||
# ── 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;
|
||
|
||
# Hardware security key (FIDO2/U2F/GPG) support. Without pcscd, tokens like
|
||
# YubiKeys often silently fail in WebAuthn (browsers), SSH, and GPG.
|
||
services.pcscd.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;
|
||
|
||
# Drive health monitoring (SMART).
|
||
services.smartd = {
|
||
enable = lib.mkDefault true;
|
||
notifications.x11.enable = lib.mkDefault true;
|
||
notifications.wall.enable = lib.mkDefault true;
|
||
};
|
||
# Explicitly enable systembus-notify to resolve a mkDefault conflict
|
||
# between earlyoom (true) and smartd (false).
|
||
services.systembus-notify.enable = true;
|
||
|
||
# Enable I2C and DDC/CI by default for external monitor brightness control.
|
||
nomarchy.hardware.i2c.ddcci = 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";
|
||
};
|
||
};
|
||
|
||
# Background filesystem health checks. Scans all BTRFS filesystems to detect
|
||
# and (if RAID/dup) correct bitrot before it propagates.
|
||
services.btrfs.autoScrub = lib.mkIf ((config.fileSystems."/".fsType or "") == "btrfs") {
|
||
enable = lib.mkDefault true;
|
||
interval = lib.mkDefault "monthly";
|
||
};
|
||
|
||
# ── 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
|
||
# GeistMono (57 MB) — the Boreal theme's mono face; modern and
|
||
# geometric, unlike the ten defaults. Cheap enough for the ISO
|
||
# (Iosevka above was rejected at 1.1 GB; this is a compact family).
|
||
nerd-fonts.geist-mono
|
||
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
|
||
nomarchy-doctor # read-only health check (System › Doctor)
|
||
nomarchy-control-center # TUI control center
|
||
|
||
# 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"
|
||
before=$(readlink -f /run/current-system)
|
||
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
|
||
# What did that update actually change? Package-level diff of the
|
||
# old vs new generation (the informative half of "informative +
|
||
# rock-stable"); never fails the run.
|
||
after=$(readlink -f /run/current-system)
|
||
if [ "$before" = "$after" ]; then
|
||
echo "sys-update: no changes — the system is identical."
|
||
else
|
||
echo "sys-update: what changed:"
|
||
${pkgs.nvd}/bin/nvd diff "$before" "$after" || true
|
||
fi
|
||
'')
|
||
# The no-update twin (hardware-QA request): rebuild the system
|
||
# against the CURRENT lock — config changes only, no `nix flake
|
||
# update` — mirroring how home-update never touches the lock.
|
||
# Same snapshot-first path when available.
|
||
(pkgs.writeShellScriptBin "sys-rebuild" ''
|
||
set -e
|
||
if [ "$(id -u)" -eq 0 ]; then
|
||
echo "sys-rebuild: run as your normal user (it sudos the rebuild itself)" >&2
|
||
exit 1
|
||
fi
|
||
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||
before=$(readlink -f /run/current-system)
|
||
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
|
||
# Same what-changed diff as sys-update (the twins stay twins).
|
||
after=$(readlink -f /run/current-system)
|
||
if [ "$before" = "$after" ]; then
|
||
echo "sys-rebuild: no changes — the system is identical."
|
||
else
|
||
echo "sys-rebuild: what changed:"
|
||
${pkgs.nvd}/bin/nvd diff "$before" "$after" || true
|
||
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
|
||
hyprpicker
|
||
] ++ 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) — the primary `nomarchy-menu snapshot`
|
||
# target. The "2.2 segfault" is unprivileged-only (libbtrfsutil
|
||
# unprivileged subvolume iteration, btrfs-progs 6.17.1, fixed upstream
|
||
# after); the pkexec launcher runs it as root, where it works —
|
||
# VM-proven, guarded by checks.snapshot-gui.
|
||
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant
|
||
# Keyboard-driven snapper browser/restore — the menu's fallback when the
|
||
# GUI is absent, and handy over SSH. 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";
|
||
};
|
||
};
|
||
};
|
||
}
|