Finishing the four remaining "opt-in services" candidates — but two of
them aren't service-shaped, so the package-vs-toggle principle applies:
- Nextcloud client → a bare tray app; already in the app-suite menu,
not a service.
- WireGuard → NetworkManager (on distro-wide) imports .conf
tunnels natively, so there's no daemon to "just
enable". Only the wg/wg-quick CLI is missing →
wireguard-tools added to the app-suite menu.
The two that ARE config-backed become nomarchy.services.* toggles:
- openrgb → services.hardware.openrgb (daemon + device udev rules).
- restic → a small scaffolded surface (repository / passwordFile /
paths) over services.restic.backups: daily timer, 7/4/6
retention, --exclude-caches, repo auto-init. Asserts that
repository + passwordFile are set when enabled. passwordFile
is typed `str`, not `path`, so a flake-relative secret can't
be copied into the world-readable store.
README option table and ROADMAP (services now "Seventeen shipped";
cloud-sync / networking / backup / devices areas updated) + the downstream
system.nix examples. Both branches eval-verified; the restic assertions
confirmed firing when repo/password are unset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
279 lines
10 KiB
Nix
279 lines
10 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;
|
|
in
|
|
{
|
|
options.nomarchy.services = {
|
|
tailscale.enable = lib.mkEnableOption ''
|
|
Tailscale, the mesh VPN — ships the daemon; authenticate once with
|
|
`sudo 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'';
|
|
|
|
printing.enable = lib.mkEnableOption ''
|
|
CUPS printing with Avahi/mDNS, so network printers are auto-discovered
|
|
(add vendor drivers via `services.printing.drivers`)'';
|
|
|
|
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)'';
|
|
|
|
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 [
|
|
(lib.mkIf cfg.tailscale.enable {
|
|
services.tailscale.enable = true;
|
|
})
|
|
|
|
(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;
|
|
};
|
|
})
|
|
|
|
(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
|
|
};
|
|
};
|
|
})
|
|
];
|
|
}
|