Week 12 of 2026

Development log of Erna

6 items
  1. Setup DevEnv with typst and tinymist (Typst LSP)
  2. Describe the problem
  3. Implement the solution and describe the math
  4. Improve the document layout
  5. Add some images from placedog.net (cute)
  6. Implement the solution

Setup DevEnv with typst and tinymist (Typst LSP)

On by Tad Lispy

new file mode 100644
index 0000000..c5d670d
--- /dev/null
+++ b/.envrc
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+if ! has nix_direnv_version || ! nix_direnv_version 3.1.0; then
+  source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.1.0/direnvrc" "sha256-yMJ2OVMzrFaDPn7q8nCBZFRYpL/f0RcHzhmw/i6btJM="
+fi
+
+export DEVENV_IN_DIRENV_SHELL=true
+
+watch_file flake.nix
+watch_file flake.lock
+if ! use flake . --no-pure-eval; then
+  echo "devenv could not be built. The devenv environment was not loaded. Make the necessary changes to devenv.nix and hit enter to try again." >&2
+fi
new file mode 100644
index 0000000..f03ce8a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.devenv
+.direnv
new file mode 100644
index 0000000..fdbdf50
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,849 @@
+{
+  "nodes": {
+    "cachix": {
+      "inputs": {
+        "devenv": [
+          "devenv"
+        ],
+        "flake-compat": [
+          "devenv",
+          "flake-compat"
+        ],
+        "git-hooks": [
+          "devenv",
+          "git-hooks"
+        ],
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1767714506,
+        "narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
+        "owner": "cachix",
+        "repo": "cachix",
+        "rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "latest",
+        "repo": "cachix",
+        "type": "github"
+      }
+    },
+    "cachix_2": {
+      "inputs": {
+        "devenv": [
+          "devenv",
+          "crate2nix"
+        ],
+        "flake-compat": [
+          "devenv",
+          "crate2nix"
+        ],
+        "git-hooks": "git-hooks",
+        "nixpkgs": "nixpkgs"
+      },
+      "locked": {
+        "lastModified": 1767714506,
+        "narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
+        "owner": "cachix",
+        "repo": "cachix",
+        "rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "latest",
+        "repo": "cachix",
+        "type": "github"
+      }
+    },
+    "cachix_3": {
+      "inputs": {
+        "devenv": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable"
+        ],
+        "flake-compat": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable"
+        ],
+        "git-hooks": "git-hooks_2",
+        "nixpkgs": "nixpkgs_2"
+      },
+      "locked": {
+        "lastModified": 1767714506,
+        "narHash": "sha256-WaTs0t1CxhgxbIuvQ97OFhDTVUGd1HA+KzLZUZBhe0s=",
+        "owner": "cachix",
+        "repo": "cachix",
+        "rev": "894c649f0daaa38bbcfb21de64be47dfa7cd0ec9",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "latest",
+        "repo": "cachix",
+        "type": "github"
+      }
+    },
+    "crate2nix": {
+      "inputs": {
+        "cachix": "cachix_2",
+        "crate2nix_stable": "crate2nix_stable",
+        "devshell": "devshell_2",
+        "flake-compat": "flake-compat_2",
+        "flake-parts": "flake-parts_2",
+        "nix-test-runner": "nix-test-runner_2",
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ],
+        "pre-commit-hooks": "pre-commit-hooks_2"
+      },
+      "locked": {
+        "lastModified": 1773440526,
+        "narHash": "sha256-OcX1MYqUdoalY3/vU67PEx8m6RvqGxX0LwKonjzXn7I=",
+        "owner": "nix-community",
+        "repo": "crate2nix",
+        "rev": "e697d3049c909580128caa856ab8eb709556a97b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "repo": "crate2nix",
+        "type": "github"
+      }
+    },
+    "crate2nix_stable": {
+      "inputs": {
+        "cachix": "cachix_3",
+        "crate2nix_stable": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable"
+        ],
+        "devshell": "devshell",
+        "flake-compat": "flake-compat",
+        "flake-parts": "flake-parts",
+        "nix-test-runner": "nix-test-runner",
+        "nixpkgs": "nixpkgs_3",
+        "pre-commit-hooks": "pre-commit-hooks"
+      },
+      "locked": {
+        "lastModified": 1769627083,
+        "narHash": "sha256-SUuruvw1/moNzCZosHaa60QMTL+L9huWdsCBN6XZIic=",
+        "owner": "nix-community",
+        "repo": "crate2nix",
+        "rev": "7c33e664668faecf7655fa53861d7a80c9e464a2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "ref": "0.15.0",
+        "repo": "crate2nix",
+        "type": "github"
+      }
+    },
+    "devenv": {
+      "inputs": {
+        "cachix": "cachix",
+        "crate2nix": "crate2nix",
+        "flake-compat": "flake-compat_3",
+        "flake-parts": "flake-parts_3",
+        "git-hooks": "git-hooks_3",
+        "nix": "nix",
+        "nixd": "nixd",
+        "nixpkgs": [
+          "nixpkgs"
+        ],
+        "rust-overlay": "rust-overlay"
+      },
+      "locked": {
+        "lastModified": 1774052327,
+        "narHash": "sha256-gQhiHj8q5NAa8jGTmoaS8FRgo8bVoAL2difjmcLtdgo=",
+        "owner": "cachix",
+        "repo": "devenv",
+        "rev": "43c650cae3ca65b6095819e4613614c242588cd7",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "devenv",
+        "type": "github"
+      }
+    },
+    "devshell": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1768818222,
+        "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
+        "owner": "numtide",
+        "repo": "devshell",
+        "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "devshell",
+        "type": "github"
+      }
+    },
+    "devshell_2": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1768818222,
+        "narHash": "sha256-460jc0+CZfyaO8+w8JNtlClB2n4ui1RbHfPTLkpwhU8=",
+        "owner": "numtide",
+        "repo": "devshell",
+        "rev": "255a2b1725a20d060f566e4755dbf571bbbb5f76",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "devshell",
+        "type": "github"
+      }
+    },
+    "flake-compat": {
+      "locked": {
+        "lastModified": 1733328505,
+        "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
+        "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
+        "revCount": 69,
+        "type": "tarball",
+        "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
+      },
+      "original": {
+        "type": "tarball",
+        "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
+      }
+    },
+    "flake-compat_2": {
+      "locked": {
+        "lastModified": 1733328505,
+        "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
+        "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
+        "revCount": 69,
+        "type": "tarball",
+        "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
+      },
+      "original": {
+        "type": "tarball",
+        "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
+      }
+    },
+    "flake-compat_3": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1767039857,
+        "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
+        "type": "github"
+      },
+      "original": {
+        "owner": "edolstra",
+        "repo": "flake-compat",
+        "type": "github"
+      }
+    },
+    "flake-parts": {
+      "inputs": {
+        "nixpkgs-lib": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1768135262,
+        "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "type": "github"
+      }
+    },
+    "flake-parts_2": {
+      "inputs": {
+        "nixpkgs-lib": [
+          "devenv",
+          "crate2nix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1768135262,
+        "narHash": "sha256-PVvu7OqHBGWN16zSi6tEmPwwHQ4rLPU9Plvs8/1TUBY=",
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "rev": "80daad04eddbbf5a4d883996a73f3f542fa437ac",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "type": "github"
+      }
+    },
+    "flake-parts_3": {
+      "inputs": {
+        "nixpkgs-lib": [
+          "devenv",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1772408722,
+        "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "flake-parts",
+        "type": "github"
+      }
+    },
+    "git-hooks": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "crate2nix",
+          "cachix",
+          "flake-compat"
+        ],
+        "gitignore": "gitignore",
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "cachix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1765404074,
+        "narHash": "sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=",
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "rev": "2d6f58930fbcd82f6f9fd59fb6d13e37684ca529",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "type": "github"
+      }
+    },
+    "git-hooks_2": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "cachix",
+          "flake-compat"
+        ],
+        "gitignore": "gitignore_2",
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "cachix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1765404074,
+        "narHash": "sha256-+ZDU2d+vzWkEJiqprvV5PR26DVFN2vgddwG5SnPZcUM=",
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "rev": "2d6f58930fbcd82f6f9fd59fb6d13e37684ca529",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "type": "github"
+      }
+    },
+    "git-hooks_3": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "flake-compat"
+        ],
+        "gitignore": "gitignore_5",
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1772893680,
+        "narHash": "sha256-JDqZMgxUTCq85ObSaFw0HhE+lvdOre1lx9iI6vYyOEs=",
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "rev": "8baab586afc9c9b57645a734c820e4ac0a604af9",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "git-hooks.nix",
+        "type": "github"
+      }
+    },
+    "gitignore": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "cachix",
+          "git-hooks",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1709087332,
+        "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "type": "github"
+      }
+    },
+    "gitignore_2": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "cachix",
+          "git-hooks",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1709087332,
+        "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "type": "github"
+      }
+    },
+    "gitignore_3": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "pre-commit-hooks",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1709087332,
+        "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "type": "github"
+      }
+    },
+    "gitignore_4": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "pre-commit-hooks",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1709087332,
+        "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "type": "github"
+      }
+    },
+    "gitignore_5": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "git-hooks",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1709087332,
+        "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
+        "type": "github"
+      },
+      "original": {
+        "owner": "hercules-ci",
+        "repo": "gitignore.nix",
+        "type": "github"
+      }
+    },
+    "nix": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "flake-compat"
+        ],
+        "flake-parts": [
+          "devenv",
+          "flake-parts"
+        ],
+        "git-hooks-nix": [
+          "devenv",
+          "git-hooks"
+        ],
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ],
+        "nixpkgs-23-11": [
+          "devenv"
+        ],
+        "nixpkgs-regression": [
+          "devenv"
+        ]
+      },
+      "locked": {
+        "lastModified": 1773936165,
+        "narHash": "sha256-iL6V03FP1vLJ/YJr0KHcNP+0lyyM9pT4rnRSk57DSYc=",
+        "owner": "cachix",
+        "repo": "nix",
+        "rev": "185e962dbc1b4925f5da3d05725a11e2ecea4a14",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "devenv-2.32",
+        "repo": "nix",
+        "type": "github"
+      }
+    },
+    "nix-test-runner": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1588761593,
+        "narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
+        "owner": "stoeffel",
+        "repo": "nix-test-runner",
+        "rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "stoeffel",
+        "repo": "nix-test-runner",
+        "type": "github"
+      }
+    },
+    "nix-test-runner_2": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1588761593,
+        "narHash": "sha256-FKJykltAN/g3eIceJl4SfDnnyuH2jHImhMrXS2KvGIs=",
+        "owner": "stoeffel",
+        "repo": "nix-test-runner",
+        "rev": "c45d45b11ecef3eb9d834c3b6304c05c49b06ca2",
+        "type": "github"
+      },
+      "original": {
+        "owner": "stoeffel",
+        "repo": "nix-test-runner",
+        "type": "github"
+      }
+    },
+    "nixd": {
+      "inputs": {
+        "flake-parts": [
+          "devenv",
+          "flake-parts"
+        ],
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ],
+        "treefmt-nix": "treefmt-nix"
+      },
+      "locked": {
+        "lastModified": 1773634079,
+        "narHash": "sha256-49qb4QNMv77VOeEux+sMd0uBhPvvHgVc0r938Bulvbo=",
+        "owner": "nix-community",
+        "repo": "nixd",
+        "rev": "8ecf93d4d93745e05ea53534e8b94f5e9506e6bd",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-community",
+        "repo": "nixd",
+        "type": "github"
+      }
+    },
+    "nixpkgs": {
+      "locked": {
+        "lastModified": 1765186076,
+        "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs-src": {
+      "flake": false,
+      "locked": {
+        "lastModified": 1773597492,
+        "narHash": "sha256-hQ284SkIeNaeyud+LS0WVLX+WL2rxcVZLFEaK0e03zg=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "a07d4ce6bee67d7c838a8a5796e75dff9caa21ef",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs_2": {
+      "locked": {
+        "lastModified": 1765186076,
+        "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixos-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs_3": {
+      "locked": {
+        "lastModified": 1769433173,
+        "narHash": "sha256-Gf1dFYgD344WZ3q0LPlRoWaNdNQq8kSBDLEWulRQSEs=",
+        "owner": "NixOS",
+        "repo": "nixpkgs",
+        "rev": "13b0f9e6ac78abbbb736c635d87845c4f4bee51b",
+        "type": "github"
+      },
+      "original": {
+        "owner": "NixOS",
+        "ref": "nixpkgs-unstable",
+        "repo": "nixpkgs",
+        "type": "github"
+      }
+    },
+    "nixpkgs_4": {
+      "inputs": {
+        "nixpkgs-src": "nixpkgs-src"
+      },
+      "locked": {
+        "lastModified": 1773704619,
+        "narHash": "sha256-LKtmit8Sr81z8+N2vpIaN/fyiQJ8f7XJ6tMSKyDVQ9s=",
+        "owner": "cachix",
+        "repo": "devenv-nixpkgs",
+        "rev": "906534d75b0e2fe74a719559dfb1ad3563485f43",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "ref": "rolling",
+        "repo": "devenv-nixpkgs",
+        "type": "github"
+      }
+    },
+    "pre-commit-hooks": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "flake-compat"
+        ],
+        "gitignore": "gitignore_3",
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "crate2nix_stable",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1769069492,
+        "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=",
+        "owner": "cachix",
+        "repo": "pre-commit-hooks.nix",
+        "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "pre-commit-hooks.nix",
+        "type": "github"
+      }
+    },
+    "pre-commit-hooks_2": {
+      "inputs": {
+        "flake-compat": [
+          "devenv",
+          "crate2nix",
+          "flake-compat"
+        ],
+        "gitignore": "gitignore_4",
+        "nixpkgs": [
+          "devenv",
+          "crate2nix",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1769069492,
+        "narHash": "sha256-Efs3VUPelRduf3PpfPP2ovEB4CXT7vHf8W+xc49RL/U=",
+        "owner": "cachix",
+        "repo": "pre-commit-hooks.nix",
+        "rev": "a1ef738813b15cf8ec759bdff5761b027e3e1d23",
+        "type": "github"
+      },
+      "original": {
+        "owner": "cachix",
+        "repo": "pre-commit-hooks.nix",
+        "type": "github"
+      }
+    },
+    "root": {
+      "inputs": {
+        "devenv": "devenv",
+        "nixpkgs": "nixpkgs_4",
+        "systems": "systems"
+      }
+    },
+    "rust-overlay": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1773630837,
+        "narHash": "sha256-zJhgAGnbVKeBMJOb9ctZm4BGS/Rnrz+5lfSXTVah4HQ=",
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "rev": "f600ea449c7b5bb596fa1cf21c871cc5b9e31316",
+        "type": "github"
+      },
+      "original": {
+        "owner": "oxalica",
+        "repo": "rust-overlay",
+        "type": "github"
+      }
+    },
+    "systems": {
+      "locked": {
+        "lastModified": 1681028828,
+        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+        "owner": "nix-systems",
+        "repo": "default",
+        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+        "type": "github"
+      },
+      "original": {
+        "owner": "nix-systems",
+        "repo": "default",
+        "type": "github"
+      }
+    },
+    "treefmt-nix": {
+      "inputs": {
+        "nixpkgs": [
+          "devenv",
+          "nixd",
+          "nixpkgs"
+        ]
+      },
+      "locked": {
+        "lastModified": 1772660329,
+        "narHash": "sha256-IjU1FxYqm+VDe5qIOxoW+pISBlGvVApRjiw/Y/ttJzY=",
+        "owner": "numtide",
+        "repo": "treefmt-nix",
+        "rev": "3710e0e1218041bbad640352a0440114b1e10428",
+        "type": "github"
+      },
+      "original": {
+        "owner": "numtide",
+        "repo": "treefmt-nix",
+        "type": "github"
+      }
+    }
+  },
+  "root": "root",
+  "version": 7
+}
new file mode 100644
index 0000000..74cf48c
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,39 @@
+{
+  inputs = {
+    nixpkgs.url = "github:cachix/devenv-nixpkgs/rolling";
+    systems.url = "github:nix-systems/default";
+    devenv.url = "github:cachix/devenv";
+    devenv.inputs.nixpkgs.follows = "nixpkgs";
+  };
+
+  nixConfig = {
+    extra-trusted-public-keys = "devenv.cachix.org-1:w1cLUi8dv3hnoSPGAuibQv+f9TZLr6cv/Hm9XgU50cw=";
+    extra-substituters = "https://devenv.cachix.org";
+  };
+
+  outputs = { self, nixpkgs, devenv, systems, ... } @ inputs:
+    let
+      forEachSystem = nixpkgs.lib.genAttrs (import systems);
+    in
+    {
+      devShells = forEachSystem
+        (system:
+          let
+            pkgs = nixpkgs.legacyPackages.${system};
+          in
+          {
+            default = devenv.lib.mkShell {
+              inherit inputs pkgs;
+              modules = [
+                {
+                  # https://devenv.sh/reference/options/
+                  packages = with pkgs; [
+                    typst
+                    tinymist
+                  ];
+                }
+              ];
+            };
+          });
+    };
+}
new file mode 100644
index 0000000..d4c65e2
--- /dev/null
+++ b/playground.typ
@@ -0,0 +1,11 @@
+= Layout Playground
+
++ First thing
++ Second thing
+
+Here?
+
+There?
+
+#circle(fill: red, width: 100% )[#align(center)[Hello there]]
+

