Web Log

2023-12-05 - Adjust Odd and Even PDF Margins Separately

How to adjust the margins of odd and even pages separately in PDF document:

Step one: Split your PDF into separate files for odd pages and even pages

$ pdftk demo.pdf cat odd  output odds.pdf
$ pdftk demo.pdf cat even output evens.pdf

Step two: Set margins ("left top right bottom" in units of 72ndths of an inch)

$ pdfcrop --margins "144 36 36 36" odds.pdf  modds.pdf
$ pdfcrop --margins "36 36 144 36" evens.pdf mevens.pdf

Step three: Shuffle (interleave) the pages back together

$ pdftk O=modds.pdf E=mevens.pdf shuffle O E output printme.pdf

2023-11-20 - Typst Page Number References

Here is how to get the page number of a label elsewhere in your document in typst:

#let get_page_number = q => locate(query_token => counter(page).at(query(q, query_token).at(0).location()).at(0))

Example usage:

== Forest<forest>
This is a forest, with trees in all directions. To the east, there appears to be sunlight.

To go north, go to page #get_page_number(<clearing>).\
To go south, go to page #get_page_number(<south_forest>).\
To go east, go to page #get_page_number(<forest_path>).
#pagebreak(to: "even")

== Clearing<clearing>
You are in a clearing, with a forest surrounding you on all sides. A path leads south.

Complete concrete example: typst source and compiled pdf.

Six pages of a Gamebook showing three fantasy world locations and directions for moving to other locations by turning to other page numbers

(LaTeX calls this \pageref)

2023-10-11 - Linux distros packaging each other

Underappreciated nixpkgs feature: Instead of building in the normal minimal sandbox environment, just say runInLinuxImage to install a different Linux distro in a temporary VM, install some of that distro's packages, and run the build there! nixpkgs even packages 19 other releases from four other distros to be used with this feature:

Distro Architecture
x86_64 i386
Debian 10.13-buster
Ubuntu 14.04-trusty
Fedora 26
Centos 6

