<feed xmlns='http://www.w3.org/2005/Atom'>
<title>dotemacs/scripts, 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-25T12:30:04+00:00</updated>
<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>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>fix(calendar-sync): run the sync from a timer instead of the agenda hook</title>
<updated>2026-08-03T14:39:08+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-08-03T14:39:08+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=eb0c2191b77e795cca7884e0690ea51c515527fd'/>
<id>urn:sha1:eb0c2191b77e795cca7884e0690ea51c515527fd</id>
<content type='text'>
The hourly timer armed itself from org-agenda-mode-hook, so a session where
I never opened the agenda never synced at all. After a reboot that is every
session until the first agenda call. I found it with all three calendar
files still frozen at the pre-reboot write, five hours stale.

The deferral had a real reason. Feed URLs behind :secret-host live in
authinfo.gpg, and starting at load would prompt for a passphrase on a cold
gpg-agent. The cost was worse than what it bought.

A user timer owns the schedule now, every ten minutes.
calendar-sync-auto-start defaults to nil so the editor no longer arms its
own. scripts/calendar-sync-run is the batch entry point, and it blocks
until every calendar leaves the syncing state. The pipeline is asynchronous
end to end, so a batch Emacs that returns early exits zero having written
nothing.

Installing it on a machine that resolves its feeds through :secret-host
surfaced two bugs the inline-url machine could never show:

- cj/auth-source-secret-value called auth-source-search behind nothing but
  a declare-function, which quiets the compiler and loads nothing. An
  interactive Emacs always has auth-source in by the time anyone calls, so
  the omission stayed invisible until a batch -Q sync died on a void
  function. The require is guarded on fboundp. An unconditional one reloads
  auth-source over whatever is already there, which replaced a caller's
  stubbed search mid-call and sent an existing test out to the real
  authinfo.

- A synchronous failure in one calendar aborted the whole loop. The async
  callbacks record their own failures, but they never run when the error
  lands before a process starts. Resolving a :secret-host feed signals
  outright on a cold agent. Failures are contained per calendar now, so one
  bad feed no longer costs the other two.

A failed row prints the reason it recorded. Batch Emacs discards the
*Messages* buffer the interactive path logs to, so without it the journal
shows only "error", with no way to tell a cold agent from a revoked token.
</content>
</entry>
<entry>
<title>feat(agenda): refresh the render cache on a timer, without the daemon</title>
<updated>2026-07-31T16:04:42+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-31T16:04:42+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=445a0be315f3d6fff585f1558262fb5dacd10d12'/>
<id>urn:sha1:445a0be315f3d6fff585f1558262fb5dacd10d12</id>
<content type='text'>
The surface reading this has to stay correct while Emacs is down, and
emacsclient is exactly the thing that cannot. So the writer is a batch
Emacs and a user timer runs it every five minutes.

- scripts/agenda-render-cache writes the cache from a batch Emacs. It
  resolves the agenda file list through the config's own resolver rather
  than restating it, so adding a calendar source stays a one-place change.
- The timer uses OnCalendar so Persistent actually applies. On a monotonic
  schedule it is silently ignored, and a machine that slept would come back
  to a stale file with nothing to trigger a catch-up.
- The window is now three days, yesterday through tomorrow. A consumer
  drawing a rolling window centred on now has nothing to draw for the part
  of its span outside today, which late in the evening is half the surface.

The keyword list moved to user-constants, where a batch Emacs can reach it
without this config's package dependencies. Without it org did not
recognise DOING or VERIFY, so it stopped parsing those headlines as tasks
at all: the keyword and priority cookie stayed glued to the front of every
title and each row reported no keyword and not-done. The file parsed
cleanly and was wrong, which is the failure worth guarding.

The bats tests run against a fixture and this checkout, not the machine's
real agenda and installed config. Asserting over live data let the same
suite pass on a day that happened to have no keyworded entries.
</content>
</entry>
<entry>
<title>chore: remove the obsolete remote-reset bootstrap script</title>
<updated>2026-07-24T21:10:33+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-24T21:10:33+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=c24dbe25d25bee514393b54bc3cbd5e7d638fd9b'/>
<id>urn:sha1:c24dbe25d25bee514393b54bc3cbd5e7d638fd9b</id>
<content type='text'>
- It was a one-time post-provisioning step to set repo remotes.
- Both repos already carry the correct cjennings.net origins.
- Neither uses the direct github remote it re-added.
- It also cd'd to a path that doesn't exist and ran on regardless.
- Deleted rather than hardened, since the end-state is already in place.
</content>
</entry>
<entry>
<title>feat(theme-studio): add seeding-engine model and pure seed() (phase 1)</title>
<updated>2026-07-04T23:08:01+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-04T23:08:01+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=f407959b4c30fa809722b62266640711c916a772'/>
<id>urn:sha1:f407959b4c30fa809722b62266640711c916a772</id>
<content type='text'>
I turned the coloring guide's role/seed table into an executable engine. A new pure module, seed-core.js, holds the seed model as data: a named palette whose accent shades are OKLCH-generated from the dupre anchors (reusing the colormath core), the role-to-treatment table, and a face-to-role map per owned tier, plus a pure seed() that projects the table onto syntax, UI, and org faces.

