1
0
Fork 0
No description
  • Python 46.9%
  • Shell 27.1%
  • Go Template 13.8%
  • Lua 9.7%
  • CSS 1%
  • Other 1.5%
Find a file
niyari 0dd64f7888
Some checks failed
ci / lint (push) Failing after 16s
ansible: rtkit — RT scheduling for pipewire (phase 2)
Verified after re-login: the fresh sway session carries memlock unlimited +
rtprio 98 (Bitwig side done), but user@.service still reports LimitRTPRIO=0
— PAM limits don't reach systemd user units, and pipewire/wireplumber/
pipewire-pulse all remain TS. rtkit closes that gap: pipewire's module-rt
requests RT over dbus from the rtkit daemon, which also watchdogs and
demotes runaway RT threads.

AI-assisted-by: ollama-cloud/glm-5.3 (via OpenCode)
2026-10-29 23:56:23 +00:00
.forgejo/workflows ci: only sh -n shell templates, pipefail, render everything 2026-09-29 15:35:57 +01:00
ansible ansible: rtkit — RT scheduling for pipewire (phase 2) 2026-10-29 23:56:23 +00:00
home sway: XDG_CURRENT_DESKTOP=sway:GNOME so Electron picks libsecret 2026-10-29 01:18:09 +00:00
nix Pinned wine-staging 9.20 + yabridge stack via nix 2026-10-29 00:19:37 +00:00
pkgs/bin ansible: rtkit — RT scheduling for pipewire (phase 2) 2026-10-29 23:56:23 +00:00
.gitignore Migrate dotfiles from Stow to chezmoi + Ansible 2026-07-08 01:19:19 +01:00
.secrets.baseline Set up prek + mise workspace for git hooks 2026-06-21 12:49:27 +01:00
AGENTS.md Pinned wine-staging 9.20 + yabridge stack via nix 2026-10-29 00:19:37 +00:00
LICENSE Rename UNLICENSE to LICENSE 2026-05-17 12:52:34 +01:00
mise.toml Migrate dotfiles from Stow to chezmoi + Ansible 2026-07-08 01:19:19 +01:00
packages.md packages: add NVIDIA compute/32-bit stack (cuda, ocl-icd, opencl-nvidia, lib32-nvidia-utils) 2026-09-26 01:49:55 +01:00
prek.toml Pinned wine-staging 9.20 + yabridge stack via nix 2026-10-29 00:19:37 +00:00
README.md ansible: realtime-privileges + realtime role for pro-audio 2026-10-29 23:43:00 +00:00

dotfiles

Laptop configuration split across two tools by scope:

  • home/ — chezmoi source state for everything in ~ (dotfiles, ~/.config, ~/bin, user systemd units). Secrets in ~ are GPG-encrypted chezmoi entries (see Secrets).
  • ansible/ — a lean single-host playbook for system provisioning (/etc, SMB mounts, packages, system systemd). Mirrors the homelab's local-install pattern (pyproject + uv.lock + .venv + local collections).
  • pkgs/ — pkg-audit.py, the package manifest/audit script (~/bin/pkg-audit.py symlinks here).

Setup

# 1. pinned tool versions (prek, mise, chezmoi, uv, …) + git hooks
mise install

prek install

# 2. apply ~ dotfiles via chezmoi. `init` renders home/.chezmoi.toml.tmpl
#    into ~/.config/chezmoi/chezmoi.toml (sourceDir, gpg recipient); then apply
chezmoi init -S ~/code/dotfiles/home
chezmoi apply

# 3. system provisioning via ansible
cd ansible && uv sync && uv run ansible-galaxy collection install -r requirements.yaml
uv run ansible-playbook laptop.yml --ask-become-pass

chezmoi apply is idempotent — run it after pulling changes to refresh ~. User systemd units under ~/.config/systemd/user/ are reloaded + re-enabled automatically by home/run_onchange_systemd-user-units.sh.tmpl whenever a unit changes.

Ansible roles

