# ABOUTME: systemd system and user services for the desktop session. # ABOUTME: Mostly rebuilt from moonarch/defaults/etc/systemd/, whose units use FHS paths. { pkgs, lib, moonarchPkgs, ... }: let # Days after which the nixpkgs input counts as stale. nixpkgs carries no # patch backports, it bumps versions, so an old lock means known holes stay # open until the next update — the age of the lock is the exposure. maxLockAgeDays = 7; # Reports the age of the nixpkgs input to the session's notification daemon. # # The age comes from the lock rather than the file mtime: a fresh clone or a # branch switch rewrites the mtime without changing a single package version. # # The flake is found through /etc/nixos/flake.nix, which README documents as a # symlink into the account's checkout. A machine built straight from the # remote reference has no checkout and therefore no lock to read, which is why # a missing link is not an error. flakeAgeCheck = pkgs.writeShellApplication { name = "moonix-flake-age"; runtimeInputs = with pkgs; [ bash coreutils jq libnotify xdg-terminal-exec ]; text = '' link=/etc/nixos/flake.nix [ -L "$link" ] || exit 0 flake=$(dirname "$(readlink -f "$link")") lock="$flake/flake.lock" [ -r "$lock" ] || exit 0 # root.inputs.nixpkgs names the node rather than being it: the entry is # called nixpkgs_2 as soon as a dependency locks a nixpkgs of its own. # Reading .nodes.nixpkgs directly would report a transitive input's date. locked=$(jq -r '.nodes[.nodes.root.inputs.nixpkgs].locked.lastModified' "$lock") if [ -z "$locked" ] || [ "$locked" = null ]; then echo "no nixpkgs lastModified in $lock" >&2 exit 1 fi days=$(( ( $(date +%s) - locked ) / 86400 )) [ "$days" -ge ${toString maxLockAgeDays} ] || exit 0 # The path is resolved rather than written out: `nix flake update` acts on # the working directory, so without the right one it updates nothing or # the wrong flake. printf because the body carries newlines. # # Three lines exactly: NotifToast.qml caps the body at maximumLineCount 3 # and elides the rest, so a fourth line would drop a command. body=$(printf 'cd %s\nnix flake update\nsudo nixos-rebuild switch' "$flake") # Critical rather than normal: a stale lock does not pass, it stays true # until a rebuild, and the toast is exempt from the auto-expire timer. # # -A implies --wait, so this blocks while the toast stands and prints the # chosen action; a dismissed toast leaves it empty. Nothing has to bound # that wait: while the process lives the notification is still on screen, # and a second one a day later would say the same thing. choice=$(notify-send -a moonix -u critical \ -A update="Update now" \ "nixpkgs is $days days old" "$body" || true) [ "$choice" = update ] || exit 0 # A terminal rather than a silent rebuild: nixpkgs is unstable, so the # build can fail, and sudo needs someone to answer it. xdg-terminal-exec # instead of a named emulator so switching terminals is one entry in # xdg-terminals.list. bash -lc for the PATH — neither the system nix nor # the sudo wrapper is in this script's closure, and pinning nix here # would shadow nix.package. exec xdg-terminal-exec bash -lc \ "cd \"$flake\" && nix flake update && sudo nixos-rebuild switch; \ read -r -p '[enter to close] ' _" ''; }; in { # --- Audio --- services.pipewire = { enable = true; alsa.enable = true; alsa.support32Bit = true; pulse.enable = true; jack.enable = true; }; # --- Hardware --- hardware.bluetooth.enable = true; services.blueman.enable = true; services.udisks2.enable = true; services.upower.enable = true; services.gvfs.enable = true; services.fwupd.enable = true; # power-profiles-daemon over auto-cpufreq: on amd-pstate in active mode the # platform decides the frequency from the min/max and EPP registers alone, so # the governor is the wrong knob. ppd writes EPP and the ACPI platform_profile; # where neither exists it falls back to a driver that touches no hardware. services.power-profiles-daemon.enable = true; # --- Firewall, replaces the ufw calls in post-install.sh --- networking.firewall.enable = true; # --- Session services --- # # Rebuilt rather than copied: the upstream units reference /usr/bin/, # which does not exist under Nix. systemd.user.services.kanshi = { description = "Dynamic display configuration"; partOf = [ "graphical-session.target" ]; after = [ "graphical-session.target" ]; wantedBy = [ "graphical-session.target" ]; # Without a config kanshi exits with a parse error and, with # Restart=on-failure, spins. Monitor profiles are per machine. unitConfig.ConditionPathExists = "%h/.config/kanshi/config"; serviceConfig = { Type = "simple"; ExecStart = lib.getExe pkgs.kanshi; Restart = "on-failure"; RestartSec = 3; }; }; # --- System services from defaults/etc/systemd/system/ --- # # Rebuilt rather than copied, same reason as the user units: the originals # reference /usr/bin. systemd.services.moonarch-batsaver = { description = "Restore battery conservation mode threshold"; after = [ "sysinit.target" ]; wantedBy = [ "multi-user.target" ]; unitConfig.ConditionPathExists = [ "/sys/class/power_supply/BAT0/charge_control_end_threshold" "/var/lib/moonarch/batsaver-threshold" ]; serviceConfig = { Type = "oneshot"; ExecStart = "${moonarchPkgs.moonarch-scripts}/bin/moonarch-batsaver-restore"; NoNewPrivileges = true; ProtectHome = true; PrivateTmp = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictNamespaces = true; RestrictRealtime = true; LockPersonality = true; }; }; systemd.services.moonarch-camera = { description = "Switch off all cameras on boot"; after = [ "sysinit.target" ]; wantedBy = [ "multi-user.target" ]; unitConfig.ConditionPathExistsGlob = "/sys/class/video4linux/video*"; serviceConfig = { Type = "oneshot"; ExecStart = "${moonarchPkgs.moonarch-scripts}/bin/moonarch-camera-boot"; NoNewPrivileges = true; ProtectHome = true; PrivateTmp = true; ProtectKernelModules = true; ProtectControlGroups = true; RestrictNamespaces = true; RestrictRealtime = true; LockPersonality = true; }; }; # nixpkgs ships the package but no module, and the package carries no unit of # its own, so it is declared here. Mirrors /usr/lib/systemd/user/stasis.service. systemd.user.services.stasis = { description = "Stasis Wayland Idle Manager"; partOf = [ "graphical-session.target" ]; after = [ "graphical-session.target" ]; wantedBy = [ "graphical-session.target" ]; # stasis runs its actions as `sh -lc ` and spawns niri and pactl # directly, all resolved against this PATH. pulseaudio is here for pactl # alone; it stays off systemPackages, see modules/desktop.nix. path = with pkgs; [ bash swaylock brightnessctl niri pulseaudio ]; serviceConfig = { Type = "simple"; ExecStart = "${lib.getExe pkgs.stasis}"; ExecReload = "${lib.getExe pkgs.stasis} reload"; Restart = "on-failure"; RestartSec = 2; }; }; systemd.user.services.cliphist-text = { description = "Clipboard history manager (text)"; partOf = [ "graphical-session.target" ]; wantedBy = [ "graphical-session.target" ]; serviceConfig = { Type = "simple"; ExecStartPre = "${pkgs.runtimeShell} -c 'mkdir -p $XDG_RUNTIME_DIR/cliphist && ${pkgs.cliphist}/bin/cliphist wipe'"; ExecStart = "${pkgs.wl-clipboard}/bin/wl-paste --watch ${pkgs.cliphist}/bin/cliphist -db-path %t/cliphist/db store"; Restart = "on-failure"; RestartSec = 3; }; }; systemd.user.services.cliphist-image = { description = "Clipboard history manager (image)"; partOf = [ "graphical-session.target" ]; after = [ "cliphist-text.service" ]; wantedBy = [ "graphical-session.target" ]; serviceConfig = { Type = "simple"; ExecStart = "${pkgs.wl-clipboard}/bin/wl-paste --type image --watch ${pkgs.cliphist}/bin/cliphist -db-path %t/cliphist/db store"; Restart = "on-failure"; RestartSec = 3; }; }; # Night light stays disabled by default: it is a user toggle. Enabling it # here would create a system-scope symlink that overrides any # `systemctl --user disable`. systemd.user.services.wlsunset = { description = "Wlsunset night light (blue light filter)"; partOf = [ "graphical-session.target" ]; after = [ "graphical-session.target" "kanshi.service" ]; wantedBy = [ ]; serviceConfig = { Type = "simple"; ExecStartPre = "${pkgs.coreutils}/bin/sleep 2"; ExecStart = "${lib.getExe pkgs.wlsunset} -T 6500 -t 5000 -S 00:00 -s 00:01"; Restart = "on-failure"; RestartSec = 3; }; }; # --- Flake age --- # # A user unit rather than a system one: the notification goes to the session's # daemon, and the checkout it reads belongs to the account. systemd.user.services.moonix-flake-age = { description = "Warn when the nixpkgs input has gone stale"; serviceConfig = { Type = "oneshot"; ExecStart = lib.getExe flakeAgeCheck; # ProtectHome is left off deliberately: the lock sits in the account's # checkout. The rest costs nothing for a script that only reads a file. NoNewPrivileges = true; PrivateTmp = true; RestrictNamespaces = true; RestrictRealtime = true; LockPersonality = true; }; }; systemd.user.timers.moonix-flake-age = { description = "Daily nixpkgs staleness check"; wantedBy = [ "timers.target" ]; # Relative to the user manager, not the calendar: a desktop that is off at # midnight would otherwise report on a session nobody is looking at. The # delay after login gives the notification daemon in the bar time to claim # the D-Bus name. timerConfig = { OnStartupSec = "5min"; OnUnitActiveSec = "1d"; }; }; # The polkit agent is not a service here: niri spawns it via # spawn-at-startup in defaults/xdg/niri/config.kdl. Only the FHS path it # references has to exist — see modules/desktop.nix. }