<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotfiles/dot_local, branch master</title>
<subtitle>My linux config and rc files</subtitle>
<id>https://git.sommerfeld.dev/dotfiles/atom/dot_local?h=master</id>
<link rel='self' href='https://git.sommerfeld.dev/dotfiles/atom/dot_local?h=master'/>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/'/>
<updated>2026-06-05T10:06:02Z</updated>
<entry>
<title>Move more host tooling to Nix</title>
<updated>2026-06-05T10:06:02Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-06-05T10:06:02Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=b0e83e2ee3fc328e55119ee7c1f09ad7ed20a635'/>
<id>urn:sha1:b0e83e2ee3fc328e55119ee7c1f09ad7ed20a635</id>
<content type='text'>
</content>
</entry>
<entry>
<title>fix(systemd,scripts): unhardcode /usr/bin paths for nix-migrated tools</title>
<updated>2026-06-05T10:05:58Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-06-05T10:05:58Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=24d832de3ad0bf749fd63fc5239a57371b2fdc3e'/>
<id>urn:sha1:24d832de3ad0bf749fd63fc5239a57371b2fdc3e</id>
<content type='text'>
The chezmoi-owned user units and ~/.local/bin wrapper scripts called
the migrated tools by absolute /usr/bin/ path. After the move to nix,
those binaries live under ~/.nix-profile/bin (no /usr/bin alias).

systemd user units: drop the /usr/bin/ prefix on cliphist-{text,image}
(wl-paste), inhibridge, swayidle, swayrd, waybar, and the inner wob
in wob.service (outer /usr/bin/sh stays, sh is system). systemd
resolves bare names through the unit's inherited PATH, which includes
~/.nix-profile/bin via hm-session-vars.

dictate: default_model now points at
~/.nix-profile/share/whisper-cpp-models/ggml-base.bin (overridable via
$WHISPER_MODEL). Header rewritten to mention nix instead of AUR.

yt-dlp / streamlink wrappers: pass $HOME/.nix-profile/bin/&lt;tool&gt; to
_sandbox-net-parser so the bwrap-sandboxed binary is resolved
explicitly (the wrappers shadow PATH lookup inside their own
~/.local/bin so re-entry would loop).
</content>
</entry>
<entry>
<title>refactor(flatpak): route mpv and thunderbird via flatpak; drop system pkgs</title>
<updated>2026-05-29T10:18:16Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-05-29T10:18:16Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=cd1c92b746a51a6994281f34a5f773c37d1d2dfe'/>
<id>urn:sha1:cd1c92b746a51a6994281f34a5f773c37d1d2dfe</id>
<content type='text'>
Both org.mozilla.thunderbird and io.mpv.Mpv are already installed via
flatpak, but several places still launched the system binaries (because
they were in PATH). Worse, `mpv` was kept on the host *only* for the
streamlink-launches-mpv path, and `thunderbird` was being pulled in as
a hard dep of external-editor-revived even though it was never the
mailer actually used. Untangle both.

Thunderbird
-----------
* dot_config/sway/executable_tb-toggle.sh,
  dot_config/sway/executable_tb-autostart.sh:
    swap `thunderbird` → `flatpak run org.mozilla.thunderbird`. The
    `app_id` matcher in sway config already targets the flatpak id, so
    the scratchpad-stash and Super+t toggle keep working unchanged.
* etc/pacman.conf:
    add `AssumeInstalled = thunderbird=999.0-1`. external-editor-revived
    (AUR) hard-depends on `thunderbird`; this satisfies the dep without
    installing the package. Run `sudo pacman -Rns thunderbird` after
    deploy to remove the now-unneeded system binary.
* meta/base.txt: document the AssumeInstalled trick next to the
  external-editor-revived entry.

mpv
---
* dot_config/streamlink/config: `player=mpv` → `player=flatpak run
  io.mpv.Mpv`. The flatpak already pulls in our ~/.config/mpv via the
  read-only filesystem override (see
  run_onchange_after_deploy-flatpak-overrides.sh.tmpl), so behavior is
  unchanged.