ansible/laptop.yml (run with uv run ansible-playbook laptop.yml --ask-become-pass) applies these roles in order:

  • pacman — enables the [multilib] repository in /etc/pacman.conf by uncommenting the shipped block in place (idempotent; no templating to avoid clobbering local edits or fighting .pacnew). Runs first so 32-bit packages from the manifest can resolve.
  • packages — declarative install of the official-repo and Flatpak package set from roles/packages/vars/manifest.yml. The manifest splits packages into packages (official repos — installed by the role, including each entry's opt_deps) and aur_packages (AUR — installed by the aur role below), classified at authoring time via pacman -Slq so the role needs no runtime classifier. Runs first so per-role package tasks below are idempotent no-ops. Machine-specific entries carry an only_on list of DMI system_vendor strings (e.g. panafanpwr → Panasonic Corporation, nvidia-open → LENOVO); the playbook exposes ansible_facts.system_vendor as dmi_vendor in pre_tasks, and the packages/aur roles filter out entries whose only_on doesn't match. pkg-audit.py is kept reconciled with the manifest and applies the same only_on gate (reads /sys/class/dmi/id/system_vendor directly). See AGENTS.md for details.
  • locale — enables en_GB.UTF-8 / en_US.UTF-8 via locale_gen.
  • makepkg — drops a makepkg.conf.d override (debug -> !debug); config only.
  • aur — installs the manifest's aur_packages via yay run as a dedicated aur_builder user (passwordless pacman in sudoers, since makepkg can't run as root but needs sudo to install). Bootstraps yay itself via makepkg when absent. Runs after makepkg so AUR builds inherit the !debug OPTIONS.
  • smb_mounts — CIFS shares via systemd .mount/.automount, credentials from ansible-vault.
  • lowbat — the 99-lowbat.rules hibernate-on-low-battery udev rule.
  • timezone — sets the system timezone and enables systemd-timesyncd.
  • resolved — systemd-resolved DNS + avahi mDNS, /etc/resolv.conf stub, nsswitch.conf .local wiring.
  • realtime — adds the login user to the realtime group created by realtime-privileges (packages manifest), granting the ArchWiki pro-audio pam_limits (memlock unlimited, rtprio 98). Fixes Bitwig's memlock warning; audio apps launched from the session can opt into SCHED_FIFO.
  • tlp — TLP power-management drop-in (/etc/tlp.d) + service.
  • bootbackup — two pacman hooks (ArchWiki System backup pattern) that rsync /boot to /.bootbackup/<timestamp>_{pre,post}/ on any transaction touching a kernel vmlinuz. Gives the ext4+LUKS root a rollback point for kernel upgrades without btrfs/LVM snapshots. linux-lts stays the live fallback; these snapshots are for restoring a working /boot from a chroot or live USB.
  • nix — installs nix (multi-user) and enables nix-daemon. The store lives in /nix/store; profiles live under ~/.local/state/nix/profiles/. The wine/yabridge stack is installed from nix/flake.nix into a named profile — see "Wine & yabridge via nix" below.

Wine & yabridge via nix

Windows VST/VST3/CLAP plugins run through yabridge on pinned wine-staging 9.20, installed from nix — none of it from pacman:

  • Why nix: yabridge's Wine host is a winelib binary coupled to the Wine line it was built against. Arch's yabridge is built for new-WoW64 wine 10/11 and cannot run under wine 9 — but wine 9 staging is the last-known-good line for some plugins. nixpkgs nixos-24.11 is frozen with yabridge 5.1.0 built against wineWowPackages.staging (wine-staging 9.20, classic WoW64), so nix/flake.nix just re-exports the coherent stack: host binaries and runtime wine from the same evaluation, bitbridge (32-bit plugin support) included.
  • Named profile: the stack composition is codified as the yabridge-stack buildEnv in the flake; the named profile ~/.local/state/nix/profiles/yabridge holds exactly that one entry — one generation history, never put on PATH. run_onchange_nix-profile.sh.tmpl converges the profile on every chezmoi apply: it hashes nix/flake.nix + nix/flake.lock into its rendered content, so a composition change or pin bump makes apply upgrade the profile (nix profile upgrade; plain install silently skips when the entry exists). Fresh machines bootstrap the same way — first apply installs. Rollback: nix profile rollback --profile ~/.local/state/nix/profiles/yabridge.
  • Exposure is selective (chezmoi-managed symlinks, so the surface is reviewed in git): ~/bin/yabridgectl is the only command the shell gets; the yabridge host wrapper pairs live in ~/.local/share/yabridge, where the DAW discovers them via the XDG data home — no profile bin/ is ever exported to PATH. The host wrappers hardcode wine's store path, so no WINELOADER is needed.
  • Ad-hoc activation: nix-yabridge (~/bin) gives a subshell with the profile's full bin — the single interactive entry, so prefix maintenance always uses the same wine the plugins run (no devShell: it evaluates from the flake on every entry and can desync from the profile after a lock bump). One-shots pass through: nix-yabridge -c wine64 setup.exe. The ❄️ prompt indicator detects these shells by store bins on PATH — plain nix shell sets no IN_NIX_SHELL.
  • Env: ~/.zprofile exports only NIX_PROFILES — the yabridge chainloader requires it to find libyabridge-*.so in the profile's lib.
  • Plugins: live in the ~/.wine prefix; yabridgectl sync mirrors the yabridge .so wrappers into ~/.vst{,3}/yabridge (Bitwig picks them up from there).

Secrets

