aboutsummaryrefslogtreecommitdiffstatshomepage
Commit message (Collapse)AuthorAgeFilesLines
* feat(suspend): re-enable suspend on s2idle, drop diagnostic scaffoldingLibravatar sommerfeld3 days11-51/+36
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | Confirmed root cause: this hardware's S3 (deep) firmware path triggers a fatal wake-from-suspend hang only on linux-hardened. INIT_ON_FREE + slab hardening + tighter locking turn a latent driver race that stock linux gets away with into an unrecoverable panic so early the journal isn't even flushed. mem_sleep_default=s2idle bypasses the BIOS S3 path entirely (s0ix is a pure-kernel low-power state) and suspends/resumes reliably under hardened. This is a widespread Lenovo S3 firmware issue across post-2018 ThinkPads (see Ubuntu T560, X1C9/10/11 reports). Lenovo themselves moved newer firmwares to s2idle-only. Not a linux-hardened bug per se; just hardened being a strict enough kernel to make the bug fatal. Keep: * mem_sleep_default=s2idle in etc/kernel/cmdline-linux-hardened.tmpl (only the hardened UKI; stock linux keeps unchanged shared cmdline) Revert (all the diagnostic / speculative scaffolding from the last few commits): * MODULES=(intel_lpss_pci) → MODULES=() — Arch wiki touchpad fix was not the cause here * nmi_watchdog=panic softlockup_panic=1 panic=10 — only needed to auto-reboot during diagnosis * no_console_suspend — diagnostic-only * etc/systemd/logind.conf.d/20-no-suspend.conf — masking workaround * sleep-target masking block in run_onchange_after_deploy-etc.sh.tmpl, replaced with a one-shot cleanup that removes any leftover /dev/null symlinks from systems that ran the previous version * systemd-pstore.service from systemd-units/system.txt — added only to catch the diagnostic panic * diagnose-suspend.sh helper (and its .gitignore/.chezmoiignore entries) * sway suspend → lock-session keybind workaround * power-menu.sh Suspend entry restoration * KEYBINDS.md docs
* fix(suspend): switch hardened to s2idle, keep console alive, archive pstoreLibravatar sommerfeld3 days2-1/+2
| | | | | | | | | | | | | | | | | | | | Previous attempt (early-loading intel_lpss_pci) did not fix the wake-from-suspend panic on linux-hardened. The journal of the failed boot ends cleanly at the last sync with no panic, oops, or even 'PM: suspend entry' message — the kernel dies so fast nothing is flushed, even with panic=10 + watchdog knobs. Three changes to make progress: * mem_sleep_default=s2idle: switch S3 'deep' (broken firmware path on Coffee Lake ThinkPads) to s2idle / s0ix. Many Lenovo machines only suspend reliably via s2idle; the stock linux kernel may be masking the issue elsewhere. * no_console_suspend: keep console alive across the suspend/resume cycle so the panic actually prints somewhere visible, instead of being eaten when the framebuffer goes dark. * systemd-pstore.service: archive /sys/fs/pstore/* to /var/lib/systemd/pstore/ on every boot, so the next panic (if EFI variables capture it) survives. Drop 'quiet' from hardened cmdline so console messages are visible.
* fix(suspend): load intel_lpss_pci from initramfs (Arch wiki touchpad fix)Libravatar sommerfeld3 days3-6/+5
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | Symptoms (Intel CPU + linux-hardened + blinking caps lock + hard hang on resume from S3) are a direct match for the Arch wiki entry: https://wiki.archlinux.org/title/Power_management/Suspend_and_hibernate#Touchpad_causes_a_kernel_panic_on_resume https://bbs.archlinux.org/viewtopic.php?id=231881 When intel_lpss_pci is loaded late (via udev after userspace is up), the touchpad/I2C controller it parents can be torn down by suspend before the module's resume callback is registered, leading to a NULL-deref panic during resume. The kernel never makes it far enough to flush logs — which matches our 'PM: suspend entry (deep)' being the last journal line. Fix: load intel_lpss_pci from the initramfs so it's available before the suspend/resume code path runs. Why this only bites linux-hardened: the hardening config enables INIT_ON_FREE, slab freelist hardening, page poisoning, and stricter pointer validation, which turn what's a silent UAF on stock linux into an immediate panic on hardened. Stock 'just works' by accident. Also drop the speculative init_on_free=0 from the hardened cmdline now that we have a targeted hypothesis. Keep nmi_watchdog=panic + softlockup_panic=1 + panic=10 as belt-and-braces: if this fix is wrong, the next hang will auto-reboot with a usable panic log in 'journalctl -b -1 -k' instead of needing the power button again.
* feat(suspend): hardened-only init_on_free=0 + hang-detection cmdlineLibravatar sommerfeld3 days4-2/+9
| | | | | | | | | | | | | | | | | | | | | | | | Split the hardened UKI cmdline off the shared etc/kernel/cmdline.tmpl so we can carry workarounds without poking the stock linux build. Daily-driving linux-hardened on this hardware has reliably hung on resume from S3: black screen, blinking caps-lock + power LED, only the power button helps. The kernel journal stops at 'PM: suspend entry (deep)' with nothing after, so the freeze is below the level where logs can flush — characteristic of a hard hang inside a device driver's suspend/resume callback rather than a userspace bug. linux-hardened defaults init_on_free=1, which zeroes pages on free. On Intel + iwlwifi/i915/nvme stacks this routinely surfaces latent UAFs as suspend hangs that are invisible on stock linux. Drop that knob to 0 for the hardened cmdline as the working hypothesis. Add nmi_watchdog=panic, softlockup_panic=1, panic=10 so if the next attempt still wedges, a stuck CPU self-panics and auto-reboots within ~10s, giving us a 'journalctl -b -1 -k' trace to look at instead of having to force-power-off blindly. Stock linux is untouched.
* feat(suspend): disable system suspend until hardened kernel resume issue is ↵Libravatar sommerfeld3 days5-21/+47
| | | | | | | | | | | | | | | | | | | | | | | fixed linux-hardened wedges on resume from S3 (NVMe/i915/iwlwifi driver UAF exposed by INIT_ON_FREE + slab hardening). Until root-caused, take suspend off the table while keeping lock + DPMS intact. - etc/systemd/logind.conf.d/20-no-suspend.conf: lid close, suspend key, hibernate key all map to 'lock'; IdleAction=ignore (swayidle drives DPMS+swaylock independently). - run_onchange_after_deploy-etc.sh.tmpl: mask sleep.target, suspend.target, hibernate.target, hybrid-sleep.target, suspend-then-hibernate.target via /etc/systemd/system -> /dev/null symlinks. Catches 'systemctl suspend' from any source. - dot_config/sway/config: XF86Sleep and system-mode 's' now run loginctl lock-session instead of systemctl suspend. - dot_config/sway/executable_power-menu.sh: drop Suspend entry. - KEYBINDS.md: reflect new behaviour. To re-enable later: remove the logind drop-in + symlink loop, then sudo systemctl daemon-reload.
* fix(suspend): only inhibit for SSH-spawned zellij sessionsLibravatar sommerfeld3 days3-13/+37
| | | | | | | | | | | | | | | | | | | A local zellij session (sway terminal, attended) shouldn't keep the laptop awake — that's the user actively in front of the machine, and normal suspend behaviour should apply. Only zellij sessions that were spawned from an SSH context need the persistent inhibit, so detach + disconnect leaves the host awake until the session ends. Use /proc/<pid>/environ to detect SSH-spawned zellij: the daemonised zellij server is exec'd by the client and Linux preserves the exec-time environment for the life of the process, so SSH_CONNECTION= survives the SSH session closing. Walk every running `zellij` pid; hold the lock as long as at least one of them has SSH_CONNECTION in its environ. The .path unit still fires on every zellij socket creation, but if no SSH-spawned zellij exists the watcher exits immediately and the service stops with no harm done — a couple of cheap process spawns per local session start, no inhibitor side-effects.
* fix(iwd): revert MAC randomization — broke DHCPLibravatar sommerfeld3 days1-23/+0
| | | | | | | | | | | | | | | | | | `AddressRandomization=network` made iwd present a per-SSID random MAC to every Wi-Fi network. On networks that pin DHCP leases or 802.1X access to a specific hardware MAC (corporate Wi-Fi, routers with DHCP reservations, MAC-filtered networks) this means iwd associates fine but DHCP never completes — the new MAC is unknown to the upstream. The privacy gain is marginal when the user only connects to a small set of known APs anyway, and the cost (no IP on a familiar network) is much worse than the threat model justified. Drop the override entirely; iwd's defaults (permanent MAC, no IP config — systemd-networkd remains the IP-layer authority via etc/systemd/network/30-wifi-bond0.network) match what we actually want. If we want privacy MAC again later, the right place is a systemd .link file with MACAddressPolicy=random, applied per-interface, not iwd-wide.
* feat(suspend): hold inhibit lock while any zellij session existsLibravatar sommerfeld3 days5-5/+65
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | The SSH-shell inhibitor in dot_zprofile is bound to the lifetime of the login shell, so it disappears the moment the user detaches a zellij session and disconnects — defeating the whole point of using zellij for persistent remote work. Add a user-scope path+service+watcher trio that ties the inhibit lock to the existence of zellij sessions instead: - dot_local/bin/executable_zellij-inhibit-watcher Polls `zellij list-sessions --short` every 15s, exits when none remain. Override poll interval via $ZELLIJ_INHIBIT_POLL. - dot_config/systemd/user/zellij-inhibit-suspend.service Wraps the watcher in `systemd-inhibit --what=sleep:idle:handle-lid-switch --mode=block`. When the watcher exits, the service stops and the lock is released. - dot_config/systemd/user/zellij-inhibit-suspend.path Activates the service whenever $XDG_RUNTIME_DIR/zellij becomes non-empty (i.e. zellij creates its first session socket). Re-fires on every empty→non-empty transition. Enable via systemd-units/user.txt (the .path unit; the service is on-demand). The existing SSH-shell inhibitor is kept as a backstop for non-zellij remote sessions and is now documented as such. VM (nix/vm.nix) deliberately not updated: the Ubuntu remote-dev VM never suspends, so the inhibit machinery would be inert there.
* feat(zsh): inhibit suspend while an SSH session is activeLibravatar sommerfeld3 days1-0/+17
| | | | | | | | | | | | | | | A remote session is useless if the laptop suspends mid-command, but logind doesn't suppress lid-close or idle-suspend for SSH sessions on its own — you have to hold an explicit inhibitor lock. When $SSH_CONNECTION is set, re-exec the login shell under `systemd-inhibit --what=sleep:idle:handle-lid-switch --mode=block` so the lock is bound to the shell's lifetime: it covers swayidle, logind's HandleLidSwitch, and any other consumer that respects inhibit locks, and it's released the moment the SSH session ends. A guard env var prevents recursion if the user nests a login shell inside the wrapped one (e.g. `exec zsh -l`).
* feat(suspend): bounce snx-rs around system sleepLibravatar sommerfeld3 days2-0/+48
| | | | | | | | | | | | | | | | | | | | | | snx-rs (Check Point VPN) doesn't notice that its tunnel died during suspend: the IKE keepalive is interrupted and the SAML cookie may expire, but the daemon happily sits on dead sockets after resume. `snxctl status` keeps reporting "Connected" while no traffic actually flows, so the user has to manually disconnect+reconnect. Install an /etc/systemd/system-sleep/ hook that stops the user-scope snx-rs.service before suspend and starts it on resume. The tunnel is left disconnected after resume; the waybar toggle (or any `snxctl connect`) re-establishes it, going through SAML only if the cached cookie has actually expired. The hook enumerates logged-in users via loginctl and skips any that don't have snx-rs.service enabled, so it's a no-op on machines that don't use the VPN. Also teach run_onchange_after_deploy-etc.sh.tmpl to install files under etc/systemd/system-sleep/ with mode 0755 (systemd ignores sleep hooks that aren't executable).
* feat(podman): switch rootless storage driver to btrfsLibravatar sommerfeld3 days4-4/+19
| | | | | | | | | | | | | | | | | | | | | | | | | fuse-overlayfs is dog-slow on `podman commit` (and noticeably slower than native overlay/btrfs for layer extraction in general) because every read/write round-trips through a FUSE daemon. The kernel overlay driver does not support btrfs as a lowerdir, so on a btrfs root fs the choices were: - fuse-overlayfs (slow, but works) - btrfs (native subvolume + CoW snapshot per layer; fast) Switching graph drivers is destructive — the on-disk layout is incompatible, so a one-time `podman system reset --force` is required. A migration helper script lives at the repo root (gitignored, chezmoiignored) that snapshots stateful containers, exports images and volumes, runs the reset, and restores everything on the new driver. Drops fuse-overlayfs from meta/base.txt — no longer needed and pulls in libfuse3 transitively for nothing. (Flatpak still depends on it for its own sandbox; pacman won't actually uninstall the binary while flatpak is around — that's fine.) VM (nix/vm.nix) is unaffected: it sets its own storage.conf inline with driver=overlay since its rootfs is ext4.
* fix(hardened): restore podman compatibility on linux-hardenedLibravatar sommerfeld3 days2-0/+8
| | | | | | | | | | | | | | | | | Two breakages observed on first linux-hardened boot: 1. `podman run` failed because linux-hardened sets kernel.unprivileged_userns_clone=0 by default (stock linux: 1). Rootless podman requires unprivileged user namespaces. Restoring the stock-kernel default via sysctl — this is a documented hardened knob meant to be flipped back if you actually use rootless containers. No-op on stock kernel. 2. "kernel does not support overlay fs: 'overlay' is not supported over btrfs". Kernel overlayfs cannot use a btrfs subvolume as lowerdir; podman needs fuse-overlayfs as the user-mode shim. ~10-30% slower I/O than native overlay but works correctly and is the upstream recommendation for btrfs-backed rootless storage.
* docs(bootstrap): also suggest fallback UKI EFI entriesLibravatar sommerfeld3 days1-1/+7
| | | | | | | | Pair each default UKI entry with its fallback so the boot order list mirrors the four UKIs mkinitcpio produces. Fallback entries are optional — UEFI firmware menus can usually pick UKIs from /EFI/Linux/ directly — but having named entries lets you reorder / --bootnext them without dropping into the firmware menu.
* Revert "refactor(boot): drop linux-hardened-fallback UKI"Libravatar sommerfeld3 days1-1/+4
| | | | | | | | | Keeping the fallback after all — leaves the door open to dropping the stock 'linux' package entirely once linux-hardened is proven as a daily driver. Without hardened-fallback, that future single-kernel config would have zero autodetect recovery path. This reverts commit c0c9183.
* refactor(boot): drop linux-hardened-fallback UKILibravatar sommerfeld3 days1-4/+1
| | | | | | | | | Stock linux-fallback already covers the 'autodetect missed a module' recovery scenario, regardless of which kernel you tried to boot. hardened being opt-in means a hardened-default failure naturally falls back to stock — no need for hardened-fallback as a second safety net. Saves ESP space and mkinitcpio regen time on each linux-hardened update.
* docs(bootstrap): mention optional linux-hardened EFI entryLibravatar sommerfeld3 days1-0/+4
| | | | | | The hardened kernel ships as a parallel UKI; document its efibootmgr registration alongside the stock one. Stock stays default-boot; hardened is selected on demand (efibootmgr --bootnext or firmware menu).
* feat(sandbox): bwrap wrappers for mpv, yt-dlp, streamlinkLibravatar sommerfeld3 days5-0/+75
| | | | | | | | | | | | | | | | | | | | | | | | | These three tools are the native (non-flatpak) network parsers in the install set — every other internet-facing app is already flatpak'd. The threat model is a RCE in a subtitle/extractor/muxer that walks $HOME looking for SSH keys, GPG keyring, pass store, cloud tokens, etc. Approach (defence in depth, not full sandboxing): - bwrap --bind / / keeps Wayland, PipeWire, DBus, GPU, hwaccel and all config files working transparently. - --tmpfs over known-sensitive dirs (.ssh, .gnupg, .password-store, .config/gh, .config/op, .aws, .local/share/keyrings) blanks them from the sandbox view; a compromised parser literally cannot see them. - inner PATH stripped of ~/.local/bin so streamlink's spawn of `mpv` resolves to /usr/bin/mpv and does not re-enter the sandbox. - --die-with-parent + --new-session for tidy lifecycle. - Escape hatch: SANDBOX=0 mpv ... bypasses for one invocation. - Graceful degradation if bwrap is missing (warns and execs anyway). bubblewrap added explicitly to meta/base.txt (was implicit via flatpak). Wrappers in ~/.local/bin shadow /usr/bin via dot_zprofile:15 PATH order. Not symlinked into the Ubuntu VM (nix/vm.nix does not touch ~/.local/bin), which is fine: those tools on the headless VM don't need sandboxing.
* feat(boot): add linux-hardened as parallel UKILibravatar sommerfeld3 days2-0/+21
| | | | | | | | | | | | | | | | | | | | Installs linux-hardened + linux-hardened-headers alongside the stock linux kernel. Stock kernel remains the default; linux-hardened is opt-in via efibootmgr --bootnext after the EFI entry is registered (one-time host-side step, documented in the preset). After first 'just pkg-apply', mkinitcpio auto-builds /boot/EFI/Linux/arch-linux-hardened.efi from the new preset (sharing etc/kernel/cmdline.tmpl with the stock UKI — same LUKS root, no kernel-specific cmdline knobs). Host-side EFI entry registration: sudo efibootmgr --create --disk /dev/nvme0n1 --part 1 \ --label 'Arch Hardened' --loader '\\EFI\\Linux\\arch-linux-hardened.efi' Roll back any time by removing both packages and the preset file; the stock kernel and its UKI are untouched.
* feat(iwd): per-SSID MAC randomisationLibravatar sommerfeld3 days1-0/+23
| | | | | | | | | | | AddressRandomization=network: iwd generates a deterministic per-SSID random MAC. Hardware MAC is never exposed on Wi-Fi; reconnects to the same network reuse the same MAC, so DHCP leases, WPA-EAP creds and captive portals stay stable. EnableNetworkConfiguration=false keeps systemd-networkd authoritative for IP — the existing 30-wifi-bond0.network setup is unaffected and the wlan interface still gets enslaved into bond0.
* feat(polkit): restrict systemd + udisks system actions to active local sessionsLibravatar sommerfeld3 days2-0/+26
| | | | | | | | | | | | | | | | Two narrow defence-in-depth rules: - 52-systemd-local-only: org.freedesktop.systemd1.* requires both subject.local and subject.active. Wheel-via-sudo-rs is on a different path (sudoers) and is not affected. Stops a non-active or remote polkit caller from start/stop/restart of system units. - 53-udisks-system-mount: filesystem-mount-system and modify-system require subject.active. The everyday USB auto-mount path uses filesystem-mount (no -system suffix) and is unaffected. Audited against current workflow (virt-manager, networkctl, USB mount, bluetoothctl, fwupdmgr) — none of these break.
* feat(sysctl): kernel info-disclosure + ICMP/IPv6 RA hardeningLibravatar sommerfeld3 days1-1/+39
| | | | | | | | | | | | | | | | | | | | | Adds standard KSPP-style sysctl hardening that does not interfere with the existing dev workflow: - kptr_restrict=2, unprivileged_bpf_disabled=1, bpf_jit_harden=2 - kexec_load_disabled=1 (no kexec in use) - fs.suid_dumpable=0 - ICMP broadcast/bogus-error ignores - tcp_timestamps=0 (BBR+cake do not need RFC1323 timestamps) - IPv6 RA disabled at kernel layer (systemd-networkd is authoritative) - explicit tcp_syncookies=1 Drops 'kernel.yama.ptrace_scope = 0' so the kernel default 1 (parent only) applies. Debugging own builds via 'gdb ./a.out', 'lldb -- ./bin', 'rust-gdb' still works; only attach-by-PID now needs sudo, accepted trade-off. Intentionally kept dev-permissive: kernel.sysrq=1, kernel.dmesg_restrict=0, kernel.perf_event_paranoid=-1
* chore(nix): flake.lock update (home-manager, nixpkgs, tuicr)Libravatar sommerfeld3 days1-9/+9
|
* fix(nix): tuicr switched to packages.${system}.default schemaLibravatar sommerfeld3 days1-3/+1
| | | | | | | | Upstream tuicr commit 5b19712 migrated from the legacy `defaultPackage.<system>` flake output to the standard `packages.<system>.default`, which broke `nix-update` with: error: attribute 'defaultPackage' missing
* fix(nftables): waydroid DHCP/DNS ingress, drop manual NAT tableLibravatar sommerfeld9 days1-19/+9
| | | | | | | | | | Mirror the libvirt pattern by accepting DHCP+DNS on waydroid0 so the Android container's DhcpClient can lease an IP from dnsmasq. Remove the manual ip nat MASQUERADE table: waydroid-container installs its own MASQUERADE rule via iptables-nft compat, so the explicit table is redundant (and was clobbering anything else in ip nat via the destroy table).
* fix(sway): disable shortcut inhibitor for waydroid windowsLibravatar sommerfeld9 days1-0/+1
|
* fix(nftables): add MASQUERADE for waydroid0Libravatar sommerfeld9 days1-3/+19
| | | | | | | waydroid-container ships only the iptables-legacy code path for adding its POSTROUTING MASQUERADE; on a host with pure nftables the rule never lands and the Android container has no outbound NAT. Declare it explicitly in our nftables.conf for determinism.
* Revert "fix(sysctl): enable net.ipv4.ip_forward for NAT bridges"Libravatar sommerfeld9 days1-5/+0
| | | | This reverts commit eca1a71fc486690489f7aef671d7beccc2ec3f25.
* fix(sysctl): enable net.ipv4.ip_forward for NAT bridgesLibravatar sommerfeld9 days1-0/+5
| | | | | | | waydroid (and libvirt with finicky guests) need the host to route between their NAT bridge and the upstream NIC. libvirtd usually enables this on demand but it doesn't persist, so the container has no internet on a fresh boot until something else flips the bit.
* fix(net): positive-match physical NICs into bond0Libravatar sommerfeld9 days1-14/+9
| | | | | | | | | | Name= negation list was failing in practice — veth/waydroid interfaces were still being enslaved into bond0, taking down host networking. Switch to positive matching: Path=pci-*|platform-* AND Name=en* AND Type=ether. Virtual interfaces (veth, virbr, waydroid0, docker0, ...) have no udev ID_PATH and never start with 'en', so they're cleanly excluded by the AND of all three keys.
* fix(net): keep waydroid0 out of bond0, allow it through nftablesLibravatar sommerfeld9 days2-2/+10
| | | | | | | | | systemd-networkd's Type=ether matcher was enslaving waydroid0 into bond0 the moment 'waydroid session start' ran, taking down the host's default route. Mirror the libvirt/docker negation pattern. Also mirror the existing virbr0 forward accepts for waydroid0 so the Android container can actually reach the internet through MASQUERADE.
* feat(tuicr): side-by-side diff, mouse, space leaderLibravatar sommerfeld10 days1-0/+3
|
* feat(tuicr): configure gruvbox-dark themeLibravatar sommerfeld10 days2-0/+4
| | | | | | Add dot_config/tuicr/config.toml with theme = "gruvbox-dark". Symlinked from nix/vm.nix per the symlink invariant so the same config applies on both host (via chezmoi) and VM (via home-manager).
* fix(ssh): make agent.sock symlink concurrent-connection-safeLibravatar sommerfeld10 days2-13/+32
| | | | | | | | | | | | | | | | | Previously every new login retargeted ~/.ssh/agent.sock to its own per-connection forwarded socket. That broke a multi-connection setup when the most-recent connection (which 'won' the symlink) dropped: all surviving connections' panes would point at a dead socket until a fresh login from a surviving connection re-ran zprofile. zprofile: only retarget when the existing symlink target is dead (sshd unlinks the per-connection socket on disconnect, so [[ -S ]] on the resolved path is a reliable liveness probe). First connection seeds the symlink, subsequent logins keep using it. ssh-agent-refresh: scan /tmp/ssh-*/agent.* for any live forwarded socket and retarget to the first that responds to ssh-add. Lets the surviving connection recover without waiting for a new login shell.
* feat(zsh): recover Arch site-functions + HELPDIR after removing system zshLibravatar sommerfeld10 days1-3/+18
| | | | | | | | | | | | | | | | Switching to nix's zsh on the Arch host left two functional gaps the Arch zsh package used to fill: 1. /usr/share/zsh/site-functions in fpath: pacman, paru, systemctl, journalctl, flatpak, docker, kubectl, makepkg etc. drop their completions there. nix zsh's compiled-in fpath doesn't include /usr/share so we lose all of them silently. Added that path (and vendor-completions for the VM's apt-installed completions) to the existing fpath loop, guarded by [[ -d ]]. 2. HELPDIR for the run-help / help-alias machinery: needed so 'help cd' etc. find the per-builtin help docs. Pick the first existing version dir, preferring nix-profile so it matches the running zsh version.
* fix(nix,zsh): tuicr flake schema + restore XDG_DATA_DIRSLibravatar sommerfeld10 days3-2/+132
| | | | | | | | | | | | | tuicr's upstream flake uses the legacy 'defaultPackage.<system>' output schema, not 'packages.<system>.default' — fixes the home-manager switch error 'attribute packages missing' at nix/flake.nix:28. zsh: removing the system zsh package took /etc/zsh/zprofile with it, which used to 'source /etc/profile' and pull in /etc/profile.d/*.sh (flatpak.sh, nix.sh, etc.). Reconstruct XDG_DATA_DIRS in dot_zprofile defensively, including per-user + system flatpak exports + nix-profile share, so 'flatpak update' stops warning and desktop entries from flatpak/nix-installed apps work in launchers (fuzzel).
* fix(sway): propagate PATH / GPG env into systemd --user + dbusLibravatar sommerfeld10 days1-2/+11
| | | | | | | | | | | | Waybar (and other user services) was inheriting the bare pre-login PATH from systemd --user, missing ~/.nix-profile/bin and ~/.local/bin. Modules that call nix-provisioned binaries (pass, python3, ncat from common.nix) silently picked up system copies instead — symptom was waybar showing different output from the same script when invoked manually (thunderbird tb-unread.sh, wifi-status.sh). Also propagate GNUPGHOME and GPG_TTY so pinentry / pass-otp inside user services behave the same as in the interactive shell.
* fix(ssh): stabilise forwarded ssh-agent socket across reconnectsLibravatar sommerfeld10 days2-3/+40
| | | | | | | | | | | | | | | | | | | | Forwarded SSH_AUTH_SOCK lives at /tmp/ssh-XXX/agent.NNN — a per-connection path that disappears on disconnect, leaving every long-running zellij pane (and its children: claude-code, nvim, …) pointing at a dead socket. Reattaching after reconnect doesn't help: the env was captured when zellij first started. Fix: maintain ~/.ssh/agent.sock as a symlink, re-aimed at the live forwarded socket on every login (zprofile). Export the stable path so processes inherit a value that survives reconnects — git fetch / commit signing keep working in re-attached zellij panes with zero per-pane re-export. Adds 'ssh-agent-refresh' helper for transitional panes still holding the dead per-connection path: re-exports SSH_AUTH_SOCK to the stable symlink and validates with ssh-add -l. Already-running children (claude-code) must still be restarted since env is inherited, not observed.
* docs(rules): document nix/vm.nix symlink invariantLibravatar sommerfeld10 days1-0/+11
| | | | | | | | Make it explicit that adding, removing, or renaming any chezmoi-deployed config requires a matching update to nix/vm.nix's xdg.configFile / home.file blocks when the corresponding binary is in common.nix. Drives the audit performed in the previous commit.
* feat(nix): audit + expand vm xdg.configFile symlink coverageLibravatar sommerfeld10 days1-1/+38
| | | | | | | | | | | | | | | | | | The VM doesn't run chezmoi, so every config the host gets via chezmoi must reach the VM via a nix symlink. Audit found gaps for tools whose binary IS in common.nix but whose dot_config tree was unlinked: bat, lsd, yazi, ripgrep, fd, wget, npm, ipython, gdb, clangd, ccache Plus the new tuicr claude-code skill (under ~/.claude/skills/tuicr/, NOT ~/.config — uses home.file instead of xdg.configFile). Reorganises the block by category and adds an INVARIANT comment pointing at the rule in .github/copilot-instructions.md. GUI/wayland-only tools (sway/mako/waybar/fuzzel/mpv/zathura/etc) stay unlinked: the VM is headless.
* feat(claude): add tuicr skill (zellij-adapted)Libravatar sommerfeld10 days2-0/+212
| | | | | | | | | | | | | Adapts upstream agavra/tuicr's tmux-based wrapper to zellij: - Detects $ZELLIJ instead of $TMUX - Spawns child via 'zellij action new-pane --floating' (or --direction with TUICR_PANE_MODE=split / TUICR_SPLIT_DIR=down) - Replaces tmux 'wait-for' with a mkfifo / printf done sentinel - Uses 'pgrep -x tuicr' for the cross-pane already-running probe (zellij has no list-panes equivalent) SKILL.md updated for zellij keybinds and a nix install path note.
* feat(nix): add tuicr from upstream flake to common profileLibravatar sommerfeld10 days2-1/+15
| | | | | | tuicr (TUI git-change reviewer) isn't packaged in nixpkgs, so pull it as a flake input with an overlay exposing pkgs.tuicr. The companion claude-code skill lives in dot_claude/skills/tuicr/ (separate commit).
* fix(just): chsh to nix-managed zsh after nix-switchLibravatar sommerfeld10 days1-0/+14
| | | | | | | | | | | | | | zsh now lives in nix/common.nix instead of meta/base.txt. Removing the pacman zsh package leaves /etc/passwd dangling at /usr/bin/zsh, so new login terminals die with 'shell not found'. Mirror the chsh logic from nix/bootstrap.sh (which only runs on the VM during first-time provisioning) into the nix-switch recipe so every `just sync` / `just init` re-asserts the login shell — and the host gets the same treatment as the VM. Idempotent: skips when the shell already matches, skips when ~/.nix-profile/bin/zsh is missing (pre-bootstrap state).
* refactor(pkg): drop provider-resolution fallback in mark_explicitLibravatar sommerfeld10 days1-9/+4
| | | | | | | | | Now that meta/*.txt is conventionally required to list installed package names (not virtual providers — see preceding commit dropping ttf-font-awesome in favour of the already-declared otf-font-awesome), the intersection with pacman -Qq is unnecessary. Failing loudly on a virtual-provider entry is actually useful: it surfaces a data-entry mistake instead of silently masking it.
* chore(pkg): drop redundant ttf-font-awesome from base.txtLibravatar sommerfeld10 days1-1/+0
| | | | | | | ttf-font-awesome is a virtual provided by otf-font-awesome (already declared on the line above) — paru resolves the former to the latter, so listing both adds nothing and confuses the mark-explicit step in pkg-apply.
* fix(pkg): skip mark-explicit for packages resolved to a different providerLibravatar sommerfeld10 days1-1/+9
| | | | | | | | | | paru may resolve a declared name to a provider with a different package name (e.g. ttf-font-awesome -> otf-font-awesome). Calling `pacman -D --asexplicit` on the declared name then fails with 'could not find or read package' and aborts the recipe. Intersect the declared list with `pacman -Qq` before bumping reasons; names not present in the local DB are silently skipped.
* fix(pkg): mark declared packages as explicit on applyLibravatar sommerfeld10 days1-2/+16
| | | | | | | | | | | | | | | `paru -S --needed` skips packages already on disk, so anything pulled in transitively first and *later* added to meta/*.txt stays marked as 'installed as a dependency' in the local pacman DB — and keeps showing up under `pacopt`. After each install pass, force the declared set to 'explicitly installed' via `pacman -D --asexplicit`. This treats meta/*.txt as the source of truth for install reason: anything listed there is explicit, anything else is a transitive dep. Idempotent on already-explicit packages (pacman just prints 'install reason is already explicit', which we discard).
* feat(pkg): declare btrfs-progs in base.txtLibravatar sommerfeld10 days2-6/+7
| | | | | | | | Root filesystem is btrfs; the userspace tools are needed for routine maintenance (scrub, balance, subvolume management) and inspection (`btrfs filesystem usage` — the only honest reporter on btrfs since plain `df` doesn't account for metadata/profiles/unallocated). Also used by the mkinitcpio btrfs hook at boot.
* feat(nix): add ipython to common profileLibravatar sommerfeld10 days1-0/+1
| | | | | | Interactive python REPL. Uses python3Packages.ipython so only the `ipython` binary lands on PATH — no stray system `python`/`python3`, preserving the 'tools managed by uv per-project' policy in common.nix.
* feat(pkg): declare linux + dosfstools in base.txtLibravatar sommerfeld10 days1-0/+2
| | | | | | | | | linux: previously installed only as an Optional Dep of base. Promote to an explicit declaration so it stops showing up under pacopt. dosfstools: required by udisks2 (and libblockdev-fs) for mounting FAT volumes — USB sticks, the EFI system partition, etc. Universally useful on any desktop install.
* feat(zsh): enrich pacopt with reverse-optdep infoLibravatar sommerfeld10 days1-1/+35
| | | | | | | | | | | Promote pacopt from a plain alias to a function. In addition to listing packages that remain installed solely as someone's optional dependency, each package is now annotated with its parent(s) and the upstream reason text from the parent's Optional Deps field. Implementation is pacman-only (no expac): one awk pass over 'pacman -Qi' builds a reverse index of every (parent, optdep, reason) edge in the local DB, then per leaf package the index is filtered for matching deps.