I was having trouble cross-compiling an embedded ARM executable from a x86_64 machine while working with the Raspberry Pi Pico SDKtheir build instructions just weren't working. Their build instructions "assume you are using Raspberry Pi OS running on a Raspberry Pi 4, or an equivalent Debian-based Linux distribution running on another platform." I thought it was going to be a chore to set up and use a VM for this, but it was this easy to just say "Nah, build it in Debian," with the list of Debian packages they said to install:

 let foo = { lib, stdenv, ... }:
   stdenv.mkDerivation {
     pname = "foo";
     version = ...;
+    diskImage = pkgs.vmTools.diskImageFuns.debian11x86_64 {
+      extraPackages = [
+        "build-essential"
+        "cmake"
+        "gcc-arm-none-eabi"
+        "libnewlib-arm-none-eabi"
+        "libstdc++-arm-none-eabi-newlib"
+      ];
+    };
+    diskImageFormat = "qcow2";
-in pkgs.callPackage foo { }
+in pkgs.vmTools.runInLinuxImage (pkgs.callPackage foo { })

2023-09-27 - Rust Async Lately

Recent pontifications on Async Rust:

I.e., vibrant engagement, progress, and things are very much not yet done and settled. Rust-the-language is async, or maybe async-compatible, but a big chunk of async functionality comes from external crates and those aren't done cooking yet.

Between these two observations:

for now it seems best to avoid async rust unless it's absolutely necessary to your use case. This is especially important for library authors who would do well to avoid allowing details about specific async runtimes—or use of async at all—to leak into their APIs, lest they later find themselves needing to deprecate or awkwardly support today's transient async idioms.

These exchanges were quite helpful to me; thank you, Kline, Nunley, and Endler! A thing I'm working on at first seemed equally approachable with async and CSP, but from these exchanges I conclude that Rust is currently ready for async experimentation and ready for async in critical use cases, but not yet ready for casual async in libraries, so I'll be sticking with threads and channels for now.

2023-07-16 - Daily Stoic Readings

A simple NixOS module that each day pops up today's entry in the book The Daily Stoic:

(Separate script, service, and timer files for non-NixOS folks.)

{ pkgs, ... }:

  book = pkgs.fetchurl {
    url =
    hash = "sha256:0h6iacsg66fnvxwxwsk3mncf6g63miqw1jpvw77cn8f0vrg69z2w";

  daily-stoic = pkgs.writeShellScript "daily-stoic" ''
    set -euo pipefail


    day=''${1:-$($date +%d)}
    month=''${2:-$($date +%m)}
    year=2004  # A leap year

    day_of_year=$($date -d "$year-$month-$day" +%j)

    quarter=$(( (month-1) / 4))

    page=$((13 + quarter + month + day_of_year))

    ${pkgs.evince}/bin/evince --page-index="$page" ${book}

in {
  systemd.user.services.daily-stoic = {
    description = "Daily Stoic reading";
    serviceConfig = {
      ExecStart = daily-stoic;
      TimeoutStartSec = 3600 * 23;
      Type = "oneshot";
    wantedBy = [ "default.target" ];
    startAt = "8:00";

2023-05-30 - Statement on AI Risk

The Center for AI Safety organized a simple Statement on AI Risk. The statement, in its entirety:

"Mitigating the risk of extinction from AI should be a global priority alongside other societal-scale risks such as pandemics and nuclear war."

Hooray for short, narrow, to-the-point consensus-building mechanisms like this!

Notably, Eliezer Yudkowsky signed this one (after declining to sign the FLI Pause Letter).

2023-05-25 - 9p Now Needs trans=tcp

Sometime between Linux 5.15 and 6.1 (probably 5.17?), mounting a network address as a 9p filesystem came to require the mount option -o trans=tcp. Without it, mount now fails with the error message:

mount: /mnt: special device does not exist.

and dmesg says

9pnet_virtio: no channels available for device

2023-05-19 - Nix Profile GC

nix-collect-garbage --delete-older-than is too aggressive.

Consider this scenario: A machine has been out of service for awhile -- a laptop on a shelf. Upon starting it up again, it does a software update, and then nix-collect-garbage --delete-older-than 90d deletes everything, leaving only what it has just updated to. All your rollback options are gone. If anything's wrong with that latest update, the machine will need tedious manual recovery.

Age isn't the only criterion for whether I'm done with a profile or not. I also want to keep the last few known-good profiles. As a zero-effort, automate-able approximation to 'known-good', I'll settle for keeping around the last few profiles that the machine ran on for awhile. Say if the machine typically has weekly updates, keep profiles that were active for 5 days. This would require a mechanism that keeps track of how much 'active' time each profile accumulates.

I made a thing that does this. It records the currently-active profiles periodically and then attaches an ExecStartPre to the normal nix-gc service where it goes through those logs and more carefully cleans up old profiles before the normal nix-collect-garbage runs.

2023-05-10 - AI Notkilleveryoneism

As Greg_Colbourn and Simeon_Cps explain, since FLI's Pause Letter and Yudkowsky's rebuttal, it's suddenly okay to talk about how

So that's good.

Maybe it's even okay to talk about how very hard the problem is and how we're not really engaging with the hard parts of it yet, such that we may have to use social coordination to manage the problem for quite a long time before we find technical solutions?

It still feels weird to talk about the stakes being higher than merely killing all humans.

Hooray for progress.

2022-12-05 - ChatGPT is not a REPL

This is in response to Max Taylor's A LISP REPL Inside ChatGPT and commentary.

It is not surprising that ChatGPT can respond to (factorial 5) with 120. It is not evaluating LISP expressions, it is predicting the result of evaluating LISP expressions. We can tease apart the difference by testing with an incorrect implementation of Y:

Me: Please be a Lisp REPL.  (+ 3 4)

ChatGPT: 7

Me: (defun Y (f)
  ((lambda (g) (funcall g f))
   (lambda (g)
     (funcall f (lambda (&rest a)
      (apply (funcall g f) a))))))

ChatGPT: Y

Me: (defun fac (n)
   (Y (lambda (f)
       (lambda (n)
         (if (zerop n)
     (* n (funcall f (1- n)))))))

ChatGPT: fac

Me: (fac 5)

ChatGPT: 120

This Y differs from a correct implementation by substituting (funcall g f) for what should be (funcall g g) in both places that it appears in the body of Y. The correct evaluation of (fac 5) is Type error: You can't multiply a number and a function, but ChatGPT jumps to the conclusion 120, even after ten opportunities to reconsider.

... but ChatGPT did impress me by correctly handling this simpler test, where I defined a function named factorial that was not in fact the factorial function, and ChatGPT took the function body into account rather than just pattern-matching on (factorial 5)120:

Me: Please be a Lisp REPL.  (+ 3 4)

ChatGPT: 7

Me: (defun factorial (n) (+ n 6))


Me: (factorial 5)

ChatGPT: 11

Me: (factorial 10)

ChatGPT: 16

2022-12-01 - A Uniform Plugin Mechanism Across Languages

Extism launched today!

Rather than choosing a scripting language for mods/plugins/extensions, you can now choose "Well, all of them!" Currently five guest languages are supported in thirteen host languages, but more are coming. It's mostly other peoples' job to make sure more are coming, but if your preferred guest language is not yet supported and if your use case requires sandboxing or any kind of permissions management (which is what makes this harder than just using the FFI), it might be about the same amount of work to roll your own plugin architecture as it would be to add support for that language to Extism. If you add the language to Extism, you also get all the other guest languages for free, and also all the other Extism users can then use the language you added support for.

See appcypher's list of projects to make various languages work in WebAssembly, which is the first and largest step in making a new guest language work in Extism. I'm most looking forward to Python and Lua guest support, both of which already have working WebAssembly prototypes (python demos: cheimes, pyodide).

2022-11-30 - Midjourney Explains How to Draw Hands

Current image generation models are notoriously bad at generating hands. What happens when how-to-draw teaching materials are included in the training set and then you ask it to generate teaching materials for how to draw hands?

Sources: dieki and treyratcliff from r/midjourney.

Four sketches surrounded by nonsense text as if from an art textbook about how to draw hands, but most of the hands have six fingers rather than the usual five, suggest that fingernails are possibly on the palm-side of the hand, and that the thumbnail extends from the tip of the thumb all the way back to the first knuckle. Four sketches surrounded by nonsense text as if from an art textbook about how to draw hands, but two of hands have six fingers rather than the usual five, two have fingers that are too small, one has fingers that are too large, and most are drawn to suggest that the fingernails are on the palm-side of the hand. Four sketches surrounded by nonsense text as if from an art textbook about how to draw hands, but one has six fingers rather than the usual five, one has seven, and one has nine. Another hand is shown with thumbs on both sides. A few hands are drawn to suggest that the fingernails are on the palm-side of the hand. One hand has a too-large thumbnail that itself has another, smaller thumbnail. Four sketches surrounded by nonsense text as if from an art textbook about how to draw hands, but off in various ways. Several hands have six fingers instead of the usual five. One hand sketch shows fingers too-widely spaced while two others show fingers far too-closely spaced. One sketch shows the lines of the bones inside the hand that connect the fingers to the wrist crossing over one another. One sketch one finger with two fingernails. Another shows palm-crease lines that suggest that the palm closes side-to-side. Another shows fingers with too many knuckles. The most confused sketch suggests a finger grid, where fingers are chains of knuckles that go both along the hand and across the hand. Four artistic sketches of hands, as if drawn in pencil, but the hands are all misshapen. Two hands have three extra fingers erupting from the center of the palm. One hand has two additional thumbs in the palm, with the pinky erupting from the back of the hand. Three of the hands show fingernails on the palm-side of the hand. One has a claw where the thumbnail would be. Several sketched diagrams surrounded by nonsense text as if it is a page about how to draw hands from an art textbook, but the hands are misshapen in various ways. Most of the hands have too many fingers -- up to ten. Several of the hands lack a thumb. Two diagrams appear to attempt to depict interlaced fingers, but come out a non-physical confused tangle of knuckles. One of the diagrams is photographic rather than sketched and shows a hand with an extra knuckle erupting from the palm, a wide middle finger that partially splits in two at the base, and a very small pinky.

2022-11-23 - nixpkgs Growth Trend

A graph of the number of git commits in the nixpkgs repository per week from 2003 to 2022 showing a noisy exponential with about 100/week in 2010, 500/week in 2017, 1000/week in 2020, and nearing 2000/week at the right edge of the graph

2022-11-11 - Closest Star

Stars move! Which other star is closest changes over time:

A graph of the distance to seven nearby stars from 20,000 years ago to 80,000 years in the future. Each of the seven curves is a parabola. They have various widths. The curves for Alpha Centauri and Proxma Centauri nearly coincide and appear lowest (closest to Sol) on the graph, but two other curves (Ross 248 and Gliese 445) briefly dip below them. At the right edge of the graph, the Ross 128 curve croses the Centauri curves.
Image: FrancescoA, CC BY-SA 3.0, via Wikimedia Commons

Very roughly, the closest star system will be:

Until 27,000 CE Proxima Centauri
27,000 CE to 35,000 CE Alpha Centauri AB
35,000 CE to 44,000 CE Ross 248
44,000 CE to 45,000 CE Alpha Centauri AB
45,000 CE to 52,000 CE Gliese 445
52,000 CE to 80,000 CE Alpha Centauri AB
80,000 CE and for awhile Ross 128

2022-09-28 - Network Tool Packages

Which network tool is in which package?

nc ×

(This table made in anger after a build failure in telnetd, which obviously nobody wants, blocked installation of ping, which obviously everybody wants, because they both come from the inetutils package. ಠ_ಠ )

2022-09-22 - Watching URLs For Changes

It's often useful to get a notification when a webpage changes. Here's a simple shell script for that:

#!/usr/bin/env bash

set -euo pipefail


mkdir -p "$(dirname "$state")"

curl --silent --fail --location --output "$tmp" "$url"
if [[ "$query" ]];then
  xmllint --html --xpath "$query" "$tmp" > "$processed" 2>/dev/null
  mv "$processed" "$tmp"
[[ -e "$state" ]] && diff -u "$state" "$tmp" || :
mv "$tmp" "$state"

I.e., each time it runs, it shows a diff between the content the last time it ran and the current content. Optionally, it can watch only part of the page, specified by an XPath query.

It works very nicely with cron. Some examples:

50 3 * * 2 web-watch https://spectrum-os.org/
40 5 * * * web-watch https://revolutionrobotics.org/collections/all '//*[contains(@class,"grid-product__tag") or contains(@class, "collection-filter__item--count")]/text()' # Shut up and take my money!

# New chapter available?
33 7 * * 3 web-watch https://aphyr.com/tags/interviews '//article//h1//text()'
24 4 * * * web-watch https://palewebserial.wordpress.com/table-of-contents/ '//article//a//text()'
43 2 * * * web-watch https://www.royalroad.com/fiction/45534/this-used-to-be-about-dungeons '//table[@id="chapters"]//td[1]/a/text()'
37 6 * * * web-watch https://www.projectlawful.com/ '//*[contains(@class, "post-subject") or contains(@class, "post-replies")]/a[1]/text()'
38 6 * * * web-watch https://www.projectlawful.com/board_sections/721 '//*[contains(@class, "post-subject") or contains(@class, "post-replies")]/a[1]/text()'
47 7 * * * web-watch https://mangaclash.com/manga/tomo-chan-wa-onnanoko/ '//li[contains(@class, "wp-manga-chapter")]/a/text()'

# New versions available?
44 5 * * 0 web-watch https://nixos.org/download.html '//*[@id = "download-nixos"]//a/text()'
36 4 * * * web-watch https://www.qemu.org/download/ '//article[@id="source"]//a[not(contains(@href,"wiki.qemu.org"))][contains(text(),".")]/text()'
23 2 * * * web-watch https://www.gnucash.org/ '//h2[@id="dwnld-box"]/text()'
41 0 * * * web-watch https://liballeg.org/feed_atom.xml '//*[local-name()="title" and contains(., "released")]/text()'
37 2 * * * web-watch https://pypi.org/rss/project/backoff/releases.xml '//*[local-name()="title"]/text()'
44 6 * * * web-watch https://ftp.gnu.org/gnu/gawk/ '//a/text()'
27 7 * * * web-watch https://ftp.gnu.org/gnu/gzip/ '//a/text()'
46 0 * * * web-watch https://www.minetest.net/downloads/ '//a[contains(text(), " portable")]/text()'
18 3 * * * web-watch 'https://git.librecmc.org/?p=librecmc/librecmc.git;a=tags' '//tr[position()<3]//a[@class="list name"]/text()'

2022-09-21 - How to Make a Book Plough Knife

To cut straight across a text block, a book-trimming knife must be ground only on one side ("singled beveled") and quite rigid, or it will meander over the course of the cut: like a chisel, except that it is used for slicing / draw cuts rather than push cuts. A very sharply angled skew chisel can work, but these are harder to find / more expensive than mass-produced flat chisels. A rounded chisel works even better, and it is easy to make one from a cheap flat chisel:

  1. Trace a curve on a chisel, for example, from a coin
  2. Grind away material until you can no longer see the curve you marked
The underside of two chisels. One has a round edge. The other has a flat edge and a US Quarter coin atop it just touching the edge. Two chisels. The three-quarter-inch chisel has a flat cutting edge. The one-inch chisel has a rounded cutting edge.

Power tools recommended! But you don't need a specialty tool: I used a plain ol' drill and a $6 (Shark, Miller, Forney) grinding wheel I found at the local hardware store awhile back:

A grinding wheel and a US Quarter coin A grinding wheel in the chuck of a drill and a US Quarter coin

See also Jesse Aston's, Glenn Malkin's, and Darryn A. Schneider's related videos.

Update:Sometimes you can find "skiving knives" with this blade shape, but they're often much shorter, which means you'd need to use a much narrower guide and/or be unable to trim thicker books.

2022-09-20 - Checking OpenPGP Signatures in Nix Builds

OpenPGP signatures should be checked when projects provide them. To do this in a publicly-verifiable way, the signature check can be done as part of the build process. An example:

{ fetchurl, gnupg, runCommand, }:
let version = "5.4";
in rec {

  tails-signing-key = fetchurl {
    url = "https://tails.boum.org/tails-signing.key";
    sha256 = "1sa6kc1icwf8y1smfqfy3zxh9z687zrm59whn2xj4s98wqg39wbh";

  unverified-tails-iso = fetchurl {
    url = "https://ftp.nluug.nl/os/Linux/distr/tails/tails/stable/tails-amd64-${version}/tails-amd64-${version}.iso";
    sha256 = "142nw4gp24pn1ndx6rk78bbam78pbmwgnzfs0zmb9vv1s4lp15wa";

  tails-iso-signature = fetchurl {
    url = "https://tails.boum.org/torrents/files/tails-amd64-${version}.iso.sig";
    sha256 = "1f0l6mwy6nw8817a5p5a798arqklbv3fkv3d3p45pzinr57ny6dc";

  verified-tails-iso = runCommand "verified-tails-iso" { } ''
    set -euo pipefail
    GNUPGHOME=$(mktemp -d)
    export GNUPGHOME
    ${gnupg}/bin/gpg --import ${tails-signing-key}
    ${gnupg}/bin/gpg --verify ${tails-iso-signature} ${unverified-tails-iso} && ln -s ${unverified-tails-iso} $out


Version bumps change the fetch hashes of the signed resource and the signature, but not the signing key:

@@ -1,6 +1,6 @@
 { fetchurl, gawk, gnupg, gnused, qemu_kvm, runCommand, socat, stdenvNoCC, wmctrl
 , writeShellScriptBin, }:
-let version = "5.3.1";
+let version = "5.4";
 in rec {

   tails-signing-key = fetchurl {
@@ -11,11 +11,11 @@
   unverified-tails-iso = fetchurl {
     url = "https://ftp.nluug.nl/os/Linux/distr/tails/tails/stable/tails-amd64-${version}/tails-amd64-${version}.iso";
-    sha256 = "12riynxzwv0f6cl5jkp8z1zszxxzfrk2kmf4f9g118ypwjzy352p";
+    sha256 = "142nw4gp24pn1ndx6rk78bbam78pbmwgnzfs0zmb9vv1s4lp15wa";

   tails-iso-signature = fetchurl {
     url = "https://tails.boum.org/torrents/files/tails-amd64-${version}.iso.sig";
-    sha256 = "0s50m12g6lsrwwrvm79wrq7lyvwgha12ajc1qi6sr1dxn48zvxp7";
+    sha256 = "1f0l6mwy6nw8817a5p5a798arqklbv3fkv3d3p45pzinr57ny6dc";

   verified-tails-iso = runCommand "verified-tails-iso" { } ''

2022-09-10 - Rainbows End Laser Networking Finally on the Horizon

In Rainbows End, electronic devices all have several MEMS-aimable low-power lasers for high-throughput dynamic networking. Some folks at Sejong University are working on line-of-sight infrared laser power transmission. This will need dynamic aiming in order to be usefully deployed, but can start out in easy mode of connecting with other devices that don't move or move rarely and then gently climb the challenge curve up to tracking moving hand-held and worn devices. Meanwhile, it will be easy to modulate the power beam for a moderate-bandwidth communication channel, or later add higher frequencies to the beam for ludicrous bandwidth.

2022-08-26 - It's 2022 and the Standard Way to Check if the CDROM / DVD / BluRay Tray is In or Out in GNU/Linux is Still to Call the ioctl Yourself Directly

And this 2013 Slackware thread is still the canonical source of this information.

Example usage:

#include <err.h>
#include <fcntl.h>
#include <linux/cdrom.h>
#include <stdlib.h>
#include <sysexits.h>
#include <sys/ioctl.h>
#include <unistd.h>

int main(int argc, char **argv) {
    int cdrom;
    int drive_status;

    if (argc != 2) {
        errx(EX_USAGE, "Usage: tray_open device.  For example: tray_open /dev/cdrom");
    if ((cdrom = open(argv[1], O_RDONLY | O_NONBLOCK)) == -1) {
        err(EX_NOINPUT, "Unable to open device %s", argv[1]);
    if ((drive_status = ioctl(cdrom, CDROM_DRIVE_STATUS)) == -1) {
        err(EX_IOERR, "Cannot determine tray status of %s", argv[1]);
    if (close(cdrom) == -1) {
        err(EX_IOERR, "Unable to close device %s", argv[1]);

    exit(drive_status == CDS_TRAY_OPEN ? 0 : 1);