aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--.github/copilot-instructions.md2
-rw-r--r--README.md60
-rw-r--r--justfile158
-rw-r--r--systemd-units/system/.ignore (renamed from systemd-units/.ignore)0
-rw-r--r--systemd-units/system/base.txt (renamed from systemd-units/base.txt)0
-rw-r--r--systemd-units/system/btc.txt (renamed from systemd-units/btc.txt)0
-rw-r--r--systemd-units/user/.ignore4
7 files changed, 140 insertions, 84 deletions
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index 02ce8d4..31c9842 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -15,7 +15,7 @@ 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 paired by name with `meta/` groups (e.g. `systemd-units/base.txt` ↔ `meta/base.txt`). Units listed here are enabled by `just unit-apply` (run automatically by `just init`). Inspect with `just unit-list`, detect drift with `just unit-status`.
+- `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`.
- `firefox/` contains Firefox/LibreWolf hardening overrides (`user-overrides.js`) and custom CSS (`chrome/userChrome.css`). Deployed by `run_onchange_after_deploy-firefox.sh.tmpl`.
- `dot_local/bin/executable_doasedit` (deployed to `~/.local/bin/doasedit`) is a small wrapper that emulates `sudoedit` for `doas`.
- `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`.
diff --git a/README.md b/README.md
index 9f18ef9..1e2ce98 100644
--- a/README.md
+++ b/README.md
@@ -75,40 +75,40 @@ chezmoi apply -v
Everything is driven by [just](https://just.systems/) recipes against four parallel models:
-| Directory | Managed by | Purpose |
-| ------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------ |
-| `dot_*`, `private_dot_*` | chezmoi | Dotfiles deployed to `$HOME`. Prefixes: `dot_` → `.`, `private_` → `0600`, `executable_` → `+x`. |
-| `meta/*.txt` | `just pkg-apply`, `just pkg-status` | Plain-text package lists (one per line, `#` comments). Groups: `base`, `dev`, `wayland`, etc. |
-| `systemd-units/*.txt` | `just unit-apply`, `just unit-status` | Units to enable, paired by name with a `meta/` group (`base.txt` ↔ `base.txt`). |
-| `etc/` | `run_onchange_after_deploy-etc.sh.tmpl` | System-level configs deployed to `/etc/` via a chezmoi onchange hook. |
-| `firefox/` | `run_onchange_after_deploy-firefox.sh.tmpl` | LibreWolf `user-overrides.js` + `userChrome.css` (kept under the familiar `firefox/` name). |
+| Directory | Managed by | Purpose |
+| ----------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `dot_*`, `private_dot_*` | chezmoi | Dotfiles deployed to `$HOME`. Prefixes: `dot_` → `.`, `private_` → `0600`, `executable_` → `+x`. |
+| `meta/*.txt` | `just pkg-apply`, `just pkg-status` | Plain-text package lists (one per line, `#` comments). Groups: `base`, `dev`, `wayland`, etc. |
+| `systemd-units/{system,user}/*.txt` | `just unit-apply`, `just unit-status` | Units to enable, split by scope. `system/` files pair by name with `meta/` groups (`system/base.txt` ↔ `meta/base.txt`); `user/` files are standalone. Recipe group token: `<name>` / `system:<name>` / `user:<name>`. |
+| `etc/` | `run_onchange_after_deploy-etc.sh.tmpl` | System-level configs deployed to `/etc/` via a chezmoi onchange hook. |
+| `firefox/` | `run_onchange_after_deploy-firefox.sh.tmpl` | LibreWolf `user-overrides.js` + `userChrome.css` (kept under the familiar `firefox/` name). |
## Recipes at a glance
Run `just` or `just --list` for the full menu. Recipes follow a `DOMAIN-VERB` scheme across four domains (`dotfiles`, `etc`, `pkg`, `unit`) with chezmoi-aligned verbs (`add`, `forget`, `re-add`, `apply`, `diff`, `merge`, `status`). Top-level dispatchers sniff argument shape and delegate.
-| Category | Recipe | Effect |
-| ------------- | ------------------------------------------ | ---------------------------------------------------------------------------------- |
-| Setup | `just init` | First-time setup: chezmoi init, git hooks, apply, base packages, curated units |
-| Day-to-day | `just sync` | `apply` + `pkg-fix` + `unit-apply` (full reconcile) |
-| | `just apply` | `chezmoi apply` — atomically deploys dotfiles AND /etc |
-| | `just re-add [PATH]` | Pull live changes back into the repo (dotfiles + /etc) |
-| Add / forget | `just add PATH` | Dispatches to `dotfiles-add` (path) or `etc-add` (`/etc/...`) |
-| | `just add GROUP NAME…` | Dispatches to `pkg-add` (bare names) or `unit-add` (ends in `.service`/`.timer`/…) |
-| | `just forget …` | Same shape as `add`; delegates to the right `*-forget` |
-| Packages | `just pkg-apply [GROUP…]` | Install listed groups, or every group if none given |
-| | `just pkg-fix` | Top up missing packages in already-adopted groups |
-| | `just pkg-list [GROUP]` | Show per-group install coverage |
-| Units | `just unit-apply` | Enable every unit in the adopted `systemd-units/*.txt` lists |
-| | `just unit-list [GROUP]` | List curated units with their state |
-| /etc | `just etc-diff`, `just etc-re-add`, | See /etc workflow below |
-| | `just etc-restore`, `just etc-untrack` | |
-| Inspection | `just status` | Combined dotfile + /etc + package + unit drift |
-| | `just diff [PATH]`, `just merge [PATH]` | Dispatch to `dotfiles-*` or `etc-*` by path |
-| | `just doctor` | Verify tooling for `just check` is installed |
-| Quality gates | `just fmt [PATH]`, `just check-fmt [PATH]` | Format / check formatting (all languages below) |
-| | `just lint [PATH]` | Run linters (selene, shellcheck, ruff, taplo) |
-| | `just check [PATH]` | `check-fmt` + `lint` (the pre-commit hook and CI both run this) |
+| Category | Recipe | Effect |
+| ------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
+| Setup | `just init` | First-time setup: chezmoi init, git hooks, apply, base packages, curated units |
+| Day-to-day | `just sync` | `apply` + `pkg-fix` + `unit-apply` (full reconcile) |
+| | `just apply` | `chezmoi apply` — atomically deploys dotfiles AND /etc |
+| | `just re-add [PATH]` | Pull live changes back into the repo (dotfiles + /etc) |
+| Add / forget | `just add PATH` | Dispatches to `dotfiles-add` (path) or `etc-add` (`/etc/...`) |
+| | `just add GROUP NAME…` | Dispatches to `pkg-add` (bare names) or `unit-add` (ends in `.service`/`.timer`/…) |
+| | `just forget …` | Same shape as `add`; delegates to the right `*-forget` |
+| Packages | `just pkg-apply [GROUP…]` | Install listed groups, or every group if none given |
+| | `just pkg-fix` | Top up missing packages in already-adopted groups |
+| | `just pkg-list [GROUP]` | Show per-group install coverage |
+| Units | `just unit-apply` | Enable every unit in the adopted `systemd-units/{system,user}/*.txt` lists (system via `sudo`, user via `systemctl --user`) |
+| | `just unit-list [GROUP]` | List curated units with their state |
+| /etc | `just etc-diff`, `just etc-re-add`, | See /etc workflow below |
+| | `just etc-restore`, `just etc-untrack` | |
+| Inspection | `just status` | Combined dotfile + /etc + package + unit drift |
+| | `just diff [PATH]`, `just merge [PATH]` | Dispatch to `dotfiles-*` or `etc-*` by path |
+| | `just doctor` | Verify tooling for `just check` is installed |
+| Quality gates | `just fmt [PATH]`, `just check-fmt [PATH]` | Format / check formatting (all languages below) |
+| | `just lint [PATH]` | Run linters (selene, shellcheck, ruff, taplo) |
+| | `just check [PATH]` | `check-fmt` + `lint` (the pre-commit hook and CI both run this) |
`fmt` / `check-fmt` / `lint` cover: Lua (stylua, selene), shell (shfmt, shellcheck), Python (ruff), TOML (taplo), justfile (`just --fmt`), plus Markdown/JSON/YAML/CSS (prettier). Each accepts either no argument (whole repo) or a single file path.
@@ -119,7 +119,7 @@ Four sources of drift are tracked independently and combined by `just status`:
- **Dotfiles** (`just dotfiles-status`): live `$HOME` files differ from the repo. Resolve with `just apply` (repo → home), `just re-add PATH` (home → repo), `just diff PATH`, or `just merge PATH`.
- **Packages** (`just pkg-status`): installed but undeclared, or declared but missing. Resolve by adding to a `meta/` group (`just add GROUP PKG`) or uninstalling.
- **/etc** (`just etc-status` / `just etc-diff`): modified package configs or user-created files in `/etc` that aren't in the repo. Resolve with `just etc-re-add PATH` (track), `just etc-restore PATH` (revert to package default), or `just etc-untrack PATH`.
-- **Units** (`just unit-status`): enabled units not in any `systemd-units/*.txt`, or declared units that aren't enabled.
+- **Units** (`just unit-status`): enabled units not in any `systemd-units/{system,user}/*.txt`, or declared units that aren't enabled (checked for both scopes).
## Git hooks
diff --git a/justfile b/justfile
index f5725ad..32d831e 100644
--- a/justfile
+++ b/justfile
@@ -397,27 +397,26 @@ 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.
-# List curated systemd units grouped by systemd-units/<group>.txt with their state; pass a group to narrow
+# 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="":
#!/bin/sh
- if [ -n '{{ group }}' ]; then
- files="systemd-units/{{ group }}.txt"
- [ -f "$files" ] || { echo "error: $files does not exist" >&2; exit 1; }
- else
- files="systemd-units/*.txt"
- fi
- for file in $files; do
- [ -f "$file" ] || continue
+ _render() {
+ scope=$1 file=$2
+ sctl="systemctl"; [ "$scope" = user ] && sctl="systemctl --user"
group=$(basename "$file" .txt)
- echo "=== $group ==="
+ echo "=== ${scope}:${group} ==="
sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "$file" | while read -r u; do
- en=$(systemctl is-enabled "$u" 2>/dev/null); en=${en:-unknown}
- ac=$(systemctl is-active "$u" 2>/dev/null); ac=${ac:-unknown}
+ en=$($sctl is-enabled "$u" 2>/dev/null); en=${en:-unknown}
+ ac=$($sctl is-active "$u" 2>/dev/null); ac=${ac:-unknown}
case "$en" in
- enabled|static|alias) c_en=32 ;;
- disabled|masked|not-found) c_en=31 ;;
- *) c_en=33 ;;
+ enabled|enabled-runtime|static|alias|indirect|generated) c_en=32 ;;
+ disabled|masked|not-found) c_en=31 ;;
+ *) c_en=33 ;;
esac
case "$ac" in
active) c_ac=32 ;;
@@ -426,73 +425,121 @@ unit-list group="":
esac
printf ' %-34s \033[%sm%-10s\033[0m \033[%sm%s\033[0m\n' "$u" "$c_en" "$en" "$c_ac" "$ac"
done
- 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; }
+ _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
-# Enable all curated systemd units (idempotent, soft-fail per unit)
+# Enable all curated systemd units (idempotent, soft-fail per unit); walks both system/ and user/ trees
unit-apply:
#!/bin/sh
- for file in systemd-units/*.txt; do
+ for file in systemd-units/system/*.txt; do
[ -f "$file" ] || continue
sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "$file" | while read -r u; do
sudo systemctl enable --now "$u" \
- || echo " warn: could not enable $u" >&2
+ || 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
+ systemctl --user enable --now "$u" \
+ || echo " warn: could not enable $u (user)" >&2
done
done
-# Show drift between curated units and actually-enabled systemd units
+# Show drift between curated units and actually-enabled systemd units (system + user)
unit-status:
#!/bin/sh
- echo "=== Unit drift ==="
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
- cat systemd-units/*.txt 2>/dev/null \
- | sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' | sort -u > "$tmp/curated"
- if [ -f systemd-units/.ignore ]; then
- sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' systemd-units/.ignore | sort -u > "$tmp/ignore"
- else
- : > "$tmp/ignore"
- fi
- # Curated units missing from the system: use is-enabled to correctly
- # handle instantiated template units (which list-unit-files does not show).
- while read -r u; do
- [ -z "$u" ] && continue
- state=$(systemctl is-enabled "$u" 2>/dev/null || true)
- case "$state" in
- enabled|enabled-runtime|alias|static|indirect|generated) ;;
- *) echo " not-enabled: $u" ;;
- esac
- done < "$tmp/curated"
- # Enabled unit files not in curated (minus ignore list).
- systemctl list-unit-files --state=enabled --no-legend 2>/dev/null \
- | awk '{print $1}' | grep -vE '@\.' | sort -u > "$tmp/enabled"
- comm -13 "$tmp/curated" "$tmp/enabled" | comm -23 - "$tmp/ignore" | sed 's/^/ uncurated: /'
+ _drift() {
+ 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"
+ else
+ : > "$tmp/ignore"
+ fi
+ # Curated units missing from the system: is-enabled correctly handles
+ # instantiated template units (list-unit-files does not show those).
+ while read -r u; do
+ [ -z "$u" ] && continue
+ state=$($sctl is-enabled "$u" 2>/dev/null || true)
+ case "$state" in
+ enabled|enabled-runtime|alias|static|indirect|generated) ;;
+ *) echo " not-enabled: $u" ;;
+ esac
+ done < "$tmp/curated"
+ # Enabled unit files not in curated (minus ignore list).
+ $sctl list-unit-files --state=enabled --no-legend 2>/dev/null \
+ | awk '{print $1}' | grep -vE '@\.' | sort -u > "$tmp/enabled"
+ comm -13 "$tmp/curated" "$tmp/enabled" | comm -23 - "$tmp/ignore" | sed 's/^/ uncurated: /'
+ }
+ _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)
+# 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:
#!/bin/sh
set -eu
- file="systemd-units/{{ group }}.txt"
+ 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
- echo "$u already in {{ group }}.txt"
+ echo "$u already in ${scope}:${name}"
else
echo "$u" >> "$file"
- echo "added $u to {{ group }}.txt"
+ echo "added $u to ${scope}:${name}"
fi
done
for u in {{ units }}; do
- sudo systemctl enable --now "$u" \
- || echo " warn: could not enable $u" >&2
+ if [ "$scope" = user ]; then
+ systemctl --user enable --now "$u" \
+ || echo " warn: could not enable $u (user)" >&2
+ else
+ sudo systemctl enable --now "$u" \
+ || echo " warn: could not enable $u (system)" >&2
+ fi
done
# Remove one or more units from a group list and disable them
unit-forget group +units:
#!/bin/sh
set -eu
- file="systemd-units/{{ group }}.txt"
+ 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
@@ -500,14 +547,19 @@ unit-forget group +units:
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 {{ group }}.txt"
+ echo "removed $u from ${scope}:${name}"
else
- echo "$u not in {{ group }}.txt"
+ echo "$u not in ${scope}:${name}"
fi
done
for u in {{ units }}; do
- sudo systemctl disable --now "$u" \
- || echo " warn: could not disable $u" >&2
+ if [ "$scope" = user ]; then
+ systemctl --user disable --now "$u" \
+ || echo " warn: could not disable $u (user)" >&2
+ else
+ sudo systemctl disable --now "$u" \
+ || echo " warn: could not disable $u (system)" >&2
+ fi
done
# ═══════════════════════════════════════════════════════════════════
diff --git a/systemd-units/.ignore b/systemd-units/system/.ignore
index 32f2225..32f2225 100644
--- a/systemd-units/.ignore
+++ b/systemd-units/system/.ignore
diff --git a/systemd-units/base.txt b/systemd-units/system/base.txt
index 6f8582a..6f8582a 100644
--- a/systemd-units/base.txt
+++ b/systemd-units/system/base.txt
diff --git a/systemd-units/btc.txt b/systemd-units/system/btc.txt
index b30199c..b30199c 100644
--- a/systemd-units/btc.txt
+++ b/systemd-units/system/btc.txt
diff --git a/systemd-units/user/.ignore b/systemd-units/user/.ignore
new file mode 100644
index 0000000..1a7e6d9
--- /dev/null
+++ b/systemd-units/user/.ignore
@@ -0,0 +1,4 @@
+# Systemd user-scope units to suppress from `just unit-status` uncurated output.
+# Typically defaults pulled in by graphical-session / xdg-desktop-portal that
+# are neither worth curating nor worth disabling. One unit per line, # comments OK.
+