<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotemacs/modules, branch main</title>
<subtitle>My Emacs configuration
</subtitle>
<id>https://git.cjennings.net/dotemacs/atom?h=main</id>
<link rel='self' href='https://git.cjennings.net/dotemacs/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/'/>
<updated>2026-08-26T18:25:39+00:00</updated>
<entry>
<title>test: repair the seven files red under Emacs 31.1</title>
<updated>2026-08-26T18:25:39+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-26T18:25:39+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=474a9bed31c583761e55b6072f07fcfd6d095471'/>
<id>urn:sha1:474a9bed31c583761e55b6072f07fcfd6d095471</id>
<content type='text'>
None of the seven was a config regression. Each file had its own cause:

- config-utilities: the tests mocked fboundp with cl-letf. fset on a subr autoloads comp-run, which requires bytecomp, whose defun of byte-compile-file replaced the mock installed earlier in the same cl-letf. 30.2 hid it because ert happened to load bytecomp first. I extracted cj/--compile-elisp-file with an injectable availability predicate (fboundp by default), made cj/compile-this-elisp-buffer the wrapper, and had the tests inject a predicate instead of redefining a primitive. Added the missing no-compiler case.
- system-defaults-functions: the file let-bound use-package-always-ensure before anything declared it special. 30.2 downgraded that to a "Failed to parse package recentf" warning; 31.1 moves the defcustom to autoload time, where it is fatal at load. One defvar before the let.
- calibredb-epub-config: the jump tests stubbed calibredb while it was still an autoload, so the module's require loaded the real definition over the stub and the real command always ran. Require calibredb before any stub.
- agenda-query--render: org 9.8.7 parses the priority cookie with (looking-at org-priority-regexp), whose lazy prefix swallows everything between the stars and the cookie. The fixture now has no cookie and pins that the keyword is not recognised.
- prog-general-yas-activation: python-ts-mode prompts to install a missing grammar, which a batch run cannot answer. The test skips on treesit-ready-p rather than on the mode existing, and the module comment says why.
- integration-recurring-events: the fixtures are America/Chicago and the assertions expect Chicago rendering, so the file only passed on a machine in Central time. TZ is pinned in setup and restored in teardown.

The integration failure had been invisible since 08-17 because make test stops after a red unit phase. make test exits 0 again for the first time since then.
</content>
</entry>
<entry>
<title>fix(startup): keep the native-comp log alive so the compile queue drains</title>
<updated>2026-08-26T18:05:37+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-26T18:05:37+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=9b102d6fb23074bebc626bfe4e61c677c006ae37'/>
<id>urn:sha1:9b102d6fb23074bebc626bfe4e61c677c006ae37</id>
<content type='text'>
comp-run parks every async native-compile worker on *Async-native-compile-log* and each worker's sentinel reads that buffer back before it starts the next job. The startup sweep in cj/dashboard-only killed the buffer, and killing a buffer SIGHUPs every :noquery process under it. Each sentinel then died with "Selecting deleted buffer" and comp--run-async-workers never ran again. The queue sat stranded for the life of the daemon and nothing it had queued was ever cached. The 31.1 upgrade emptied the eln cache. After that every boot queued about 240 files and spawned eight compilers at the moment of the first frame, then killed them, so the next boot did it all again. That storm on a cold boot is what sat behind the grey window before the dashboard.

I added the buffer to cj/undead-buffer-list so the sweep buries it, the same choice *Warnings* got in 63831060. The "Selecting deleted buffer" errors I filed in June as an async race were this.