* dot_local/bin/executable_linkhandler: same swap for inline video URLs.
* dot_local/bin/executable_mpv: deleted. The wrapper only existed to
  bwrap /usr/bin/mpv into _sandbox-net-parser; flatpak's own sandbox
  supersedes that.
* dot_local/bin/executable__sandbox-net-parser,
  dot_local/bin/executable_streamlink: comment refresh — mpv is no
  longer one of the tools this wraps, and the streamlink wrapper now
  forwards to the flatpak player rather than nested-bwrap caveats.
* meta/base.txt: drop `mpv` from the host package list and update the
  surrounding comment.

README.md: refresh the media row of the stack table to match.

On-host steps:

    chezmoi apply -v
    sudo pacman -Syu                          # picks up AssumeInstalled
    sudo pacman -Rns thunderbird mpv          # safe now
    flatpak install -y flathub org.mozilla.thunderbird io.mpv.Mpv
    swaymsg reload                            # pick up new tb scripts
</content>
</entry>
<entry>
<title>refactor(suspend): gate suspend on AC, drop bespoke zellij inhibit</title>
<updated>2026-05-29T10:18:15Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-05-29T10:18:15Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=ec3734c5ef9fcfe97c21cd19f198ec779ab5f052'/>
<id>urn:sha1:ec3734c5ef9fcfe97c21cd19f198ec779ab5f052</id>
<content type='text'>
New, simpler suspend policy:

  AC plugged in   -&gt; never auto-suspends (lid close ignored, idle no-op)
  On battery only -&gt; lid close suspends, swayidle suspends at 30 min idle

This replaces the SSH/zellij-aware inhibit machinery with a rule that
matches the user's mental model: if you don't want the machine to
sleep, plug it in. Long-running tasks (builds, downloads, SSH
sessions, headless services) just need AC.

Changes:

* etc/systemd/logind.conf.d/20-lid-ac.conf: set
  HandleLidSwitchExternalPower=ignore so logind itself handles the AC
  case at the source. No userspace daemon, no race, no rate-limit risk.
* dot_local/bin/on-battery-suspend: tiny POSIX wrapper that exits 0
  when any /sys/class/power_supply/{AC,ADP}*/online == 1, else execs
  `systemctl suspend`.
* dot_config/systemd/user/swayidle.service: add
  `timeout 1800 on-battery-suspend`. Idle suspend now exists, but only
  when on battery.
* Delete zellij-inhibit-suspend.{path,service} + watcher script and
  remove the entry from systemd-units/user.txt. The .path
  re-trigger storm bug is moot because the whole mechanism is gone.

Manual suspends (sway XF86Sleep keybind, sway power submode `s`,
`systemctl suspend` over SSH) still always work regardless of AC --
explicit user intent wins.

Also drop /migrate-podman-to-btrfs.sh from .gitignore; the one-off
migration script has been deleted now that the user has switched their
podman storage to the btrfs driver.

On-host steps to apply:

  chezmoi apply -v
  systemctl --user daemon-reload
  systemctl --user reset-failed zellij-inhibit-suspend.service zellij-inhibit-suspend.path || true
  systemctl --user stop zellij-inhibit-suspend.path zellij-inhibit-suspend.service || true
  systemctl --user disable zellij-inhibit-suspend.path || true
  systemctl --user restart swayidle.service
  # logind drop-in is reloaded automatically by the etc deploy script.

Verify:

  systemctl status systemd-logind | grep -i lid
  loginctl show-session $XDG_SESSION_ID | grep -i lid
  # Unplug AC -&gt; close lid -&gt; should suspend.
  # Plug AC   -&gt; close lid -&gt; nothing happens.