Two stores, one GPG root of trust (the primary ed25519 key that also unlocks pass):

  • ~ secrets — GPG-encrypted chezmoi entries (home/**/encrypted_*.asc). chezmoi decrypts them on apply via the recipient in ~/.config/chezmoi/chezmoi.toml. Add one with chezmoi add --encrypt <file>.
  • /etc / system secrets — ansible-vault. SMB credentials + the rest of ansible/group_vars/laptop.yml are vault-encrypted; the vault password is sourced from pass (dotfiles/ansible-vault-password) via ansible/vault-password.sh.

~/.gnupg/gpg-agent.conf is chezmoi-managed (home/private_dot_gnupg/private_gpg-agent.conf, plaintext — it holds no secrets, just agent tuning) and raises the passphrase cache beyond gpg-agent's defaults (10min idle / 2hr max) to default-cache-ttl 3600 (1hr idle) / max-cache-ttl 28800 (8hr max), since both pass (feeding mbsync/aerc/ imapnotify) and chezmoi's encrypted entries prompt through the same cached passphrase. After editing, reload the running agent with gpg-connect-agent reloadagent /bye.

USB secrets backup

Offline cold backup of GPG keys, SSH keys, and the Bitwarden vault to a LUKS-encrypted USB stick — a physical copy that survives if the NAS (/mnt/backup) is gone. The restic home backup covers the working tree; this covers the root of trust.

One-time setup (per stick), run as root:

sudo ~/bin/usb-secrets-init.sh

Prompts for the device path (shows lsblk first), wipes it, and creates a single LUKS2 partition formatted ext4 with label secrets-backup. Has guards against formatting a mounted device or the system SSD.

Refresh the backup each time you want to update the stick:

~/bin/usb-secrets-backup.sh

Plug the stick in, unlock it via the udiskie polkit prompt, then run the script. Each run writes a self-contained timestamped snapshot directory under /run/media/$USER/secrets-backup/secrets/<timestamp>/:

  • GPG — gpg --export-secret-keys (primary + subkeys, ASCII-armoured, non-interactive) and ownertrust (the trust database, stored separately from the keys — without it GPG forgets which keys you vouch for after a restore).
  • SSH — rsync -a --delete ~/.ssh/ so rotated keys are removed, not just added.
  • Bitwarden — bw export --format encrypted_json. Requires an active BW_SESSION; the script runs bw unlock --raw if one isn't set.
  • pass — rsync -a --delete of ~/.password-store/ (or $PASSWORD_STORE_DIR) into the snapshot. Entries are already GPG-encrypted at rest by pass, so the copy is safe on the stick without LUKS. .git is excluded — snapshots are point-in-time, and a restore is a copy-back, not a clone.

A LAST-BACKUP file at the secrets root records the latest snapshot timestamp for a quick "what's current" check. Snapshots accumulate; prune old ones with rm -r secrets/<old-timestamp>/ when needed. Everything written is already secret-protected at the file level (GPG exports and SSH keys are passphrase-encrypted, the BW export is master-password-encrypted), so LUKS is defence in depth, not the only layer.

Systemd user services

A chezmoi run_onchange hook (home/run_onchange_systemd-user-units.sh.tmpl) reloads the user manager and re-enables the session services this repo wants running. It covers two groups:

  • chezmoi-managed sway-session units — kanshi, swayidle, sway-power-key, sway-suspend-key, sway-hibernate-key, battery-notify.timer, wlsunset, update-check.timer, backup-nag.timer, udiskie, mbsync.timer: unit files ship in ~/.config/systemd/user/ via chezmoi.
  • package-provided services (files in /usr/lib/systemd/user/) — mako (notifications), cliphist (clipboard history; runs wl-paste --watch cliphist store), waybar, pipewire/pipewire-pulse/ wireplumber (audio), gcr-ssh-agent.socket/gnome-keyring-daemon.socket/ p11-kit-server.socket (auth/keyring), com.nextcloud.desktopclient.nextcloud, mpris-proxy, thunar, and the per-account goimapnotify@<account>.service instances (one per mail account — see Mail sync).

thunar ships without an [Install] (D-Bus-activated), so thunar.service.d/install.conf adds one before enabling. The hook enables with --now, so services start immediately when run inside an active session and at the next login otherwise.

Some apps start via XDG autostart (handled by sway-xdg-autostart.target) from desktop files installed by their own packages — no manual configuration needed:

  • blueman-applet — from blueman
  • nm-applet — from network-manager-applet
  • bitwarden — from ~/.config/autostart/bitwarden.desktop

Kanshi manages output profiles (resolution, scale, position). Switch profiles manually with:

kanshictl switch <profile-name>
# profiles: mobile, docked-laptop-only, docked-external-only, docked-both-screens

Sway session (no display manager)

