Some checks failed
Check / eval (push) Has been cancelled
System is Connectivity / Devices / Recovery / Preferences under the same six-entry root. Recovery lists desktop (HM), system boot, and BTRFS files once each with cost labels. Control Center is gone: blur/gaps live in Look & Feel; update checks and Bluetooth/Printing package toggles in Preferences (with rebuilds). docs/RECOVERY paths updated.
321 lines
13 KiB
Nix
321 lines
13 KiB
Nix
# Opt-in services & integrations — off by default, one toggle each, enabled
|
||
# in system.nix (see the commented examples in the downstream template).
|
||
# These are system-side: their own `nomarchy.services.*` namespace, distinct
|
||
# from the distro's core `nomarchy.system.*` toggles. `username` (from
|
||
# specialArgs) is read lazily, only where a service needs it.
|
||
{ config, lib, pkgs, ... }@args:
|
||
|
||
let
|
||
cfg = config.nomarchy.services;
|
||
|
||
# Menu Preferences › Printing writes settings.printing.enable; read it
|
||
# from the state file, the only place it exists on the NixOS side (the
|
||
# hardware.nix/timezone.nix bridge). Missing/invalid JSON fails closed via
|
||
# state-read.nix rather than a raw stack. null = key absent, which
|
||
# leaves the option's own default alone.
|
||
svcState =
|
||
if config.nomarchy.system.stateFile != null
|
||
then import ../state-read.nix { inherit lib; } config.nomarchy.system.stateFile
|
||
else { };
|
||
statePrinting =
|
||
let v = (svcState.settings or { }).printing.enable or null;
|
||
in if builtins.isBool v then v else null;
|
||
in
|
||
{
|
||
options.nomarchy.services = {
|
||
tailscale.enable = lib.mkEnableOption ''
|
||
Tailscale, the mesh VPN — ships the daemon and makes the login user its
|
||
operator, so `tailscale up/down/set` and the System → VPN menu work
|
||
without sudo. Authenticate once (the menu's Connect, or `tailscale up`)'';
|
||
|
||
syncthing.enable = lib.mkEnableOption ''
|
||
Syncthing continuous file sync, running as the login user — add
|
||
folders and devices in the web UI at http://127.0.0.1:8384'';
|
||
|
||
podman.enable = lib.mkEnableOption ''
|
||
Podman for rootless containers, with `docker` aliased to it and DNS on
|
||
the default network; the login user gets a subuid/subgid range'';
|
||
|
||
flatpak.enable = lib.mkEnableOption ''
|
||
Flatpak, with the Flathub remote added system-wide — install apps with
|
||
`flatpak install flathub <app>`'';
|
||
|
||
pika.enable = lib.mkEnableOption ''
|
||
Pika Backup, a friendly GUI for scheduled Borg backups (the app;
|
||
configure repositories and schedules inside it)'';
|
||
|
||
steam.enable = lib.mkEnableOption ''
|
||
Steam (Valve's game store/launcher) via programs.steam — which wires up
|
||
the 32-bit graphics libraries, controller udev rules and Remote-Play
|
||
firewall ports a bare package can't. Unfree'';
|
||
|
||
libvirt.enable = lib.mkEnableOption ''
|
||
libvirt/KVM virtualisation with the virt-manager GUI — runs the
|
||
libvirtd daemon and adds the login user to the `libvirtd` group, so VMs
|
||
can be managed without root'';
|
||
|
||
obs.enable = lib.mkEnableOption ''
|
||
OBS Studio for screen recording and streaming, plus a v4l2loopback
|
||
virtual camera — so an OBS scene can be selected as a webcam in Zoom,
|
||
Teams or a browser (screen capture itself already works over the
|
||
desktop's PipeWire portal)'';
|
||
|
||
docker.enable = lib.mkEnableOption ''
|
||
Docker (rootful) with the login user in the `docker` group. Mutually
|
||
exclusive with the podman toggle's docker-compat (both provide the
|
||
`docker` command) — enable only one'';
|
||
|
||
kdeconnect.enable = lib.mkEnableOption ''
|
||
KDE Connect for phone integration — shared clipboard, notifications and
|
||
file transfer with a paired phone; opens the firewall ports it needs'';
|
||
|
||
gamemode.enable = lib.mkEnableOption ''
|
||
Feral GameMode — a daemon games ask to apply a performance CPU governor
|
||
and IO/nice priorities while running (via `gamemoderun`)'';
|
||
|
||
adb.enable = lib.mkEnableOption ''
|
||
the Android platform tools (adb/fastboot) for flashing/debugging phones.
|
||
systemd ≥258 applies the device uaccess udev rules automatically, so the
|
||
tools alone are enough — no group dance'';
|
||
|
||
wireshark.enable = lib.mkEnableOption ''
|
||
Wireshark (the Qt GUI) for packet capture, with the login user in the
|
||
`wireshark` group so capture works without root'';
|
||
|
||
ollama.enable = lib.mkEnableOption ''
|
||
Ollama, a local LLM runtime serving an API on 127.0.0.1:11434 (pair it
|
||
with the lmstudio/alpaca GUIs). CPU by default — set
|
||
`services.ollama.acceleration` natively for GPU offload'';
|
||
|
||
# default stays a plain `false` here; the state bridge is a mkDefault
|
||
# below, from settings.printing.enable (menu Preferences › Printing
|
||
# toggle) — see ROADMAP § state bridges (#116) for why the old read of
|
||
# `config.nomarchy.settings` never worked.
|
||
printing.enable = lib.mkEnableOption ''
|
||
CUPS printing with Avahi/mDNS, so network printers are auto-discovered
|
||
(add vendor drivers via `services.printing.drivers`); the menu's
|
||
System ▸ Printers entry opens the system-config-printer GUI'' // {
|
||
default = false;
|
||
defaultText = lib.literalExpression
|
||
"(settings.printing.enable from state.json) or false";
|
||
};
|
||
|
||
openrgb.enable = lib.mkEnableOption ''
|
||
the OpenRGB daemon and GUI for controlling RGB lighting on peripherals
|
||
and (where supported) motherboards — installs the udev rules for device
|
||
access. Some motherboard controllers also need `hardware.i2c.enable`'';
|
||
|
||
restic = {
|
||
enable = lib.mkEnableOption ''
|
||
a scheduled (daily) restic backup — headless, complementing the Pika
|
||
GUI. Backs up `paths` with sane retention (7 daily / 4 weekly /
|
||
6 monthly); needs a `repository` and a `passwordFile` (below). List
|
||
and restore with the generated `restic-nomarchy` wrapper (repo +
|
||
password baked in), e.g. `restic-nomarchy snapshots`'';
|
||
|
||
repository = lib.mkOption {
|
||
type = lib.types.str;
|
||
default = "";
|
||
example = "/mnt/backup/restic";
|
||
description = ''
|
||
The restic repository as a path or URL — e.g. `/mnt/backup/restic`,
|
||
`sftp:user@host:/srv/restic`, `b2:bucket:/path`. Required when
|
||
`restic.enable` is set.
|
||
'';
|
||
};
|
||
|
||
passwordFile = lib.mkOption {
|
||
type = lib.types.nullOr lib.types.str;
|
||
default = null;
|
||
example = "/etc/nomarchy/restic-password";
|
||
description = ''
|
||
Path to a file holding the repository password. Give an absolute
|
||
path that lives ON THE MACHINE — not a `./file` in the flake, which
|
||
would copy the secret into the world-readable Nix store. Required
|
||
when `restic.enable` is set.
|
||
'';
|
||
};
|
||
|
||
paths = lib.mkOption {
|
||
type = lib.types.listOf lib.types.str;
|
||
default = [ "/home" ];
|
||
description = "Paths to include in the backup.";
|
||
};
|
||
};
|
||
};
|
||
|
||
config = lib.mkMerge [
|
||
# The in-flake state drives the toggle; mkDefault so a hand-set
|
||
# nomarchy.services.printing.enable in system.nix still pins it (the
|
||
# greeter.autoLogin shape). mkIf, not a fallback expression, so an absent
|
||
# key leaves the option default as the single source of `false`.
|
||
(lib.mkIf (statePrinting != null) {
|
||
nomarchy.services.printing.enable = lib.mkDefault statePrinting;
|
||
})
|
||
|
||
(lib.mkIf cfg.tailscale.enable {
|
||
services.tailscale.enable = true;
|
||
# Let the login user drive tailscale (up/down/set — and so the VPN menu's
|
||
# Tailscale controls) without sudo. It's already in wheel, so the operator
|
||
# grant is no real new privilege, just skips the password prompt. The
|
||
# module's tailscaled-set unit applies this after the daemon starts.
|
||
# mkDefault so a downstream can drop or replace it (e.g. extraSetFlags = []).
|
||
services.tailscale.extraSetFlags = lib.mkDefault [ "--operator=${args.username}" ];
|
||
})
|
||
|
||
(lib.mkIf cfg.syncthing.enable {
|
||
services.syncthing = {
|
||
enable = true;
|
||
user = args.username;
|
||
# Manage folders/devices in the GUI; don't let the (empty) declarative
|
||
# config wipe them on a rebuild.
|
||
overrideDevices = false;
|
||
overrideFolders = false;
|
||
guiAddress = "127.0.0.1:8384";
|
||
};
|
||
})
|
||
|
||
(lib.mkIf cfg.podman.enable {
|
||
virtualisation.podman = {
|
||
enable = true;
|
||
dockerCompat = lib.mkDefault true; # `docker` → podman (off if you run real docker)
|
||
defaultNetwork.settings.dns_enabled = true; # DNS for rootless containers
|
||
};
|
||
# Rootless podman needs a subuid/subgid range for the user.
|
||
users.users.${args.username}.autoSubUidGidRange = lib.mkDefault true;
|
||
})
|
||
|
||
(lib.mkIf cfg.flatpak.enable {
|
||
services.flatpak.enable = true;
|
||
# Flatpak ships no remotes; add Flathub system-wide. Idempotent
|
||
# (--if-not-exists), and it reads the .flatpakrepo over the network, so
|
||
# it waits for connectivity and simply retries next boot if offline.
|
||
systemd.services.nomarchy-flathub = {
|
||
description = "Add the Flathub Flatpak remote";
|
||
wantedBy = [ "multi-user.target" ];
|
||
wants = [ "network-online.target" ];
|
||
after = [ "network-online.target" ];
|
||
path = [ pkgs.flatpak ];
|
||
serviceConfig.Type = "oneshot";
|
||
script = ''
|
||
flatpak remote-add --system --if-not-exists flathub \
|
||
https://dl.flathub.org/repo/flathub.flatpakrepo
|
||
'';
|
||
};
|
||
})
|
||
|
||
(lib.mkIf cfg.pika.enable {
|
||
environment.systemPackages = [ pkgs.pika-backup ];
|
||
})
|
||
|
||
(lib.mkIf cfg.steam.enable {
|
||
# The Steam package alone misses the system bits a game needs: the
|
||
# 32-bit graphics stack, controller udev rules, Remote-Play ports.
|
||
programs.steam.enable = true;
|
||
})
|
||
|
||
(lib.mkIf cfg.libvirt.enable {
|
||
virtualisation.libvirtd.enable = true;
|
||
programs.virt-manager.enable = true;
|
||
# Group membership lets the user reach the system libvirtd socket
|
||
# (manage VMs) without sudo. Merges with the user's other extraGroups.
|
||
users.users.${args.username}.extraGroups = [ "libvirtd" ];
|
||
})
|
||
|
||
(lib.mkIf cfg.obs.enable {
|
||
environment.systemPackages = [ pkgs.obs-studio ];
|
||
# Virtual camera: OBS streams a scene into a v4l2 loopback device that
|
||
# Zoom/Teams/browsers then pick up as a webcam. exclusive_caps=1 is the
|
||
# bit that makes those apps actually list the device.
|
||
boot.extraModulePackages = [ config.boot.kernelPackages.v4l2loopback ];
|
||
boot.kernelModules = [ "v4l2loopback" ];
|
||
boot.extraModprobeConfig = ''
|
||
options v4l2loopback devices=1 video_nr=10 card_label="OBS Virtual Camera" exclusive_caps=1
|
||
'';
|
||
})
|
||
|
||
(lib.mkIf cfg.docker.enable {
|
||
virtualisation.docker.enable = true;
|
||
users.users.${args.username}.extraGroups = [ "docker" ];
|
||
# Podman's docker-compat and real Docker both claim the `docker`
|
||
# command — guard against enabling both (unless you turn that off).
|
||
assertions = [{
|
||
assertion = !(cfg.podman.enable && config.virtualisation.podman.dockerCompat);
|
||
message = ''
|
||
nomarchy.services: docker and podman both provide the `docker`
|
||
command. Enable only one, or set
|
||
`virtualisation.podman.dockerCompat = false`.
|
||
'';
|
||
}];
|
||
})
|
||
|
||
(lib.mkIf cfg.kdeconnect.enable {
|
||
programs.kdeconnect.enable = true; # installs the app and opens its ports
|
||
})
|
||
|
||
(lib.mkIf cfg.gamemode.enable {
|
||
programs.gamemode.enable = true;
|
||
})
|
||
|
||
(lib.mkIf cfg.adb.enable {
|
||
# `programs.adb` was removed — systemd ≥258 handles the device uaccess
|
||
# udev rules, so the tools package is all that's needed now.
|
||
environment.systemPackages = [ pkgs.android-tools ];
|
||
})
|
||
|
||
(lib.mkIf cfg.wireshark.enable {
|
||
programs.wireshark = {
|
||
enable = true;
|
||
package = pkgs.wireshark; # the Qt GUI (the module defaults to the CLI)
|
||
};
|
||
users.users.${args.username}.extraGroups = [ "wireshark" ];
|
||
})
|
||
|
||
(lib.mkIf cfg.ollama.enable {
|
||
services.ollama.enable = true;
|
||
})
|
||
|
||
(lib.mkIf cfg.printing.enable {
|
||
services.printing.enable = true;
|
||
# mDNS so network printers show up without manual setup.
|
||
services.avahi = {
|
||
enable = true;
|
||
nssmdns4 = true;
|
||
openFirewall = true;
|
||
};
|
||
# The CUPS admin GUI — the menu's System ▸ Printers entry execs it
|
||
# (self-gated on this binary), so it ships with the printing service.
|
||
environment.systemPackages = [ pkgs.system-config-printer ];
|
||
})
|
||
|
||
(lib.mkIf cfg.openrgb.enable {
|
||
services.hardware.openrgb.enable = true;
|
||
})
|
||
|
||
(lib.mkIf cfg.restic.enable {
|
||
assertions = [
|
||
{
|
||
assertion = cfg.restic.repository != "";
|
||
message = "nomarchy.services.restic.repository must be set when restic is enabled.";
|
||
}
|
||
{
|
||
assertion = cfg.restic.passwordFile != null;
|
||
message = "nomarchy.services.restic.passwordFile must be set when restic is enabled.";
|
||
}
|
||
];
|
||
services.restic.backups.nomarchy = {
|
||
repository = cfg.restic.repository;
|
||
passwordFile = cfg.restic.passwordFile;
|
||
paths = cfg.restic.paths;
|
||
initialize = true; # create the repo on first run if it doesn't exist
|
||
extraBackupArgs = [ "--exclude-caches" ];
|
||
pruneOpts = [ "--keep-daily 7" "--keep-weekly 4" "--keep-monthly 6" ];
|
||
timerConfig = {
|
||
OnCalendar = "daily";
|
||
Persistent = true; # catch up a missed run after downtime
|
||
};
|
||
};
|
||
})
|
||
];
|
||
}
|