Describe the problem

On by Tad Lispy

index d4c65e2..b166f27 100644
--- a/playground.typ
+++ b/playground.typ
@@ -1,11 +1,17 @@
== Layout Playground
=
-+ First thing
-+ Second thing
+#let images = ((60, 120), (120, 200), (100, 40), (300, 250))
=
-Here?
+We have #images.len() images.
=
-There?
+#for ((width, height)) in images {
+    rect(width: width * 1pt, height: height * 1pt, align(center + horizon)[#width × #height])
+}
+
+The goal is to put them in a row, so that:
+
++ The whole available width of the page is used.
++ All images have the same height.
++ The aspect ratio of every image is preserved.
=
-#circle(fill: red, width: 100% )[#align(center)[Hello there]]
=

Implement the solution and describe the math

On by Tad Lispy

I haven't figured out yet how to get the actual dimensions of the parent container. There are layout and measure functions, but they confuse me.

index b166f27..340560d 100644
--- a/playground.typ
+++ b/playground.typ
@@ -1,11 +1,25 @@
== Layout Playground
=
-#let images = ((60, 120), (120, 200), (100, 40), (300, 250))
+#let images = (
+    (60, 120, red),
+    (120, 200, green),
+    (100, 30, blue),
+    (300, 250, yellow)
+)
=
=We have #images.len() images.
=
-#for ((width, height)) in images {
-    rect(width: width * 1pt, height: height * 1pt, align(center + horizon)[#width × #height])
+#let rects = images.map( image => {
+    let (width, height, color) = image
+    rect(
+        width: width * 1pt,
+        height: height * 1pt,
+        fill: color,
+        align(center + horizon)[#width × #height])
+})
+
+#for r in rects {
+    r
=}
=
=The goal is to put them in a row, so that:
@@ -15,3 +29,127 @@ The goal is to put them in a row, so that:
=+ The aspect ratio of every image is preserved.
=
=
+== Step 1
+
+Calculate individual aspect ratios as $w / h$.
+
+#let aspects = images.map(size => (
+    width: size.at(0) * 1pt,
+    height: size.at(1) * 1pt,
+    ratio: size.at(0) / size.at(1))
+)
+
+#table(columns: 2,
+    [*Size*], [*Aspect ratio*],
+    ..for (width, height, ratio) in aspects {
+        ([#width × #height], [#calc.round(digits: 3, ratio)])
+    }
+)
+
+== Step 2
+
+Sum the aspect ratios of all the images to get the total aspect ratio of the row of images.
+
+#let total_aspect_ratio = aspects.map(a => a.ratio).sum()
+
+#rect(
+    height: 100% / total_aspect_ratio,
+    width: 100%,
+    align(center + horizon)[1 × #calc.round(digits: 3, total_aspect_ratio)]
+)
+
+== Step 3
+
+Take available width of the parent container (e.g. page - margins).
+
+// #let available_width = layout(size => size.width)
+// FIXME: This gives content, I want a Length. For now let's just assume it:
+#let available_width = 300pt
+
+#let straightedge = line(length: available_width)
+
+Available width: #available_width
+#straightedge
+
+For each image:
+
++ calculate the `fraction` of the width it can take, as `image_aspect_ratio / total_aspect_ratio`.
++ calculate the `final_width` as `available_width * fraction`
++ calculate the `scaling_factor` as `final_width / image_width`
++ scale the image by the `scaling_factor`
+
+
+
+#box()[
+    #grid(
+        columns: images.len(),
+    
+        ..rects.enumerate().map(((index, this_rect)) => {
+            let aspect = aspects.at(index)
+            let image_aspect_ratio = aspect.ratio
+            let width_fraction = image_aspect_ratio / total_aspect_ratio
+            let final_width = width_fraction * available_width
+            let scaling_factor = final_width / aspect.width * 100%
+
+            scale(this_rect, scaling_factor, reflow: true)
+        })
+    )
+]
+#straightedge
+
+Adding gaps (gutter) between images is not difficult. Just subtract the width of the gutter (its length × number of images - 1) from the available space.
+
+#let gutter = 2pt
+
+
+#let total_gutter = gutter * (rects.len() - 1)
+#box()[
+    With gutter of #gutter
+
+    #straightedge
+    #grid(
+        columns: images.len(),
+        column-gutter: gutter,
+
+        ..rects.enumerate().map(((index, this_rect)) => {
+            let aspect = aspects.at(index)
+            let image_aspect_ratio = aspect.ratio
+            let width_fraction = image_aspect_ratio / total_aspect_ratio
+            let final_width = width_fraction * (available_width - total_gutter)
+            let scaling_factor = final_width / aspect.width * 100%
+
+            scale(this_rect, scaling_factor, reflow: true)
+        })
+    )
+]
+
+#let gutter = 16pt
+
+
+#let total_gutter = gutter * (rects.len() - 1)
+#box()[
+    With gutter of #gutter
+
+    #straightedge
+    #grid(
+        columns: images.len(),
+        column-gutter: gutter,
+
+        ..rects.enumerate().map(((index, this_rect)) => {
+            let aspect = aspects.at(index)
+            let image_aspect_ratio = aspect.ratio
+            let width_fraction = image_aspect_ratio / total_aspect_ratio
+            let final_width = width_fraction * (available_width - total_gutter)
+            let scaling_factor = final_width / aspect.width * 100%
+
+            scale(this_rect, scaling_factor, reflow: true)
+        })
+    )
+]
+
+
+*TODO*: Pack it as a function
+
+*TODO*: Use actual images (figure out how to get their dimensions or aspect ratios)
+
+*TODO*: How to get the actual width of a parent container

