feat: Tier 1 system features — snapper, hibernate, containers, libvirt, keyring

Five opt-in modules lifted from bernardo/nixos and adapted to Nomarchy's
nomarchy.system.* option namespace. All default off (except keyring which
defaults on); evaluation of the existing VM/ISO is unchanged when the
toggles are unset.

- core/system/snapper.nix: BTRFS timeline snapshots (5h/7d), nixos-rebuild-snap
  wrapper that pre-snaps before each switch using the running hostname.
  Auto-skips when / isn't BTRFS so impermanence/non-BTRFS hosts are safe.
- core/system/hibernate.nix: suspend-then-hibernate on lid/idle/power-key
  with configurable idleMinutes (default 30). Description warns swap is
  required.
- core/system/containers.nix: rootless Podman with dockerCompat + dns +
  podman-compose, podman-tui, dive. Better default than the docker daemon
  for a desktop distro.
- core/system/virtualization.nix: extends the existing uwsm/Hyprland file
  with a libvirt + virt-manager + OVMF branch behind
  nomarchy.system.virtualization.libvirt.enable.
- core/system/pam.nix: GNOME Keyring auto-unlock at SDDM/login/hyprlock
  plus gcr-ssh-agent so SSH keys flow through the keyring instead of a
  separate ssh-agent. Default on.
- core/system/options.nix: declares the five new options.
- core/system/default.nix: imports the four new files.
- installer/install.sh: surfaces all five toggles as commented one-liners
  in the "Optional Nomarchy modules" section of the generated system.nix.
  Verified via the existing dry-run / generator smoke test.

Verified each toggle lights up the right NixOS option (services.snapper,
logind IdleAction, virtualisation.podman/libvirtd, pam.sddm.enableGnomeKeyring)
via nix eval against extendModules. VM and live-ISO toplevels still build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Bernardo Magri
2026-04-25 11:18:15 +01:00
parent 220fc7f699
commit 4ddc91b930
8 changed files with 208 additions and 1 deletions

View File

@@ -0,0 +1,23 @@
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.system.containers;
in
{
config = lib.mkIf cfg.enable {
virtualisation.podman = {
enable = true;
# `docker` and `docker-compose` invocations transparently route to
# podman. Pairs cleanly with rootless mode.
dockerCompat = true;
defaultNetwork.settings.dns_enabled = true;
};
environment.systemPackages = with pkgs; [
podman
podman-compose
podman-tui
dive
];
};
}

View File

@@ -18,6 +18,11 @@
./impermanence.nix ./impermanence.nix
./browser.nix ./browser.nix
./makima.nix ./makima.nix
# Tier 1 system features (all opt-in via nomarchy.system.*).
./snapper.nix
./hibernate.nix
./containers.nix
./pam.nix
../../themes/engine/plymouth.nix ../../themes/engine/plymouth.nix
../../themes/engine/sddm.nix ../../themes/engine/sddm.nix
]; ];

24
core/system/hibernate.nix Normal file
View File

@@ -0,0 +1,24 @@
{ config, lib, ... }:
let
cfg = config.nomarchy.system.hibernation;
in
{
config = lib.mkIf cfg.enable {
# Wait this long after suspend before hibernating, and use the same
# delay as the idle-action timeout so the two paths agree.
systemd.sleep.extraConfig = ''
HibernateDelaySec=${toString (cfg.idleMinutes * 60)}
'';
services.logind = {
settings.Login = {
HandleLidSwitch = lib.mkDefault "suspend-then-hibernate";
HandleLidSwitchExternalPower = lib.mkDefault "suspend";
HandlePowerKey = "hibernate";
IdleAction = "suspend-then-hibernate";
IdleActionSec = toString (cfg.idleMinutes * 60);
};
};
};
}

View File

@@ -51,5 +51,56 @@
default = "summer-night"; default = "summer-night";
description = "Selected system theme."; description = "Selected system theme.";
}; };
# ----- Tier 1 system features (all opt-in, no behavioural change off) ---
snapper = {
enable = lib.mkEnableOption ''
Snapper-driven BTRFS timeline snapshots of `/`. Auto-disables when
`/` isn't BTRFS. Includes a `nixos-rebuild-snap` wrapper that takes
a "Pre-rebuild" snapshot before each switch.
'';
};
hibernation = {
enable = lib.mkEnableOption ''
suspend-then-hibernate (lid close, idle, power button). NOTE: this
requires a disk swap device or swapfile sized to at least RAM
zRAM alone is not enough.
'';
idleMinutes = lib.mkOption {
type = lib.types.int;
default = 30;
description = "Idle minutes before suspend-then-hibernate fires.";
};
};
containers = {
enable = lib.mkEnableOption ''
Rootless Podman with Docker compatibility (`docker` `podman`),
plus podman-compose, podman-tui and dive.
'';
};
virtualization = {
libvirt = {
enable = lib.mkEnableOption ''
libvirt daemon + virt-manager + OVMF. The user must be in the
`libvirtd` group.
'';
};
};
keyring = {
enable = lib.mkOption {
type = lib.types.bool;
default = true;
description = ''
Auto-unlock GNOME Keyring at SDDM/Hyprland login and route SSH
keys through `gcr-ssh-agent`. Default on near-universal QoL
improvement.
'';
};
};
}; };
} }

