<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotfiles/dot_config/systemd/user, branch master</title>
<subtitle>My linux config and rc files</subtitle>
<id>https://git.sommerfeld.dev/dotfiles/atom/dot_config/systemd/user?h=master</id>
<link rel='self' href='https://git.sommerfeld.dev/dotfiles/atom/dot_config/systemd/user?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>refactor(mail): migrate protonmail-bridge from pacman 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=4d8cf1bc30a076e2976787051d28d8072f8f5321'/>
<id>urn:sha1:4d8cf1bc30a076e2976787051d28d8072f8f5321</id>
<content type='text'>
Move the ProtonMail Bridge off the AUR protonmail-bridge-core package
and onto nix/host.nix, consistent with the other migrated user-leaf
tools. Since the AUR package previously supplied the systemd user unit
(customized via a drop-in), ship a repo-owned
dot_config/systemd/user/protonmail-bridge.service instead: it runs the
nix binary by absolute %h/.nix-profile/bin path with --noninteractive
and folds the former drop-in's PASSWORD_STORE_DIR into the unit, so the
now-redundant protonmail-bridge.service.d/override.conf is removed.

Drop protonmail-bridge-core from meta/base.txt (the git send-email Perl
prereqs stay). No vm.nix change: the bridge is host-only and user units
are not symlinked on the headless VM.
</content>
</entry>
<entry>
<title>fix(systemd): ship poweralertd.service user unit</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=7414a25165e1469cacee3fa9d8e9dd25df640aa0'/>
<id>urn:sha1:7414a25165e1469cacee3fa9d8e9dd25df640aa0</id>
<content type='text'>
poweralertd was migrated to nix (host.nix) but, like mako, the nix
package does not ship a systemd user unit on the manager's search
path. sway-session.target's Wants=poweralertd.service referenced a
non-existent unit (previously the pacman package supplied
/usr/lib/systemd/user/poweralertd.service), so battery/AC
notifications never started at login.

Add a repo-owned poweralertd.service (absolute nix-profile path) and
register it in systemd-units/user.txt.
</content>
</entry>
<entry>
<title>fix(systemd): ship mako.service user unit</title>
<updated>2026-06-05T10:06:01Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-06-05T10:06:01Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=c613f5b23418c5b68c6116ec22ce46d1a1fb42a2'/>
<id>urn:sha1:c613f5b23418c5b68c6116ec22ce46d1a1fb42a2</id>
<content type='text'>
The nix mako package does not ship a systemd user unit on the user
manager's search path, so sway-session.target's Wants=mako.service
referenced a non-existent unit after the pacman-&gt;nix migration
(previously the Arch mako package provided /usr/lib/systemd/user/
mako.service). mako only started on first D-Bus notification, never
eagerly at session login.

Add a repo-owned mako.service (Type=dbus, org.freedesktop.Notifications)
using the absolute nix-profile path, matching the other sway-session
units, and register it in systemd-units/user.txt.
</content>
</entry>
<entry>
<title>fix(systemd): drop StopWhenUnneeded from sway-session.target</title>
<updated>2026-06-05T10:06:01Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-06-05T10:06:01Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=9ec3c16d06acc3c496634f4952e1f8dc73a7de1c'/>
<id>urn:sha1:9ec3c16d06acc3c496634f4952e1f8dc73a7de1c</id>
<content type='text'>
The target reached active then was immediately garbage-collected:
  Reached target sway compositor session
  Stopped target sway compositor session
  Stopping swayr.../Waybar...
Nothing holds a reverse dependency on sway-session.target, so the first
"stop unneeded units" pass (triggered when any Wanted service transitions,
e.g. a ConditionEnvironment skip during the env-import race) found it
unneeded and StopWhenUnneeded=yes tore it down, cascading via PartOf/
BindsTo to every session service. Manual `systemctl --user start` worked
because that starts the service directly, not the GC-prone target.

StopWhenUnneeded has been latent since 030848c; the nix migration's
changed startup timing exposed it. The canonical sway-session.target
omits it; teardown still works via BindsTo=graphical-session.target and
user-manager shutdown at logout (swaymsg exit).
</content>
</entry>
<entry>
<title>fix(systemd): use absolute %h/.nix-profile/bin paths in user units</title>
<updated>2026-06-05T10:06:00Z</updated>
<author>
<name>sommerfeld</name>
<email>sommerfeld@sommerfeld.dev</email>
</author>
<published>2026-06-05T10:06:00Z</published>
<link rel='alternate' type='text/html' href='https://git.sommerfeld.dev/dotfiles/commit/?id=cd050f0bdfea14f031e4a671a366c77d4f62b19d'/>
<id>urn:sha1:cd050f0bdfea14f031e4a671a366c77d4f62b19d</id>
<content type='text'>
The previous environment.d fix was insufficient: even with the nix profile
on the --user manager's PATH (confirmed via `systemctl --user
show-environment`), bare-name ExecStart= still fails 203/EXEC. systemd's
--user manager does not resolve a bare ExecStart binary against the
imported/environment.d PATH.

Invoke each unit's main binary by absolute path %h/.nix-profile/bin/&lt;name&gt;
(waybar, swayidle, swayrd, inhibridge, wl-paste, wob). %h expands to $HOME
at unit load. Secondary lookups those binaries/scripts perform (cliphist,
swaymsg, playerctl) still rely on PATH, which environment.d provides — so
that file stays, with its comment corrected to reflect this split.
</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(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>
</feed>
