aboutsummaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
-rw-r--r--README.md19
-rwxr-xr-xbootstrap.sh45
-rw-r--r--dot_config/nvim/lua/plugins/treesitter.lua10
-rw-r--r--dot_config/pacman/makepkg.conf9
-rw-r--r--dot_config/paru/paru.conf25
-rw-r--r--dot_config/systemd/user/pass-secret-service.service12
-rwxr-xr-xdot_config/waybar/executable_update-status.sh2
-rw-r--r--dot_config/zsh/dot_zprofile6
-rw-r--r--dot_config/zsh/dot_zshrc4
-rw-r--r--dot_local/bin/executable_arch-news-check216
-rw-r--r--dot_local/bin/symlink_su1
-rw-r--r--dot_local/bin/symlink_sudo1
-rw-r--r--dot_local/bin/symlink_sudoedit1
-rw-r--r--dot_local/bin/symlink_visudo1
-rw-r--r--etc/pacman.conf4
-rw-r--r--etc/udev/rules.d/51-hid-digitalbitbox.rules3
-rw-r--r--etc/udev/rules.d/52-hid-digitalbitbox.rules3
-rw-r--r--etc/udev/rules.d/53-hid-bitbox02.rules3
-rw-r--r--etc/udev/rules.d/54-hid-bitbox02.rules3
-rw-r--r--justfile25
-rw-r--r--meta/base.txt41
-rw-r--r--nix/common.nix1
-rw-r--r--nix/host.nix57
-rwxr-xr-xrun_onchange_after_deploy-etc.sh.tmpl25
-rwxr-xr-xrun_onchange_after_deploy-firefox.sh.tmpl21
25 files changed, 409 insertions, 129 deletions
diff --git a/README.md b/README.md
index d8e603b..2a1e88e 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ My Arch Linux configuration, managed with [chezmoi](https://www.chezmoi.io/).
- **Wayland only.** No X server, no display manager. Sway starts from `exec sway` at the end of the zsh login shell on TTY1 (autologin via a host-local `getty@tty1` drop-in that's deliberately gitignored).
- **XDG everywhere.** Every tool is pushed to `$XDG_CONFIG_HOME` / `$XDG_CACHE_HOME` / `$XDG_DATA_HOME` — `~` stays clean. Zsh itself lives under `$XDG_CONFIG_HOME/zsh`, bootstrapped by a single-line `dot_zshenv`.
-- **[sudo-rs](https://github.com/trifectatechfoundation/sudo-rs), not the C sudo.** Memory-safe Rust rewrite, drop-in CLI compatible. Same one that Ubuntu 25.10 ships as default.
+- **[sudo-rs](https://github.com/trifectatechfoundation/sudo-rs), not the C sudo.** Memory-safe Rust rewrite, drop-in CLI compatible. Exposed through user-scoped `~/.local/bin` shims instead of system-wide `/usr/local/bin` shadows.
- **GPG for everything signable.** Commits and tags are signed; the same GPG agent also serves SSH authentication — one key, one cache, one PIN entry.
- **Secrets via [`pass`](https://www.passwordstore.org/).** API keys and tokens are pulled into env vars at shell init; nothing is committed.
- **Plain-text over configuration-as-code.** Packages and enabled units are tracked as one-per-line `.txt` files in `meta/` and `systemd-units/`, diffed against `pacman -Qeq` and `systemctl list-unit-files`. No DSL, no state file.
@@ -18,7 +18,7 @@ My Arch Linux configuration, managed with [chezmoi](https://www.chezmoi.io/).
| Category | Choice |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| OS & base | [Arch Linux](https://archlinux.org/), [paru](https://github.com/Morganamilo/paru) for AUR, [sudo-rs](https://github.com/trifectatechfoundation/sudo-rs) for privilege escalation |
+| OS & base | [Arch Linux](https://archlinux.org/) with pacman for official packages, [Nix](https://nixos.org/) / Home Manager for user-leaf tools, [sudo-rs](https://github.com/trifectatechfoundation/sudo-rs) for privilege escalation |
| Dotfile manager | [chezmoi](https://www.chezmoi.io/) (dotfiles and `/etc` both deployed via `chezmoi apply`) |
| Task runner | [just](https://just.systems/) — every maintenance action is a recipe (see below) |
| Shell | [zsh](https://www.zsh.org/), relocated to `$XDG_CONFIG_HOME/zsh`; plugins via [zinit](https://github.com/zdharma-continuum/zinit) |
@@ -29,7 +29,7 @@ My Arch Linux configuration, managed with [chezmoi](https://www.chezmoi.io/).
| Bar / launcher | [waybar](https://github.com/Alexays/Waybar), [fuzzel](https://codeberg.org/dnkl/fuzzel) |
| Notifications | [mako](https://github.com/emersion/mako) |
| Lock screen | [swaylock](https://github.com/swaywm/swaylock) |
-| Browser | [LibreWolf](https://librewolf.net/) (Flathub `io.gitlab.librewolf-community` for bubblewrap host-isolation), hardened via `user-overrides.js` + `userChrome.css` (kept under `firefox/` by name for recognizability) |
+| Browser | [LibreWolf](https://librewolf.net/) (Flathub `io.gitlab.librewolf-community` for bubblewrap host-isolation), hardened via nixpkgs' Arkenfox `user.js` + `firefox/user-overrides.js` and `userChrome.css` (kept under `firefox/` by name for recognizability) |
| Mail | [Thunderbird](https://www.thunderbird.net/) (Flathub `org.mozilla.thunderbird`) against [ProtonMail Bridge](https://proton.me/mail/bridge) + Radicale (CalDAV/CardDAV); non-private prefs tracked under `thunderbird/` |
| Secrets & identity | [GPG](https://gnupg.org/) (commit signing + SSH auth via gpg-agent), [pass](https://www.passwordstore.org/) |
| Media & viewers | [mpv](https://mpv.io/) (Flathub `io.mpv.Mpv`; streamlink launches it via `flatpak run`), [zathura](https://pwmt.org/projects/zathura/) (Flathub `org.pwmt.zathura`), [yazi](https://yazi-rs.github.io/) |
@@ -56,12 +56,11 @@ curl -fsSL https://raw.githubusercontent.com/sommerfelddev/dotfiles/master/boots
```
The script installs pacman prerequisites, enables `%wheel` in sudoers,
-builds `paru-bin` from the AUR, clones this repo to `~/dotfiles`, runs
-`just init`, enables recommended systemd units (fstrim, timesyncd,
-resolved, reflector, paccache, pkgstats, acpid, cpupower, iwd, plus tlp
-on laptops), refreshes the pacman mirrorlist, and creates XDG user
-directories. On EFI systems missing an Arch boot entry, it prints the
-`efibootmgr` command to register the UKI (run after your first
+clones this repo to `~/dotfiles`, runs `just init`, enables recommended
+systemd units (fstrim, timesyncd, resolved, reflector, paccache, pkgstats,
+acpid, cpupower, iwd, plus tlp on laptops), refreshes the pacman mirrorlist,
+and creates XDG user directories. On EFI systems missing an Arch boot entry,
+it prints the `efibootmgr` command to register the UKI (run after your first
`mkinitcpio -P`).
## Setup on an existing system
@@ -81,7 +80,7 @@ Everything is driven by [just](https://just.systems/) recipes against four paral
| `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). |
+| `firefox/` | `run_onchange_after_deploy-firefox.sh.tmpl` | LibreWolf hardening: renders nixpkgs' Arkenfox `user.js` plus `firefox/user-overrides.js` into the Flatpak profile, and deploys `userChrome.css` (kept under the familiar `firefox/` name). |
| (cartão de cidadão) | `run_onchange_after_deploy-pteid-pkcs11.sh.tmpl` | Bridges the `pt.gov.autenticacao` flatpak's PKCS#11 module into the NSS DB of every flatpak that needs cartão de cidadão (LibreWolf, Thunderbird, Okular, LibreOffice) — `--filesystem` + `--socket=pcsc` override + `modutil -add` per NSS DB (per-profile for Mozilla apps, shared `~/.pki/nssdb` for Okular/LibreOffice). No-op unless `pt.gov.autenticacao` is installed. |
| (Thunderbird native editor) | `run_onchange_after_deploy-tb-eer.sh.tmpl` | Bridges `external-editor-revived` (host pacman package) into the Thunderbird flatpak: deploys a `flatpak-spawn --host` wrapper into the sandbox's `~/.mozilla/native-messaging-hosts/` and rewrites the manifest `path` to point at it. No-op unless TB flatpak + EER host package are both installed. |
| (flatpak config sharing) | `run_onchange_after_deploy-flatpak-overrides.sh.tmpl` | Read-only `--filesystem=xdg-config/<app>:ro` overrides so the zathura and mpv flatpaks read our chezmoi-managed `~/.config/<app>/` instead of a separate in-sandbox copy. |
diff --git a/bootstrap.sh b/bootstrap.sh
index 79e6307..6ffbd76 100755
--- a/bootstrap.sh
+++ b/bootstrap.sh
@@ -5,7 +5,7 @@
#
# Prerequisites (from the Arch installation guide):
# - A regular user already exists and is a member of the 'wheel' group.
-# - You are logged in as that user (paru/makepkg refuse to run as root).
+# - You are logged in as that user.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/sommerfelddev/dotfiles/master/bootstrap.sh | sh
@@ -23,7 +23,7 @@ die() {
exit 1
}
-# 0. refuse root — paru/makepkg won't run as root
+# 0. refuse root — Home Manager and the user-owned dotfiles expect a real user.
[ "$(id -u)" -ne 0 ] || die "run this as your regular user, not root"
# 1. user must be in wheel (required so the sudoers rule we enable takes effect)
@@ -31,14 +31,11 @@ id -nG "$USER" | tr ' ' '\n' | grep -qx wheel ||
die "user '$USER' must be in the 'wheel' group"
# 2. install sudo + pacman prerequisites, enable wheel in sudoers.
-# `chezmoi` and `paru` are intentionally NOT in this list — chezmoi
-# is run ephemerally via `nix-shell` below for the one-shot deploy,
-# and paru lands in ~/.nix-profile/bin after the first nix-switch
-# (we install `just init`'s AUR deps using *that* nix-store paru,
-# not a manually built paru-bin). `just` and `git` stay on pacman
-# so the script + early `just nix-switch` work before the nix
+# `chezmoi` is intentionally NOT in this list — it lands in
+# ~/.nix-profile/bin after the first nix-switch. `just` and `git` stay
+# on pacman so the script + early `just nix-switch` work before the nix
# profile is activated.
-PREREQS='sudo git base-devel just efibootmgr nix'
+PREREQS='sudo git just efibootmgr nix'
SUDOERS_SED='s/^# *\(%wheel ALL=(ALL:ALL\(:ALL\)*) ALL\)/\1/'
if ! command -v sudo >/dev/null 2>&1; then
@@ -86,29 +83,27 @@ else
fi
cd "$DOTFILES_DIR"
-# 6. nix-switch FIRST. This installs paru + chezmoi (plus the wayland
-# session tools, qrencode, torsocks, lshw, yt-dlp, streamlink,
-# tesseract, whisper-cpp, …) into ~/.nix-profile/bin so the
-# subsequent `just init` finds them on PATH. The repo is already a
-# valid Nix flake — we don't need chezmoi to have run yet.
-log 'running nix-switch (installs paru + user-leaf tools from nix)'
+# 6. nix-switch FIRST. This installs chezmoi (plus the wayland session
+# tools, qrencode, torsocks, lshw, yt-dlp, streamlink, tesseract,
+# whisper-cpp, …) into ~/.nix-profile/bin so the subsequent `just init`
+# finds them on PATH. The repo is already a valid Nix flake — we don't
+# need chezmoi to have run yet.
+log 'running nix-switch (installs chezmoi + user-leaf tools from nix)'
just nix-switch
# Add nix-profile to PATH for the remaining steps so freshly installed
-# tools (paru, chezmoi) are picked up immediately. Login shells will
+# tools (chezmoi, etc.) are picked up immediately. Login shells will
# resolve it via /etc/profile.d/hm-session-vars.sh after re-login.
export PATH="$HOME/.nix-profile/bin:$PATH"
# 7. run just init — this deploys chezmoi, installs the 'base' meta list
-# (which pulls in sudo-rs via the nix-profile paru), deploys
-# /etc/sudoers-rs, /etc/pam.d/sudo, creates
-# /usr/local/bin/{sudo,su,visudo,sudoedit} symlinks pointing at
-# sudo-rs (PATH precedence shadows /usr/bin/sudo), and installs git
-# hooks. The classic 'sudo' package stays installed because
-# base-devel hard-depends on it; that's harmless — the binary is
-# never invoked once /usr/local/bin/sudo is in place. `just init`
-# also re-runs nix-switch as its last step (a no-op since step 6
-# already activated the profile).
+# (which pulls in sudo-rs via pacman), deploys
+# /etc/sudoers-rs, /etc/pam.d/sudo, creates user-scoped
+# ~/.local/bin/{sudo,su,visudo,sudoedit} symlinks pointing at sudo-rs,
+# and installs git hooks. The classic 'sudo' package is only a bootstrap
+# helper and may be removed if it shows up as undeclared. `just init`
+# also re-runs nix-switch as its last step (a no-op since step 6 already
+# activated the profile).
log 'running just init'
just init
diff --git a/dot_config/nvim/lua/plugins/treesitter.lua b/dot_config/nvim/lua/plugins/treesitter.lua
index 4f3ad31..0cba10f 100644
--- a/dot_config/nvim/lua/plugins/treesitter.lua
+++ b/dot_config/nvim/lua/plugins/treesitter.lua
@@ -1,5 +1,15 @@
require("treewalker").setup({})
+-- nvim-treesitter's main branch shells out to tree-sitter, whose Rust cc crate
+-- respects CC. On the Arch host, Home Manager sets this to a Nix compiler
+-- wrapper so parser builds keep working without global base-devel.
+if
+ (vim.env.CC == nil or vim.env.CC == "")
+ and vim.env.NVIM_TREESITTER_CC ~= nil
+then
+ vim.env.CC = vim.env.NVIM_TREESITTER_CC
+end
+
vim.keymap.set(
{ "n", "v" },
"<a-k>",
diff --git a/dot_config/pacman/makepkg.conf b/dot_config/pacman/makepkg.conf
deleted file mode 100644
index 7233660..0000000
--- a/dot_config/pacman/makepkg.conf
+++ /dev/null
@@ -1,9 +0,0 @@
-CFLAGS="-march=native -Ofast -pipe -fomit-frame-pointer"
-RUSTFLAGS="-C opt-level=2 -C target-cpu=native"
-MAKEFLAGS="-j4"
-BUILDDIR=/tmp/makepkg
-COMPRESSGZ=(pigz -c -f -n)
-COMPRESSBZ2=(pbzip2 -c -f)
-COMPRESSXZ=(xz -c -z - --threads=0)
-COMPRESSZST=(zstd -c -z -q - --threads=0)
-PKGEXT='.pkg.tar.zst'
diff --git a/dot_config/paru/paru.conf b/dot_config/paru/paru.conf
deleted file mode 100644
index 6a3632c..0000000
--- a/dot_config/paru/paru.conf
+++ /dev/null
@@ -1,25 +0,0 @@
-#
-# /etc/paru.conf
-# ~/.config/paru/paru.conf
-#
-# See the paru.conf(5) manpage for options
-
-#
-# GENERAL OPTIONS
-#
-[options]
-PgpFetch
-Devel
-Provides
-DevelSuffixes = -git -cvs -svn -bzr -darcs -always
-# BottomUp
-RemoveMake
-SudoLoop
-#UseAsk
-CombinedUpgrade
-BatchInstall
-# UpgradeMenu
-NewsOnUpgrade
-CleanAfter
-SaveChanges
-NoWarn = sommerfeld-*
diff --git a/dot_config/systemd/user/pass-secret-service.service b/dot_config/systemd/user/pass-secret-service.service
new file mode 100644
index 0000000..d7dc564
--- /dev/null
+++ b/dot_config/systemd/user/pass-secret-service.service
@@ -0,0 +1,12 @@
+[Unit]
+Description=Expose the libsecret D-Bus API with pass as backend
+Documentation=https://github.com/grimsteel/pass-secret-service
+
+[Service]
+Type=dbus
+BusName=org.freedesktop.secrets
+ExecStart=%h/.nix-profile/bin/pass-secret-service
+
+[Install]
+WantedBy=default.target
+Alias=dbus-org.freedesktop.secrets.service
diff --git a/dot_config/waybar/executable_update-status.sh b/dot_config/waybar/executable_update-status.sh
index cbd899e..a300245 100755
--- a/dot_config/waybar/executable_update-status.sh
+++ b/dot_config/waybar/executable_update-status.sh
@@ -3,7 +3,7 @@
# upgraded recently. Source of truth is /var/log/pacman.log — the last
# "[PACMAN] starting full system upgrade" entry. No daemon, no -Sy
# polling, no opinion about *which* updates are pending; this only
-# tracks whether you've run `paru -Syu` (or equivalent) lately.
+# tracks whether you've run `pacman -Syu` lately.
#
# States, by hours since last full upgrade:
# < 24h empty (hidden via :empty rule in style.css)
diff --git a/dot_config/zsh/dot_zprofile b/dot_config/zsh/dot_zprofile
index b95079b..20852db 100644
--- a/dot_config/zsh/dot_zprofile
+++ b/dot_config/zsh/dot_zprofile
@@ -29,6 +29,12 @@ export XDG_CACHE_HOME="$HOME/.cache"
# even if /etc/profile.d snippets try to prepend duplicates.
[[ -r /etc/profile ]] && emulate sh -c 'source /etc/profile'
+# Home Manager writes host-specific session variables here. Keep this after
+# /etc/profile so Nix/HM values can provide user-level tool wiring without
+# being clobbered by distro profile snippets.
+[[ -r "$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh" ]] &&
+ emulate sh -c ". \"$HOME/.nix-profile/etc/profile.d/hm-session-vars.sh\""
+
# ── Locale ────────────────────────────────────────────────────────────────────
export LANG=en_US.UTF-8
diff --git a/dot_config/zsh/dot_zshrc b/dot_config/zsh/dot_zshrc
index c697594..d78c9e5 100644
--- a/dot_config/zsh/dot_zshrc
+++ b/dot_config/zsh/dot_zshrc
@@ -46,7 +46,7 @@ fpath=($XDG_DATA_HOME/zsh/completion $fpath)
# Pull in completions from both nix-installed packages AND the system
# package manager (pacman on Arch host, apt on Ubuntu VM). nix zsh's
# default fpath only includes its own nix-store dirs, so without these
-# entries we miss completions for pacman, paru, systemctl, flatpak,
+# entries we miss completions for pacman, systemctl, flatpak,
# docker, kubectl, etc. on the host, and apt/snap on the VM.
for _d in "$HOME/.nix-profile/share/zsh/site-functions" \
"$HOME/.nix-profile/share/zsh/vendor-completions" \
@@ -77,7 +77,7 @@ zstyle ':completion:*' completer _expand_alias _complete _ignored _match _approx
zstyle ':completion:*:default' list-colors ${(s.:.)LS_COLORS} # colorize file completions like ls
zstyle ':completion:*' use-cache on # cache completions (speeds up pip, dpkg, etc.)
zstyle ':completion:*' cache-path "$XDG_CACHE_HOME/zsh"
-zstyle ':completion:*' rehash true # rebuild PATH hash on every completion (catches paru, cargo, pip, manual installs)
+zstyle ':completion:*' rehash true # rebuild PATH hash on every completion (catches cargo, pip, manual installs)
zstyle ':completion:*:match:*' original only # only show original when pattern-matching
zstyle ':completion:*:functions' ignored-patterns '_*' # hide internal completion functions
zstyle ':completion:*:*:kill:*' menu yes select # interactive menu for kill completion
diff --git a/dot_local/bin/executable_arch-news-check b/dot_local/bin/executable_arch-news-check
new file mode 100644
index 0000000..1d78a78
--- /dev/null
+++ b/dot_local/bin/executable_arch-news-check
@@ -0,0 +1,216 @@
+#!/usr/bin/env dash
+set -eu
+
+mark_read=false
+for arg in "$@"; do
+ case "$arg" in
+ --mark-read)
+ mark_read=true
+ ;;
+ -h | --help)
+ cat <<'EOF'
+usage: arch-news-check [--mark-read]
+
+Check Arch Linux news before a system upgrade. New feed entries are printed
+and followed by a "Proceed with upgrade? [Y/n]" prompt. Choosing yes records
+the currently visible feed items as seen.
+EOF
+ exit 0
+ ;;
+ *)
+ printf 'arch-news-check: unknown option: %s\n' "$arg" >&2
+ exit 2
+ ;;
+ esac
+done
+
+prompt_proceed() {
+ if [ ! -t 0 ]; then
+ printf 'arch-news-check: non-interactive shell; proceeding by default\n' >&2
+ return 0
+ fi
+
+ while :; do
+ printf ':: Proceed with upgrade? [Y/n] ' >/dev/tty
+ IFS= read -r answer </dev/tty || answer=
+ case "$answer" in
+ '' | [Yy] | [Yy][Ee][Ss]) return 0 ;;
+ [Nn] | [Nn][Oo]) return 1 ;;
+ *) printf 'Please answer yes or no.\n' >/dev/tty ;;
+ esac
+ done
+}
+
+if ! command -v python3 >/dev/null 2>&1; then
+ if [ "$mark_read" = true ]; then
+ printf 'arch-news-check: python3 not found; cannot mark Arch news as read\n' >&2
+ exit 1
+ fi
+ printf 'arch-news-check: python3 not found; skipping Arch news check\n' >&2
+ prompt_proceed
+ exit $?
+fi
+
+exec python3 - "$@" <<'PY'
+from __future__ import annotations
+
+import email.utils
+import html
+import json
+import os
+import re
+import sys
+import textwrap
+import urllib.error
+import urllib.request
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+FEED_URL = os.environ.get("ARCH_NEWS_FEED_URL", "https://archlinux.org/feeds/news/")
+STATE_FILE = Path(
+ os.environ.get(
+ "ARCH_NEWS_STATE_FILE",
+ Path(os.environ.get("XDG_STATE_HOME", Path.home() / ".local/state"))
+ / "arch-news-check"
+ / "seen.json",
+ )
+)
+
+
+def usage_error(message: str) -> int:
+ print(f"arch-news-check: {message}", file=sys.stderr)
+ return 2
+
+
+def prompt_proceed() -> bool:
+ if not sys.stdin.isatty():
+ print("arch-news-check: non-interactive shell; proceeding by default", file=sys.stderr)
+ return True
+
+ while True:
+ answer = input(":: Proceed with upgrade? [Y/n] ").strip().lower()
+ if answer in {"", "y", "yes"}:
+ return True
+ if answer in {"n", "no"}:
+ return False
+ print("Please answer yes or no.")
+
+
+def fetch_feed() -> bytes:
+ request = urllib.request.Request(
+ FEED_URL,
+ headers={"User-Agent": "arch-news-check/1.0 (+https://archlinux.org/)"},
+ )
+ with urllib.request.urlopen(request, timeout=10) as response:
+ return response.read()
+
+
+def clean_text(value: str) -> str:
+ value = html.unescape(value)
+ value = re.sub(r"<[^>]+>", " ", value)
+ value = re.sub(r"\s+", " ", value)
+ return value.strip()
+
+
+def parse_date(value: str) -> str:
+ if not value:
+ return ""
+ try:
+ return email.utils.parsedate_to_datetime(value).date().isoformat()
+ except (TypeError, ValueError):
+ return value
+
+
+def parse_feed(raw_xml: bytes) -> list[dict[str, str]]:
+ root = ET.fromstring(raw_xml)
+ entries: list[dict[str, str]] = []
+ for item in root.findall("./channel/item"):
+ title = clean_text(item.findtext("title", ""))
+ link = clean_text(item.findtext("link", ""))
+ guid = clean_text(item.findtext("guid", "")) or link or title
+ published = parse_date(clean_text(item.findtext("pubDate", "")))
+ summary = clean_text(item.findtext("description", ""))
+ if guid:
+ entries.append(
+ {
+ "id": guid,
+ "title": title or "(untitled)",
+ "link": link,
+ "published": published,
+ "summary": summary,
+ }
+ )
+ return entries
+
+
+def load_seen() -> set[str]:
+ try:
+ data = json.loads(STATE_FILE.read_text())
+ except FileNotFoundError:
+ return set()
+ except (OSError, json.JSONDecodeError):
+ return set()
+ seen = data.get("seen", [])
+ if not isinstance(seen, list):
+ return set()
+ return {item for item in seen if isinstance(item, str)}
+
+
+def save_seen(ids: set[str]) -> None:
+ STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
+ tmp = STATE_FILE.with_suffix(".tmp")
+ tmp.write_text(json.dumps({"seen": sorted(ids)}, indent=2) + "\n")
+ tmp.replace(STATE_FILE)
+
+
+def print_news(entries: list[dict[str, str]]) -> None:
+ print(":: New Arch Linux news")
+ for entry in entries:
+ date = f" ({entry['published']})" if entry["published"] else ""
+ print(f" -> {entry['title']}{date}")
+ if entry["link"]:
+ print(f" {entry['link']}")
+ if entry["summary"]:
+ summary = textwrap.shorten(entry["summary"], width=500, placeholder=" ...")
+ print(textwrap.indent(textwrap.fill(summary, width=88), " "))
+ print()
+
+
+def main(argv: list[str]) -> int:
+ mark_read = False
+ for arg in argv:
+ if arg == "--mark-read":
+ mark_read = True
+ elif arg in {"-h", "--help"}:
+ return usage_error("help is handled by the shell wrapper")
+ else:
+ return usage_error(f"unknown option: {arg}")
+
+ try:
+ entries = parse_feed(fetch_feed())
+ except (ET.ParseError, OSError, urllib.error.URLError) as exc:
+ print(f"arch-news-check: failed to read Arch news: {exc}", file=sys.stderr)
+ return 0 if prompt_proceed() else 1
+
+ current_ids = {entry["id"] for entry in entries}
+ if mark_read:
+ save_seen(current_ids)
+ print("Marked current Arch Linux news as read.")
+ return 0
+
+ seen = load_seen()
+ unread = [entry for entry in entries if entry["id"] not in seen]
+ if not unread:
+ return 0
+
+ print_news(unread)
+ if not prompt_proceed():
+ return 1
+
+ save_seen(current_ids)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main(sys.argv[1:]))
+PY
diff --git a/dot_local/bin/symlink_su b/dot_local/bin/symlink_su
new file mode 100644
index 0000000..73045e0
--- /dev/null
+++ b/dot_local/bin/symlink_su
@@ -0,0 +1 @@
+/usr/bin/su-rs
diff --git a/dot_local/bin/symlink_sudo b/dot_local/bin/symlink_sudo
new file mode 100644
index 0000000..0830fa1
--- /dev/null
+++ b/dot_local/bin/symlink_sudo
@@ -0,0 +1 @@
+/usr/bin/sudo-rs
diff --git a/dot_local/bin/symlink_sudoedit b/dot_local/bin/symlink_sudoedit
new file mode 100644
index 0000000..0830fa1
--- /dev/null
+++ b/dot_local/bin/symlink_sudoedit
@@ -0,0 +1 @@
+/usr/bin/sudo-rs
diff --git a/dot_local/bin/symlink_visudo b/dot_local/bin/symlink_visudo
new file mode 100644
index 0000000..535dcf1
--- /dev/null
+++ b/dot_local/bin/symlink_visudo
@@ -0,0 +1 @@
+/usr/bin/visudo-rs
diff --git a/etc/pacman.conf b/etc/pacman.conf
index 8f8be59..aaa7fef 100644
--- a/etc/pacman.conf
+++ b/etc/pacman.conf
@@ -22,8 +22,8 @@ HoldPkg = pacman glibc
Architecture = auto
# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup
-# llama.cpp-vulkan: AUR rebuilds on every llama.cpp commit (1-2 hour build);
-# pin and update manually with `paru -S llama.cpp-vulkan` when intended.
+# llama.cpp-vulkan: formerly pinned as an AUR package because every upstream
+# commit triggered a 1-2 hour rebuild. Kept as a reminder if it returns.
#IgnorePkg =
#IgnoreGroup =
diff --git a/etc/udev/rules.d/51-hid-digitalbitbox.rules b/etc/udev/rules.d/51-hid-digitalbitbox.rules
new file mode 100644
index 0000000..49654ed
--- /dev/null
+++ b/etc/udev/rules.d/51-hid-digitalbitbox.rules
@@ -0,0 +1,3 @@
+# BitBox V1. Sourced from Shift Crypto's Linux post-install rules:
+# digitalbitbox/bitbox-wallet-app d045d25ac1a3839bcb7993d95bf2501bc1e86780.
+SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="dbb%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2402"
diff --git a/etc/udev/rules.d/52-hid-digitalbitbox.rules b/etc/udev/rules.d/52-hid-digitalbitbox.rules
new file mode 100644
index 0000000..cded163
--- /dev/null
+++ b/etc/udev/rules.d/52-hid-digitalbitbox.rules
@@ -0,0 +1,3 @@
+# BitBox V1 hidraw. Sourced from Shift Crypto's Linux post-install rules:
+# digitalbitbox/bitbox-wallet-app d045d25ac1a3839bcb7993d95bf2501bc1e86780.
+KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2402", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="dbbf%n"
diff --git a/etc/udev/rules.d/53-hid-bitbox02.rules b/etc/udev/rules.d/53-hid-bitbox02.rules
new file mode 100644
index 0000000..977b1f4
--- /dev/null
+++ b/etc/udev/rules.d/53-hid-bitbox02.rules
@@ -0,0 +1,3 @@
+# BitBox02. Sourced from Shift Crypto's Linux post-install rules:
+# digitalbitbox/bitbox-wallet-app d045d25ac1a3839bcb7993d95bf2501bc1e86780.
+SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02_%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403"
diff --git a/etc/udev/rules.d/54-hid-bitbox02.rules b/etc/udev/rules.d/54-hid-bitbox02.rules
new file mode 100644
index 0000000..e0484ac
--- /dev/null
+++ b/etc/udev/rules.d/54-hid-bitbox02.rules
@@ -0,0 +1,3 @@
+# BitBox02 hidraw. Sourced from Shift Crypto's Linux post-install rules:
+# digitalbitbox/bitbox-wallet-app d045d25ac1a3839bcb7993d95bf2501bc1e86780.
+KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02-%n"
diff --git a/justfile b/justfile
index 287955b..6936149 100644
--- a/justfile
+++ b/justfile
@@ -64,9 +64,18 @@ nix-switch:
# Update everything: system packages, flatpaks, nix flake inputs
update: pkg-update flatpak-update nix-update
-# Upgrade all system + AUR packages
+# Upgrade official Arch packages, after showing newly published Arch news.
pkg-update:
- paru -Syu
+ @just arch-news-check
+ @sudo pacman -Syu
+
+# Show new Arch Linux news and ask whether to proceed, like paru's NewsOnUpgrade.
+arch-news-check:
+ @sh "{{ justfile_directory() }}/dot_local/bin/executable_arch-news-check"
+
+# Mark the current Arch Linux news feed as seen without running an upgrade.
+arch-news-read:
+ @sh "{{ justfile_directory() }}/dot_local/bin/executable_arch-news-check" --mark-read
# Refresh nix flake inputs (nixpkgs, home-manager) then re-activate the profile.
@@ -1001,7 +1010,7 @@ pkg-status:
fi
just undeclared | sed 's/^/ undeclared: /'
-# Print undeclared packages one per line, unindented (pipe to 'paru -Rs -' to remove pacman entries)
+# Print undeclared packages one per line, unindented.
undeclared:
#!/usr/bin/env dash
active=$(just _active-packages)
@@ -1079,7 +1088,7 @@ pkg-list group="":
pkg-apply *groups:
#!/usr/bin/env dash
set -eu
- # `paru -S --needed` is a no-op for already-installed packages, which
+ # `pacman -S --needed` is a no-op for already-installed packages, which
# means a package pulled in transitively (and later declared in
# meta/*.txt) stays marked "installed as a dependency" forever and
# keeps showing up under `pacopt`. After each install pass, force the
@@ -1102,7 +1111,7 @@ pkg-apply *groups:
fi
pkgs=$(sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' "$file")
[ -n "$pkgs" ] || continue
- printf '%s\n' "$pkgs" | paru -S --needed --noconfirm --ask=4 -
+ printf '%s\n' "$pkgs" | sudo pacman -S --needed --noconfirm -
mark_explicit "$pkgs"
done
else
@@ -1110,7 +1119,7 @@ pkg-apply *groups:
| xargs -0 cat \
| sed -E 's/[[:space:]]*#.*$//; /^[[:space:]]*$/d' \
| sort -u)
- printf '%s\n' "$all_pkgs" | paru -S --needed --noconfirm --ask=4 -
+ printf '%s\n' "$all_pkgs" | sudo pacman -S --needed --noconfirm -
mark_explicit "$all_pkgs"
[ -f meta/flatpak.txt ] && just _flatpak-install
fi
@@ -1140,7 +1149,7 @@ pkg-fix:
if [ "$group" = "flatpak" ]; then
just _flatpak-install
else
- echo "$pkgs" | paru -S --needed --noconfirm --ask=4 -
+ echo "$pkgs" | sudo pacman -S --needed --noconfirm -
fi
fi
done
@@ -1167,7 +1176,7 @@ pkg-add group +pkgs:
https://dl.flathub.org/repo/flathub.flatpakrepo >/dev/null
flatpak install --user -y --noninteractive flathub {{ pkgs }}
else
- paru -S --needed {{ pkgs }}
+ sudo pacman -S --needed {{ pkgs }}
fi
# Remove one or more packages from a group list (does NOT uninstall; the package may belong to other groups)
diff --git a/meta/base.txt b/meta/base.txt
index c0cb1da..d817340 100644
--- a/meta/base.txt
+++ b/meta/base.txt
@@ -3,14 +3,13 @@
# net / debug+trace / docs / secrets — all provisioned via Home-Manager
# from nix/common.nix and live under ~/.nix-profile/bin (first in PATH).
# What stays on pacman in this section is the pieces tightly coupled to
-# the distro (man-db/man-pages files), the system runtime (sudo-rs,
-# base/base-devel), and things needed pre-bootstrap or by other system
-# packages transitively. User-leaf CLIs/daemons (chezmoi, paru, qrencode,
-# torsocks, lshw, xdg-utils, syncthing) now come from nix/host.nix.
+# the distro, the system runtime (sudo-rs, base), and things needed
+# pre-bootstrap or by other system packages transitively. User-leaf
+# CLIs/daemons/docs (chezmoi, man-db/man-pages, qrencode, torsocks,
+# lshw, xdg-utils, syncthing) now come from nix.
acpid
arch-audit
base
-base-devel
btrfs-progs
cpupower
dosfstools
@@ -23,16 +22,12 @@ linux-hardened-headers
linux-lts
linux-lts-headers
lostfiles
-man-db
-man-pages
nfs-utils
nftables
ocl-icd
# Provides paccache for the repo-owned cache cleanup hook under
# etc/pacman.d/hooks/.
pacman-contrib
-pbzip2
-pigz
pkgstats
rebuild-detector
reflector
@@ -53,7 +48,7 @@ ell
bolt
# --- nix (multi-user daemon mode for hermetic per-project dev shells via
-# `nix develop` + direnv `use flake`. Not a replacement for paru/pacman,
+# `nix develop` + direnv `use flake`. Not a replacement for 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
@@ -62,14 +57,10 @@ bolt
# direnv's source_url with a content hash, so no extra package needed.) ---
nix
-# --- dev (system-coupled runtimes only — base-devel ships gcc/ld/as/make
-# for general-purpose builds; the orchestrators (cmake/ninja/ccache/
-# sccache), debuggers and toolchain-specific compilers/linkers live in
-# nix instead. clang/lld/mold/rustup/go are intentionally absent — when
-# a project needs them, the project's flake.nix + direnv `.envrc`
-# provide them. The podman stack (podman, crun, conmon, netavark,
-# aardvark-dns, slirp4netns, passt, podman-compose, podman-docker) now
-# comes from nix/common.nix — unified across host and VM.) ---
+# --- dev (system-coupled runtime only). No base-devel: AUR/makepkg is not
+# part of the normal system, and project toolchains come from direnv + nix
+# devShells. Build orchestrators, debuggers, and user CLIs live in
+# nix/common.nix. ---
perf # links against running kernel ABI; must match kernel pkg
# --- sound ---
@@ -114,22 +105,12 @@ libnotify
# Lock screen (setuid; PAM-coupled)
swaylock
-# org.freedesktop.secrets D-Bus implementation backed by pass. Required
-# by Signal Desktop (flatpak) and other libsecret consumers. Ships both
-# a D-Bus activation file and a systemd user unit; we enable the unit
-# explicitly so it's visible in `systemctl --user status`. Stores
-# secrets under ~/.password-store/secret-service/.
-pass-secret-service-bin
-
# Ships ZSA's upstream udev rules (50-oryx.rules, 50-wally.rules) to
# /usr/lib/udev/rules.d/ so VID 3297 hidraw nodes get TAG+=uaccess.
# Required for VIA / usevia.app (WebHID) and Wally flashing of the
# ErgoDox EZ / Moonlander / Voyager.
zsa-udev
-# Udev rules for BitBox hardware wallet access from Sparrow.
-bitbox-udev
-
# QR (system lib used by zbarcam; the qrencode CLI is in nix/host.nix)
zbar
xorg-xwayland # needed for zbarcam's X11 preview
@@ -148,10 +129,6 @@ qt5ct
qt6ct
xdg-user-dirs
-# --- 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 now comes from nix/host.nix (the bridge binary + its
# repo-owned user unit dot_config/systemd/user/protonmail-bridge.service).
diff --git a/nix/common.nix b/nix/common.nix
index 2a71de4..c1cf551 100644
--- a/nix/common.nix
+++ b/nix/common.nix
@@ -117,6 +117,7 @@
doxygen
# Docs
+ less
tldr
man-db
man-pages
diff --git a/nix/host.nix b/nix/host.nix
index ca8bcc4..de68230 100644
--- a/nix/host.nix
+++ b/nix/host.nix
@@ -43,6 +43,37 @@ let
platforms = platforms.all;
};
};
+ pass-secret-service-rust = pkgs.rustPlatform.buildRustPackage rec {
+ pname = "pass-secret-service";
+ version = "0.7.0";
+
+ src = pkgs.fetchFromGitHub {
+ owner = "grimsteel";
+ repo = "pass-secret-service";
+ rev = "v${version}";
+ hash = "sha256-cBDGxF1ETyszwHZJwN8n+lwKcpOU8Xt1XTOGbUHj9UI=";
+ };
+
+ cargoHash = "sha256-Ko8LlgPG6kl+pZ47jrFnKdc+9i7/eh9DMRtG2SWQGjQ=";
+ nativeBuildInputs = [ pkgs.makeWrapper ];
+
+ postInstall = ''
+ wrapProgram "$out/bin/pass-secret-service" \
+ --prefix PATH : "${lib.makeBinPath [ pkgs.gnupg ]}"
+ '';
+
+ meta = {
+ description = "Implementation of org.freedesktop.secrets using pass";
+ homepage = "https://github.com/grimsteel/pass-secret-service";
+ license = lib.licenses.gpl3Only;
+ platforms = lib.platforms.linux;
+ mainProgram = "pass-secret-service";
+ };
+ };
+ arkenfox-userjs-profile = pkgs.runCommand "arkenfox-userjs-profile-${pkgs.arkenfox-userjs.version}" { } ''
+ install -Dm644 ${pkgs.arkenfox-userjs}/user.js $out/share/arkenfox-userjs/user.js
+ install -Dm644 ${pkgs.arkenfox-userjs}/user.cfg $out/share/arkenfox-userjs/user.cfg
+ '';
in
{
imports = [ ./common.nix ];
@@ -50,6 +81,11 @@ in
home.username = builtins.getEnv "USER";
home.homeDirectory = builtins.getEnv "HOME";
+ # Keep Nix's compiler out of PATH, but make it available to host Neovim for
+ # nvim-treesitter parser builds. The Nix-provided Neovim loads these parser
+ # .so files, so using the Nix compiler wrapper is the coherent ABI choice.
+ home.sessionVariables.NVIM_TREESITTER_CC = "${pkgs.stdenv.cc}/bin/cc";
+
home.packages = with pkgs; [
# ── Thunderbird helpers ───────────────────────────────────────────────────
# external-editor-revived is the native-messaging host that lets the
@@ -71,6 +107,13 @@ in
# `protonmail-bridge-core`.
protonmail-bridge
+ # ── Secrets portal ────────────────────────────────────────────────────────
+ # Grimsteel's Rust org.freedesktop.secrets provider backed by pass. This is
+ # not nixpkgs' Python `pass-secret-service`; the repo-owned user unit at
+ # dot_config/systemd/user/pass-secret-service.service uses the Rust binary
+ # name and the PASSWORD_STORE_DIR drop-in.
+ pass-secret-service-rust
+
# ── Wayland session: bars, launchers, notifiers, daemons ──────────────────
# Pure user-session GUIs/daemons — no system unit, no D-Bus activation
# file under /usr/share/dbus-1, no login-manager session entry. The
@@ -136,13 +179,15 @@ in
# testing.
sparrow
- # chezmoi & paru — both are pure user CLIs. `paru` wraps pacman+makepkg
- # but doesn't link them; it just shells out. bootstrap.sh installs a
- # one-shot pacman `chezmoi` for the very first `chezmoi init --apply`,
- # then `paru -Rns chezmoi paru` after the first nix-switch drops the
- # pacman copies (the nix-profile copies on PATH take over).
+ # ── Browser hardening ────────────────────────────────────────────────────
+ # Upstream Arkenfox user.js from nixpkgs, re-exposed under share/ so the
+ # chezmoi Firefox/LibreWolf deploy hook can render it with
+ # firefox/user-overrides.js into the Flatpak profile.
+ arkenfox-userjs-profile
+
+ # Dotfile manager. bootstrap.sh uses the pacman `just` only long enough
+ # to run nix-switch; after that, this nix-profile copy is on PATH.
chezmoi
- paru
# ── OCR ──────────────────────────────────────────────────────────────────
# Override merges eng + por language data into a single derivation,
diff --git a/run_onchange_after_deploy-etc.sh.tmpl b/run_onchange_after_deploy-etc.sh.tmpl
index ba79130..5f97c70 100755
--- a/run_onchange_after_deploy-etc.sh.tmpl
+++ b/run_onchange_after_deploy-etc.sh.tmpl
@@ -27,6 +27,12 @@ find etc -type f ! -name .ignore | while IFS= read -r src; do
esac
done
+if [ -d etc/udev/rules.d ]; then
+ sudo udevadm control --reload
+ sudo udevadm trigger --subsystem-match=usb
+ sudo udevadm trigger --subsystem-match=hidraw
+fi
+
# sudo-rs: /etc/pam.d/sudo-i is a symlink to /etc/pam.d/sudo
sudo ln -sfT sudo /etc/pam.d/sudo-i
@@ -46,10 +52,15 @@ sudo systemctl daemon-reload
# (e.g. HandlePowerKey overrides) take effect without dropping sessions.
sudo systemctl kill -s HUP systemd-logind
-# Make sudo-rs the system-wide sudo via /usr/local/bin precedence.
-# Targets may not exist yet on first bootstrap (sudo-rs is installed by
-# the subsequent pkg-apply step); the symlinks resolve once it lands.
-sudo ln -sfT /usr/bin/sudo-rs /usr/local/bin/sudo
-sudo ln -sfT /usr/bin/sudo-rs /usr/local/bin/sudoedit
-sudo ln -sfT /usr/bin/su-rs /usr/local/bin/su
-sudo ln -sfT /usr/bin/visudo-rs /usr/local/bin/visudo
+# Old sudo-rs migration used /usr/local/bin to shadow classic sudo globally.
+# Current policy keeps those shims user-scoped via ~/.local/bin/symlink_*.
+for link in /usr/local/bin/sudoedit /usr/local/bin/su /usr/local/bin/visudo; do
+ target=$(readlink "$link" 2>/dev/null || true)
+ case "$target" in
+ /usr/bin/sudo-rs|/usr/bin/su-rs|/usr/bin/visudo-rs) sudo rm -f "$link" ;;
+ esac
+done
+target=$(readlink /usr/local/bin/sudo 2>/dev/null || true)
+if [ "$target" = /usr/bin/sudo-rs ]; then
+ sudo rm -f /usr/local/bin/sudo
+fi
diff --git a/run_onchange_after_deploy-firefox.sh.tmpl b/run_onchange_after_deploy-firefox.sh.tmpl
index eaecb68..c9e49be 100755
--- a/run_onchange_after_deploy-firefox.sh.tmpl
+++ b/run_onchange_after_deploy-firefox.sh.tmpl
@@ -1,10 +1,13 @@
#!/usr/bin/env dash
-# Deploy Firefox/LibreWolf hardening overrides and custom CSS.
-# chezmoi re-runs this script whenever any file under firefox/ changes.
+# Deploy Firefox/LibreWolf hardening and custom CSS.
+# chezmoi re-runs this script whenever any file under firefox/ or the nix
+# inputs that provide arkenfox-userjs change.
# firefox/ content hash: {{ output "sh" "-c" (printf "cd %q && find firefox -type f -exec sha256sum {} + | LC_ALL=C sort" .chezmoi.sourceDir) | sha256sum }}
+# arkenfox nix hash: {{ output "sh" "-c" (printf "cd %q && sha256sum nix/flake.lock nix/host.nix 2>/dev/null || true" .chezmoi.sourceDir) | sha256sum }}
set -eu
PROFILES_DIR="$HOME/.var/app/io.gitlab.librewolf-community/.librewolf"
+ARKENFOX_USER_JS="$HOME/.nix-profile/share/arkenfox-userjs/user.js"
[ -d "$PROFILES_DIR" ] || exit 0
PROFILE=$(find "$PROFILES_DIR" -maxdepth 1 -mindepth 1 -type d -name '*.default-default' | head -1)
@@ -14,7 +17,21 @@ fi
[ -n "$PROFILE" ] || exit 0
cd "$CHEZMOI_SOURCE_DIR"
+
+if [ -r "$ARKENFOX_USER_JS" ]; then
+ tmp=$(mktemp)
+ cat "$ARKENFOX_USER_JS" >"$tmp"
+ printf '\n/*** user-overrides.js ***/\n\n' >>"$tmp"
+ cat firefox/user-overrides.js >>"$tmp"
+ cp --remove-destination "$tmp" "$PROFILE/user.js"
+ rm -f "$tmp"
+ rm -f "$PROFILE/user-overrides.js"
+else
+ echo "deploy-firefox: missing $ARKENFOX_USER_JS; run 'just nix-switch' before regenerating user.js." >&2
+fi
+
find firefox -type f | while IFS= read -r src; do
+ [ "$src" = "firefox/user-overrides.js" ] && continue
dest="$PROFILE/${src#firefox/}"
mkdir -p "$(dirname "$dest")"
cp --remove-destination "$src" "$dest"