notes

things that took too long

Short write-ups, usually about something that was broken for an afternoon and turned out to have a one-line cause. Also available as RSS.

adding a window without inflating the binary

Two posts ago I said the size was the feature. So naturally the next thing I did was add a GUI, which is exactly the kind of change that turns 1.5 MB into 15.

The escape is that the window doesn't have to live in the same file. The three jobs were already mostly logic with a thin layer of printing on top, so I moved the logic into a library and left main.rs as the front-end that prints. swiss-gui is a second binary that calls the same library. One implementation, two ways in.

That's what makes the preview trustworthy. When you tweak a social card in the window, the image you see isn't a mock-up of the output — it's the same function that writes the file, called into memory instead of onto disk. If the preview and the file ever disagree, that's a bug, not a design decision.

The swiss-gui window on the Brand tab. Above is a settings panel for the social card; below it is a live preview of the card itself, dark with an amber logo tile and the words one small binary.
The Brand tab. That card at the bottom isn't a mock-up — it was rendered by the same function that writes the file.

two things that bit me

Profile overrides only work in the root manifest. I wanted opt-level = "z" everywhere except the hot egui crates, because per-frame layout is visibly sluggish when size-optimised. I put the overrides in the GUI's Cargo.toml. Cargo ignored them entirely, and said nothing about it. They have to be in the workspace root:

[profile.release.package.egui]
opt-level = 3

Since only the GUI links egui, the command line never sees any of this.

Embedded fonts don't replace the built-in ones. To make the window look like the site I embedded Inter and JetBrains Mono, which is about a megabyte of the GUI and worth it. But Inter has no arrows and no emoji, so the first build had list buttons you couldn't see. egui's own fonts stay in the stack behind Inter, purely as fallback.

The card renderer is the one place I deliberately didn't use the site's font, because swiss brand og draws the cards already on this page with your system font. Matching the site inside the window is nice. Quietly restyling every existing card is not.

The command line comes in at 1.55 MB. The window is a second download, and a much bigger one — about 10 MB, because egui is not a small toolkit. It's there for people who want it, and it costs the command line nothing.

why my certificate wouldn't issue

I pointed my domain at GitHub Pages and the Enforce HTTPS checkbox sat greyed out, with “The certificate does not exist yet.” Every check I knew how to make said my DNS was perfect: the four GitHub A records on the apex, no AAAA or CAA records, no DNSSEC, and no trace of the old host.

The actual cause was dumber, and I could not find it documented anywhere.

GitHub asks Let's Encrypt for a certificate, then polls DNS to prove you control the domain. My first check ran while the old A record was still cached, so it failed. That part is normal. What isn't obvious is that the job does not simply retry — it backs off, and the delay gets longer every time. So each subsequent attempt reused the same stale answer and it settled into a comfortable permanent failure.

There is no button to force a re-check. But the certificate request is tied to your custom domain setting, not to your site's content. So you can make it start over by removing the domain and putting it back:

git rm CNAME
git commit -m "drop custom domain"
git push

Then wait. Roughly twenty seconds later the Pages API reported "cname": "", which means it noticed. Put the file back:

printf 'example.com\n' > CNAME
git add CNAME
git commit -m "restore custom domain"
git push

A fresh certificate appeared in about twenty seconds — state: approved, expiring 2027-01-14. Same DNS the whole time.

The lesson: if your certificate won't issue, don't debug your DNS. It is almost certainly fine. Debug GitHub's cached view of it. And once the backoff has grown, waiting will never fix it — you have to make it ask again from scratch.

one 1.5 MB binary, three jobs

swiss is a command line tool that does three unrelated things I kept needing, in one binary:

  • unlock — find out what process is holding a file hostage, and optionally kill it.
  • pdf — merge, split, rotate, strip metadata, or inspect a PDF.
  • brand — turn one image into a full web branding set: favicons, an app manifest and a social card.

The interesting one is unlock, because the easy way to find a file lock on Windows is to run handle.exe from Sysinternals and parse its output. I didn't want a second download. Windows has had a proper API for this since Vista — the Restart Manager — and nobody remembers it exists. You open a session, register the file, ask who's using it, and get back a list of processes. It is exactly the API Windows Update uses to work out what's blocking a reboot.

pdf is built on lopdf. The trick that mattered most was pruning: after splitting a document you have to drop the unused objects and renumber the survivors, or each output page carries the entire original file with it. Doing that took split output from roughly a megabyte per page down to about 100 KB.

brand does its own text rendering with fontdue and writes favicon.ico by hand — it's just a header followed by PNG payloads — so there's no image processing dependency to install.

The whole thing lands at 1.52 MB. That's mostly the release profile rather than anything clever I did:

[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
panic = "abort"

Honestly the size is the feature. A tool like this is only useful if grabbing it is easier than not grabbing it.

the artwork on this site made itself

Every image on this site is generated by swiss, the same binary from the previous post. I find this funnier than it probably is. When I add a project, I don't open a design app — I run one command.

The little coloured marks next to each project are drawn with raw pixel primitives, no SVG and no fonts: a rounded square, then the glyph on top. A Swiss cross is two rectangles. The swap arrows are two rectangles and two triangles. The chat bubble is a rounded rectangle, a triangle for the tail, and three circles punched through in the background colour.

Those marks then go through swiss brand og, which draws the dark card, the accent bar, the mark on the left and the text on the right.

swiss brand og mark.png \
  --title "swiss" \
  --subtitle "one small binary, three small jobs" \
  --size 1280x720

why 1280x720 and not the usual 1200x630

Almost every guide tells you to make social cards 1200×630, because that's what Facebook standardised. These cards aren't only social previews though — they're also the thumbnails on the project grid, and that grid crop is aspect-ratio: 16/9 with object-fit: cover.

1200×630 is 1.9:1. A 16:9 box is 1.78:1. So the browser would trim roughly 43 pixels off each side, and the first thing it would eat is the accent bar down the left edge — the one element holding the whole design together.

1280×720 is exactly 16:9, so cover crops nothing at all. One image, correct in both places. Sometimes the boring answer is just the right dimensions.