aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.github/copilot-instructions.md4
-rw-r--r--README.md2
-rw-r--r--justfile175
-rw-r--r--meta/base.txt180
-rw-r--r--meta/browser.txt1
-rw-r--r--meta/bt.txt3
-rw-r--r--meta/cpp.txt10
-rw-r--r--meta/dev.txt24
-rw-r--r--meta/extra.txt23
-rw-r--r--meta/fonts.txt8
-rw-r--r--meta/fortran.txt3
-rw-r--r--meta/mail.txt9
-rw-r--r--meta/media.txt7
-rw-r--r--meta/nix.txt16
-rw-r--r--meta/sound.txt10
-rw-r--r--meta/wayland.txt59
-rw-r--r--systemd-units/system.ignore (renamed from systemd-units/system/.ignore)2
-rw-r--r--systemd-units/system.txt30
-rw-r--r--systemd-units/system/base.txt19
-rw-r--r--systemd-units/system/bt.txt4
-rw-r--r--systemd-units/system/btc.txt4
-rw-r--r--systemd-units/system/nix.txt3
-rw-r--r--systemd-units/user.ignore (renamed from systemd-units/user/.ignore)0
-rw-r--r--systemd-units/user.txt14
-rw-r--r--systemd-units/user/graphical.txt9
-rw-r--r--systemd-units/user/mail.txt2
26 files changed, 312 insertions, 309 deletions
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index d56fa93..34ea40b 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -14,8 +14,8 @@ The repo root is a chezmoi source directory. Files targeting `$HOME` use chezmoi
- `dot_config/`, `private_dot_gnupg/`, `private_dot_ssh/`, etc. — chezmoi source state mapping to `$HOME`. Prefix meanings: `dot_` → leading `.`, `private_` → restricted permissions, `executable_` → `+x` bit.
- `etc/` contains system-level configs (`/etc/` targets) — systemd units, pacman hooks, sysctl tunables, kernel module loading. Deployed by `run_onchange_after_deploy-etc.sh.tmpl`.
-- `meta/` contains plain text package lists for Arch Linux (one package per line, `#` comments). Each `.txt` file represents a group (e.g. `base.txt`, `dev.txt`, `wayland.txt`). Install with `just pkg-apply base dev` or `just pkg-apply` (all groups). Detect drift with `just pkg-status` (or `just status` for the aggregate).
-- `systemd-units/` contains plain text systemd unit lists split by scope: `systemd-units/system/<group>.txt` for system units (enabled via `sudo systemctl`) and `systemd-units/user/<group>.txt` for user units (enabled via `systemctl --user`). System groups are paired by name with `meta/` groups (e.g. `systemd-units/system/base.txt` ↔ `meta/base.txt`); user groups stand alone. Units listed here are enabled by `just unit-apply` (run automatically by `just init`, walks both scopes). Inspect with `just unit-list`, detect drift with `just unit-status`. The recipe group token is `<name>` or `system:<name>` (both → `system/<name>.txt`) or `user:<name>` (→ `user/<name>.txt`). E.g. `just unit-add user:graphical kanshi.service`.
+- `meta/` contains plain text package lists for Arch Linux (one package per line, `#` comments). Each `.txt` file represents a group. The layout is intentionally flat: `base.txt` holds everything universal; the only other groups are truly optional / per-machine — `flatpak.txt` (magic name; per-user flatpak app IDs), `intel.txt`/`nvidia.txt` (hardware-specific), `work.txt`/`btc.txt` (per-machine policy). Install with `just pkg-apply base intel` or `just pkg-apply` (all groups). Detect drift with `just pkg-status` (or `just status` for the aggregate).
+- `systemd-units/` contains two flat plain-text files: `system.txt` (enabled via `sudo systemctl`) and `user.txt` (enabled via `systemctl --user`). One unit per line, `#` comments OK. Sibling `system.ignore` / `user.ignore` files suppress distro-default units from `unit-status` uncurated output. Units listed here are enabled by `just unit-apply` (run automatically by `just init`, walks both scopes). Inspect with `just unit-list`, detect drift with `just unit-status`. `just unit-add <unit>...` and `just unit-forget <unit>...` infer scope by probing `systemctl [--user] cat <unit>` — no group/scope argument.
- `firefox/` contains Firefox/LibreWolf hardening overrides (`user-overrides.js`) and custom CSS (`chrome/userChrome.css`). Deployed by `run_onchange_after_deploy-firefox.sh.tmpl`.
- `bootstrap.sh` at the repo root is a POSIX shell script that takes a fresh minimal Arch install (only `base`) to a fully deployed state. It installs prerequisites, enables `%wheel` sudoers, bootstraps `paru-bin` from the AUR, clones the repo, and runs `just init`. On EFI systems missing an Arch boot entry it prints the `efibootmgr` command to register the UKI. Designed to be curlable: `curl -fsSL .../bootstrap.sh | sh`.
- `.chezmoiignore` excludes non-home files (`etc/`, `meta/`, `systemd-units/`, `firefox/`, docs) from deployment to `$HOME`.
diff --git a/README.md b/README.md
index 9c5a18c..83af3e3 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,7 @@ Four sources of drift are tracked independently and combined by `just status`:
## Firewall
-Stateful nftables firewall with a laptop profile: default-deny inbound, allow outbound, loopback + established + ICMP/ICMPv6 + DHCPv6 client only. Ruleset at `etc/nftables.conf`; enabled via `nftables.service` in `systemd-units/system/base.txt`. Kernel hardening (rp_filter, no redirects, no source-route, log_martians) lives in `etc/sysctl.d/99-sysctl.conf`.
+Stateful nftables firewall with a laptop profile: default-deny inbound, allow outbound, loopback + established + ICMP/ICMPv6 + DHCPv6 client only. Ruleset at `etc/nftables.conf`; enabled via `nftables.service` in `systemd-units/system.txt`. Kernel hardening (rp_filter, no redirects, no source-route, log_martians) lives in `etc/sysctl.d/99-sysctl.conf`.
The ruleset is scoped to `table inet filter` and uses `destroy table inet filter` on reload, so podman/netavark's own tables are preserved. Don't `systemctl stop nftables` — the package ExecStop runs a global `nft flush ruleset` which would nuke podman rules. Reload with `sudo systemctl reload nftables` or `sudo nft -f /etc/nftables.conf` instead.
diff --git a/justfile b/justfile
index b4dfc65..f05b825 100644
--- a/justfile
+++ b/justfile
@@ -292,7 +292,7 @@ status: dotfiles-status etc-status pkg-status unit-status
# For 2-arg verbs (add, forget): the 2nd arg is the discriminator.
# ═══════════════════════════════════════════════════════════════════
-# Add one or more paths (dotfile/etc) or packages/units (GROUP + names) to the repo
+# Add one or more paths (dotfile/etc), units, or packages (GROUP + names) to the repo
add +args:
#!/usr/bin/env bash
set -eo pipefail
@@ -302,20 +302,21 @@ add +args:
/etc/*|etc/*) just etc-add "${args[@]}" ; exit 0 ;;
*/*) just dotfiles-add "${args[@]}" ; exit 0 ;;
esac
- if [ ${#args[@]} -lt 2 ]; then
- echo "error: add needs either a path, or a GROUP plus one or more pkg/unit names" >&2
- echo " (got single bare word: $first)" >&2
- exit 1
- fi
- for a in "${args[@]:1}"; do
+ # Units: any arg looks like a unit (no GROUP prefix; scope is inferred).
+ for a in "${args[@]}"; do
case "$a" in
*.service|*.timer|*.socket|*.mount|*.target|*.path)
just unit-add "${args[@]}"; exit 0 ;;
esac
done
+ if [ ${#args[@]} -lt 2 ]; then
+ echo "error: add needs either a path, a unit name, or a GROUP plus one or more pkg names" >&2
+ echo " (got single bare word: $first)" >&2
+ exit 1
+ fi
just pkg-add "${args[@]}"
-# Remove one or more paths, or packages/units (GROUP + names), from tracking (leaves live state alone)
+# Remove one or more paths, units, or packages (GROUP + names) from tracking (leaves live state alone)
forget +args:
#!/usr/bin/env bash
set -eo pipefail
@@ -325,17 +326,17 @@ forget +args:
/etc/*|etc/*) just etc-forget "${args[@]}" ; exit 0 ;;
*/*) just dotfiles-forget "${args[@]}" ; exit 0 ;;
esac
- if [ ${#args[@]} -lt 2 ]; then
- echo "error: forget needs either a path, or a GROUP plus one or more pkg/unit names" >&2
- echo " (got single bare word: $first)" >&2
- exit 1
- fi
- for a in "${args[@]:1}"; do
+ for a in "${args[@]}"; do
case "$a" in
*.service|*.timer|*.socket|*.mount|*.target|*.path)
just unit-forget "${args[@]}"; exit 0 ;;
esac
done
+ if [ ${#args[@]} -lt 2 ]; then
+ echo "error: forget needs either a path, a unit name, or a GROUP plus one or more pkg names" >&2
+ echo " (got single bare word: $first)" >&2
+ exit 1
+ fi
just pkg-forget "${args[@]}"
# Show dotfile + /etc diffs; pass a path to limit to a single file
@@ -418,19 +419,21 @@ dotfiles-status:
# ═══════════════════════════════════════════════════════════════════
# Units domain (systemd)
# ═══════════════════════════════════════════════════════════════════
-# Group tokens: `<name>` == `system:<name>` → systemd-units/system/<name>.txt;
-# `user:<name>` → systemd-units/user/<name>.txt. System units use `sudo systemctl`,
-# user units use `systemctl --user` (no sudo). No-arg unit-list/unit-apply/unit-status
-# walk both trees.
+# systemd-units domain
+# ═══════════════════════════════════════════════════════════════════
+#
+# Two flat lists: systemd-units/system.txt (enabled via `sudo systemctl`)
+# and systemd-units/user.txt (enabled via `systemctl --user`). No groups.
+# unit-add / unit-forget infer scope by probing the unit's existence with
+# systemctl [--user] cat — caller doesn't pass a scope.
-# List curated systemd units grouped by systemd-units/{system,user}/<group>.txt with their state; pass a group (optionally `user:`/`system:` prefixed) to narrow
-unit-list group="":
+# List curated systemd units with their enabled/active state
+unit-list:
#!/bin/sh
_render() {
scope=$1 file=$2
sctl="systemctl"; [ "$scope" = user ] && sctl="systemctl --user"
- group=$(basename "$file" .txt)
- echo "=== ${scope}:${group} ==="
+ echo "=== ${scope} ==="
sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "$file" | while read -r u; do
en=$($sctl is-enabled "$u" 2>/dev/null); en=${en:-unknown}
ac=$($sctl is-active "$u" 2>/dev/null); ac=${ac:-unknown}
@@ -447,42 +450,27 @@ unit-list group="":
printf ' %-34s \033[%sm%-10s\033[0m \033[%sm%s\033[0m\n' "$u" "$c_en" "$en" "$c_ac" "$ac"
done
}
- g='{{ group }}'
- if [ -n "$g" ]; then
- case "$g" in
- user:*) scope=user; name=${g#user:} ;;
- system:*) scope=system; name=${g#system:} ;;
- *) scope=system; name=$g ;;
- esac
- file="systemd-units/${scope}/${name}.txt"
- [ -f "$file" ] || { echo "error: $file does not exist" >&2; exit 1; }
+ for scope in system user; do
+ file="systemd-units/${scope}.txt"
+ [ -f "$file" ] || continue
_render "$scope" "$file"
- else
- for scope in system user; do
- for file in systemd-units/"$scope"/*.txt; do
- [ -f "$file" ] || continue
- _render "$scope" "$file"
- done
- done
- fi
+ done
-# Enable all curated systemd units (idempotent, soft-fail per unit); walks both system/ and user/ trees
+# Enable all curated systemd units (idempotent, soft-fail per unit); walks system + user lists
unit-apply:
#!/bin/sh
- for file in systemd-units/system/*.txt; do
- [ -f "$file" ] || continue
- sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "$file" | while read -r u; do
+ if [ -f systemd-units/system.txt ]; then
+ sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' systemd-units/system.txt | while read -r u; do
sudo systemctl enable --now "$u" \
|| echo " warn: could not enable $u (system)" >&2
done
- done
- for file in systemd-units/user/*.txt; do
- [ -f "$file" ] || continue
- sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "$file" | while read -r u; do
+ fi
+ if [ -f systemd-units/user.txt ]; then
+ sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' systemd-units/user.txt | while read -r u; do
systemctl --user enable --now "$u" \
|| echo " warn: could not enable $u (user)" >&2
done
- done
+ fi
# Show drift between curated units and actually-enabled systemd units (system + user)
unit-status:
@@ -492,10 +480,13 @@ unit-status:
scope=$1 label=$2
sctl="systemctl"; [ "$scope" = user ] && sctl="systemctl --user"
echo "=== ${label} drift ==="
- cat systemd-units/"$scope"/*.txt 2>/dev/null \
- | sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' | sort -u > "$tmp/curated"
- if [ -f "systemd-units/$scope/.ignore" ]; then
- sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "systemd-units/$scope/.ignore" | sort -u > "$tmp/ignore"
+ if [ -f "systemd-units/${scope}.txt" ]; then
+ sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "systemd-units/${scope}.txt" | sort -u > "$tmp/curated"
+ else
+ : > "$tmp/curated"
+ fi
+ if [ -f "systemd-units/${scope}.ignore" ]; then
+ sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "systemd-units/${scope}.ignore" | sort -u > "$tmp/ignore"
else
: > "$tmp/ignore"
fi
@@ -517,30 +508,35 @@ unit-status:
_drift system "System unit"
_drift user "User unit"
-# Append one or more units to a group list and enable them (e.g. just unit-add base sshd.service; just unit-add user:graphical kanshi.service)
-unit-add group +units:
+# Append one or more units to the curated list and enable them; scope is
+
+# inferred by probing `systemctl [--user] cat <unit>` (system wins on tie).
+unit-add +units:
#!/bin/sh
set -eu
- g='{{ group }}'
- case "$g" in
- user:*) scope=user; name=${g#user:} ;;
- system:*) scope=system; name=${g#system:} ;;
- *) scope=system; name=$g ;;
- esac
- file="systemd-units/${scope}/${name}.txt"
- if [ ! -f "$file" ]; then
- echo "error: $file does not exist" >&2
- exit 1
- fi
+ _scope() {
+ u=$1
+ sys=0 usr=0
+ systemctl cat "$u" >/dev/null 2>&1 && sys=1
+ systemctl --user cat "$u" >/dev/null 2>&1 && usr=1
+ if [ "$sys" = 1 ]; then echo system
+ elif [ "$usr" = 1 ]; then echo user
+ else echo unknown
+ fi
+ }
for u in {{ units }}; do
+ scope=$(_scope "$u")
+ if [ "$scope" = unknown ]; then
+ echo "error: $u not found at either scope (install the package first)" >&2
+ exit 1
+ fi
+ file="systemd-units/${scope}.txt"
if grep -qxF "$u" "$file"; then
- echo "$u already in ${scope}:${name}"
+ echo "$u already in ${scope}"
else
echo "$u" >> "$file"
- echo "added $u to ${scope}:${name}"
+ echo "added $u to ${scope}"
fi
- done
- for u in {{ units }}; do
if [ "$scope" = user ]; then
systemctl --user enable --now "$u" \
|| echo " warn: could not enable $u (user)" >&2
@@ -550,30 +546,26 @@ unit-add group +units:
fi
done
-# Remove one or more units from a group list and disable them
-unit-forget group +units:
+# Remove one or more units from the curated list and disable them; scope is
+
+# inferred from which list currently contains the unit.
+unit-forget +units:
#!/bin/sh
set -eu
- g='{{ group }}'
- case "$g" in
- user:*) scope=user; name=${g#user:} ;;
- system:*) scope=system; name=${g#system:} ;;
- *) scope=system; name=$g ;;
- esac
- file="systemd-units/${scope}/${name}.txt"
- if [ ! -f "$file" ]; then
- echo "error: $file does not exist" >&2
- exit 1
- fi
for u in {{ units }}; do
- if grep -qxF "$u" "$file"; then
- sed -i "/^$(printf '%s' "$u" | sed 's/[]\/$*.^[]/\\&/g')\$/d" "$file"
- echo "removed $u from ${scope}:${name}"
- else
- echo "$u not in ${scope}:${name}"
+ scope=
+ for s in system user; do
+ if [ -f "systemd-units/${s}.txt" ] && grep -qxF "$u" "systemd-units/${s}.txt"; then
+ scope=$s; break
+ fi
+ done
+ if [ -z "$scope" ]; then
+ echo "$u not in any curated list" >&2
+ continue
fi
- done
- for u in {{ units }}; do
+ file="systemd-units/${scope}.txt"
+ sed -i "/^$(printf '%s' "$u" | sed 's/[]\/$*.^[]/\\&/g')\$/d" "$file"
+ echo "removed $u from ${scope}"
if [ "$scope" = user ]; then
systemctl --user disable --now "$u" \
|| echo " warn: could not disable $u (user)" >&2
@@ -1026,7 +1018,7 @@ pkg-list group="":
fi
done
-# Install one or more package groups, or all groups if none given (e.g. just pkg-apply base dev)
+# Install one or more package groups, or all groups if none given (e.g. just pkg-apply base intel)
pkg-apply *groups:
#!/bin/sh
set -eu
@@ -1079,7 +1071,7 @@ pkg-fix:
fi
done
-# Append one or more packages to a group list and install them (e.g. just pkg-add dev ripgrep fd)
+# Append one or more packages to a group list and install them (e.g. just pkg-add base ripgrep fd)
pkg-add group +pkgs:
#!/bin/sh
set -eu
@@ -1135,6 +1127,7 @@ _install-hooks:
# Install all flatpaks declared in meta/flatpak.txt. Flathub IDs are batched
# into a single install call; URL bundles are downloaded and installed only
# when the app id is not already present (use `flatpak-update` to pick up
+
# new versions of bundle entries).
_flatpak-install:
#!/bin/sh
diff --git a/meta/base.txt b/meta/base.txt
index 9c56e52..81b93c7 100644
--- a/meta/base.txt
+++ b/meta/base.txt
@@ -1,3 +1,4 @@
+# --- core ---
acpid
base
base-devel
@@ -66,3 +67,182 @@ zsh-completions
zsh-history-substring-search
zsh-syntax-highlighting
zram-generator
+
+# --- bluetooth ---
+bluez
+bluez-utils
+ell
+
+# --- nix (multi-user daemon mode for hermetic per-project dev shells via
+# `nix develop` + direnv `use flake`. Not a replacement for paru/pacman,
+# not home-manager, not NixOS — just a sandboxed second package manager
+# that gives every project a reproducible toolchain pinned in its own
+# flake.lock. Pairs with: systemd-units/system.txt (enables
+# nix-daemon.socket), etc/nix/nix.conf, dot_config/direnv/direnvrc,
+# dot_config/nix/templates/. nix-direnv itself is loaded at runtime via
+# direnv's source_url with a content hash, so no extra package needed.) ---
+nix
+
+# --- dev ---
+android-tools
+ccache
+clang
+cmake
+curl
+difftastic
+direnv
+doxygen
+gdb
+git-absorb
+git-delta
+github-cli
+go
+hyperfine
+jdk-openjdk
+just
+lld
+lldb
+luarocks
+mergiraf
+mold
+ninja
+npm
+perf
+podman-compose
+podman-docker
+rust-analyzer
+rustup
+samply
+sccache
+strace
+t-rec
+uv
+valgrind
+
+# --- sound ---
+alsa-utils
+pipewire
+pipewire-alsa
+pipewire-jack
+pipewire-pulse
+playerctl
+pulsemixer
+# noisetorch # optional
+
+# --- fonts ---
+noto-fonts-emoji
+otf-font-awesome
+otf-latinmodern-math
+ttf-dejavu
+ttf-fira-code
+ttf-font-awesome
+ttf-noto-nerd
+woff2-font-awesome
+
+# --- wayland session ---
+# Compositor
+sway
+xdg-desktop-portal-wlr
+xdg-desktop-portal-gtk
+qt5-wayland
+qt6-wayland
+
+# Bar & launcher
+waybar
+fuzzel
+# wofi: secondary picker used only by mako-history.sh — needs --hide-search
+# and per-key custom bindings, neither of which fuzzel supports.
+wofi
+
+# Terminal
+ghostty
+
+# Notifications
+mako
+libnotify
+poweralertd
+
+# Lock screen
+swaylock
+swayidle
+
+# Clipboard
+wl-clipboard
+cliphist
+
+# Screenshots & recording
+grim
+slurp
+wf-recorder
+
+# Wayland typing (used by dictate, etc)
+wtype
+
+# Emoji picker (AUR; tiny shell script, multi-backend — we drive it through wofi)
+bemoji
+
+# Image viewer
+imv
+
+# QR
+zbar
+xorg-xwayland # needed for zbarcam's X11 preview
+
+# Document viewer is the org.pwmt.zathura flatpak (see meta/flatpak.txt) so
+# PDFs handed off from the browser/mail sandbox stay sandboxed.
+
+# Misc
+brightnessctl
+libfido2
+perl-file-mimeinfo
+qt5ct
+qt6ct
+xdg-user-dirs
+wl-mirror
+
+# --- browser (LibreWolf flatpak; arkenfox-user.js is the host-side
+# hardening overlay deployed by run_onchange_after_deploy-firefox.sh.tmpl) ---
+arkenfox-user.js
+
+# --- mail (host-side bits the org.mozilla.Thunderbird flatpak depends on) ---
+protonmail-bridge-core
+# git send-email Perl prereqs (SMTP via local Bridge on 127.0.0.1:1025)
+perl-authen-sasl
+perl-mime-tools
+perl-net-smtp-ssl
+# Native messaging host binary for External Editor Revived; bridged into
+# the TB flatpak by run_onchange_after_deploy-tb-eer.sh.tmpl.
+external-editor-revived
+
+# --- media (native mpv kept for streamlink piping and the /tmp/mpvsocket
+# IPC integration; the io.mpv.Mpv flatpak (meta/flatpak.txt) is set as
+# the mimeapps default for video/* so files handed off by the
+# browser/mail sandbox stay sandboxed) ---
+mpv
+streamlink
+yt-dlp
+
+# --- desktop extras ---
+gpg-tui
+pandoc-bin
+syncthing
+udisks2
+
+# Flatpak runtime (apps tracked in meta/flatpak.txt)
+flatpak
+
+# Smartcard stack (cartão de cidadão reader + PKCS#11 bridge into flatpak
+# browsers). pcscd.socket is enabled by systemd-units/system.txt.
+pcsclite
+ccid
+
+# OCR (used by ~/.local/bin/ocr)
+tesseract
+tesseract-data-eng
+tesseract-data-por
+
+# Speech-to-text (used by ~/.local/bin/dictate)
+# `base` multilingual: ~142 MB, ~7-10x realtime on a 4c CPU. Override
+# WHISPER_MODEL in the script's environment to use a different ggml model.
+whisper.cpp-vulkan
+whisper.cpp-model-base
diff --git a/meta/browser.txt b/meta/browser.txt
deleted file mode 100644
index de2d297..0000000
--- a/meta/browser.txt
+++ /dev/null
@@ -1 +0,0 @@
-arkenfox-user.js
diff --git a/meta/bt.txt b/meta/bt.txt
deleted file mode 100644
index d835748..0000000
--- a/meta/bt.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-bluez
-bluez-utils
-ell
diff --git a/meta/cpp.txt b/meta/cpp.txt
deleted file mode 100644
index e4e11f6..0000000
--- a/meta/cpp.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-ccache
-clang
-cmake
-doxygen
-gdb
-lld
-lldb
-ninja
-perf
-valgrind
diff --git a/meta/dev.txt b/meta/dev.txt
deleted file mode 100644
index 5f26ad7..0000000
--- a/meta/dev.txt
+++ /dev/null
@@ -1,24 +0,0 @@
-android-tools
-curl
-difftastic
-direnv
-git-absorb
-git-delta
-github-cli
-go
-hyperfine
-jdk-openjdk
-just
-luarocks
-mergiraf
-mold
-npm
-podman-compose
-podman-docker
-rust-analyzer
-rustup
-samply
-sccache
-strace
-t-rec
-uv
diff --git a/meta/extra.txt b/meta/extra.txt
deleted file mode 100644
index 956d0c1..0000000
--- a/meta/extra.txt
+++ /dev/null
@@ -1,23 +0,0 @@
-gpg-tui
-pandoc-bin
-syncthing
-udisks2
-
-# Flatpak runtime (apps tracked in meta/flatpak.txt)
-flatpak
-
-# Smartcard stack (cartão de cidadão reader + PKCS#11 bridge into flatpak browsers).
-# pcscd.socket is enabled by systemd-units/system/base.txt.
-pcsclite
-ccid
-
-# OCR (used by ~/.local/bin/ocr)
-tesseract
-tesseract-data-eng
-tesseract-data-por
-
-# Speech-to-text (used by ~/.local/bin/dictate)
-# `base` multilingual: ~142 MB, ~7-10x realtime on a 4c CPU. Override
-# WHISPER_MODEL in the script's environment to use a different ggml model.
-whisper.cpp-vulkan
-whisper.cpp-model-base
diff --git a/meta/fonts.txt b/meta/fonts.txt
deleted file mode 100644
index 603fb44..0000000
--- a/meta/fonts.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-noto-fonts-emoji
-otf-font-awesome
-otf-latinmodern-math
-ttf-dejavu
-ttf-fira-code
-ttf-font-awesome
-ttf-noto-nerd
-woff2-font-awesome
diff --git a/meta/fortran.txt b/meta/fortran.txt
deleted file mode 100644
index 4bb1d35..0000000
--- a/meta/fortran.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-fortitude-bin
-fortran-fpm-bin
-gcc-fortran
diff --git a/meta/mail.txt b/meta/mail.txt
deleted file mode 100644
index 74f6214..0000000
--- a/meta/mail.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# Host-side bits the org.mozilla.Thunderbird flatpak depends on.
-protonmail-bridge-core
-# git send-email Perl prereqs (SMTP via local Bridge on 127.0.0.1:1025)
-perl-authen-sasl
-perl-mime-tools
-perl-net-smtp-ssl
-# Native messaging host binary for External Editor Revived; bridged into the
-# TB flatpak by run_onchange_after_deploy-tb-eer.sh.tmpl.
-external-editor-revived
diff --git a/meta/media.txt b/meta/media.txt
deleted file mode 100644
index 8e5b5c2..0000000
--- a/meta/media.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-# Native mpv is kept for streamlink piping and the /tmp/mpvsocket IPC
-# integration; the io.mpv.Mpv flatpak (meta/flatpak.txt) is set as the
-# mimeapps default for video/* so files handed off by the browser/mail
-# sandbox stay sandboxed.
-mpv
-streamlink
-yt-dlp
diff --git a/meta/nix.txt b/meta/nix.txt
deleted file mode 100644
index e5c4bb4..0000000
--- a/meta/nix.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-# Nix package manager (multi-user daemon mode).
-#
-# Used purely for hermetic per-project dev shells via `nix develop` +
-# direnv `use flake`. Not a replacement for paru/pacman, not home-manager,
-# not NixOS — just a sandboxed second package manager that gives every
-# project a reproducible toolchain pinned in its own flake.lock.
-#
-# Pairs with:
-# - systemd-units/system/nix.txt (enables nix-daemon.socket)
-# - etc/nix/nix.conf (flakes, trusted-users=@wheel, GC roots)
-# - dot_config/direnv/direnvrc (loads nix-direnv; pinned via source_url)
-# - dot_config/nix/templates/ (flake templates: nix flake init -t ~/.config/nix/templates)
-#
-# nix-direnv itself is not packaged for Arch — it's loaded at runtime via
-# direnv's source_url with a content hash, so no extra package needed.
-nix
diff --git a/meta/sound.txt b/meta/sound.txt
deleted file mode 100644
index 5e9c45e..0000000
--- a/meta/sound.txt
+++ /dev/null
@@ -1,10 +0,0 @@
-alsa-utils
-pipewire
-pipewire-alsa
-pipewire-jack
-pipewire-pulse
-playerctl
-pulsemixer
-
-# Optional
-# noisetorch
diff --git a/meta/wayland.txt b/meta/wayland.txt
deleted file mode 100644
index b41668b..0000000
--- a/meta/wayland.txt
+++ /dev/null
@@ -1,59 +0,0 @@
-# Compositor
-sway
-xdg-desktop-portal-wlr
-xdg-desktop-portal-gtk
-qt5-wayland
-qt6-wayland
-
-# Bar & launcher
-waybar
-fuzzel
-# wofi: secondary picker used only by mako-history.sh — needs --hide-search
-# and per-key custom bindings, neither of which fuzzel supports.
-wofi
-
-# Terminal
-ghostty
-
-# Notifications
-mako
-libnotify
-poweralertd
-
-# Lock screen
-swaylock
-swayidle
-
-# Clipboard
-wl-clipboard
-cliphist
-
-# Screenshots & recording
-grim
-slurp
-wf-recorder
-
-# Wayland typing (used by dictate, etc)
-wtype
-
-# Emoji picker (AUR; tiny shell script, multi-backend — we drive it through wofi)
-bemoji
-
-# Image viewer
-imv
-
-# QR
-zbar
-xorg-xwayland # needed for zbarcam's X11 preview
-
-# Document viewer is the org.pwmt.zathura flatpak (see meta/flatpak.txt) so
-# PDFs handed off from the browser/mail sandbox stay sandboxed.
-
-# Misc
-brightnessctl
-libfido2
-perl-file-mimeinfo
-qt5ct
-qt6ct
-xdg-user-dirs
-wl-mirror
diff --git a/systemd-units/system/.ignore b/systemd-units/system.ignore
index 32f2225..a3d0af6 100644
--- a/systemd-units/system/.ignore
+++ b/systemd-units/system.ignore
@@ -1,4 +1,4 @@
-# Systemd units to suppress from 'just services-drift' uncurated output.
+# Systemd units to suppress from `just unit-status` uncurated output.
# Typically distro defaults enabled by systemd presets that are neither
# worth curating nor worth disabling. One unit per line, # comments OK.
diff --git a/systemd-units/system.txt b/systemd-units/system.txt
new file mode 100644
index 0000000..5d1d936
--- /dev/null
+++ b/systemd-units/system.txt
@@ -0,0 +1,30 @@
+# System-scope systemd units to enable. One unit per line, # comments OK.
+# Enabled by `just unit-apply` via `sudo systemctl enable`.
+
+# --- core ---
+systemd-timesyncd.service
+systemd-resolved.service
+systemd-oomd.service
+reflector.timer
+paccache.timer
+acpid.service
+cpupower.service
+iwd.service
+nftables.service
+systemd-networkd.service
+systemd-networkd-wait-online.service
+tlp.service
+pcscd.socket
+smartd.service
+btrfs-scrub@-.timer
+fwupd-refresh.timer
+
+# --- bluetooth ---
+bluetooth.service
+
+# --- btc ---
+tor.service
+
+# --- nix (socket-activated builder daemon; the .service spawns on first
+# client connect, the .socket is what gets enabled) ---
+nix-daemon.socket
diff --git a/systemd-units/system/base.txt b/systemd-units/system/base.txt
deleted file mode 100644
index 1e3af9b..0000000
--- a/systemd-units/system/base.txt
+++ /dev/null
@@ -1,19 +0,0 @@
-# Systemd units to enable when the 'base' meta group is installed.
-# One unit per line, # comments OK.
-
-systemd-timesyncd.service
-systemd-resolved.service
-systemd-oomd.service
-reflector.timer
-paccache.timer
-acpid.service
-cpupower.service
-iwd.service
-nftables.service
-systemd-networkd.service
-systemd-networkd-wait-online.service
-tlp.service
-pcscd.socket
-smartd.service
-btrfs-scrub@-.timer
-fwupd-refresh.timer
diff --git a/systemd-units/system/bt.txt b/systemd-units/system/bt.txt
deleted file mode 100644
index 985b8dc..0000000
--- a/systemd-units/system/bt.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# Systemd units to enable when the 'bt' meta group is installed.
-# One unit per line, # comments OK.
-
-bluetooth.service
diff --git a/systemd-units/system/btc.txt b/systemd-units/system/btc.txt
deleted file mode 100644
index b30199c..0000000
--- a/systemd-units/system/btc.txt
+++ /dev/null
@@ -1,4 +0,0 @@
-# Systemd units to enable when the 'btc' meta group is installed.
-# One unit per line, # comments OK.
-
-tor.service
diff --git a/systemd-units/system/nix.txt b/systemd-units/system/nix.txt
deleted file mode 100644
index de99aaa..0000000
--- a/systemd-units/system/nix.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-# Nix builder daemon — socket-activated, idle-friendly.
-# The .service spawns on first client connect; the .socket is what gets enabled.
-nix-daemon.socket
diff --git a/systemd-units/user/.ignore b/systemd-units/user.ignore
index 9f95ce0..9f95ce0 100644
--- a/systemd-units/user/.ignore
+++ b/systemd-units/user.ignore
diff --git a/systemd-units/user.txt b/systemd-units/user.txt
new file mode 100644
index 0000000..37e7e7a
--- /dev/null
+++ b/systemd-units/user.txt
@@ -0,0 +1,14 @@
+# User-scope systemd units to enable. One unit per line, # comments OK.
+# Enabled by `just unit-apply` via `systemctl --user enable`.
+
+# --- sway session (all WantedBy=sway-session.target; sway itself starts
+# sway-session.target on login) ---
+cliphist-image.service
+cliphist-text.service
+display-watcher.service
+signal.service
+swayidle.service
+waybar.service
+
+# --- mail (overridden via drop-ins in dot_config/systemd/user/) ---
+protonmail-bridge.service
diff --git a/systemd-units/user/graphical.txt b/systemd-units/user/graphical.txt
deleted file mode 100644
index af6cf4f..0000000
--- a/systemd-units/user/graphical.txt
+++ /dev/null
@@ -1,9 +0,0 @@
-# User units that compose the sway graphical session.
-# All are WantedBy=sway-session.target so enabling them wires them into
-# the session; sway itself starts sway-session.target on login.
-cliphist-image.service
-cliphist-text.service
-display-watcher.service
-signal.service
-swayidle.service
-waybar.service
diff --git a/systemd-units/user/mail.txt b/systemd-units/user/mail.txt
deleted file mode 100644
index 1f7804c..0000000
--- a/systemd-units/user/mail.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-# User services we override via drop-ins in dot_config/systemd/user/.
-protonmail-bridge.service