Improve the document layout

On by Tad Lispy

Put the "images" in a grid, break page on h2

index 340560d..fbfd23b 100644
--- a/playground.typ
+++ b/playground.typ
@@ -18,9 +18,11 @@ We have #images.len() images.
=        align(center + horizon)[#width × #height])
=})
=
-#for r in rects {
-    r
-}
+#grid(
+    columns: (auto, auto),
+    gutter: 10pt,
+    ..rects
+)
=
=The goal is to put them in a row, so that:
=
@@ -28,6 +30,10 @@ The goal is to put them in a row, so that:
=+ All images have the same height.
=+ The aspect ratio of every image is preserved.
=
+#show heading.where(depth: 2): body => {
+    pagebreak(weak: true)
+    body
+}
=
=== Step 1
=

Add some images from placedog.net (cute)

On by Tad Lispy

Get the dimensions of sample 2 images.

new file mode 100644
index 0000000..e637019
Binary files /dev/null and b/300x600.jpg differ
new file mode 100644
index 0000000..64dd9f1
Binary files /dev/null and b/360x480.jpg differ
new file mode 100644
index 0000000..1dfa8ab
Binary files /dev/null and b/500x480.jpg differ
new file mode 100644
index 0000000..d0c7387
Binary files /dev/null and b/780x300.jpg differ
index fbfd23b..cb902a2 100644
--- a/playground.typ
+++ b/playground.typ
@@ -154,8 +154,26 @@ Adding gaps (gutter) between images is not difficult. Just subtract the width of
=]
=
=
-*TODO*: Pack it as a function
+== Use actual images
+
+#let i = image("300x600.jpg")
+
+#box[
+    The following image size is #context measure(i)
+
+    #i
+]
+
+#let i = image("500x480.jpg")
+#box[
+    The following image size is #context measure(i)
+
+    #i
+]
+
+== *TODO*: Pack it as a function
+
+
+== *TODO*: How to get the actual width of a parent container
=
-*TODO*: Use actual images (figure out how to get their dimensions or aspect ratios)
=
-*TODO*: How to get the actual width of a parent container