The new tests pin the name to comp-async-buffer-name and run the sweep with a live :noquery process parked on the buffer. The process survives, while the same process on an ordinary buffer dies with it.
</content>
</entry>
<entry>
<title>fix(telega): build the server image locally to meet tdlib 1.8.66</title>
<updated>2026-08-25T12:30:04+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-25T12:30:04+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=91fbac722e83cd108a295542a3c88de66d209fe8'/>
<id>urn:sha1:91fbac722e83cd108a295542a3c88de66d209fe8</id>
<content type='text'>
MELPA's 2026-08-18 telega raised telega-tdlib-min-version to 1.8.66. The pinned upstream image ships 1.8.64, so telega killed its own server on every launch. Upstream's only 1.8.66 image won't start: it's missing libglycin (zevlg/telega.el#596).

docker/telega-server/Dockerfile derives from that upstream image, pinned by digest, and adds the one missing package. make telega-image builds it as cj/telega-server:1.8.66-glycin, cj/telega-docker-image defaults to that tag, and a test holds the two equal. cj/telega now refuses to launch with a make telega-image hint when the image isn't built, since docker would otherwise try to pull a local-only tag from the registry. setup-telega.sh points at the make target instead of M-x telega-server-build.

I chose the local build over pinning the telega package back to 20260706.2147. The derived image satisfies both machines' floors. package.el can't pin a version and MELPA serves only its latest build, so nothing would enforce the package pin. When #596 is fixed upstream the pin returns to a registry digest and the Dockerfile goes.
</content>
</entry>
<entry>
<title>feat(term): add cj/term-tmux-detach on C-; x d</title>
<updated>2026-08-25T11:51:16+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-25T11:51:16+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=9b53d707f5e2dfe5237ae4db338aa65e83d9c6b2'/>
<id>urn:sha1:9b53d707f5e2dfe5237ae4db338aa65e83d9c6b2</id>
<content type='text'>
A keyboard C-b inside the Claude Code pane never reaches tmux as a prefix. It lands as stray text in the TUI's input, so C-b d can't detach the client. The new command writes C-b d into the pty instead, the same path cj/term-copy-mode-dwim already uses for C-b [. It sits on cj/term-map "d" next to copy-mode on "c", and outside tmux it writes nothing and says so.
</content>
</entry>
<entry>
<title>fix(startup): keep *Warnings* alive so 31.1's first frame is Wayland</title>
<updated>2026-08-25T11:28:25+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-25T11:28:25+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=638310601a39e7f9ade59ea85794a21167905f31'/>
<id>urn:sha1:638310601a39e7f9ade59ea85794a21167905f31</id>
<content type='text'>
Emacs 31.1's warnings.el defers daemon-startup warnings into a one-shot after-make-frame-functions closure that holds the *Warnings* buffer object and displays it on the first client frame. The startup sweep in cj/dashboard-only killed that buffer first. On the first emacsclient -c of every session, display-buffer then signaled inside make-frame and server.el answered -window-system-unsupported. emacsclient silently retried on $DISPLAY, and the frame opened on XWayland behind the pgtk "unsupported under X" dialog. Only that first frame was affected, which made it look like a display problem rather than a config one.

I added *Warnings* to cj/undead-buffer-list so the sweep buries it instead, the same choice desktop.el makes in desktop-clear-preserve-buffers. Two things follow. C-x k on *Warnings* now buries it. A startup that produced warnings shows them on the first frame, because the deferred closure finally has a live buffer. As a backstop, cj/warning--display-buffer-if-live wraps warning--display-buffer so a dead buffer (or a name that resolves to none) is skipped rather than passed on. That function only exists from Emacs 31, and advising the undefined symbol on 30.2 is harmless.

The new tests pin *Warnings* to the undead list and check the sweep leaves it live. The guard tests cover live and dead buffers plus names that do and don't resolve, and run the real 31.x function on a dead buffer.
</content>
</entry>
<entry>
<title>fix(weather): ensure xterm-color so the weather buffer renders</title>
<updated>2026-08-21T21:27:50+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-21T21:27:50+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=47cf2453034b6447e65482cc640326735ba93e65'/>
<id>urn:sha1:47cf2453034b6447e65482cc640326735ba93e65</id>
<content type='text'>
M-x wttrin died on a missing xterm-color. wttrin declares it in Package-Requires, but nothing here reads that header. The block loads my local checkout with :load-path, and use-package applies use-package-always-ensure only when :load-path is absent. So package.el was never involved and xterm-color never got installed.

It came free under the :vc form this replaced, since package-vc resolves requirements. Naming xterm-color explicitly restores it without giving up the checkout.

Only the weather buffer broke. The require fires on the render path. The mode line reads the JSON feed and kept working.
</content>
</entry>
<entry>
<title>fix(packages): survive a failed install instead of aborting startup</title>
<updated>2026-08-14T09:12:55+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-14T09:12:55+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=b7f6984983ea947dd969fa2be9200b53bbf447fe'/>
<id>urn:sha1:b7f6984983ea947dd969fa2be9200b53bbf447fe</id>
<content type='text'>
A fresh install pulls around 190 packages over the network. One transient download error left the config two thirds loaded, with hooks registered for packages that never installed.

use-package already warns and continues when an install fails, but it guards with condition-case-unless-debug, which does nothing while debug-on-error is set. early-init sets that for all of startup so my own errors stay loud, and it's what disarms the guard. I inhibited the debugger for the install alone, so the rest of init keeps it. Failures are recorded and reported when startup finishes, and cj/retry-failed-package-installs picks them up.

Editing hooks are silenced during an install. Generating autoloads visits .el files, which is how one missing package breaks the install of unrelated ones. A session-wide retry budget bounds the retrying. Installs stop after five failures in a row, since a run that long means the network is gone rather than one package being unlucky.

:vc packages get the same treatment through advice. use-package drops :ensure whenever :vc is present, and its clone path carries no error handling. A machine without credentials for the git host aborted init the same way.

make bootstrap installs everything headlessly, so a fresh machine never meets the debugger. It refuses to run against byte-compiled modules, where use-package consumes :ensure at compile time and a pass would report success having installed nothing.
</content>
</entry>
<entry>
<title>feat(google-keep): load machine-local settings from google-keep.local.el</title>
<updated>2026-08-10T06:59:40+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-10T06:59:40+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=ebed67a6e285f7667591034b0bc5f962c45e6307'/>
<id>urn:sha1:ebed67a6e285f7667591034b0bc5f962c45e6307</id>
<content type='text'>
The bridge's interpreter path and account email are machine-local, and
this config discards customize output, so nothing persisted them across
daemon restarts. The module now loads google-keep.local.el when readable
(the calendar-sync.local.el shape) before the interpreter check, so a
locally-set venv path is what gets verified. A broken local file reports
instead of breaking module load. Three tests cover load, absence, and
the error path.
</content>
</entry>
<entry>
<title>fix(music): size a playlist by its content, not by its symlink</title>
<updated>2026-08-03T20:27:17+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-03T20:27:17+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=6f90ec5b660c6a2552ac7284a3d9d963771fbbc2'/>
<id>urn:sha1:6f90ec5b660c6a2552ac7284a3d9d963771fbbc2</id>
<content type='text'>
cj/music--append-track-to-m3u-file decided whether to prepend a newline by
seeking to a byte offset from file-attributes, which doesn't follow
symlinks. On a stow-deployed playlist that measures the link string instead
of the file.

Two failure modes, and I only went looking for the second. All 100 symlinked
playlists read the wrong byte, so a file already ending in a newline looked
unterminated and gained a blank line on every append. On the 31 whose link
string is longer than their content the range fell outside the file
entirely. Nothing was inserted, and char-after handed nil to a numeric
comparison, so the append died with a wrong-type error.

The probe now reads the file and checks its last character. That takes
file-attributes out of the path, so this class can't come back. The largest
real playlist is 11 KB and the read costs 0.11 ms, so being clever about
offsets bought nothing.

A test per mode, because one fixture can't show both. Seed the blank-line
case with content shorter than the link string and it lands on the error
path instead, going red for the wrong reason and proving nothing.

The cheap patch was guarding char-after against nil. That would have
silenced the crash on 31 playlists and left the blank line live on all 100.
</content>
</entry>
<entry>
<title>feat(music): write an m3u path absolute when it leaves the playlist's directory</title>
<updated>2026-08-03T19:38:20+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-03T19:38:20+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=a457ade9207be48f6bfaa519046d7acea390ff1b'/>
<id>urn:sha1:a457ade9207be48f6bfaa519046d7acea390ff1b</id>
<content type='text'>
A track under the playlist's own directory still writes relative. Anything
outside it now writes the absolute path instead of a ../ chain. Playlists in
the mpd directory point into ~/music, so the relative form there was four
levels of .. and broke the moment anything moved.

I took the trade knowingly. ambience.m3u is the only playlist anywhere using
../ lines, and it reaches its audio through a sibling directory. So a newly
appended track there writes absolute while its hand-written lines stay
relative, and its header comment needs a matching edit. The alternative was
an exception for tracks inside the deployed tree, which would be a rule
serving exactly one file.

The comment I wrote for the earlier half of this argued one direction only.
Absolute survives the playlist moving, which I said. Relative survives the
library moving, which I didn't, and for a track in the same tree the ../ form
survives strictly more moves rather than fewer. Rewritten to name the trade
instead of implying there isn't one.

The boundary case is a directory whose name begins with two dots. The check
looks for a leading "../", so a real subdirectory called ..hidden sits under
the playlist and must stay relative. It has a test now, because loosening
that check to ".." would break that case and nothing else.
</content>
</entry>
</feed>