seed() owns three default sources: syntax, UI, and org among packages. It returns packages.org-mode only, so the roughly twenty non-org bespoke packages keep their curated APPS seeds untouched. The output already matches the shape the import path consumes, so phase 2 can wire it to open-seeded without a new format.

Builtins land on blue-grey and calls on a quieter gold, the two shades dupre lacked. Definitions come out gold and bold, state faces tint the background with no foreground, links underline, and the org heading ramp descends in lightness with level 1 strongest.

The module inlines into the page below the colormath core like the other pure cores, so the browser runs the code the Node tests import. #seedtest asserts the representative faces resolve correctly and that magit keeps its curated seed. test-seed-core.mjs covers the model, each tier, and seed()'s purity.
</content>
</entry>
<entry>
<title>fix(theme-studio): persist regexp/doc/negation/warning syntax faces</title>
<updated>2026-07-04T16:35:22+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-04T16:35:22+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=0be572e43f8118f6c678a536b3d97d7e976e840f'/>
<id>urn:sha1:0be572e43f8118f6c678a536b3d97d7e976e840f</id>
<content type='text'>
Five syntax categories (rxgb, rxgc, dmark, neg, warn) rendered as editable rows but had no COLS entry, so build_syntax never gave them a SYNTAX slot. They exported nothing and re-imported nothing, so every saved theme dropped them. I added the five to COLS, where the default-face seed fills their real colors and they round-trip through save and import. A regression test now asserts every editable category has a COLS slot.
</content>
</entry>
<entry>
<title>chore(theme-studio): update WIP theme state</title>
<updated>2026-07-04T04:25:19+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-04T04:25:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=7ea4c30bb997e06608d68b01c36fca394ec8b140'/>
<id>urn:sha1:7ea4c30bb997e06608d68b01c36fca394ec8b140</id>
<content type='text'>
</content>
</entry>
<entry>
<title>feat(theme-studio): pin the remaining chrome at absolute heights</title>
<updated>2026-07-03T04:02:48+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-03T04:02:48+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=298e24aa8a0fdd88d2ae8ecb514b3e18b2b4bb5b'/>
<id>urn:sha1:298e24aa8a0fdd88d2ae8ecb514b3e18b2b4bb5b</id>
<content type='text'>
tab-bar, tab-line, line-number, and line-number-current-line join mode-line in the chrome height seeds (apply_modeline_height_default generalized to apply_chrome_height_defaults), each pinned at absolute 130 so no bar or gutter tracks a buffer's enlarged default face. header-line and mode-line-inactive stay unseeded on purpose: both inherit mode-line, so the pin reaches them through the chain and their own value would duplicate state. The line-number pair is seeded individually because the generated theme's explicit specs leave their :inherit unspecified at runtime.

header-line, tab-bar, and tab-line also join the UI faces table, so all chrome heights are editable through the size column; the mock-completeness gate exempts the three faces the mock deliberately doesn't draw. WIP.json reconciled and the theme regenerated; every chrome face resolves :height 130 in the live daemon, pins and inherit chains both.
</content>
</entry>
<entry>
<title>feat(theme-studio): previews render the chosen face height</title>
<updated>2026-07-03T03:50:27+00:00</updated>
<author>
<name>Craig Jennings</name>
<email>c@cjennings.net</email>
</author>
<published>2026-07-03T03:50:27+00:00</published>
<link rel='alternate' type='text/html' href='https://git.cjennings.net/dotemacs/commit/?id=0ffd6f5a450e716e7ef3297d4bec2fda36649cdf'/>
<id>urn:sha1:0ffd6f5a450e716e7ef3297d4bec2fda36649cdf</id>
<content type='text'>
heightCssValue maps a face's height to CSS from its stored kind: a relative multiplier renders as em, an absolute 1/10pt value as true pt, with legacy objects falling back to integer/fractional inference. uiCss feeds it to the mock editor, so the mode-line, mode-line-inactive, and line-number bars visibly thicken with an absolute height while the buffer text stays put; paintUI scales the UI row's sample text; the package-preview span builder swaps its em-only sizing for the same kind-aware value.

faceCss now accepts a unit-carrying string for fontSize alongside the existing em number.
</content>
</entry>
</feed>