Sway is the only session. Boot lands on the getty; logging in on VT1 auto-execs sway via the XDG_VTNR = 1 guard in ~/.zprofile (home/dot_zprofile.tmpl). Other VTs give a plain shell for running labwc or a second sway. LightDM and the XFCE X11 session are retired: their one config hook, ~/.xprofile (Qt 2x scaling, sourced only by LightDM's Xsession wrapper), is gone from chezmoi. chezmoi does not delete targets it no longer manages, so remove the applied ~/.xprofile by hand, and remove lightdm + lightdm-gtk-greeter by hand too (never provisioned).

Env vars live in two layers, each with one job:

  • ~/.zprofile — all user/session env (PATH, EDITOR, the gcr SSH socket) plus the vendor-gated SWAY_UNSUPPORTED_GPU=true (LENOVO only, DMI sys_vendor checked at chezmoi-apply time). Without it wlroots refuses to start sway on the ThinkPad's NVIDIA GPU; with it, sway logs a permanent "unsupported GPU" warning that is expected noise. Login shells source it and sway's children inherit everything via the VT1 exec.
  • systemd user manager — env for user services (waybar, kanshi, mako). sway-systemd's /etc/sway/config.d/10-systemd-session.conf imports compositor vars (WAYLAND_DISPLAY, SWAYSOCK, XDG_CURRENT_DESKTOP) into the user manager and D-Bus activation once sway runs.

XDG_CURRENT_DESKTOP is deliberately sway:GNOME, not bare sway. Chromium and Electron safeStorage select their secret-store backend by desktop name; bare sway reads as unknown and falls back to basic_text, so Signal refuses to decrypt its keyring-encrypted database key. The GNOME token also makes the systemd XDG autostart generator match GNOME-only entries, so the gnome-keyring and at-spi autostarts are masked (the keyring is owned by gnome-keyring-daemon.socket, the a11y bus comes up via D-Bus activation) and nm-applet is pinned — its NotShowIn=GNOME would otherwise hide it. All four overrides are chezmoi-managed in home/dot_config/autostart/.

One caveat to know about: this env story relies on sway being launched from a login shell (the VT1 exec). If a session manager that starts sway without one ever comes back (greetd, SDDM), SWAY_UNSUPPORTED_GPU must move to /etc/environment (pam_env) first.

aerc

Terminal mail client, chezmoi-managed at home/dot_config/aerc/ (applied to ~/.config/aerc/). aerc.conf + binds.conf are plaintext; accounts.conf is GPG-encrypted (encrypted_private_accounts.conf.asc) since it holds account identities. No plaintext passwords are stored — each account pulls credentials from pass via source-cred-cmd / outgoing-cred-cmd (e.g. pass show email/<entry>).

Mail sync (isync/mbsync + goimapnotify)

mbsync (from isync) syncs IMAP mailboxes to local Maildirs that aerc reads. ~/.mbsyncrc is chezmoi-managed and GPG-encrypted (encrypted_private_dot_mbsyncrc.asc), one IMAPAccount/Channel pair per account, each pulling its password from pass via PassCmd.

goimapnotify watches each account over IMAP IDLE and runs mbsync <channel> on new/changed/deleted mail — near-instant sync instead of polling. Its per-account configs live at home/dot_config/imapnotify/, GPG-encrypted (encrypted_private_<account>.yaml.asc, applied to ~/.config/imapnotify/<account>.yaml). Each account runs as an instance of the package-provided goimapnotify@.service template (enabled by run_onchange_systemd-user-units.sh.tmpl). mbsync.timer (chezmoi-owned, at home/dot_config/systemd/user/) is a periodic fallback that runs mbsync -a every 15 minutes as a safety net in case an IDLE connection drops silently.

CI

Forgejo Actions workflow at .forgejo/workflows/ci.yml runs on push to main and on pull requests. A single lint job in an alpine:3.20 container:

  • chezmoi verify — chezmoi execute-template against home/ to confirm the source state parses.
  • templated shell syntax — renders every *.tmpl under home/ and pipes it to sh -n.
  • shellcheck — lints all tracked .sh files and chezmoi executable_ shell scripts (.py and .tmpl excluded). Currently set to --severity=error; tighten to warning once the pre-existing info-level nits are cleaned up.
  • yamllint — lints repo-owned ansible yaml (vendored collections under ansible/.ansible/ skipped).

Git hooks

Pre-commit hooks are managed by prek, with tool versions pinned via mise. Hooks include secret scanning (detect-secrets with a committed .secrets.baseline), private key detection, and standard hygiene checks (trailing whitespace, EOF, large files, YAML/TOML/ JSON validation, etc.).

mise install          # installs the pinned tool versions
mise exec -- prek install   # installs the prek git hooks

The .secrets.baseline file is committed to the repo. The hook only fails on secrets NOT in the baseline. To add a new safe-but-flagged value to the baseline:

uvx detect-secrets scan --baseline .secrets.baseline

To allowlist a single line without regenerating the baseline:

secret = "hunter2"  # pragma: allowlist secret