# 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. Fingerprint line only
    # when fprintd is enabled (no permanent nag on machines without a
    # reader); doctor is always on PATH via systemPackages.
    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
        nomarchy-doctor                     read-only health check
        SUPER+?                             keybindings cheatsheet
        SUPER+M → System › Firmware         check LVFS firmware updates (fwupd)
    '' + lib.optionalString config.nomarchy.hardware.fingerprint.enable ''
        SUPER+M → System › Fingerprint      enroll a finger (fprintd)
    '');

    # 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;
    };
    # Core security: enable AppArmor to confine desktop apps and services.
    security.apparmor.enable = true;
    security.apparmor.killUnconfinedConfinables = false;

    # Core stability: reboot automatically after 10s on a kernel panic
    # (prevents the system from hanging indefinitely on a black screen).
    boot.kernelParams = [ "panic=10" "oops=panic" ];

    # 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
      nomarchy-detect-hw  # post-install hardware re-probe (HARDWARE.md §8)

      # 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 -euo pipefail
        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)
        log=$(mktemp)
        trap 'rm -f "$log"' EXIT
        set +e
        if command -v nixos-rebuild-snap >/dev/null 2>&1; then
          sudo nixos-rebuild-snap "$@" 2>&1 | tee "$log"
        else
          sudo nixos-rebuild switch --flake "$flake#default" "$@" 2>&1 | tee "$log"
        fi
        rc=''${PIPESTATUS[0]}
        set -e
        if [ "$rc" -ne 0 ]; then
          echo
          echo "sys-update: rebuild FAILED (exit $rc). Last lines:"
          tail -n 40 "$log" || true
          echo
          echo "Diagnose:  nomarchy-doctor"
          echo "Recovery:  docs/RECOVERY.md  (boot menu generations / snapper)"
          exit "$rc"
        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 -euo pipefail
        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)
        log=$(mktemp)
        trap 'rm -f "$log"' EXIT
        set +e
        if command -v nixos-rebuild-snap >/dev/null 2>&1; then
          sudo nixos-rebuild-snap "$@" 2>&1 | tee "$log"
        else
          sudo nixos-rebuild switch --flake "$flake#default" "$@" 2>&1 | tee "$log"
        fi
        rc=''${PIPESTATUS[0]}
        set -e
        if [ "$rc" -ne 0 ]; then
          echo
          echo "sys-rebuild: rebuild FAILED (exit $rc). Last lines:"
          tail -n 40 "$log" || true
          echo
          echo "Diagnose:  nomarchy-doctor"
          echo "Recovery:  docs/RECOVERY.md  (boot menu generations / snapper)"
          exit "$rc"
        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 -euo pipefail
        if [ "$(id -u)" -eq 0 ]; then
          echo "home-update: run as your normal user" >&2
          exit 1
        fi
        flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
        log=$(mktemp)
        trap 'rm -f "$log"' EXIT
        set +e
        home-manager switch --flake "$flake" "$@" 2>&1 | tee "$log"
        rc=''${PIPESTATUS[0]}
        set -e
        if [ "$rc" -ne 0 ]; then
          echo
          echo "home-update: switch FAILED (exit $rc). Last lines:"
          tail -n 40 "$log" || true
          echo
          echo "Diagnose:  nomarchy-doctor"
          echo "Recovery:  home-manager generations  (or docs/RECOVERY.md)"
          exit "$rc"
        fi
      '')

      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";
      };
    };
  };
}