</content>
</entry>
<entry>
<title>fix(suspend): make zellij inhibit watcher resilient to local-only sessions</title>
<updated>2026-05-29T10:18:15Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-05-29T10:18:15Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=3848d890979bd5fafae92054f85061edf10edff3'/>
<id>urn:sha1:3848d890979bd5fafae92054f85061edf10edff3</id>
<content type='text'>
The previous watcher exited immediately whenever no SSH-spawned zellij
was present. That caused a start-rate-limit storm:

  .path triggers service (zellij dir non-empty)
   -&gt; watcher exits because no SSH zellij
   -&gt; service stops
   -&gt; .path retriggers (zellij dir still non-empty)
   -&gt; ... 5 starts in 10s, systemd stops the path unit
   -&gt; no inhibitor ever again, even after you SSH in

Restructure so the watcher stays alive for the entire zellij socket
directory lifetime and acquires/releases its own systemd-inhibit lock
dynamically based on SSH-zellij presence:

* Watcher now polls and exits only when the zellij socket dir is empty,
  matching the .path's trigger condition so it never re-fires while
  zellij is alive.
* systemd-inhibit removed from ExecStart - watcher self-inhibits via a
  child 'systemd-inhibit ... sleep infinity' it can terminate on demand.
* StartLimitIntervalSec=0 on the service as belt-and-braces against
  any future regression of the cycle.

Recovery from the rate-limit hit:
  systemctl --user reset-failed zellij-inhibit-suspend.service zellij-inhibit-suspend.path
  systemctl --user daemon-reload
  systemctl --user restart zellij-inhibit-suspend.path
</content>
</entry>
<entry>
<title>fix(suspend): only inhibit for SSH-spawned zellij sessions</title>
<updated>2026-05-29T10:18:14Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-05-29T10:18:14Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=0711f1b4a4045c583c63f494a61262ed1146a944'/>
<id>urn:sha1:0711f1b4a4045c583c63f494a61262ed1146a944</id>
<content type='text'>
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/&lt;pid&gt;/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.
</content>
</entry>
<entry>
<title>feat(suspend): hold inhibit lock while any zellij session exists</title>
<updated>2026-05-29T10:18:13Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-05-29T10:18:13Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=01df321e907b6c8568bb8622eb44a5c1486a0631'/>
<id>urn:sha1:01df321e907b6c8568bb8622eb44a5c1486a0631</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>feat(sandbox): bwrap wrappers for mpv, yt-dlp, streamlink</title>
<updated>2026-05-29T10:18:12Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-05-29T10:18:12Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=1a8a19e6286aa58c5a46f03882f8f09e54456051'/>
<id>urn:sha1:1a8a19e6286aa58c5a46f03882f8f09e54456051</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>revert: drop snxctl-chromium wrapper, snx-rs works with default browser now</title>
<updated>2026-05-19T14:16:09Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-05-19T14:16:09Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=027fa12fc3fbc138dc8bbbb50b066735943d8b27'/>
<id>urn:sha1:027fa12fc3fbc138dc8bbbb50b066735943d8b27</id>
<content type='text'>
User confirms snx-rs's SAML loopback no longer needs chromium routing.
Remove:

- dot_local/bin/snxctl-chromium             (PATH-override wrapper)
- dot_local/share/snx-rs/bin/xdg-open       (chromium shim)
- snx-rs LibreWolf SAML note in user-overrides.js

The waybar snx-vpn toggle now just runs `snxctl connect` detached,
no wrapper indirection.
</content>
</entry>
<entry>
<title>refactor(snxctl-chromium): drop daemon drop-in, override snxctl's PATH</title>
<updated>2026-05-14T09:58:38Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-05-14T09:58:38Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=0b506ac67c33939732bdd91d39a8a632bcbe0841'/>
<id>urn:sha1:0b506ac67c33939732bdd91d39a8a632bcbe0841</id>
<content type='text'>
snx-rs.service is a system unit, not --user, so the prior approach of
overriding the daemon's PATH via a systemd drop-in could never apply.
And it wasn't needed anyway: snxctl itself runs opener::open(url)
in-process, so prepending the shim dir to snxctl's PATH is enough.

- Drop dot_config/systemd/user/snx-rs.service.d/10-chromium-saml.conf.
- snxctl-chromium now just sets PATH and exec's snxctl connect.
- xdg-open shim no longer forces --new-window so chromium can reuse a
  warm window (faster SAML round-trip).
</content>
</entry>
</feed>