Implement the solution

On by Tad Lispy

Works with actual images and fills the parent container. Wrapped in a image-row function.

index cb902a2..ef39395 100644
--- a/playground.typ
+++ b/playground.typ
@@ -171,9 +171,121 @@ Adding gaps (gutter) between images is not difficult. Just subtract the width of
=    #i
=]
=
-== *TODO*: Pack it as a function
+== How to get the actual width of a parent container
=
+#context layout(size => [
+    The parent layout can only be accessed inside a `context`. Here the width is  #size.width. It is best to wrap the whole computation in ```typst context layout(size => { ... })``` expression.
+])
=
-== *TODO*: How to get the actual width of a parent container
+== Pack it as a function
=
+#let image-row(gutter: 0pt, ..paths) = {
+    context {
+        let images = paths.pos().map(path => {
+            let content = image(path)
+            let size = measure(content)
+            let ratio = size.width / size.height
+            return (path: path, content: content, ratio: ratio, ..size)
+        })
+
+
+        let total_ratio = images.fold(0, (acc, item) => acc + item.ratio)
+        let total_gutter = gutter * (images.len() - 1)
+
+        layout(parent => grid(
+            columns: images.len(),
+            column-gutter: gutter,
+
+            ..images.map((item) => {
+                let width_fraction = item.ratio / total_ratio
+                let final_width = width_fraction * (parent.width - total_gutter)
+                let scaling_factor = final_width / item.width * 100%
+
+                scale(item.content, scaling_factor, reflow: true)
+            })
+        ))
+    }
+}
+
+Now we have everything we need! Here are some examples.
+
+=== 4 images, no gutter
+
+#image-row(
+    "300x600.jpg",
+    "500x480.jpg",
+    "360x480.jpg",
+    "780x300.jpg",
+)
+
+=== 2 image, gutter of 2t
+
+#image-row(
+    gutter: 2pt,
+    "300x600.jpg",
+    "780x300.jpg",
+)
+
+=== 1 image, gutter set, but has no effect
+
+#image-row(
+    gutter: 2pt,
+    "780x300.jpg",
+)
+
+=== Inside a figure
+
+
+#figure(
+    caption: [Some lovely dogs here])[
+        #image-row(
+            gutter: 4pt,
+            "360x480.jpg",
+            "780x300.jpg",
+            "300x600.jpg",
+            "500x480.jpg",
+        )
+    ]
+
+#pagebreak()
+
+=== Inside containers of various sizes
+
+#for width in (40%, 60%, 85%) {
+    align(center, rect(
+        width: width       
+        
+    )[
+        #layout(size => [This container is #size.width wide])
+        #image-row(
+            gutter: 4pt,
+            "360x480.jpg",
+            "780x300.jpg",
+            "300x600.jpg",
+            "500x480.jpg",
+        )
+    ])
+}
+
+
+#pagebreak()
+
+=== Multiple rows
+
+#image-row(
+    gutter: 4pt,
+    "300x600.jpg",
+    "780x300.jpg",
+)
+#v(4pt, weak: true)
+#image-row(
+    gutter: 4pt,
+    "360x480.jpg",
+    "500x480.jpg",
+)
+
+== TODO: Implement `image-rows` function
=
++ Multiple rows, each in an array
++ Automatic vertical spacing
++ In a box, to prevent page breaks inside