<feed xmlns='http://www.w3.org/2005/Atom'>
<title>archsetup/tests, branch main</title>
<subtitle>Builds a full dev workstation from a bare Arch Linux install.
</subtitle>
<id>https://git.cjennings.net/archsetup/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/archsetup/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/'/>
<updated>2026-06-11T16:29:41+00:00</updated>
<entry>
<title>feat(scripts): package auditor + fix the four packages it caught</title>
<updated>2026-06-11T16:29:41+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-11T16:29:41+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=1f89523739a575f5dec19616ec44df4143df4866'/>
<id>urn:sha1:1f89523739a575f5dec19616ec44df4143df4866</id>
<content type='text'>
scripts/audit-packages.sh extracts every pacman_install/aur_install package (loop lists included) and verifies each against its declared source — sync dbs for official, one batched RPC query for AUR — flagging movers in both directions. Unit-tested against fixture installers with fake pacman/curl.

First real run over 420 packages found four that vanished from both sources, each now fixed: libva-mesa-driver folded into mesa (line dropped), nvidia-dkms replaced by nvidia-open-dkms (Turing+; legacy cards are the preflight task's problem), swww replaced by awww (its successor, already what both machines run), and libappindicator-gtk3 replaced by libayatana-appindicator. Fifteen AUR entries that graduated to official repos still install fine via yay and are left as-is.
</content>
</entry>
<entry>
<title>refactor: drop in-repo dotfiles/, move stow tooling to the dotfiles repo</title>
<updated>2026-06-02T17:16:38+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-06-02T17:16:38+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=b10cba594db836c0747066addad48bda4d30cd02'/>
<id>urn:sha1:b10cba594db836c0747066addad48bda4d30cd02</id>
<content type='text'>
Since the installer clones DOTFILES_REPO into ~/.dotfiles and stows from there, the in-repo dotfiles/ tree was dead weight. Nothing reads it at install time. I removed it (831 files) now that both machines are migrated.

The Makefile's stow / restow / reset / unstow / import targets and the dotfile-script unit suites moved to the dotfiles repo. They sit alongside the scripts they manage and run standalone (cd ~/.dotfiles &amp;&amp; make ...). This Makefile keeps the VM-integration targets and the installer-helper suite (safe-rm-rf).

I updated CLAUDE.md and README.md so stow operations run from ~/.dotfiles, and the dotfile-management, theme, and unit-test sections point at the standalone repo. The README was already describing the old in-repo model from before the installer switched to cloning. This brings it in line.
</content>
</entry>
<entry>
<title>feat(notify): add --silent flag, volume knob, and level sound files</title>
<updated>2026-05-22T00:16:34+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-22T00:16:34+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=f7079db3aa3e0073df6ce5409d4b6de0a431e26f'/>
<id>urn:sha1:f7079db3aa3e0073df6ce5409d4b6de0a431e26f</id>
<content type='text'>
The touchpad toggle's notification was too loud, and the eight notify sounds varied by ~13 dB in RMS loudness — bug and fail came out two to three times louder than info or security.

I added a --silent flag to notify (shows the popup, plays no sound) and a NOTIFY_VOLUME knob (paplay scale, default 65536) so the master level can drop without re-encoding. toggle-touchpad now passes --silent on both enable and disable. normalize-notify-sounds.sh measures each .ogg and shifts it to a uniform -31 dB mean. It writes through the file instead of mv-ing over it, so the stow symlinks survive when the script runs against the live sound dir. I re-encoded all eight sounds to the new level.

Tests: a new tests/notify suite (12 tests) covers --silent, the volume knob, flag composition, and the error paths.
</content>
</entry>
<entry>
<title>feat(hyprland): add airplane-mode waybar toggle</title>
<updated>2026-05-21T21:48:47+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-21T21:48:47+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=09f4d205fe463faf676f95e798d08e8bf498be96'/>
<id>urn:sha1:09f4d205fe463faf676f95e798d08e8bf498be96</id>
<content type='text'>
I added a laptop-only waybar button that drops the machine into a low-power state and restores it on a second click. Engaging turns wifi off, sets the CPU energy-performance preference to power, dims the backlight to 35%, and stops network-only services (tailscale, proton-vpn, avahi, cups, wsdd, geoclue, sshd, fail2ban, syncthing). Bluetooth is left alone so earbuds keep working.

Disengaging replays the state recorded when airplane mode was engaged rather than writing hardcoded defaults. A lever already in its low-power position is left untouched: wifi that was already off stays off, and a service that was already stopped isn't restarted.

The indicator hides itself on machines with no battery, so desktops never show the button. State lives in $XDG_RUNTIME_DIR/airplane-state, and the bar refreshes the moment the toggle fires via a realtime signal.
</content>
</entry>
<entry>
<title>feat(hyprland): add touchpad state indicator to waybar</title>
<updated>2026-05-21T01:55:13+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-21T01:55:13+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=62e45197bb4d440e002b731715343e565cf50ff3'/>
<id>urn:sha1:62e45197bb4d440e002b731715343e565cf50ff3</id>
<content type='text'>
The $mod+F9 toggle and the toggle-touchpad / touchpad-auto scripts already worked, but the scripts lived only in ~/.local/bin and were never committed, and there was no way to see the touchpad's state at a glance.

I committed both scripts into the repo so stow installs them, and added a waybar indicator: a waybar-touchpad status script and a custom/touchpad module that shows a mouse glyph when the touchpad is on and a mouse-off glyph (in the dupre orange) when it's off. The scripts signal the module with pkill -RTMIN+9 waybar after each state change, so the icon updates the moment the touchpad toggles. touchpad-auto now runs at login via exec-once.

The waybar-touchpad script has unit tests under tests/waybar-touchpad/ covering the enabled, disabled, and missing-state-file cases.
</content>
</entry>
<entry>
<title>fix(installer): guard constructed-path rm -rf deletes</title>
<updated>2026-05-21T01:53:58+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-21T01:53:58+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=cb209a2d01f5c17024738b490c8fa109959b5303'/>
<id>urn:sha1:cb209a2d01f5c17024738b490c8fa109959b5303</id>
<content type='text'>
Three rm -rf sites in archsetup delete paths built from variables: $state_dir for --fresh, and $source_dir/$prog_name for the git and AUR clone-retry cleanups. If a path variable were empty or malformed (preflight skipped, a degenerate git URL), the delete could expand to a top-level or otherwise unintended directory.

I added a safe_rm_rf &lt;path&gt; &lt;allowed_prefix&gt; helper that refuses to run unless the target is absolute, free of '..', deeper than a bare top-level dir, strictly inside the allowed prefix, and a real directory rather than a symlink. On the happy path it delegates to rm -rf, so successful installs are unchanged. The helper is self-contained and defined before the top-level --fresh handler, which runs before the logging helpers exist.

I covered the guard with unit tests under tests/safe-rm-rf/ that source the real function and exercise normal, boundary, and error cases against temp directories.
</content>
</entry>
<entry>
<title>feat(tmux-util): add rename subcommand (fzf pick + prompt)</title>
<updated>2026-05-19T18:17:06+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-19T18:17:06+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=f91586248f467a2ba7a62f76caee1f40927655d9'/>
<id>urn:sha1:f91586248f467a2ba7a62f76caee1f40927655d9</id>
<content type='text'>
tmux-util rename closes out the original six-subcommand plan. The flow:
1. fzf-pick a session from the list.
2. Prompt for a new name on stdin.
3. Bail with a useful message on empty input, same-as-old, or conflict with an existing session.
4. Otherwise `tmux rename-session -t &lt;old&gt; &lt;new&gt;` and confirm.

The conflict check uses `tmux has-session -t =&lt;new&gt;` with the same `=`-prefix exact-match guard as the go subcommand. Without it, tmux's default prefix matching would let `rename foo` succeed even when a session named `foobar` already exists, then surprise the user later.

5 new tests cover Normal cases (pick + rename happy path) and Boundary cases (no sessions, fzf cancel, empty new name, same-as-old no-op, conflict with existing session). The test harness's run_script grew an `stdin=` param so tests can feed the prompt input. fake-tmux picked up a rename-session handler that mutates the state file. Total suite: 48 tests, all green.

Six subcommands shipped: go, pick, ls, find, reap, rename. The original "no args prints help" requirement still holds, and the stub-test for unimplemented subcommands got removed since everything's wired now.
</content>
</entry>
<entry>
<title>feat(tmux-util): add pick subcommand (fzf session switcher)</title>
<updated>2026-05-19T18:14:52+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-19T18:14:52+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=95e1e0be4905ff5d37b52f889f5dda65543e2b51'/>
<id>urn:sha1:95e1e0be4905ff5d37b52f889f5dda65543e2b51</id>
<content type='text'>
tmux-util pick lists every session ("attached"/"detached" plus name), pipes it through fzf, and attaches or switch-clients to the chosen one (matching the go subcommand's inside-vs-outside-tmux discipline).

If the user cancels fzf (Ctrl-C, Esc, empty selection), the pipeline returns empty and pick exits 0 without touching tmux state.

The new fake-fzf testing fake is driven by env vars:
- FAKE_FZF_CHOICE_LINE=&lt;N&gt;: return the Nth line of stdin as the selection
- FAKE_FZF_CHOICE=&lt;string&gt;: return the literal string (ignores stdin)
- (neither): exit 130 to simulate cancel

4 new tests cover Normal cases (pick second line, inside/outside tmux behavior) and Boundary cases (no sessions, user cancellation). Total suite: 43 tests, all green.
</content>
</entry>
<entry>
<title>feat(tmux-util): add find subcommand</title>
<updated>2026-05-19T18:09:23+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-19T18:09:23+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=331d9421f1ca56afdaf0d238082f83d82103a795'/>
<id>urn:sha1:331d9421f1ca56afdaf0d238082f83d82103a795</id>
<content type='text'>
tmux-util find &lt;pattern&gt; walks every pane across every session, queries each pane's foreground command (`#{pane_current_command}`), and prints the location of any pane whose command matches the pattern. Output format: `&lt;session&gt;:&lt;window&gt;.&lt;pane&gt; &lt;command&gt;`, one per line.

Pattern is a basic ERE (passed through `grep -E`), so anchors and alternation work. Substring matching is the common case.

Exit code:
- 0 with output: matches found
- 1 with no output: no matches (lets you script around it)
- 2 with usage on stderr: missing or empty pattern

7 new tests cover Normal cases (single match, multi-match across sessions, format verification) and Boundary cases (no matches, no sessions, missing pattern, empty pattern). fake-tmux now parses pid:cmd entries in the state file so panes can carry a synthetic command name. Total suite: 39 tests, all green.
</content>
</entry>
<entry>
<title>feat(tmux-util): add go subcommand (attach-or-create)</title>
<updated>2026-05-19T18:01:19+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-05-19T18:01:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/archsetup/commit/?id=685272399d7dbc35aea6028d6741963399d84e3f'/>
<id>urn:sha1:685272399d7dbc35aea6028d6741963399d84e3f</id>
<content type='text'>
tmux-util go &lt;name&gt; attaches to a session named &lt;name&gt; if it exists, creates it otherwise. Behavior depends on whether the caller is already inside tmux:

- Outside tmux: `tmux attach-session -t &lt;name&gt;` (existing) or `tmux new-session -s &lt;name&gt; -c $PWD` (new).
- Inside tmux (TMUX env set): `tmux switch-client -t &lt;name&gt;` (existing) or `tmux new-session -d -s &lt;name&gt; -c $PWD` followed by `switch-client` (new). Attaching from inside tmux would nest sessions and break the outer view, so the inside path uses switch-client instead.

The existence check uses `tmux has-session -t =&lt;name&gt;` with the leading `=` to force exact-match. Without it, tmux does prefix matching, which would let `go foo` resolve to a session named `foobar`.

I added 6 new tests covering both inside/outside-tmux paths, both create/attach paths, plus error handling for missing or empty name arguments. fake-tmux picked up handlers for new-session (mutates state), attach-session and switch-client (record-only), and the `=`-prefix form of has-session. Total suite: 32 tests, all green.
</content>
</entry>
</feed>
