A rebuild against an unchanged lock installs nothing new, so a machine built daily can still run months-old packages. A user timer reads the locked lastModified of the nixpkgs input and notifies once it passes seven days, offering a terminal that runs the update.
270 lines
10 KiB
Nix
270 lines
10 KiB
Nix
# 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 <command>` 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.
|
|
}
|