28
core/system/pam.nix Normal file
View File

@@ -0,0 +1,28 @@
{ config, lib, ... }:
let
cfg = config.nomarchy.system.keyring;
in
{
config = lib.mkIf cfg.enable {
# Auto-unlock GNOME Keyring at SDDM autologin and at login. hyprlock
# gets the same treatment so the session keyring stays unlocked when
# the screen lock disengages.
security.pam.services = {
login.enableGnomeKeyring = true;
sddm.enableGnomeKeyring = true;
hyprlock.enableGnomeKeyring = true;
};
# Run the keyring + the gcr SSH agent. Disabling `programs.ssh.startAgent`
# ensures keys flow through the keyring's agent (so unlock-on-login
# carries over to ssh) instead of a separate ssh-agent process.
services.gnome.gnome-keyring.enable = true;
services.gnome.gcr-ssh-agent.enable = true;
programs.ssh.startAgent = lib.mkForce false;
# Point downstream tooling at the gcr socket so `ssh` / `git` / etc.
# find the keyring's keys without per-user shell config.
environment.sessionVariables.SSH_AUTH_SOCK = "$XDG_RUNTIME_DIR/gcr/ssh";
};
}

42
core/system/snapper.nix Normal file
View File

@@ -0,0 +1,42 @@
{ config, lib, pkgs, ... }:
let
cfg = config.nomarchy.system.snapper;
rootIsBtrfs = (config.fileSystems."/".fsType or "") == "btrfs";
active = cfg.enable && rootIsBtrfs;
in
{
config = lib.mkIf active {
# `nixos-rebuild-snap`: take a Snapper pre-rebuild snapshot, then run
# `nixos-rebuild switch` against the current host. The hostname is read
# from the running config so this script works on every machine without
# editing.
environment.systemPackages = [
(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 .#${config.networking.hostName} "$@"
'')
];
services.snapper.configs = {
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";
};
};
};
}

View File

@@ -1,6 +1,11 @@
{ lib, ... }: { config, lib, pkgs, ... }:
let
libvirt = config.nomarchy.system.virtualization.libvirt.enable;
in
{ {
# uwsm + Hyprland session — present on every Nomarchy install regardless
# of the optional libvirt branch below.
programs.uwsm = { programs.uwsm = {
enable = lib.mkDefault true; enable = lib.mkDefault true;
waylandCompositors.hyprland = { waylandCompositors.hyprland = {
@@ -8,4 +13,14 @@
prettyName = "Hyprland"; prettyName = "Hyprland";
}; };
}; };
# Optional: libvirt + virt-manager + OVMF. Toggle with
# `nomarchy.system.virtualization.libvirt.enable = true;`. The user must
# be in the `libvirtd` group to drive virsh / virt-manager.
virtualisation.libvirtd.enable = lib.mkIf libvirt true;
environment.systemPackages = lib.mkIf libvirt (with pkgs; [
virt-manager
qemu
OVMF
]);
} }

View File

@@ -883,6 +883,25 @@ EOF
# programs.steam.enable = true; # programs.steam.enable = true;
# programs.gamemode.enable = true; # programs.gamemode.enable = true;
# --- Optional Nomarchy modules ---
# Each line is a one-shot toggle for a Tier 1 system feature.
# BTRFS timeline snapshots of /. Auto-skips on non-BTRFS.
# nomarchy.system.snapper.enable = true;
# Suspend-then-hibernate on lid/idle/power-key. Requires disk swap.
# nomarchy.system.hibernation.enable = true;
# nomarchy.system.hibernation.idleMinutes = 30;
# Rootless Podman with \`docker\` compatibility.
# nomarchy.system.containers.enable = true;
# libvirt + virt-manager + OVMF.
# nomarchy.system.virtualization.libvirt.enable = true;
# GNOME Keyring auto-unlock + gcr SSH agent (default: on).
# nomarchy.system.keyring.enable = false;
system.stateVersion = "25.11"; system.stateVersion = "25.11";
} }
EOF EOF