diff --git a/core/system/containers.nix b/core/system/containers.nix new file mode 100644 index 0000000..491579e --- /dev/null +++ b/core/system/containers.nix @@ -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 + ]; + }; +} diff --git a/core/system/default.nix b/core/system/default.nix index eedc3be..ff55996 100644 --- a/core/system/default.nix +++ b/core/system/default.nix @@ -18,6 +18,11 @@ ./impermanence.nix ./browser.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/sddm.nix ]; diff --git a/core/system/hibernate.nix b/core/system/hibernate.nix new file mode 100644 index 0000000..ce19b2e --- /dev/null +++ b/core/system/hibernate.nix @@ -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); + }; + }; + }; +} diff --git a/core/system/options.nix b/core/system/options.nix index 3813aae..03145a0 100644 --- a/core/system/options.nix +++ b/core/system/options.nix @@ -51,5 +51,56 @@ default = "summer-night"; 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. + ''; + }; + }; }; } diff --git a/core/system/pam.nix b/core/system/pam.nix new file mode 100644 index 0000000..14ced58 --- /dev/null +++ b/core/system/pam.nix @@ -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"; + }; +} diff --git a/core/system/snapper.nix b/core/system/snapper.nix new file mode 100644 index 0000000..95b93ca --- /dev/null +++ b/core/system/snapper.nix @@ -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"; + }; + }; + }; +} diff --git a/core/system/virtualization.nix b/core/system/virtualization.nix index d475d0f..45f4f36 100644 --- a/core/system/virtualization.nix +++ b/core/system/virtualization.nix @@ -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 = { enable = lib.mkDefault true; waylandCompositors.hyprland = { @@ -8,4 +13,14 @@ 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 + ]); } diff --git a/installer/install.sh b/installer/install.sh index 693476c..1e6c997 100755 --- a/installer/install.sh +++ b/installer/install.sh @@ -883,6 +883,25 @@ EOF # programs.steam.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"; } EOF