Files
Nomarchy/modules/nixos/services.nix
Bernardo Magri 856a9d1a49
Some checks failed
Check / eval (push) Has been cancelled
feat(menu): Recovery scopes (#111), System IA (#105), drop Control Center (#110)
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.
2026-07-15 11:49:57 +01:00

321 lines
13 KiB
Nix
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
};
};
})
];
}