about summary refs log tree commit diff
path: root/users/tazjin
diff options
context:
space:
mode:
Diffstat (limited to 'users/tazjin')
-rw-r--r--users/tazjin/OWNERS6
-rw-r--r--users/tazjin/aoc2019/default.nix26
-rw-r--r--users/tazjin/aoc2019/solution-day1.el28
-rw-r--r--users/tazjin/aoc2019/solution-day2.el53
-rw-r--r--users/tazjin/aoc2019/solution-day3.el64
-rw-r--r--users/tazjin/aoc2019/solution-day4.el73
-rw-r--r--users/tazjin/aoc2020/default.nix26
-rw-r--r--users/tazjin/aoc2020/solution-day1.el44
-rw-r--r--users/tazjin/aoc2020/solution-day2.el54
-rw-r--r--users/tazjin/aoc2020/solution-day3.el43
-rw-r--r--users/tazjin/aoc2020/solution-day4.el98
-rw-r--r--users/tazjin/aoc2020/solution-day5.el61
-rw-r--r--users/tazjin/aoc2020/solution-day6.el40
-rw-r--r--users/tazjin/aoc2020/solution-day7.el92
-rw-r--r--users/tazjin/aoc2020/solution-day8.el63
-rw-r--r--users/tazjin/aoc2022/day1.rs27
-rw-r--r--users/tazjin/aoc2023/day1.el52
-rw-r--r--users/tazjin/aoc2023/day2.el64
-rw-r--r--users/tazjin/aoc2023/day3.el110
-rw-r--r--users/tazjin/atom-feed/default.nix141
-rw-r--r--users/tazjin/avatar.jpegbin38763 -> 81953 bytes
-rw-r--r--users/tazjin/blog/default.nix71
-rw-r--r--users/tazjin/blog/fragments.nix98
-rw-r--r--users/tazjin/blog/posts.nix23
-rw-r--r--users/tazjin/blog/posts/best-tools.md84
-rw-r--r--users/tazjin/blog/posts/nixery-layers.md6
-rw-r--r--users/tazjin/blog/posts/reliably-switch-buffers.md18
-rw-r--r--users/tazjin/blog/posts/reversing-watchguard-vpn.md28
-rw-r--r--users/tazjin/blog/posts/thoughts.md142
-rw-r--r--users/tazjin/blog/posts/tvix-eval-talk-2023.md19
-rw-r--r--users/tazjin/chase-geese/default.nix13
-rw-r--r--users/tazjin/cloud-dns/oslo-pub8
-rw-r--r--users/tazjin/default.nix30
-rw-r--r--users/tazjin/dns/default.nix13
-rwxr-xr-xusers/tazjin/dns/import (renamed from users/tazjin/cloud-dns/import)5
-rw-r--r--users/tazjin/dns/kontemplate.works.zone (renamed from users/tazjin/cloud-dns/kontemplate-works)0
-rw-r--r--users/tazjin/dns/tazj.in.zone (renamed from users/tazjin/cloud-dns/root-tazj-in)0
-rw-r--r--users/tazjin/docs/install-zfs.md116
-rw-r--r--users/tazjin/dotfiles/default.nix3
-rw-r--r--users/tazjin/dotfiles/dunstrc54
-rw-r--r--users/tazjin/dt/CMakeLists.txt16
-rw-r--r--users/tazjin/dt/README.md11
-rw-r--r--users/tazjin/dt/default.nix15
-rw-r--r--users/tazjin/dt/dt.cc79
-rw-r--r--users/tazjin/elisp-deps/deps.el83
-rw-r--r--users/tazjin/emacs/config/bindings.el63
-rw-r--r--users/tazjin/emacs/config/custom.el27
-rw-r--r--users/tazjin/emacs/config/desktop.el331
-rw-r--r--users/tazjin/emacs/config/functions.el248
-rw-r--r--users/tazjin/emacs/config/init.el186
-rw-r--r--users/tazjin/emacs/config/look-and-feel.el102
-rw-r--r--users/tazjin/emacs/config/mail-setup.el16
-rw-r--r--users/tazjin/emacs/config/modes.el37
-rw-r--r--users/tazjin/emacs/config/settings.el44
-rw-r--r--users/tazjin/emacs/default.nix304
-rw-r--r--users/tazjin/finito/default.nix4
-rw-r--r--users/tazjin/finito/finito-core/src/lib.rs41
-rw-r--r--users/tazjin/finito/finito-door/src/lib.rs34
-rw-r--r--users/tazjin/finito/finito-postgres/src/error.rs26
-rw-r--r--users/tazjin/finito/finito-postgres/src/lib.rs191
-rw-r--r--users/tazjin/finito/finito-postgres/src/tests.rs11
-rw-r--r--users/tazjin/generator-example/.gitignore1
-rw-r--r--users/tazjin/generator-example/Cargo.lock124
-rw-r--r--users/tazjin/generator-example/Cargo.toml9
-rw-r--r--users/tazjin/generator-example/README.md11
-rw-r--r--users/tazjin/generator-example/src/main.rs115
-rw-r--r--users/tazjin/gio-list-apps/.gitignore1
-rw-r--r--users/tazjin/gio-list-apps/Cargo.lock616
-rw-r--r--users/tazjin/gio-list-apps/Cargo.toml11
-rw-r--r--users/tazjin/gio-list-apps/default.nix14
-rw-r--r--users/tazjin/gio-list-apps/src/lib.rs31
-rw-r--r--users/tazjin/gruber-darker.qss508
-rw-r--r--users/tazjin/hanebuschtag.txt66
-rw-r--r--users/tazjin/home/khamovnik.nix10
-rw-r--r--users/tazjin/home/persistence.nix42
-rw-r--r--users/tazjin/home/shared.nix91
-rw-r--r--users/tazjin/home/tverskoy.nix18
-rw-r--r--users/tazjin/home/zamalek.nix11
-rw-r--r--users/tazjin/homepage/default.nix72
-rw-r--r--users/tazjin/homepage/entries.nix111
-rw-r--r--users/tazjin/homepage/feed.nix29
-rw-r--r--users/tazjin/homepage/header.html21
-rw-r--r--users/tazjin/homepage/static/jetbrains-mono-bold-italic.woff2bin53364 -> 0 bytes
-rw-r--r--users/tazjin/homepage/static/jetbrains-mono-bold.woff2bin49892 -> 0 bytes
-rw-r--r--users/tazjin/homepage/static/jetbrains-mono-italic.woff2bin50936 -> 0 bytes
-rw-r--r--users/tazjin/homepage/static/jetbrains-mono.woff2bin48700 -> 0 bytes
-rw-r--r--users/tazjin/homepage/static/tazjin.css166
-rw-r--r--users/tazjin/keys.nix10
-rw-r--r--users/tazjin/keys/default.nix12
-rw-r--r--users/tazjin/kinesis/README.md10
-rwxr-xr-xusers/tazjin/kinesis/advantage2/qwerty.txt6
-rw-r--r--users/tazjin/nisp/transform.el137
-rw-r--r--users/tazjin/nix.svg50
-rw-r--r--users/tazjin/nixos/.gitignore1
-rw-r--r--users/tazjin/nixos/README.md17
-rw-r--r--users/tazjin/nixos/camden/default.nix110
-rw-r--r--users/tazjin/nixos/default.nix52
-rw-r--r--users/tazjin/nixos/frog/default.nix92
-rw-r--r--users/tazjin/nixos/khamovnik/default.nix133
-rw-r--r--users/tazjin/nixos/koptevo/default.nix187
-rw-r--r--users/tazjin/nixos/modules/airsonic.nix32
-rw-r--r--users/tazjin/nixos/modules/chromium.nix30
-rw-r--r--users/tazjin/nixos/modules/default.nix2
-rw-r--r--users/tazjin/nixos/modules/desktop.nix55
-rw-r--r--users/tazjin/nixos/modules/fonts.nix24
-rw-r--r--users/tazjin/nixos/modules/geesefs.nix38
-rw-r--r--users/tazjin/nixos/modules/hidpi.nix19
-rw-r--r--users/tazjin/nixos/modules/home-config.nix19
-rw-r--r--users/tazjin/nixos/modules/laptop.nix15
-rw-r--r--users/tazjin/nixos/modules/miniflux.nix22
-rw-r--r--users/tazjin/nixos/modules/persistence.nix26
-rw-r--r--users/tazjin/nixos/modules/physical.nix105
-rw-r--r--users/tazjin/nixos/modules/predlozhnik.nix10
-rw-r--r--users/tazjin/nixos/modules/tgsa.nix29
-rw-r--r--users/tazjin/nixos/tverskoy/default.nix175
-rw-r--r--users/tazjin/nixos/zamalek/default.nix88
-rw-r--r--users/tazjin/presentations/bootstrapping-2018/default.nix38
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/README.md12
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/cppnix-example-lexer.cpp13
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/cppnix-example-smuggling.cpp12
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/default.nix63
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/presentation.pdfpc98
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/presentation.tex148
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/.gitignore2
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/Cargo.lock899
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/Cargo.toml7
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/index.html7
-rw-r--r--users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/src/main.rs41
-rw-r--r--users/tazjin/renderMarkdown.nix9
-rw-r--r--users/tazjin/rlox/.gitignore3
-rw-r--r--users/tazjin/rlox/Cargo.lock6
-rw-r--r--users/tazjin/rlox/Cargo.toml10
-rw-r--r--users/tazjin/rlox/README.md7
-rw-r--r--users/tazjin/rlox/default.nix5
-rw-r--r--users/tazjin/rlox/examples/builtins.lox1
-rw-r--r--users/tazjin/rlox/examples/fib.lox6
-rw-r--r--users/tazjin/rlox/examples/func.lox5
-rw-r--r--users/tazjin/rlox/examples/hello.lox34
-rw-r--r--users/tazjin/rlox/examples/if.lox7
-rw-r--r--users/tazjin/rlox/examples/scope.lox19
-rw-r--r--users/tazjin/rlox/examples/scope2.lox10
-rw-r--r--users/tazjin/rlox/examples/slow.lox9
-rw-r--r--users/tazjin/rlox/examples/var.lox8
-rw-r--r--users/tazjin/rlox/rustfmt.toml1
-rw-r--r--users/tazjin/rlox/src/bytecode/chunk.rs93
-rw-r--r--users/tazjin/rlox/src/bytecode/compiler.rs702
-rw-r--r--users/tazjin/rlox/src/bytecode/errors.rs51
-rw-r--r--users/tazjin/rlox/src/bytecode/interner/mod.rs87
-rw-r--r--users/tazjin/rlox/src/bytecode/interner/tests.rs24
-rw-r--r--users/tazjin/rlox/src/bytecode/mod.rs30
-rw-r--r--users/tazjin/rlox/src/bytecode/opcode.rs56
-rw-r--r--users/tazjin/rlox/src/bytecode/tests.rs152
-rw-r--r--users/tazjin/rlox/src/bytecode/value.rs37
-rw-r--r--users/tazjin/rlox/src/bytecode/vm.rs272
-rw-r--r--users/tazjin/rlox/src/main.rs71
-rw-r--r--users/tazjin/rlox/src/scanner.rs284
-rw-r--r--users/tazjin/rlox/src/treewalk/errors.rs59
-rw-r--r--users/tazjin/rlox/src/treewalk/interpreter.rs498
-rw-r--r--users/tazjin/rlox/src/treewalk/interpreter/builtins.rs25
-rw-r--r--users/tazjin/rlox/src/treewalk/interpreter/tests.rs97
-rw-r--r--users/tazjin/rlox/src/treewalk/mod.rs6
-rw-r--r--users/tazjin/rlox/src/treewalk/parser.rs700
-rw-r--r--users/tazjin/rlox/src/treewalk/resolver.rs199
-rw-r--r--users/tazjin/russian/helpers.el7
-rw-r--r--users/tazjin/russian/roots.el28
-rw-r--r--users/tazjin/russian/russian.el97
-rw-r--r--users/tazjin/russian/words.el723
-rw-r--r--users/tazjin/rustfmt.toml22
-rw-r--r--users/tazjin/secrets/default.nix3
-rw-r--r--users/tazjin/secrets/geesefs-tazjins-files.age18
-rw-r--r--users/tazjin/secrets/miniflux.age14
-rw-r--r--users/tazjin/secrets/secrets.nix16
-rw-r--r--users/tazjin/secrets/tgsa-yandex.agebin0 -> 3082 bytes
-rw-r--r--users/tazjin/tgsa/.gitignore3
-rw-r--r--users/tazjin/tgsa/Cargo.lock1567
-rw-r--r--users/tazjin/tgsa/Cargo.toml18
-rw-r--r--users/tazjin/tgsa/default.nix17
-rw-r--r--users/tazjin/tgsa/src/main.rs403
-rw-r--r--users/tazjin/tgsa/src/translate.rs191
-rw-r--r--users/tazjin/tvix-eval-value.d298
-rw-r--r--users/tazjin/yddns/.gitignore1
-rw-r--r--users/tazjin/yddns/Cargo.lock1425
-rw-r--r--users/tazjin/yddns/Cargo.toml12
-rw-r--r--users/tazjin/yddns/default.nix9
-rw-r--r--users/tazjin/yddns/src/main.rs142
185 files changed, 15422 insertions, 1664 deletions
diff --git a/users/tazjin/OWNERS b/users/tazjin/OWNERS
index c86f6eaa6a..ba1c065348 100644
--- a/users/tazjin/OWNERS
+++ b/users/tazjin/OWNERS
@@ -1,3 +1,3 @@
-inherited: false
-owners:
-  - tazjin
+set noparent
+
+tazjin
diff --git a/users/tazjin/aoc2019/default.nix b/users/tazjin/aoc2019/default.nix
new file mode 100644
index 0000000000..a1798f4001
--- /dev/null
+++ b/users/tazjin/aoc2019/default.nix
@@ -0,0 +1,26 @@
+# Solutions for Advent of Code 2019, written in Emacs Lisp.
+#
+# For each day a new file is created as "solution-day$n.el".
+{ depot, ... }:
+
+let
+  inherit (builtins) attrNames filter head listToAttrs match readDir;
+  dir = readDir ./.;
+  matchSolution = match "solution-(.*)\.el";
+  isSolution = f: (matchSolution f) != null;
+  getDay = f: head (matchSolution f);
+
+  solutionFiles = filter (e: dir."${e}" == "regular" && isSolution e) (attrNames dir);
+  solutions = map
+    (f:
+      let day = getDay f; in {
+        name = day;
+        value = depot.nix.writeElispBin {
+          name = "aoc2019";
+          deps = p: with p; [ dash s ht ];
+          src = ./. + ("/" + f);
+        };
+      })
+    solutionFiles;
+in
+listToAttrs solutions
diff --git a/users/tazjin/aoc2019/solution-day1.el b/users/tazjin/aoc2019/solution-day1.el
new file mode 100644
index 0000000000..d805c22ec8
--- /dev/null
+++ b/users/tazjin/aoc2019/solution-day1.el
@@ -0,0 +1,28 @@
+;; Advent of Code 2019 - Day 1
+(require 'dash)
+
+;; Puzzle 1:
+
+(defvar day-1/input
+  '(83285 96868 121640 51455 128067 128390 141809 52325 68310 140707 124520 149678
+          87961 52040 133133 52203 117483 85643 84414 86558 65402 122692 88565 61895
+          126271 128802 140363 109764 53600 114391 98973 124467 99574 69140 144856
+          56809 149944 138738 128823 82776 77557 51994 74322 64716 114506 124074
+          73096 97066 96731 149307 135626 121413 69575 98581 50570 60754 94843 72165
+          146504 53290 63491 50936 79644 119081 70218 85849 133228 114550 131943
+          67288 68499 80512 148872 99264 119723 68295 90348 146534 52661 99146 95993
+          130363 78956 126736 82065 77227 129950 97946 132345 107137 79623 148477
+          88928 118911 75277 97162 80664 149742 88983 74518))
+
+(defun calculate-fuel (mass)
+  (- (/ mass 3) 2))
+
+(message "Solution to day1/1: %d" (apply #'+ (-map #'calculate-fuel day-1/input)))
+
+;; Puzzle 2:
+(defun calculate-recursive-fuel (mass)
+  (let ((fuel (calculate-fuel mass)))
+    (if (< fuel 0) 0
+      (+ fuel (calculate-recursive-fuel fuel)))))
+
+(message "Solution to day1/2: %d" (apply #'+ (-map #'calculate-recursive-fuel day-1/input)))
diff --git a/users/tazjin/aoc2019/solution-day2.el b/users/tazjin/aoc2019/solution-day2.el
new file mode 100644
index 0000000000..6ecac1e201
--- /dev/null
+++ b/users/tazjin/aoc2019/solution-day2.el
@@ -0,0 +1,53 @@
+;; -*- lexical-binding: t; -*-
+;; Advent of Code 2019 - Day 2
+(require 'dash)
+(require 'ht)
+
+(defvar day2/input
+  [1 0 0 3 1 1 2 3 1 3 4 3 1 5 0 3 2 1 9 19 1 19 5 23 1 13 23 27 1 27 6 31
+     2 31 6 35 2 6 35 39 1 39 5 43 1 13 43 47 1 6 47 51 2 13 51 55 1 10 55
+     59 1 59 5 63 1 10 63 67 1 67 5 71 1 71 10 75 1 9 75 79 2 13 79 83 1 9
+     83 87 2 87 13 91 1 10 91 95 1 95 9 99 1 13 99 103 2 103 13 107 1 107 10
+     111 2 10 111 115 1 115 9 119 2 119 6 123 1 5 123 127 1 5 127 131 1 10
+     131 135 1 135 6 139 1 10 139 143 1 143 6 147 2 147 13 151 1 5 151 155 1
+     155 5 159 1 159 2 163 1 163 9 0 99 2 14 0 0])
+
+;; Puzzle 1
+
+(defun day2/single-op (f state idx)
+  (let* ((a (aref state (aref state (+ 1 idx))))
+         (b (aref state (aref state (+ 2 idx))))
+         (p (aref state (+ 3 idx)))
+         (result (funcall f a b)))
+    (aset state p (funcall f a b))))
+
+(defun day2/operate (state idx)
+  (pcase (aref state idx)
+    (99 (aref state 0))
+    (1 (day2/single-op #'+ state idx)
+       (day2/operate state (+ 4 idx)))
+    (2 (day2/single-op #'* state idx)
+       (day2/operate state (+ 4 idx)))
+    (other (error "Unknown opcode: %s" other))))
+
+(defun day2/program-with-inputs (noun verb)
+  (let* ((input (copy-tree day2/input t)))
+    (aset input 1 noun)
+    (aset input 2 verb)
+    (day2/operate input 0)))
+
+(message "Solution to day2/1: %s" (day2/program-with-inputs 12 2))
+
+;; Puzzle 2
+(let* ((used (ht))
+       (noun 0)
+       (verb 0)
+       (result (day2/program-with-inputs noun verb)))
+  (while (/= 19690720 result)
+    (setq noun (random 100))
+    (setq verb (random 100))
+    (unless (ht-get used (format "%d%d" noun verb))
+      (ht-set used (format "%d%d" noun verb) t)
+      (setq result (day2/program-with-inputs noun verb))))
+
+  (message "Solution to day2/2: %s%s" noun verb))
diff --git a/users/tazjin/aoc2019/solution-day3.el b/users/tazjin/aoc2019/solution-day3.el
new file mode 100644
index 0000000000..b7dfdd245f
--- /dev/null
+++ b/users/tazjin/aoc2019/solution-day3.el
@@ -0,0 +1,64 @@
+;; -*- lexical-binding: t; -*-
+;; Advent of Code 2019 - Day 3
+
+(require 'cl-lib)
+(require 'dash)
+(require 'ht)
+(require 's)
+
+(defvar day3/input/wire1
+  "R1010,D422,L354,U494,L686,U894,R212,U777,L216,U9,L374,U77,R947,U385,L170,U916,R492,D553,L992,D890,L531,U360,R128,U653,L362,U522,R817,U198,L126,D629,L569,U300,L241,U145,R889,D196,L450,D576,L319,D147,R985,U889,L941,U837,L608,D77,L864,U911,L270,D869,R771,U132,L249,U603,L36,D328,L597,U992,L733,D370,L947,D595,L308,U536,L145,U318,R55,D773,R175,D505,R483,D13,R780,U778,R445,D107,R490,U245,L587,U502,R446,U639,R150,U35,L455,D522,R866,U858,R394,D975,R513,D378,R58,D646,L374,D675,R209,U228,R530,U543,L480,U677,L912,D164,L573,U587,L784,D626,L994,U250,L215,U985,R684,D79,L877,U811,L766,U617,L665,D246,L408,U800,L360,D272,L436,U138,R240,U735,L681,U68,L608,D59,R532,D808,L104,U968,R887,U819,R346,U698,L317,U582,R516,U55,L303,U607,L457,U479,L510,D366,L583,U519,R878,D195,R970,D267,R842,U784,R9,D946,R833,D238,L232,D94,L860,D47,L346,U951,R491,D745,R849,U273,R263,U392,L341,D808,R696,U326,R886,D296,L865,U833,R241,U644,R729,D216,R661,D712,L466,D699,L738,U5,L556,D693,R912,D13,R48,U63,L877,U628,L689,D929,R74,U924,R612,U153,R417,U425,L879,D378,R79,D248,L3,U519,R366,U281,R439,D823,R149,D668,R326,D342,L213,D735,R504,U265,L718,D842,L565,U105,L214,U963,R518,D681,R642,U170,L111,U6,R697,U572,R18,U331,L618,D255,R534,D322,L399,U595,L246,U651,L836,U757,R417,D795,R291,U759,L568,U965,R828,D570,R350,U317,R338,D173,L74,D833,L650,D844,L70,U913,R594,U407,R674,D684,L481,D564,L128,D277,R851,D274,L435,D582,R469,U729,R387,D818,R443,U504,R414,U8,L842,U845,R275,U986,R53,U660,R661,D225,R614,U159,R477")
+
+(defvar day3/input/wire2
+  "L1010,D698,R442,U660,L719,U702,L456,D86,R938,D177,L835,D639,R166,D285,L694,U468,L569,D104,L234,D574,L669,U299,L124,D275,L179,D519,R617,U72,L985,D248,R257,D276,L759,D834,R490,U864,L406,U181,R911,U873,R261,D864,R260,U759,R648,U158,R308,D386,L835,D27,L745,U91,R840,U707,R275,U543,L663,U736,L617,D699,R924,U103,R225,U455,R708,U319,R569,U38,R315,D432,L179,D975,R519,D546,L295,U680,L685,U603,R262,D250,R7,U171,R261,U519,L832,U534,L471,U431,L474,U886,R10,D179,L79,D555,R452,U452,L832,U863,L367,U538,L237,D160,R441,U605,R942,U259,L811,D552,R646,D353,L225,D94,L35,D307,R752,U23,R698,U610,L379,D932,R698,D751,R178,D347,R325,D156,R471,D555,R558,D593,R773,U2,L955,U764,L735,U438,R364,D640,L757,U534,R919,U409,R361,U407,R336,D808,R877,D648,R610,U198,R340,U94,R795,D667,R811,U975,L965,D224,R565,D681,L64,U567,R621,U922,L665,U329,R242,U592,L727,D481,L339,U402,R213,D280,R656,U169,R976,D962,L294,D505,L251,D689,L497,U133,R230,D441,L90,D220,L896,D657,L500,U331,R502,U723,R762,D613,L447,D256,L226,U309,L935,U384,L740,D459,R309,D707,R952,D747,L304,D105,R977,D539,R941,D21,R291,U216,R132,D543,R515,U453,L854,D42,R982,U102,L469,D639,R559,D68,R302,U734,R980,D214,R107,D191,L730,D793,L63,U17,R807,U196,R412,D592,R330,D941,L87,D291,L44,D94,L272,D780,R968,U837,L712,D704,R163,U981,R537,U778,R220,D303,L196,D951,R163,D446,R11,D623,L72,D778,L158,U660,L189,D510,L247,D716,L89,U887,L115,U114,L36,U81,R927,U293,L265,U183,R331,D267,R745,D298,L561,D918,R299,U810,L322,U679,L739,D854,L581,U34,L862,D779,R23")
+
+;; Puzzle 1
+
+(defun wire-from (raw)
+  (-map (lambda (s)
+          (cons (substring s 0 1) (string-to-number (substring s 1))))
+        (s-split "," raw)))
+
+(defun day3/move (x y next)
+  (cl-flet ((steps (by op)
+                   (-map op (reverse (number-sequence 1 by)))))
+    (pcase next
+      (`("L" . ,by) (steps by (lambda (n) (cons (- x n) y))))
+      (`("R" . ,by) (steps by (lambda (n) (cons (+ x n) y))))
+      (`("U" . ,by) (steps by (lambda (n) (cons x (+ y n)))))
+      (`("D" . ,by) (steps by (lambda (n) (cons x (- y n))))))))
+
+(defun day3/wire-points (wire)
+  (let ((points (ht))
+        (point-list (-reduce-from
+                     (lambda (acc point)
+                       (-let* (((x . y) (car acc))
+                               (next (day3/move x y point)))
+                         (-concat next acc)))
+                     '((0 . 0)) wire)))
+    (-map (-lambda ((s . p)) (ht-set! points p s))
+          (-zip (reverse (number-sequence 0 (- (length point-list) 1))) point-list))
+    (ht-remove! points '(0 . 0))
+    points))
+
+(defun day3/closest-intersection (crossed-points)
+  (car (-sort #'<
+              (-map (-lambda ((x . y))
+                      (+ (abs x) (abs y)))
+                    crossed-points))))
+
+(defun day3/minimum-steps (wire1 wire2 crossed)
+  (car (-sort #'<
+              (-map (-lambda (p)
+                      (+ (ht-get wire1 p) (ht-get wire2 p)))
+                    crossed))))
+
+;; Example:
+(let* ((wire1-points (day3/wire-points (wire-from day3/input/wire1)))
+       (wire2-points (day3/wire-points (wire-from day3/input/wire2)))
+       (crossed-points (-filter (lambda (p) (ht-contains? wire1-points p))
+                                (ht-keys wire2-points))))
+  (message "Solution for day3/1: %d" (day3/closest-intersection crossed-points))
+  (message "Solution for day3/2: %d" (day3/minimum-steps wire1-points
+                                                         wire2-points
+                                                         crossed-points)))
diff --git a/users/tazjin/aoc2019/solution-day4.el b/users/tazjin/aoc2019/solution-day4.el
new file mode 100644
index 0000000000..2805f3f4e9
--- /dev/null
+++ b/users/tazjin/aoc2019/solution-day4.el
@@ -0,0 +1,73 @@
+;; -*- lexical-binding: t; -*-
+;; Advent of Code 2019 - Day 4
+
+(require 'cl-lib)
+(require 'dash)
+
+;; Puzzle 1
+
+(defun day4/to-digits (num)
+  "Convert NUM to a list of its digits."
+  (cl-labels ((steps (n digits)
+                     (if (= n 0) digits
+                       (steps (/ n 10) (cons (% n 10) digits)))))
+    (steps num '())))
+
+(defvar day4/input (-map #'day4/to-digits (number-sequence 128392 643281)))
+
+(defun day4/filter-password (digits)
+  "Determines whether the given rules match the supplied
+  number."
+
+  (and
+   ;; It is a six digit number
+   (= 6 (length digits))
+
+   ;; Value is within the range given in puzzle input
+   ;; (noop because the range is generated from the input)
+
+   ;; Two adjacent digits are the same (like 22 in 122345).
+   (car (-reduce-from (-lambda ((acc . prev) next)
+                        (cons (or acc (= prev next)) next))
+                      '(nil . 0) digits))
+
+   ;; Going from left to right, the digits never decrease; they only
+   ;; ever increase or stay the same (like 111123 or 135679).
+   (car (-reduce-from (-lambda ((acc . prev) next)
+                        (cons (and acc (>= next prev)) next))
+                      '(t . 0) digits))))
+
+;; Puzzle 2
+;;
+;; Additional criteria: If there's matching digits, they're not in a group.
+
+(cl-defstruct day4/acc state prev count)
+
+(defun day4/filter-longer-groups (digits)
+  (let ((res (-reduce-from
+              (lambda (acc next)
+                (cond ;; sequence is broken and count was at 1 ->
+                 ;; match!
+                 ((and (= (day4/acc-count acc) 2)
+                       (/= (day4/acc-prev acc) next))
+                  (setf (day4/acc-state acc) t))
+
+                 ;; sequence continues, counter increment!
+                 ((= (day4/acc-prev acc) next)
+                  (setf (day4/acc-count acc) (+ 1 (day4/acc-count acc))))
+
+                 ;; sequence broken, reset counter
+                 ((/= (day4/acc-prev acc) next)
+                  (setf (day4/acc-count acc) 1)))
+
+                (setf (day4/acc-prev acc) next)
+                acc)
+              (make-day4/acc :prev 0 :count 0) digits)))
+    (or (day4/acc-state res)
+        (= 2 (day4/acc-count res)))))
+
+(let* ((simple (-filter #'day4/filter-password day4/input))
+       (complex (-filter #'day4/filter-longer-groups simple)))
+  (message "Solution to day4/1: %d" (length simple))
+  (message "Solution to day4/2: %d" (length complex)))
+
diff --git a/users/tazjin/aoc2020/default.nix b/users/tazjin/aoc2020/default.nix
new file mode 100644
index 0000000000..cd89da7de4
--- /dev/null
+++ b/users/tazjin/aoc2020/default.nix
@@ -0,0 +1,26 @@
+# Solutions for Advent of Code 2020, written in Emacs Lisp.
+#
+# For each day a new file is created as "solution-day$n.el".
+{ depot, pkgs, ... }:
+
+let
+  inherit (builtins) attrNames filter head listToAttrs match readDir;
+  dir = readDir ./.;
+  matchSolution = match "solution-(.*)\.el";
+  isSolution = f: (matchSolution f) != null;
+  getDay = f: head (matchSolution f);
+
+  solutionFiles = filter (e: dir."${e}" == "regular" && isSolution e) (attrNames dir);
+  solutions = map
+    (f:
+      let day = getDay f; in depot.nix.writeElispBin {
+        name = day;
+        deps = p: with p; [ dash s ht p.f ];
+        src = ./. + ("/" + f);
+      })
+    solutionFiles;
+in
+pkgs.symlinkJoin {
+  name = "aoc2020";
+  paths = solutions;
+}
diff --git a/users/tazjin/aoc2020/solution-day1.el b/users/tazjin/aoc2020/solution-day1.el
new file mode 100644
index 0000000000..a04f43d151
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day1.el
@@ -0,0 +1,44 @@
+;; Advent of Code 2020 - Day 1
+(require 'cl)
+(require 'ht)
+(require 'dash)
+
+(defmacro hash-set (&rest elements)
+  "Define a hash-table with empty values, for use as a set."
+  (cons 'ht (-map (lambda (x) (list x nil)) elements)))
+
+;; Puzzle 1:
+
+(defvar day1/input
+  (hash-set 1645 1995 1658 1062 1472 1710 1424 1823 1518 1656 1811 1511 1320 1521 1395
+            1996 1724 1666 1637 1504 1766 534 1738 1791 1372 1225 1690 1949 1495 1436 1166
+            1686 1861 1889 1887 997 1202 1478 833 1497 1459 1717 1272 1047 1751 1549 1204
+            1230 1260 1611 1506 1648 1354 1415 1615 1327 1622 1592 1807 1601 1026 1757 1376
+            1707 1514 1905 1660 1578 1963 1292 390 1898 1019 1580 1499 1830 1801 1881 1764
+            1442 1838 1088 1087 1040 1349 1644 1908 1697 1115 1178 1224 1810 1445 1594 1894
+            1287 1676 1435 1294 1796 1350 1685 1118 1488 1726 1696 1190 1538 1780 1806 1207
+            1346 1705 983 1249 1455 2002 1466 1723 1227 1390 1281 1715 1603 1862 1744 1774
+            1385 1312 1654 1872 1142 1273 1508 1639 1827 1461 1795 1533 1304 1417 1984 28
+            1693 1951 1391 1931 1179 1278 1400 1361 1369 1343 1416 1426 314 1510 1933 1239
+            1218 1918 1797 1255 1399 1229 723 1992 1595 1191 1916 1525 1605 1524 1869 1652
+            1874 1756 1246 1310 1219 1482 1429 1244 1554 1575 1123 1194 1408 1917 1613 1773
+            1809 1987 1733 1844 1423 1718 1714 1923 1503))
+
+(message "Solution to day1/1: %s"
+         (cl-loop for first being the hash-keys of day1/input
+                  for second = (- 2020 first)
+                  when (ht-contains? day1/input second)
+                  return (* first second)))
+
+;; Puzzle 2:
+
+(message "Solution to day1/1: %s"
+         (cl-loop for first being the hash-keys of day1/input
+                  for result =
+                  (cl-loop
+                   for second being the elements of (-drop 1 (ht-keys day1/input))
+                   for third = (- 2020 first second)
+                   when (ht-contains? day1/input third)
+                   return (* first second third))
+
+                  when result return result))
diff --git a/users/tazjin/aoc2020/solution-day2.el b/users/tazjin/aoc2020/solution-day2.el
new file mode 100644
index 0000000000..5993bf3407
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day2.el
@@ -0,0 +1,54 @@
+;; Advent of Code 2020 - Day 2
+
+(require 'cl-lib)
+(require 'f)
+(require 'ht)
+(require 's)
+(require 'seq)
+
+(defvar day2/input
+  ;; This one was too large to inline.
+  (s-lines (f-read "/tmp/aoc/day2.txt")))
+
+(defun day2/count-letters (password)
+  (let ((table (ht-create)))
+    (cl-loop for char across password
+             for current = (ht-get table char)
+             do (ht-set table char
+                        (if current (+ 1 current) 1)))
+    table))
+
+(defun day2/parse (input)
+  (let* ((split (s-split " " input))
+         (range (s-split "-" (car split))))
+    (list (string-to-number (car range))
+          (string-to-number (cadr range))
+          (string-to-char (cadr split))
+          (caddr split))))
+
+(defun day2/count-with-validation (func)
+  (length (-filter
+           (lambda (password)
+             (and (not (seq-empty-p password))
+                  (apply func (day2/parse password))))
+           day2/input)))
+
+;; Puzzle 1
+
+(defun day2/validate-oldjob (min max char password)
+  (let ((count (ht-get (day2/count-letters password) char)))
+    (when count
+      (and (>= count min)
+           (<= count max)))))
+
+(message "Solution to day2/1: %s"
+         (day2/count-with-validation #'day2/validate-oldjob))
+
+;; Puzzle 2
+
+(defun day2/validate-toboggan (pos1 pos2 char password)
+  (xor (= char (aref password (- pos1 1)))
+       (= char (aref password (- pos2 1)))))
+
+(message "Solution to day2/2: %s"
+         (day2/count-with-validation #'day2/validate-toboggan))
diff --git a/users/tazjin/aoc2020/solution-day3.el b/users/tazjin/aoc2020/solution-day3.el
new file mode 100644
index 0000000000..80ea4a2264
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day3.el
@@ -0,0 +1,43 @@
+;; Advent of Code 2020 - Day 3
+
+(require 'cl-lib)
+(require 'dash)
+(require 'f)
+(require 's)
+(require 'seq)
+
+(setq day3/input
+      (-filter (lambda (s) (not (seq-empty-p s)))
+         (s-lines (f-read "/tmp/aoc/day3.txt"))))
+
+(setq day3/input-width (length (elt day3/input 0)))
+(setq day3/input-height (length day3/input))
+
+(defun day3/thing-at-point (x y)
+  "Pun intentional."
+  (when (>= day3/input-height y)
+    (let ((x-repeated (mod (- x 1) day3/input-width)))
+      (elt (elt day3/input (- y 1)) x-repeated))))
+
+(defun day3/slope (x-steps y-steps)
+  "Produce the objects encountered through this slope until the
+  bottom of the map."
+  (cl-loop for x from 1 by x-steps
+           for y from 1 to day3/input-height by y-steps
+           collect (day3/thing-at-point x y)))
+
+;; Puzzle 1
+
+(defun day3/count-trees (x-steps y-steps)
+  (cl-loop for thing being the elements of (day3/slope x-steps y-steps)
+           count (= thing ?#)))
+
+(message "Solution to day3/1: One encounters %s trees" (day3/count-trees 3 1))
+
+;; Puzzle 2
+
+(message "Solution to day3/2 %s" (* (day3/count-trees 1 1)
+                                    (day3/count-trees 3 1)
+                                    (day3/count-trees 5 1)
+                                    (day3/count-trees 7 1)
+                                    (day3/count-trees 1 2)))
diff --git a/users/tazjin/aoc2020/solution-day4.el b/users/tazjin/aoc2020/solution-day4.el
new file mode 100644
index 0000000000..034a40a955
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day4.el
@@ -0,0 +1,98 @@
+;; Advent of Code 2020 - Day 4
+
+(require 'cl-lib)
+(require 's)
+(require 'dash)
+(require 'f)
+
+(cl-defstruct day4/passport
+  byr ;; Birth Year
+  iyr ;; Issue Year
+  eyr ;; Expiration Year
+  hgt ;; Height
+  hcl ;; Hair Color
+  ecl ;; Eye Color
+  pid ;; Passport ID
+  cid ;; Country ID
+  )
+
+(defun day4/parse-passport (input)
+  (let* ((pairs (s-split " " (s-replace "\n" " " input) t))
+         (slots
+          (-map
+           (lambda (pair)
+             (pcase-let ((`(,key ,value) (s-split ":" (s-trim pair))))
+               (list (intern (format ":%s" key)) value)))
+           pairs)))
+    (apply #'make-day4/passport (-flatten slots))))
+
+(defun day4/parse-passports (input)
+  (-map #'day4/parse-passport (s-split "\n\n" input t)))
+
+(setq day4/input (day4/parse-passports (f-read "/tmp/aoc/day4.txt")))
+
+;; Puzzle 1
+
+(defun day4/validate (passport)
+  "Check that all fields except CID are present."
+  (cl-check-type passport day4/passport)
+  (and (day4/passport-byr passport)
+       (day4/passport-iyr passport)
+       (day4/passport-eyr passport)
+       (day4/passport-hgt passport)
+       (day4/passport-hcl passport)
+       (day4/passport-ecl passport)
+       (day4/passport-pid passport)))
+
+(message "Solution to day4/1: %s" (cl-loop for passport being the elements of day4/input
+                                           count (day4/validate passport)))
+
+;; Puzzle 2
+
+(defun day4/year-bound (min max value)
+  (and
+   (s-matches? (rx (= 4 digit)) value)
+   (<= min (string-to-number value) max)))
+
+(defun day4/check-unit (unit min max value)
+  (and
+   (string-match (rx (group (+? digit)) (literal unit)) value)
+   (<= min (string-to-number (match-string 1 value)) max)))
+
+(defun day4/properly-validate (passport)
+  "Opting for readable rather than clever here."
+  (and
+   (day4/validate passport)
+
+   ;; byr (Birth Year) - four digits; at least 1920 and at most 2002.
+   (day4/year-bound 1920 2002 (day4/passport-byr passport))
+
+   ;; iyr (Issue Year) - four digits; at least 2010 and at most 2020.
+   (day4/year-bound 2010 2020 (day4/passport-iyr passport))
+
+   ;; eyr (Expiration Year) - four digits; at least 2020 and at most 2030.
+   (day4/year-bound 2020 2030 (day4/passport-eyr passport))
+
+   ;; hgt (Height) - a number followed by either cm or in:
+   ;; If cm, the number must be at least 150 and at most 193.
+   ;; If in, the number must be at least 59 and at most 76.
+   (or (day4/check-unit "cm" 150 193 (day4/passport-hgt passport))
+       (day4/check-unit "in" 59 76 (day4/passport-hgt passport)))
+
+   ;; hcl (Hair Color) - a # followed by exactly six characters 0-9 or a-f.
+   (s-matches? (rx ?# (= 6 hex)) (day4/passport-hcl passport))
+
+   ;; ecl (Eye Color) - exactly one of: amb blu brn gry grn hzl oth.
+   (-contains? '("amb" "blu" "brn" "gry" "grn" "hzl" "oth")
+               (day4/passport-ecl passport))
+
+   ;; pid (Passport ID) - a nine-digit number, including leading zeroes.
+   (s-matches? (rx line-start (= 9 digit) line-end)
+               (day4/passport-pid passport))
+
+   ;; cid (Country ID) - ignored, missing or not.
+   ))
+
+(message "Solution to day4/2: %s"
+         (cl-loop for passport being the elements of day4/input
+                  count (day4/properly-validate passport)))
diff --git a/users/tazjin/aoc2020/solution-day5.el b/users/tazjin/aoc2020/solution-day5.el
new file mode 100644
index 0000000000..9bba322902
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day5.el
@@ -0,0 +1,61 @@
+;; Advent of Code 2020 - Day 5
+
+(require 'cl-lib)
+(require 'dash)
+(require 'f)
+(require 'ht)
+(require 's)
+(require 'seq)
+
+(defvar day5/input
+  (-filter (lambda (s) (not (seq-empty-p s)))
+           (s-lines (f-read "/tmp/aoc/day5.txt"))))
+
+(defun day5/lower (sequence)
+  (seq-subseq sequence 0 (/ (length sequence) 2)))
+
+(defun day5/upper (sequence)
+  (seq-subseq sequence (/ (length sequence) 2)))
+
+(defun day5/seat-id (column row)
+  (+ column (* 8 row)))
+
+(defun day5/find-seat (boarding-pass)
+  (let ((rows (number-sequence 0 127))
+        (columns (number-sequence 0 7)))
+    (cl-loop for char across boarding-pass
+             do (pcase char
+                  (?F (setq rows (day5/lower rows)))
+                  (?B (setq rows (day5/upper rows)))
+                  (?R (setq columns (day5/upper columns)))
+                  (?L (setq columns (day5/lower columns))))
+             finally return (day5/seat-id (car columns) (car rows)))))
+
+;; Puzzle 1
+
+(message "Solution to day5/1: %s"
+         (cl-loop for boarding-pass in day5/input
+                  maximize (day5/find-seat boarding-pass)))
+
+;; Puzzle 2
+
+(defun day5/all-seats-in (row)
+  (-map (lambda (column) (day5/seat-id column row))
+        (number-sequence 0 7)))
+
+(message "Solution to day5/2: %s"
+         (let ((all-seats (ht-create)))
+           (-each (-mapcat #'day5/all-seats-in (number-sequence 1 126))
+             (lambda (seat) (ht-set all-seats seat nil)))
+
+           (cl-loop for boarding-pass in day5/input
+                    do (ht-remove all-seats (day5/find-seat boarding-pass))
+
+                    ;; Remove seats that lack adjacent entries, those
+                    ;; are missing on the plane.
+                    finally return
+                    (car
+                     (-filter (lambda (seat)
+                                (and (not (ht-contains? all-seats (- seat 1)))
+                                     (not (ht-contains? all-seats (+ seat 1)))))
+                              (ht-keys all-seats))))))
diff --git a/users/tazjin/aoc2020/solution-day6.el b/users/tazjin/aoc2020/solution-day6.el
new file mode 100644
index 0000000000..8179c79af2
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day6.el
@@ -0,0 +1,40 @@
+;; Advent of Code 2020 - Day 6
+
+(require 'cl-lib)
+(require 'dash)
+(require 'f)
+(require 'ht)
+(require 's)
+
+(defvar day6/input (s-split "\n\n" (f-read "/tmp/aoc/day6.txt") t)
+  "Input, split into groups (with people in each group still distinct)")
+
+;; Puzzle 1
+
+(defun day6/count-answers (group-answers)
+  "I suspect doing it this way will be useful in puzzle 2."
+  (let ((table (ht-create)))
+    (-each group-answers
+      (lambda (answer)
+        (cl-loop for char across answer
+                 do (ht-set table char (+ 1 (or (ht-get table char)
+                                                0))))))
+    table))
+
+(message "Solution to day6/1: %s"
+         (cl-loop for group being the elements of day6/input
+                  sum (length
+                       (ht-keys
+                        (day6/count-answers (s-lines group))))))
+
+;; Puzzle 2
+
+(defun day6/count-unanimous-answers (answers)
+  (ht-reject (lambda (_key value) (not (= value (length answers))))
+             (day6/count-answers answers)))
+
+(message "Solution to day6/2: %s"
+         (cl-loop for group being the elements of day6/input
+                  sum (length
+                       (ht-keys
+                        (day6/count-unanimous-answers (s-split "\n" group t))))))
diff --git a/users/tazjin/aoc2020/solution-day7.el b/users/tazjin/aoc2020/solution-day7.el
new file mode 100644
index 0000000000..251a85fede
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day7.el
@@ -0,0 +1,92 @@
+;; Advent of Code 2020 - Day 7
+
+(require 'cl-lib)
+(require 'dash)
+(require 'f)
+(require 's)
+(require 'ht)
+
+(defvar day7/input
+  (s-lines (s-chomp (f-read "/tmp/aoc/day7.txt"))))
+
+(defun day7/parse-bag (input)
+  (string-match (rx line-start
+                    (group (one-or-more (or letter space)))
+                    "s contain "
+                    (group (one-or-more anything))
+                    "." line-end)
+                input)
+  (cons (match-string 1 input)
+        (-map
+         (lambda (content)
+           (unless (equal content "no other bags")
+             (progn
+               (string-match
+                (rx (group (one-or-more digit))
+                    space
+                    (group (one-or-more anything) "bag"))
+                content)
+               (cons (match-string 2 content)
+                     (string-to-number (match-string 1 content))))))
+         (s-split ", " (match-string 2 input)))))
+
+(defun day7/id-or-next (table bag-type)
+  (unless (ht-contains? table bag-type)
+    (ht-set table bag-type (length (ht-keys table))))
+  (ht-get table bag-type))
+
+(defun day7/build-graph (input &optional flip)
+  "Represent graph mappings directionally using an adjacency
+  matrix, because that's probably easiest.
+
+  By default an edge means 'contains', with optional argument
+  FLIP edges are inverted and mean 'contained by'."
+
+  (let ((bag-mapping (ht-create))
+        (graph (let ((length (length input)))
+                 (apply #'vector
+                        (-map (lambda (_) (make-vector length 0)) input)))))
+    (cl-loop for bag in (-map #'day7/parse-bag input)
+             for bag-id = (day7/id-or-next bag-mapping (car bag))
+             do (-each (-filter #'identity (cdr bag))
+                  (pcase-lambda (`(,contained-type . ,count))
+                    (let ((contained-id (day7/id-or-next bag-mapping contained-type)))
+                      (if flip
+                          (aset (aref graph contained-id) bag-id count)
+                        (aset (aref graph bag-id) contained-id count))))))
+    (cons bag-mapping graph)))
+
+;; Puzzle 1
+
+(defun day7/find-ancestors (visited graph start)
+  (ht-set visited start t)
+  (cl-loop for bag-count being the elements of (aref graph start)
+           using (index bag-id)
+           when (and (> bag-count 0)
+                     (not (ht-contains? visited bag-id)))
+           do (day7/find-ancestors visited graph bag-id)))
+
+(message
+ "Solution to day7/1: %s"
+ (pcase-let* ((`(,mapping . ,graph) (day7/build-graph day7/input t))
+              (shiny-gold-id (ht-get mapping "shiny gold bag"))
+              (visited (ht-create)))
+   (day7/find-ancestors visited graph shiny-gold-id)
+   (- (length (ht-keys visited)) 1)))
+
+;; Puzzle 2
+
+(defun ht-find-by-value (table value)
+  (ht-find (lambda (_key item-value) (equal item-value value)) table))
+
+(defun day7/count-contained-bags (mapping graph start)
+  (cl-loop for bag-count being the elements of (aref graph start)
+           using (index bag-id)
+           when (> bag-count 0)
+           sum (+ bag-count
+                  (* bag-count (day7/count-contained-bags mapping graph bag-id)))))
+
+(message "Solution to day7/2: %s"
+         (pcase-let* ((`(,mapping . ,graph) (day7/build-graph day7/input))
+                      (shiny-gold-id (ht-get mapping "shiny gold bag")))
+           (day7/count-contained-bags mapping graph shiny-gold-id)))
diff --git a/users/tazjin/aoc2020/solution-day8.el b/users/tazjin/aoc2020/solution-day8.el
new file mode 100644
index 0000000000..591a07fbf3
--- /dev/null
+++ b/users/tazjin/aoc2020/solution-day8.el
@@ -0,0 +1,63 @@
+;; Advent of Code 2020 - Day
+
+(require 'cl-lib)
+(require 'dash)
+(require 'f)
+(require 's)
+
+(setq day8/input
+      (apply #'vector
+             (-map (lambda (s)
+                     (pcase-let ((`(,op ,val) (s-split " " s t)))
+                       (cons (intern op) (string-to-number val))))
+                   (s-lines (s-chomp (f-read "/tmp/aoc/day8.txt"))))))
+
+(defun day8/step (code position acc)
+  (if (>= position (length code))
+      (cons 'final acc)
+
+    (let ((current (aref code position)))
+      (aset code position :done)
+      (pcase current
+        (:done (cons 'loop acc))
+        (`(nop . ,val) (cons (+ position 1) acc))
+        (`(acc . ,val) (cons (+ position 1) (+ acc val)))
+        (`(jmp . ,val) (cons (+ position val) acc))))))
+
+;; Puzzle 1
+
+(message "Solution to day8/1: %s"
+         (let ((code (copy-sequence day8/input))
+               (position 0)
+               (acc 0))
+           (cl-loop for next = (day8/step code position acc)
+                    when (equal 'loop (car next)) return (cdr next)
+                    do (setq position (car next))
+                    do (setq acc (cdr next)))))
+
+;; Puzzle 2
+
+(defun day8/flip-at (code pos)
+  (pcase (aref code pos)
+    (`(nop . ,val) (aset code pos `(jmp . ,val)))
+    (`(jmp . ,val) (aset code pos `(nop . ,val)))
+    (other (error "Unexpected flip op: %s" other))))
+
+(defun day8/try-flip (flip-at code position acc)
+  (day8/flip-at code flip-at)
+  (cl-loop for next = (day8/step code position acc)
+           when (equal 'loop (car next)) return nil
+           when (equal 'final (car next)) return (cdr next)
+           do (setq position (car next))
+           do (setq acc (cdr next))))
+
+(message "Solution to day8/2: %s"
+         (let ((flip-options (cl-loop for op being the elements of day8/input
+                                      using (index idx)
+                                      for opcode = (car op)
+                                      when (or (equal 'nop opcode)
+                                               (equal 'jmp opcode))
+                                      collect idx)))
+           (cl-loop for flip-at in flip-options
+                    for result = (day8/try-flip flip-at (copy-sequence day8/input) 0 0)
+                    when result return result)))
diff --git a/users/tazjin/aoc2022/day1.rs b/users/tazjin/aoc2022/day1.rs
new file mode 100644
index 0000000000..078eb25f03
--- /dev/null
+++ b/users/tazjin/aoc2022/day1.rs
@@ -0,0 +1,27 @@
+// AoC 2022 - day 1.
+
+fn sum_elf(elf: &str) -> usize {
+    elf.lines()
+        .map(|s| s.parse::<usize>().expect("invalid input"))
+        .sum()
+}
+
+fn group_by_elf(input: &str) -> Vec<usize> {
+    input.rsplit("\n\n").map(sum_elf).collect()
+}
+
+fn top_elf(input: &str) -> usize {
+    group_by_elf(&input).into_iter().max().unwrap()
+}
+
+fn top_n_elves(n: usize, input: &str) -> usize {
+    let mut by_elf = group_by_elf(input);
+    by_elf.sort_by(|a, b| b.cmp(a)); // high->low
+    (by_elf[..n]).iter().sum()
+}
+
+fn main() {
+    let input = std::fs::read_to_string("input").expect("input should be in file named 'input'");
+    println!("top elf: {}", top_elf(&input));
+    println!("top 3 elves: {}", top_n_elves(3, &input));
+}
diff --git a/users/tazjin/aoc2023/day1.el b/users/tazjin/aoc2023/day1.el
new file mode 100644
index 0000000000..b1a7faff02
--- /dev/null
+++ b/users/tazjin/aoc2023/day1.el
@@ -0,0 +1,52 @@
+(require 's)
+(require 'f)
+
+;; task 1
+
+(defun digit-p (c)
+  (and (> c ?0)
+       (<= c ?9)))
+
+(defun aocd1-sum-values (lines)
+  (-sum
+   (-map (lambda (line)
+           (let ((digits (-filter #'digit-p (string-to-list line))))
+             (string-to-number (string (-first-item digits) (-last-item digits)))))
+         lines)))
+
+(let ((lines (s-lines (s-trim (f-read "~/Downloads/input.txt")))))
+  (aocd1-sum-values lines))
+
+;; task 2
+
+(defun replace-written-numbers (input)
+  (with-temp-buffer
+    (insert input)
+    (let ((start 1))
+      (while (< start (point-max))
+        (format-replace-strings
+         '(("oneight" . "18")
+           ("twone" . "21")
+           ("threeight" . "38")
+           ("fiveight" . "58")
+           ("sevenine" . "79")
+           ("eightwo" . "82")
+           ("eighthree" . "83")
+           ("nineight" . "98"))
+         nil start (min (+ 10 start) (point-max)))
+        (format-replace-strings
+         '(("one" . "1")
+           ("two" . "2")
+           ("three" . "3")
+           ("four" . "4")
+           ("five" . "5")
+           ("six" . "6")
+           ("seven" . "7")
+           ("eight" . "8")
+           ("nine" . "9"))
+         nil start (min (+ 5 start) (point-max)))
+        (setq start (1+ start))))
+    (buffer-string)))
+
+(let ((lines (s-lines (s-trim (f-read "~/Downloads/input.txt")))))
+  (aocd1-sum-values (-map #'replace-written-numbers lines)))
diff --git a/users/tazjin/aoc2023/day2.el b/users/tazjin/aoc2023/day2.el
new file mode 100644
index 0000000000..9374d7862c
--- /dev/null
+++ b/users/tazjin/aoc2023/day2.el
@@ -0,0 +1,64 @@
+(require 'dash)
+(require 's)
+(require 'f)
+
+(defvar aoc23-day2-example
+
+  "Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
+Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
+Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
+Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
+Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green")
+
+;; part 1
+
+(cl-defstruct aoc23d2-set red green blue)
+
+(defun aoc23d2-parse-set (input)
+  (let ((set (make-aoc23d2-set :red 0 :green 0 :blue 0))
+        (colours (-map #'s-trim (s-split "," input))))
+    (cl-loop for colour in colours
+             do (pcase (s-split " " colour t)
+                  (`(,num "red") (setf (aoc23d2-set-red set) (string-to-number num)))
+                  (`(,num "green") (setf (aoc23d2-set-green set) (string-to-number num)))
+                  (`(,num "blue") (setf (aoc23d2-set-blue set) (string-to-number num)))))
+    set))
+
+(cl-defstruct aoc23d2-game id sets)
+
+(defun aoc23d2-parse-game (input)
+  (pcase-let* ((`(,id-str ,sets-str) (s-split-up-to ":" input 1 t))
+               (game-id (string-to-number (s-chop-left (length "Game ") id-str)))
+               (sets (-map #'aoc23d2-parse-set (s-split ";" sets-str t))))
+    (make-aoc23d2-game :id game-id :sets sets)))
+
+(defun aoc23d2-game-possible-p (game r g b)
+  (cl-every (lambda (set)
+              (and (<= (aoc23d2-set-red set) r)
+                   (<= (aoc23d2-set-green set) g)
+                   (<= (aoc23d2-set-blue set) b)))
+            (aoc23d2-game-sets game)))
+
+(let ((input (f-read "~/Downloads/input.txt")))
+  (-sum
+   (-map #'aoc23d2-game-id
+         (-filter (lambda (g) (aoc23d2-game-possible-p g 12 13 14))
+                  (-map #'aoc23d2-parse-game (s-lines (s-trim input)))))))
+
+;; part 2
+
+(defun aoc23d2-game-min-cubes-power (game)
+  (let ((r 0)
+        (g 0)
+        (b 0))
+    (-each (aoc23d2-game-sets game)
+      (lambda (set)
+        (setq r (max r (aoc23d2-set-red set)))
+        (setq g (max g (aoc23d2-set-green set)))
+        (setq b (max b (aoc23d2-set-blue set)))))
+    (* (max 1 r) (max 1 g) (max 1 b))))
+
+(let ((input (f-read "~/Downloads/input.txt")))
+  (-sum
+   (-map #'aoc23d2-game-min-cubes-power
+         (-map #'aoc23d2-parse-game (s-lines (s-trim input))))))
diff --git a/users/tazjin/aoc2023/day3.el b/users/tazjin/aoc2023/day3.el
new file mode 100644
index 0000000000..dd39c1b836
--- /dev/null
+++ b/users/tazjin/aoc2023/day3.el
@@ -0,0 +1,110 @@
+(defun aoc23d3-symbol-p (c)
+  (not (or (= c ? )
+           (and (>= c ?0)
+                (<= c ?9)))))
+
+(defun rectangle-for-bounds (bounds)
+  (let* ((start (save-excursion
+                     (goto-char (car bounds))
+                     (let ((col (current-column)))
+                       (forward-line -1)
+                       (move-to-column (max 0 (1- col))))
+                     (point)))
+         (end (save-excursion
+                (goto-char (cdr bounds))
+                (let ((col (current-column)))
+                  (forward-line 1)
+                  (move-to-column (1+ col)))
+                (point))))
+    (list start end)))
+
+(defun get-machine-part ()
+  (interactive)
+  (when-let* ((num-raw (number-at-point))
+              (num (abs num-raw))
+              ;; handles negative number edge case (bounds contain the `-')
+              (bounds-raw (bounds-of-thing-at-point 'number))
+              (bounds (if (< num-raw 0)
+                          (cons (1- (car bounds-raw)) (cdr bounds-raw))
+                        bounds-raw))
+              (rectangle (rectangle-for-bounds bounds))
+              (neighbours (apply #'concat
+                                 (apply #'extract-rectangle rectangle))))
+    (if (-any #'aoc23d3-symbol-p (string-to-list neighbours))
+        (cons num rectangle)
+      (cons nil rectangle))))
+
+
+(defun find-machine-parts (input)
+  (with-temp-buffer
+    (insert input)
+    (goto-char (point-min))
+    (save-excursion
+      (replace-string "." " "))
+
+    (cl-loop while (forward-word)
+             for result = (get-machine-part)
+             when (car result) collect (car result))))
+
+
+;; debugging
+
+(defvar aoc23d3-example "467..114..
+...*......
+..35..633.
+......#...
+617*......
+.....+.58.
+..592.....
+......755.
+...$.*....
+.664.598..")
+
+(defvar aoc23d3-example2 "12.......*..
++.........34
+.......-12..
+..78........
+..*....60...
+78..........
+.......23...
+....90*12...
+............
+2.2......12.
+.*.........*
+1.1.......56")
+
+(defvar aoc23d3-example3 "243.
+..*.
+....")
+
+(defun aoc23d3-debug (p)
+  "Interactive debugger for the solution, can be bound to a key in
+an input buffer. Dots should already have been replaced with
+spaces."
+  (interactive "P")
+  (unless p
+    (goto-char aoc23d3-last))
+  (rectangle-mark-mode 1)
+  (forward-word)
+  (setq aoc23d3-last (point))
+  (pcase (get-machine-part)
+    (`(nil ,b ,e) (progn (set-mark b)
+                          (goto-char e)
+                          (set-face-attribute 'region nil :background "#FAA0A0")))
+    (`(,num ,b ,e) (progn (set-mark b)
+                          (goto-char e)
+                          (set-face-attribute 'region nil :background "#d1ffbd")))
+    (other (deactivate-mark))))
+
+(cl-assert (= 4361 (-sum (find-machine-parts aoc23d3-example))) nil
+           "example from website is working")
+
+(cl-assert (= 413 (-sum (find-machine-parts aoc23d3-example2))) nil
+           "example from subreddit is working")
+
+(cl-assert (= 243 (-sum (find-machine-parts aoc23d3-example3))) nil
+           "example from telegram is working")
+
+;; day 1 (incomplete)
+
+(-sum (find-machine-parts (s-trim (f-read "~/Downloads/input.txt"))))
diff --git a/users/tazjin/atom-feed/default.nix b/users/tazjin/atom-feed/default.nix
deleted file mode 100644
index cd189b8675..0000000000
--- a/users/tazjin/atom-feed/default.nix
+++ /dev/null
@@ -1,141 +0,0 @@
-# This file defines functions for generating an Atom feed.
-
-{ depot, lib, ... }:
-
-with depot.nix.yants;
-
-let
-  inherit (builtins) map readFile replaceStrings;
-  inherit (lib) concatStrings concatStringsSep removeSuffix;
-  inherit (depot.third_party) runCommandNoCC;
-
-  # 'link' describes a related link to a feed, or feed element.
-  #
-  # https://validator.w3.org/feed/docs/atom.html#link
-  link = struct "link" {
-    rel = string;
-    href = string;
-  };
-
-  # 'entry' describes a feed entry, for example a single post on a
-  # blog. Some optional fields have been omitted.
-  #
-  # https://validator.w3.org/feed/docs/atom.html#requiredEntryElements
-  entry = struct "entry" {
-    # Identifies the entry using a universally unique and permanent URI.
-    id = string;
-
-    # Contains a human readable title for the entry. This value should
-    # not be blank.
-    title = string;
-
-    # Content of the entry.
-    content = option string;
-
-    # Indicates the last time the entry was modified in a significant
-    # way (in seconds since epoch).
-    updated = int;
-
-    # Names authors of the entry. Recommended element.
-    authors = option (list string);
-
-    # Related web pages, such as the web location of a blog post.
-    links = option (list link);
-
-    # Conveys a short summary, abstract, or excerpt of the entry.
-    summary = option string;
-
-    # Contains the time of the initial creation or first availability
-    # of the entry.
-    published = option int;
-
-    # Conveys information about rights, e.g. copyrights, held in and
-    # over the entry.
-    rights = option string;
-  };
-
-  # 'feed' describes the metadata of the Atom feed itself.
-  #
-  # Some optional fields have been omitted.
-  #
-  # https://validator.w3.org/feed/docs/atom.html#requiredFeedElements
-  feed = struct "feed" {
-    # Identifies the feed using a universally unique and permanent URI.
-    id = string;
-
-    # Contains a human readable title for the feed.
-    title = string;
-
-    # Indicates the last time the feed was modified in a significant
-    # way (in seconds since epoch). Recommended element.
-    updated = int;
-
-    # Entries contained within the feed.
-    entries = list entry;
-
-    # Names authors of the feed. Recommended element.
-    authors = option (list string);
-
-    # Related web locations. Recommended element.
-    links = option (list link);
-
-    # Conveys information about rights, e.g. copyrights, held in and
-    # over the feed.
-    rights = option string;
-
-    # Contains a human-readable description or subtitle for the feed.
-    subtitle = option string;
-  };
-
-  # Feed generation functions:
-
-  renderEpoch = epoch: removeSuffix "\n" (readFile (runCommandNoCC "date-${toString epoch}" {} ''
-    date --date='@${toString epoch}' --utc --iso-8601='seconds' > $out
-  ''));
-
-  escape = replaceStrings [ "<" ">" "&" "'" ] [ "&lt;" "&gt;" "&amp;" "&#39;" ];
-
-  elem = name: content: ''<${name}>${escape content}</${name}>'';
-
-  renderLink = defun [ link string ] (l: ''
-    <link href="${escape l.href}" rel="${escape l.rel}" />
-  '');
-
-  # Technically the author element can also contain 'uri' and 'email'
-  # fields, but they are not used for the purpose of this feed and are
-  # omitted.
-  renderAuthor = author: ''<author><name>${escape author}</name></author>'';
-
-  renderEntry = defun [ entry string ] (e: ''
-    <entry>
-      ${elem "title" e.title}
-      ${elem "id" e.id}
-      ${elem "updated" (renderEpoch e.updated)}
-      ${if e ? content
-        then ''<content type="html">${escape e.content}</content>''
-        else ""
-      }
-      ${if e ? summary then elem "summary" e.summary else ""}
-      ${concatStrings (map renderAuthor (e.authors or []))}
-      ${if e ? subtitle then elem "subtitle" e.subtitle else ""}
-      ${if e ? rights then elem "rights" e.rights else ""}
-      ${concatStrings (map renderLink (e.links or []))}
-    </entry>
-  '');
-
-  renderFeed = defun [ feed string ] (f: ''
-    <?xml version="1.0" encoding="utf-8"?>
-    <feed xmlns="http://www.w3.org/2005/Atom">
-      ${elem "id" f.id}
-      ${elem "title" f.title}
-      ${elem "updated" (renderEpoch f.updated)}
-      ${concatStringsSep "\n" (map renderAuthor (f.authors or []))}
-      ${if f ? subtitle then elem "subtitle" f.subtitle else ""}
-      ${if f ? rights then elem "rights" f.rights else ""}
-      ${concatStrings (map renderLink (f.links or []))}
-      ${concatStrings (map renderEntry f.entries)}
-    </feed>
-  '');
-in {
-  inherit entry feed renderFeed renderEpoch;
-}
diff --git a/users/tazjin/avatar.jpeg b/users/tazjin/avatar.jpeg
index f38f056578..f6888e01c7 100644
--- a/users/tazjin/avatar.jpeg
+++ b/users/tazjin/avatar.jpeg
Binary files differdiff --git a/users/tazjin/blog/default.nix b/users/tazjin/blog/default.nix
index d2f04aaea5..60c79f0941 100644
--- a/users/tazjin/blog/default.nix
+++ b/users/tazjin/blog/default.nix
@@ -1,59 +1,50 @@
-# This creates the static files that make up my blog from the Markdown
-# files in this repository.
-#
-# All blog posts are rendered from Markdown by cheddar.
-{ depot, lib, ... }@args:
+{ depot, lib, pkgs, ... }:
 
 with depot.nix.yants;
 
 let
-  inherit (builtins) filter hasAttr map;
-
-  # Type definition for a single blog post.
-  post = struct "blog-post" {
-    key = string; #
-    title = string;
-    date = int;
-
-    # Path to the Markdown file containing the post content.
-    content = path;
-
-    # Should this post be included in the index? (defaults to true)
-    listed = option bool;
-
-    # Is this a draft? (adds a banner indicating that the link should
-    # not be shared)
-    draft = option bool;
-
-    # Previously each post title had a numeric ID. For these numeric
-    # IDs, redirects are generated so that old URLs stay compatible.
-    oldKey = option string;
+  inherit (builtins) hasAttr filter;
+
+  config = {
+    name = "tazjin's blog";
+    baseUrl = "https://tazj.in/blog";
+    staticUrl = "https://tazj.in/static/";
+
+    footer = ''
+      <p class="footer">
+        <a class="uncoloured-link" href="https://tazj.in">homepage</a>
+        |
+        <a class="uncoloured-link" href="https://cs.tvl.fyi/">code</a>
+      </p>
+      <p class="lod">เฒ _เฒ </p>
+    '';
   };
 
+  inherit (depot.web.blog) post includePost renderPost;
+
   posts = list post (import ./posts.nix);
-  fragments = import ./fragments.nix args;
 
-  rendered = depot.third_party.runCommandNoCC "tazjins-blog" {} ''
+  rendered = pkgs.runCommand "tazjins-blog" { } ''
     mkdir -p $out
 
     ${lib.concatStringsSep "\n" (map (post:
-      "cp ${fragments.renderPost post} $out/${post.key}.html"
+      "cp ${renderPost config post} $out/${post.key}.html"
     ) posts)}
   '';
 
-  includePost = post: !(fragments.isDraft post) && !(fragments.isUnlisted post);
-in {
-  inherit post rendered;
-  static = ./static;
+in
+{
+  inherit rendered config;
 
-  # Only include listed posts
+  # Filter unlisted posts from the index
   posts = filter includePost posts;
 
   # Generate embeddable nginx configuration for redirects from old post URLs
-  oldRedirects = lib.concatStringsSep "\n" (map (post: ''
-    location ~* ^(/en)?/${post.oldKey} {
-      # TODO(tazjin): 301 once this works
-      return 302 https://tazj.in/blog/${post.key};
-    }
-  '') (filter (hasAttr "oldKey") posts));
+  oldRedirects = lib.concatStringsSep "\n" (map
+    (post: ''
+      location ~* ^(/en)?/${post.oldKey} {
+        return 301 https://tazj.in/blog/${post.key};
+      }
+    '')
+    (filter (hasAttr "oldKey") posts));
 }
diff --git a/users/tazjin/blog/fragments.nix b/users/tazjin/blog/fragments.nix
deleted file mode 100644
index 4c1ed5d8a8..0000000000
--- a/users/tazjin/blog/fragments.nix
+++ /dev/null
@@ -1,98 +0,0 @@
-# This file defines various fragments of the blog, such as the header
-# and footer, as functions that receive arguments to be templated into
-# them.
-#
-# An entire post is rendered by `renderPost`, which assembles the
-# fragments together in a runCommand execution.
-#
-# The post index is generated by //users/tazjin/homepage, not by this
-# code.
-{ depot, lib, ... }:
-
-let
-  inherit (builtins) filter map hasAttr replaceStrings toFile;
-  inherit (depot.third_party) runCommandNoCC writeText;
-  inherit (depot.users.tazjin) renderMarkdown;
-
-  # Generate a post list for all listed, non-draft posts.
-  isDraft = post: (hasAttr "draft" post) && post.draft;
-  isUnlisted = post: (hasAttr "listed" post) && !post.listed;
-
-  escape = replaceStrings [ "<" ">" "&" "'" ] [ "&lt;" "&gt;" "&amp;" "&#39;" ];
-
-  header = title: ''
-  <!DOCTYPE html>
-  <head>
-    <meta charset="utf-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <meta name="description" content="tazjin&#39;s blog">
-    <link rel="stylesheet" type="text/css" href="/static/tazjin.css" media="all">
-    <link rel="icon" type="image/webp" href="/static/favicon.webp">
-    <link rel="alternate" type="application/atom+xml" title="Atom Feed" href="/feed.atom">
-    <title>tazjin&#39;s blog: ${escape title}</title>
-  </head>
-  <body class="light">
-    <header>
-      <h1><a class="blog-title" href="/">tazjin&#39;s interblag</a> </h1>
-      <hr>
-    </header>
-  '';
-
-  footer = ''
-    <hr>
-    <footer>
-      <p class="footer">
-        <a class="uncoloured-link" href="https://tazj.in">homepage</a>
-        |
-        <a class="uncoloured-link" href="https://git.tazj.in/about">code</a>
-        |
-        <a class="uncoloured-link" href="https://twitter.com/tazjin">twitter</a>
-      </p>
-      <p class="lod">เฒ _เฒ </p>
-    </footer>
-  </body>
-  '';
-
-  draftWarning = toFile "draft.html" ''
-    <p class="cheddar-callout cheddar-warning">
-      <b>Note:</b> This post is a <b>draft</b>! Please do not share
-      the link to it without asking me first.
-    </p>
-    <hr>
-  '';
-
-  unlistedWarning = toFile "unlisted.html" ''
-    <p class="cheddar-callout cheddar-warning">
-      <b>Note:</b> This post is <b>unlisted</b>! Please do not share
-      the link to it without asking me first.
-    </p>
-    <hr>
-  '';
-
-  renderPost = post: runCommandNoCC "${post.key}.html" {} ''
-    cat ${toFile "header.html" (header post.title)} > $out
-
-    # Write the post title & date
-    echo '<article><h2 class="inline">${escape post.title}</h2>' >> $out
-    echo '<aside class="date">' >> $out
-    date --date="@${toString post.date}" '+%Y-%m-%d' >> $out
-    echo '</aside>' >> $out
-
-    ${
-      # Add a warning to draft/unlisted posts to make it clear that
-      # people should not share the post.
-
-      if (isDraft post) then "cat ${draftWarning} >> $out"
-      else if (isUnlisted post) then "cat ${unlistedWarning} >> $out"
-      else "# Your ads could be here?"
-    }
-
-    # Write the actual post through cheddar's about-filter mechanism
-    cat ${renderMarkdown post.content} >> $out
-    echo '</article>' >> $out
-
-    cat ${toFile "footer.html" footer} >> $out
-  '';
-in {
-  inherit renderPost isDraft isUnlisted;
-}
diff --git a/users/tazjin/blog/posts.nix b/users/tazjin/blog/posts.nix
index b43598d013..a95a50d766 100644
--- a/users/tazjin/blog/posts.nix
+++ b/users/tazjin/blog/posts.nix
@@ -1,6 +1,19 @@
 # This file defines all the blog posts.
 [
   {
+    key = "reliably-switch-buffers";
+    title = "ะ—ะฐั‡ะตะผ reliably-switch-buffers?";
+    content = ./posts/reliably-switch-buffers.md;
+    date = 1692882000;
+  }
+  {
+    key = "tvix-eval-talk-2023";
+    title = "[ะดะพะบะปะฐะด] tvix-eval, ะธะผะฟะปะตะผะตะฝั‚ะฐั†ะธั ัะทั‹ะบะฐ Nix ะฝะฐ Rust";
+    date = 1694102400;
+    content = ./posts/tvix-eval-talk-2023.md;
+    tagfilter = false;
+  }
+  {
     key = "emacs-is-underrated";
     title = "Emacs is the most underrated tool";
     date = 1581286656;
@@ -37,7 +50,7 @@
     key = "the-smu-problem";
     title = "The SMU-problem of messaging apps";
     date = 1450354078;
-    content =./posts/the-smu-problem.md;
+    content = ./posts/the-smu-problem.md;
     oldKey = "1450354078";
   }
   {
@@ -46,6 +59,7 @@
     date = 1423995834;
     content = ./posts/sick-in-sweden.md;
     oldKey = "1423995834";
+    listed = false;
   }
   {
     key = "nsa-zettabytes";
@@ -54,4 +68,11 @@
     content = ./posts/nsa-zettabytes.md;
     oldKey = "1375310627";
   }
+  {
+    key = "thoughts";
+    title = "Some thoughts";
+    date = 1665095948;
+    content = ./posts/thoughts.md;
+    listed = false;
+  }
 ]
diff --git a/users/tazjin/blog/posts/best-tools.md b/users/tazjin/blog/posts/best-tools.md
index e4bad8f4cd..afe61767b1 100644
--- a/users/tazjin/blog/posts/best-tools.md
+++ b/users/tazjin/blog/posts/best-tools.md
@@ -42,19 +42,32 @@ list. I own several of them and there will probably be more in the future. They
 last forever and your wrists will thank you in the future, even if you do not
 suffer from RSI yet.
 
+Kinesis have announced a split version of the Advantage. Once that is
+easily available, I will buy one and evaluate it.
+
 [advantage]: https://kinesis-ergo.com/shop/advantage2/
 
 ## Speakers
 
-The speakers that I've hooked up to my audio setup (including both record player
-& Chromecast / TV) are the [Teufel Motiv 2][motiv-2]. I've had these for over a
-decade and they're incredibly good, but unfortunately Teufel no longer makes
-them.
+There are two sets of speakers I use, unfortunately one pair has been in storage
+since I left the UK.
+
+My original favourite speakers are the [Teufel Motiv 2][motiv-2], usually hooked
+up to a Chromecast and a record player. I've had these for over a decade and
+they're incredibly good, but unfortunately Teufel no longer makes them. Mine are
+currently in a warehouse somewhere in London, and I don't know when I will see
+them again ...
 
 It's possible to grab a pair on eBay occasionally, so keep an eye out if you're
 interested!
 
+In my Moscow flat, I have a pair of [Wharfedale Diamond 12][diamond-12]
+connected to a Philips amplifier older than myself. These provide an excellent,
+balanced, "Wharfedale-sound". Some people find it needs some getting used to,
+but don't want to go back after that initial phase.
+
 [motiv-2]: https://www.teufelaudio.com/uk/pc/motiv-2-p167.html
+[diamond-12]: https://www.wharfedaleusa.com/collections/diamond-12
 
 ## Headphones
 
@@ -85,40 +98,38 @@ flat.
 
 ## Phone
 
-The best phone I have used in recent years is the [iPhone SE][se]. It was the
-*last* phone that had a reasonable size (up to 4") *and* a 3.5mm headphone jack.
+My current phone is the [Palm phone][palm-phone]. It's basically the smallest
+smartphone on the market at about the size of a credit card. I picked this phone
+because I am trying to not use a phone anymore, but some things simply require a
+smartphone.
 
-Unfortunately, it runs iOS. Despite owning a whole bunch of SEs, I have finally
-moved on to an Android phone that is only moderately larger (still by an
-annoying amount), but does at least have a headphone jack: The [Samsung Galaxy
-S10e][s10e].
+The Palm has terrible battery life but is otherwise great. I always have it in
+power saving mode and only turn on receiving calls when I'm expecting a call,.
 
-It has pretty good hardware and I can almost reach 70% of the screen, which is
-better than other phones out there right now. Unfortunately it runs Samsung's
-impossible-to-remove bloatware on top of Android, but that is still less
-annoying to use than iOS.
+Previous phones I used and liked were:
 
-QUESTION: This is the only item on this list for which I am actively seeking a
-replacement, so if you have any tips about new phones that might fit these
-criteria that I've missed please let me know!
+* [Unihertz Atom L][atom-l] - small-screen, rugged phone with headphone jack
+* Original [iPhone SE][se] - perfect-sized phone, unfortunately with iOS
 
+[palm-phone]: https://palm.com/pages/product
+[atom-l]: https://www.unihertz.com/products/atom-l
 [se]: https://en.wikipedia.org/wiki/IPhone_SE
-[s10e]: https://www.phonearena.com/phones/Samsung-Galaxy-S10e_id11114
 
 # Other stuff
 
 ## Toothbrush
 
-The [Philips Sonicare][sonicare] (note: link goes to a newer generation than
-mine) is excellent and well worth its money.
+The [Philips Sonicare][sonicare] is excellent and well worth its price.
 
 I've had it for a few years and whereas I occasionally had minor teeth issues
 before, they seem to be mostly gone now. According to my dentist the state of my
 teeth is now usually pretty good and I draw a direct correlation back to this
 thing.
 
-The newer generations come with flashy features like apps and probably more
-LEDs, but I suspect that those can just be ignored.
+It has an app and stuff, but I just ignore that.
+
+I first got one of these in about 2014, and it lasted until 2020, at which point
+I upgraded to whatever the current model was.
 
 [sonicare]: https://www.philips.co.uk/c-m-pe/electric-toothbrushes
 
@@ -126,10 +137,12 @@ LEDs, but I suspect that those can just be ignored.
 
 The [Philipps SensoTouch 3D][sensotouch] is excellent. Super-comfortable close
 face shave in no time and leaves absolutely no mess around, as far as I can
-tell! I've had this for ~5 years and it's not showing any signs of aging yet.
+tell! I've had this for ~7 years and it's not showing any serious signs of aging
+yet.
 
-Another bonus is that its battery time is effectively infinite. I've never had
-to worry when bringing it on a longer trip!
+Another bonus is that its battery time is effectively infinite (in the order of
+months of use per charge). I've never had to worry when bringing it on a longer
+trip!
 
 [sensotouch]: https://www.philips.co.uk/c-p/1250X_40/norelco-sensotouch-3d-wet-and-dry-electric-razor-with-precision-trimmer
 
@@ -146,15 +159,26 @@ The one I settled on is the [Waterfield Muzetto][muzetto] leather bag. It's one
 of those things that comes with a bit of a price tag attached, but it's well
 worth it!
 
+**Unfortunately**, just like my speakers, this bag is now in storage somewhere
+in the UK since I left the country.
+
+After moving to Moscow I quickly ran into the same problem as in London when
+using the metro, but getting another Muzetto was kind of impractical.
+
+I couldn't find any other vertical messenger bags that I liked, and ended up
+going for a more traditional one: The [Brialdi Ostin][ostin].
+
 [muzetto]: https://www.sfbags.com/collections/shoulder-messenger-bags/products/muzetto-leather-bag
+[ostin]: https://www.brialdi.ru/shop/handbags/brialdi_ostin_brown/
 
 ## Wallet
 
-My wallet is the [Bellroy Slim Sleeve][slim-sleeve]. I don't carry cash unless
-I'm attending an event in Germany and this wallet fits that lifestyle perfectly.
+My wallet is the [Bellroy Coin Wallet][coin-wallet]. It's the slimmest wallet I
+could find that can deal with the volume of cards I (have to) carry around, as
+well as with cash.
 
-It's near indestructible, looks great, is very slim and fits a ton of cards,
-business cards, receipts and whatever else you want to be lugging around with
-you!
+I've used Bellroy wallets for a long time, with the [Slim Sleeve][slim-sleeve]
+serving me in the days when I lived in no-cash countries.
 
+[coin-wallet]: https://bellroy.com/products/coin-wallet
 [slim-sleeve]: https://bellroy.com/products/slim-sleeve-wallet/default/charcoal
diff --git a/users/tazjin/blog/posts/nixery-layers.md b/users/tazjin/blog/posts/nixery-layers.md
index 3f25ceadce..26526d11b5 100644
--- a/users/tazjin/blog/posts/nixery-layers.md
+++ b/users/tazjin/blog/posts/nixery-layers.md
@@ -260,13 +260,13 @@ TIP: This is implemented in [popcount][] in Nixery.
 Hopefully this detailed design review was useful to you. You can also watch [my
 NixCon talk][talk] about Nixery for a review of some of this, and some demos.
 
-[Nixery]: https://github.com/google/nixery
+[Nixery]: https://cs.tvl.fyi/depot/-/tree/tools/nixery
 [grhmc]: https://grahamc.com/blog/nix-and-layered-docker-images
 [Nix]: https://nixos.org/nix
 [registry protocols]: https://github.com/opencontainers/distribution-spec/blob/master/spec.md
 [nixery.dev]: https://nixery.dev
 [dominator trees]: https://en.wikipedia.org/wiki/Dominator_(graph_theory)
 [gonum/graph]: https://godoc.org/gonum.org/v1/gonum/graph
-[layers.go]: https://github.com/google/nixery/blob/master/builder/layers.go
-[popcount]: https://github.com/google/nixery/tree/master/popcount
+[layers.go]: https://cs.tvl.fyi/depot/-/blob/tools/nixery/layers/layers.go
+[popcount]: https://cs.tvl.fyi/depot/-/tree/tools/nixery/popcount
 [talk]: https://www.youtube.com/watch?v=pOI9H4oeXqA
diff --git a/users/tazjin/blog/posts/reliably-switch-buffers.md b/users/tazjin/blog/posts/reliably-switch-buffers.md
new file mode 100644
index 0000000000..ec56c4b2d0
--- /dev/null
+++ b/users/tazjin/blog/posts/reliably-switch-buffers.md
@@ -0,0 +1,18 @@
+ะ’ั‡ะตั€ะฐ ะฒะตั‡ะตั€ะพะผ ะฝะฐะฟะธัะฐะป ะฝะตะบะพั‚ะพั€ั‹ะต ะฟะฐั‚ั‡ะธ ะดะปั ะผะพะตะณะพ emacs-ะบะพะฝั„ะธะณะฐ. ะ˜ั… ะฝะฐ ัะฐะผะพะผ ะดะตะปะต ะดะฐะฒะฝะพ ัƒะถะต ั…ะพั‚ะตะป ะฝะฐะฟะธัะฐั‚ัŒ, ะพะฝะธ ั€ะตัˆะฐัŽั‚ ะผะฐะปะตะฝัŒะบะธะต ะฟั€ะพะฑะปะตะผั‹ ะบะพั‚ะพั€ั‹ะต ะผะฝะต ะฟะพัั‚ะพัะฝะฝะพ ะผะตัˆะฐะปะธ. ะžะฑ ะพะดะฝะพะน ะธะท ะฟั€ะพะฑะปะตะผ ั ั…ะพั‡ัƒ ั€ะฐััะบะฐะทะฐั‚ัŒ, ะฟะพั‚ะพะผัƒ ั‡ั‚ะพ ะพะฝะฐ ะฟั€ะธะฒะตะปะฐ ะบ ั‚ะพะผัƒ, ั‡ั‚ะพ "ะฟะพั€ะพะณ ั€ะฐะทะดั€ะฐะถะตะฝะธั" ะฑั‹ะป ะฟะตั€ะตัั‚ัƒะฟะปะตะฝ.
+
+Emacs ัƒ ะผะตะฝั ะพัะฝะพะฒะฝะฐั ั‡ะฐัั‚ัŒ ัะฒะพะตะน ั€ะฐะฑะพั‡ะตะน ัั€ะตะดั‹. ะžะฝ ัƒ ะผะตะฝั ัะฒะปัะตั‚ัั, ะบะพะฝะตั‡ะฝะพ, ั‚ะตะบัั‚ะพะฒั‹ะผ ั€ะตะดะฐะบั‚ะพั€ะพะผ, ะฝะพ ะธ ะตั‰ะต ะผะตะฝะตะดะถะตั€ะพะผ ะพะบะพะฝ, ะผัะนะป-ะบะปะธะตะฝั‚ะพะผ, ั‡ะฐั‚-ะบะปะธะตะฝั‚ะพะผ ะธ ะผะฝะพะณะพ ะดั€ัƒะณะพะณะพ.
+
+ะ’ะฝัƒั‚ั€ะธ emacs ะตัั‚ัŒ ะบะพะฝั†ะตะฟั†ะธั "ะฑัƒั„ะตั€ะพะฒ", ะพะดะธะฝ ะฑัƒั„ะตั€ ะผะพะถะตั‚ ะฑั‹ั‚ัŒ ะพะดะธะฝ ะพั‚ะบั€ั‹ั‚ั‹ะน ั„ะฐะนะป ะฒ ั‚ะตะบัั‚ะพะฒะพะผ ั€ะตะดะฐะบั‚ะพั€ะต, ะพะดะธะฝ ั‡ะฐั‚ ะฝะฐ ะขะตะปะตะณั€ะฐะผะต, ะธะปะธ ะพะดะฝะพ ะดะตัะบั‚ะพะฟะฝะพะต ะพะบะฝะพ (ะฝะฐะฟั€ะธะผะตั€, ะฑั€ะฐัƒะทะตั€). ะะฐะฒะธะณะฐั†ะธั ะผะตะถะดัƒ ะฝะธะผะธ ะพััƒั‰ะตัั‚ะฒะปัะตั‚ัั ั ะฟะพะผะพั‰ัŒัŽ ะบะพะผะฐะฝะดั‹ `switch-to-buffer` (ะธะปะธ ะบะพะต-ะบะฐะบะธั… ะฐะปัŒั‚ะตั€ะฝะฐั‚ะธะฒ, ะฝะฐะฟั€ะธะผะตั€ `ivy-switch-buffer`, `helm-switch-buffer` ะธ ั‚ะฐะบ ะดะฐะปะตะต). ะ‘ัƒั„ะตั€ - ะฝะฐ ัั‚ะพั€ะพะฝะต emacs-lisp ัะฒะปัะตั‚ัั ะพะฑัŠะตะบั‚ะพะผ ั ะฝะตะบะพั‚ะพั€ั‹ะผะธ ะฟะพะปัะผะธ. ะžะดะฝะพ ะธะท ะฝะธั…: `buffer-name`.
+
+ะฃ ะฒัะตั… buffer-switch ะบะพะผะฐะฝะด ะตัั‚ัŒ ะพะดะธะฝะฐะบะพะฒะฐั ะฟั€ะพะฑะปะตะผะฐ: ะžะฝะธ ะฑะตั€ัƒั‚ ัะฟะธัะพะบ ะฑัƒั„ะตั€ะพะฒ ะธะท emacs, ะฟะพะบะฐะทั‹ะฒะฐัŽั‚ *ะธะผะตะฝะฐ* ะฑัƒั„ะตั€ะพะฒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŽ, ะธ ะฒ ั€ะตะทัƒะปัŒั‚ะฐั‚ะต ะฟะพะปัƒั‡ะฐัŽั‚ ะฒั‹ะฑั€ะฐะฝะฝะพะต *ะธะผั*. ะ—ะฐั‚ะตะผ ะพะฝะธ ะฟั€ะพััั‚ emacs ะพั‚ะบั€ั‹ั‚ัŒ ะฑัƒั„ะตั€ ั ัั‚ะธะผ ะธะผะตะฝะตะผ.
+
+ะšั‚ะพ-ั‚ะพ ะฝะฐะฒะตั€ะฝะพ ัƒะถะต ะฟะพะฝัะป ะบะฐะบะฐั ั‚ัƒั‚ ะฟั€ะพะฑะปะตะผะฐ. ะ˜ะผะตะฝะธ ะฑัƒั„ะตั€ะพะฒ ะผะพะณัƒั‚ ะผะตะฝัั‚ัŒัั, ะธ ะดะฐ, ะฝะต ั‚ะพะปัŒะบะพ ะผะพะณัƒั‚, ะฝะพ ะธ ะดะตะปะฐัŽั‚! ะะฐะฟั€ะธะผะตั€, ะขะตะปะตะณั€ะฐะผ-ะบะปะธะตะฝั‚ ะผะพะถะตั‚ ะฟะพะบะฐะทะฐั‚ัŒ ะบะฐะปะธั‡ะตัั‚ะฒะพ ะฝะตะฟั€ะพั‡ะธั‚ะฐะฝะฝั‹ั… ัะพะพะฑั‰ะตะฝะธะน ะฒ ะฝะฐะทะฒะฐะฝะธะธ, ะพะบะฝะพ ั ะฏะฝะดะตะบั ะœัƒะทั‹ะบะพะน ะผะตะฝัะตั‚ ะฝะฐะทะฒะฐะฝะธั ะฟะพ ั‚ั€ะตะบัƒ, ะธ ั‚ะฐะบ ะดะฐะปะตะต. ะŸะพะปัƒั‡ะฐะตั‚ัั ะดะพะฒะพะปัŒะฝะพ ั‡ะฐัั‚ะพ ั‚ะฐะบะฐั ัะธั‚ัƒะฐั†ะธั, ั‡ั‚ะพ ะฝะฐะทะฒะฐะฝะธะต ะผะตะฝัะตั‚ัั ะฟั€ะธ ะฒั‹ะฑะพั€ะต ะฑัƒั„ะตั€ะฐ, ะธ `switch-to-buffer` ะฑะพะปัŒัˆะต ะฝะต ะฝะฐะนะดะตั‚ ะฒั‹ะฑั€ะฐะฝะฝั‹ะน ะฑัƒั„ะตั€ ะธ ะฟั€ะพัั‚ะพ ะพั‚ะบั€ั‹ะฒะฐะตั‚ ะฝะพะฒั‹ะน, ะฟัƒัั‚ะพะน ะฑัƒั„ะตั€ ั ัั‚ะฐั€ั‹ะผ ะฝะฐะทะฒะฐะฝะธะตะผ! ะšะพะณะดะฐ ั€ะฐะทั€ะฐะฑะพั‚ั‹ะฒะฐะปะธ ัั‚ะธ ะบะพะผะฐะฝะดั‹ ะฒ emacs (ะดะฐ, ัั‚ะพ ัะพะฒะตั€ัˆะตะฝะฝะพ ะดะฐะฒะฝะพ, ะณะดะต-ั‚ะพ ะฒ 70ั…/80ั…, ะฑะพะปัŒัˆะธะฝัั‚ะฒะฐ ะฝะฐั ะฟะพะบะฐ ะฝะต ะฑั‹ะปะพ ั‚ะพะณะดะฐ!), ะพะฝะธ ะฝะธะบะพะณะดะฐ ะฝะต ัั‚ะฐะปะบะธะฒะฐะปะธััŒ ั ั‚ะฐะบะธะผะธ ัะธั‚ัƒะฐั†ะธัะผะธ, ะธ ัั‚ะพ ั€ะตัˆะตะฝะธะต, ะบะพั‚ะพั€ะพะต ั‚ะพะณะดะฐ ั…ะพั€ะพัˆะพ ั€ะฐะฑะพั‚ะฐะปะพ ั‚ะตะฟะตั€ัŒ ะฑะพะปัŒัˆะต ะฟั€ะพัั‚ะพ ะฝะต ะฐะดะตะบะฒะฐั‚ะฝะพ.
+
+ะคะธะบั ะฑั‹ะป ะฝะต ะพั‡ะตะฝัŒ ัะปะพะถะฝั‹ะผ. ะ’ะผะตัั‚ะพ ัะฟะธัะบะฐ ะธะผะตะฝ ะฑัƒั„ะตั€ะพะฒ ัะพะทะดะฐัŽ alist ั ะฝะฐะทะฒะฐะฝะธะตะผ ะธ *ั ัะฐะผะธะผ ะพะฑัŠะตะบั‚ะพะผ*, ะธ ะฟะพัะปะต ะฒั‹ะฑะพั€ะฐ ะฑัƒั„ะตั€ะฐ ั ัะฟะธัะบะฐ ะฟะตั€ะตะดะฐัŽ ะธะผะตะฝะฝะพ ัั‚ะพั‚ ะพะฑัŠะตะบั‚, ะฐ ะฝะต ั‚ะพะปัŒะบะพ ะตะณะพ ะฝะฐะทะฒะฐะฝะธะต, ะฒ ั„ัƒะฝะบั‚ั†ะธัŽ, ะบะพั‚ะพั€ะฐั ะพั‚ะบั€ั‹ะฒะฐะตั‚ ะฑัƒั„ะตั€.
+
+ะšะพะผะผะธั‚ ั ัั‚ะพะน ะฝะพะฒะพะน ั„ัƒะฝะบั†ะธะตะน ะทะดะตััŒ: cl/9147
+ะกะพะฒะตั‚ัƒัŽ ะตั‘ ะพัะพะฑะตะฝะฝะพ ะฒัะตะผ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัะผ EXWM!
+
+ะ”ะปั ะผะตะฝั ัั‚ะพ ะฝะฐัั‚ะพัั‰ะตะต ัƒะปัƒั‡ัˆะตะฝะธะต ะถะธะทะฝะธ. ะšะพะฝะตั‡ะฝะพ, ัั‚ะพ ัั‚ั€ะฐะฝะฝะพ ะทะฒัƒั‡ะธั‚, ะฝะพ ะดะฐะถะต ะตัะปะธ ะฑั‹ ัƒ ะผะตะฝั ะฑั‹ะปะฐ ั‚ะฐะบะฐั ะฟั€ะพะฑะปะตะผะฐ ะฒัะตะณะพ ั€ะฐะท ะฒ ะดะตะฝัŒ, ัั‚ะพ ะบะฐะบะธะผ-ั‚ะพ ะพะฑั€ะฐะทะพะผ ะฟั€ะธะฒะตะปะพ ะฑั‹ ะบ ัƒั…ัƒะดัˆะตะฝะธัŽ ะผะพะตะณะพ ะฝะฐัั‚ั€ะพะตะฝะธั. ะšะฐะบ ะผะฐะปะตะฝัŒะบะธะน ะบะฐะผะตัˆะตะบ ะฒ ั‚ะฒะพะตะผ ะฑะพั‚ะธะฝะบะต.
+
+ะ’ั‹ะฝัŒั‚ะต ะบะฐะผะฝะธ ะธะท ัะฒะพะธั… ะฑะพั‚ะธะฝะพะบ!
diff --git a/users/tazjin/blog/posts/reversing-watchguard-vpn.md b/users/tazjin/blog/posts/reversing-watchguard-vpn.md
index f1b779d8d9..e000d7a764 100644
--- a/users/tazjin/blog/posts/reversing-watchguard-vpn.md
+++ b/users/tazjin/blog/posts/reversing-watchguard-vpn.md
@@ -1,11 +1,11 @@
 TIP: WatchGuard has
-[responded](https://www.reddit.com/r/netsec/comments/5tg0f9/reverseengineering_watchguard_mobile_vpn/dds6knx/)
+[responded](https://web.archive.org/web/20230326041952/https://www.reddit.com/r/netsec/comments/5tg0f9/reverseengineering_watchguard_mobile_vpn/dds6knx/)
 to this post on Reddit. If you haven\'t read the post yet I\'d recommend
 doing that first before reading the response to have the proper context.
 
 ------------------------------------------------------------------------
 
-One of my current client makes use of
+One of my current clients makes use of
 [WatchGuard](http://www.watchguard.com/help/docs/fireware/11/en-US/Content/en-US/mvpn/ssl/mvpn_ssl_client-install_c.html)
 Mobile VPN software to provide access to the internal network.
 
@@ -15,22 +15,22 @@ provided, but it quickly turned out that this was only a piece of the
 puzzle.
 
 The problem is that this VPN setup is secured using 2-factor
-authentication (good!), but it does not use OpenVPN\'s default
+authentication (good!), but it does not use OpenVPN's default
 [challenge/response](https://openvpn.net/index.php/open-source/documentation/miscellaneous/79-management-interface.html)
 functionality to negotiate the credentials.
 
 Connecting with the OpenVPN config that the website supplied caused the
-VPN server to send me a token to my phone, but I simply couldn\'t figure
+VPN server to send me a token to my phone, but I simply couldn't figure
 out how to supply it back to the server. In a normal challenge/response
 setting the token would be supplied as the password on the second
 authentication round, but the VPN server kept rejecting that.
 
 Other possibilities were various combinations of username&password
-(I\'ve seen a lot of those around) so I tried a whole bunch, for example
+(I've seen a lot of those around) so I tried a whole bunch, for example
 `$password:$token` or even a `sha1(password, token)` - to no avail.
 
 At this point it was time to crank out
-[Hopper](https://www.hopperapp.com/) and see what\'s actually going on
+[Hopper](https://www.hopperapp.com/) and see what's actually going on
 in the official OS X client - which uses OpenVPN under the hood!
 
 Diving into the client
@@ -95,7 +95,7 @@ formatted the URL, opened it and checked whether the `logon_status` was
 `4` before proceeding with the `logon_id` and `chaStr` contained in the
 response.
 
-*(Code snippets from here on are Hopper\'s pseudo-Objective-C)*
+*(Code snippets from here on are Hopper's pseudo-Objective-C)*
 
 ![sslvpnLogon](/static/img/watchblob_3.webp)
 
@@ -112,7 +112,7 @@ to do something quite interesting:
 
 ![processTokenPrompt2](/static/img/watchblob_5.webp)
 
-The user\'s password was overwritten with the (verified) OTP token -
+The user's password was overwritten with the (verified) OTP token -
 before OpenVPN had even been started!
 
 Reading a bit more of the code in the subsequent
@@ -130,29 +130,29 @@ after configuring OpenVPN with the correct config file:
 TL;DR
 -----
 
-Rather than using OpenVPN\'s built-in challenge/response mechanism, the
+Rather than using OpenVPN's built-in challenge/response mechanism, the
 WatchGuard client validates user credentials *outside* of the VPN
 connection protocol and then passes on the OTP token, which seems to be
-temporarily in a \'blessed\' state after verification, as the user\'s
+temporarily in a 'blessed' state after verification, as the user's
 password.
 
-I didn\'t check to see how much verification of this token is performed
+I didn't check to see how much verification of this token is performed
 (does it check the source IP against the IP that performed the challenge
 validation?), but this certainly seems like a bit of a security issue -
 considering that an attacker on the same network would, if they time the
 attack right, only need your username and 6-digit OTP token to
 authenticate.
 
-Don\'t roll your own security, folks!
+Don't roll your own security, folks!
 
 Bonus
 -----
 
 The whole reason why I set out to do this is so I could connect to this
-VPN from Linux, so this blog post wouldn\'t be complete without a
+VPN from Linux, so this blog post wouldn't be complete without a
 solution for that.
 
-To make this process really easy I\'ve written a [little
+To make this process really easy I've written a [little
 tool](https://github.com/tazjin/watchblob) that performs the steps
 mentioned above from the CLI and lets users know when they can
 authenticate using their OTP token.
diff --git a/users/tazjin/blog/posts/thoughts.md b/users/tazjin/blog/posts/thoughts.md
new file mode 100644
index 0000000000..7ce23f9c87
--- /dev/null
+++ b/users/tazjin/blog/posts/thoughts.md
@@ -0,0 +1,142 @@
+<!--
+
+  This file contains a bunch of random thoughts I don't want to lose,
+  often resulting from conversation with other people, but that are
+  too far removed from what most people can relate to for me to just
+  publish them. Sometimes it's convenient to be able to share them,
+  though.
+
+  For that reason, if you stumble upon this file without me having
+  linked it to you intentionally, feel free to read it but keep the
+  sharing to a minimum (though do feel free to share the thoughts
+  themselves, of course).
+
+-->
+WARNING: This is not intended for a large audience. If you stumble
+upon this page by chance, please keep the sharing to a minimum.
+
+TIP: It's always work-in-progress. Things come and go. Or change. Who
+knows?
+
+---------
+
+### Three things
+
+*[mid/late 2020]*
+
+All things in the universe take the shape of one of approximately
+three things. If you had Hoogle for the entire universe, you'd
+probably find that one of them is `fmap`.
+
+There might be a few more, or a few less (or some may have been
+deprecated), but you get the idea. I guess [five][] would be a good
+number.
+
+[five]: https://principiadiscordia.com/book/23.php
+
+----------------------
+
+### Free energy principle
+
+*[mid/late 2020]*
+
+Karl Friston wrote:
+
+> The free-energy principle says that any self-organizing system that
+> is at equilibrium with its environment must minimize its free
+> energy.
+
+Or, somewhat paraphrased:
+
+> Any Markov blanket capable of modeling its environment aims to
+> reduce its level of surprise by either adapting its model, or
+> through other action.
+
+Seems reasonable to me.
+
+### More bizarre universe
+
+*[many years ago]*
+
+Douglas Adams wrote:
+
+> There is a theory which states that if ever anyone discovers exactly
+> what the Universe is for and why it is here, it will instantly
+> disappear and be replaced by something even more bizarre and
+> inexplicable. There is another theory which states that this has
+> already happened.
+
+### Alpha decay
+
+*[late 2022]*
+
+Finance people say:
+
+> Alpha Decay is commonly referred to as the loss of prediction power
+> of a trading strategy over time. As a consequence, the profitability
+> of a strategy tends to gradually decrease. Given enough time, the
+> strategy converges to having no superior predictive power and
+> returns when compared to a suitable benchmark.
+
+A market is a big optimiser. Any successful trading strategy adds
+friction in a place that the optimiser wants to remove.
+
+Alpha decay is unavoidable without changing and adapting the strategy.
+
+### Optimising universe
+
+*[late 2022]*
+
+*(thanks edef for helping me think through this one!)*
+
+Assume that the universe acts as a giant optimiser, and consider that
+the three things above are related and specialisations of more generic
+ideas:
+
+1. Every delineable entity in the universe (i.e. every *Markov
+   blanket*) attempts to reduce its level of surprise (the free energy
+   principle).
+
+2. The universe needs replacement (a more bizarre universe) if global
+   surprise drops to a minimum[^heat].
+
+3. Without improvement that outpaces the optimiser of the universe,
+   any strategy leading to (2) will get eroded by alpha decay long
+   before.
+
+4. We don't know if it is possible to outpace the optimiser from
+   within.
+
+On a personal note, it seems to me that achieving (2) is likely
+undesirable. It probably takes god[^god] a lot of resources to create
+an ever more complex universe and this process might be much less
+enjoyable than "running" (for lack of a better word) a universe. Under
+this assumption, a universe that achieves (2) faster than others might
+be a failure, and on a higher level conditions leading to its creation
+might be subject to another optimiser.
+
+Or it could be the other way around, but this seems more likely to me
+personally.
+
+### Superintelligence
+
+*[late 2022]*
+
+Under the previous assumption, achieving superintelligence is likely a
+bad idea for anyone feeling some kind of attachment to *this*
+universe.
+
+Or it might be the exact opposite, but I don't think so.
+
+-------------------------------
+
+[^heat]: Note that this is consistent with the heat death of the
+    universe.
+
+[^god]: I'm using the word "god" as the best English approximation of
+    a concept that different religions and philosophies all attempt to
+    approach. I think that for many cognitive purposes, an
+    anthropomorphised idea (as in the abrahamic religions) is useful,
+    but ideas from some Eastern religions or modern philosophers like
+    Bach or Watts are likely more aligned with the "nature of things"
+    as such.
diff --git a/users/tazjin/blog/posts/tvix-eval-talk-2023.md b/users/tazjin/blog/posts/tvix-eval-talk-2023.md
new file mode 100644
index 0000000000..4a0ec56881
--- /dev/null
+++ b/users/tazjin/blog/posts/tvix-eval-talk-2023.md
@@ -0,0 +1,19 @@
+7 ัะตะฝั‚ัะฑั€ั ั ะฒั‹ัั‚ัƒะฟะธะป ั ะดะพะบะปะฐะดะพะผ ะฟั€ะพ ั€ะตะฐะปะธะทะฐั†ะธัŽ ัะทั‹ะบะฐ Nix ะฝะฐ Rust, ะฝะฐ
+[ะœะพัะบะพะฒัะบะพะผ Rust-ะผะธั‚ะฐะฟะต][rustmsk] / [ะœะพัะบะพะฒัะบะพะผ ะบะปัƒะฑะต
+ะฟั€ะพะณั€ะฐะผะผะธัั‚ะพะฒ][progmsk].
+
+<iframe width="800" height="500" src="https://www.youtube.com/embed/7zS2_ZhwPfY?start=4013" title="RUST - ัะพะฒั€ะตะผะตะฝะฝั‹ะน ัะทั‹ะบ ะฟั€ะพะณั€ะฐะผะผะธั€ะพะฒะฐะฝะธั" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe>
+
+ะ’ะพั‚ ะฒัะต ัะฒัะทะฐะฝะฝั‹ะต ั ะฝะธะผ ััั‹ะปะบะธ, ะบะพั‚ะพั€ั‹ะต ะผะพะณัƒั‚ ะฑั‹ั‚ัŒ ะธะฝั‚ะตั€ะตัะฝั‹:
+
+* [Tvix](https://tvix.dev), ะณะปะฐะฒะฝั‹ะน ัะฐะนั‚ ะฟั€ะพะตะบั‚ะฐ
+* [TVL](https://tvl.fyi), ะฝะฐัˆะต ะพะฝะปะฐะนะฝ-ัะพะพะฑั‰ะตัั‚ะฒะพ
+* [Tvixbolt](https://bolt.tvix.dev/), ะฝะฐัˆ "godbolt" ะดะปั tvix
+* [MMTk](https://www.mmtk.io/), Rust-ะฑะธะฑะปะธะพั‚ะตะบะฐ ั ะบะพะผะฟะพะฝะตะฝั‚ะฐะผะธ ะดะปั garbage-collection
+* [ะ˜ะฝั‚ะตั€ะฒัŒัŽ / ะดะพะบะปะฐะด](https://www.youtube.com/live/0Lhahzs-Wos?si=BlFDVBUPsIpHg0p5), Nix -- ะฝะต ั‚ะพะปัŒะบะพ ะฟะฐะบะตั‚ะฝั‹ะน ะผะตะฝะตะดะถะตั€
+* [NixCon 2023](https://2023.nixcon.org/)
+* [Yew](https://yew.rs/), WASM-ั„ั€ะตะนะผะฒะพั€ะบ ะดะปั Rust
+* [tazlog](https://t.me/tazlog), ะผะพะน ะบะฐะฝะฐะป ะฝะฐ ะขะตะปะตะณะต
+
+[rustmsk]: https://t.me/ruRust_msk
+[progmsk]: https://prog.msk.ru/
diff --git a/users/tazjin/chase-geese/default.nix b/users/tazjin/chase-geese/default.nix
new file mode 100644
index 0000000000..3549f75868
--- /dev/null
+++ b/users/tazjin/chase-geese/default.nix
@@ -0,0 +1,13 @@
+# Helpers for mounting GeeseFS into the right place.
+{ depot, pkgs, ... }:
+
+pkgs.writeShellScriptBin "chase-geese" ''
+  set -ueo pipefail
+
+  echo "Fetching credentials ..."
+  eval $(pass show keys/tazjin-geesefs)
+
+  echo "Mounting the cloud ..."
+  mkdir -p ~/cloud
+  ${depot.third_party.geesefs}/bin/geesefs tazjins-files ~/cloud
+''
diff --git a/users/tazjin/cloud-dns/oslo-pub b/users/tazjin/cloud-dns/oslo-pub
deleted file mode 100644
index 674687484b..0000000000
--- a/users/tazjin/cloud-dns/oslo-pub
+++ /dev/null
@@ -1,8 +0,0 @@
-;; Do not delete these
-oslo.pub. 21600 IN NS ns-cloud-c1.googledomains.com.
-oslo.pub. 21600 IN NS ns-cloud-c2.googledomains.com.
-oslo.pub. 21600 IN NS ns-cloud-c3.googledomains.com.
-oslo.pub. 21600 IN NS ns-cloud-c4.googledomains.com.
-oslo.pub. 21600 IN SOA ns-cloud-c1.googledomains.com. cloud-dns-hostmaster.google.com. 4 21600 3600 1209600 300
-
-oslo.pub. 60 IN A 46.21.106.241
diff --git a/users/tazjin/default.nix b/users/tazjin/default.nix
new file mode 100644
index 0000000000..1b68b7127a
--- /dev/null
+++ b/users/tazjin/default.nix
@@ -0,0 +1,30 @@
+# //users/tazjin-specific CI configuration.
+{ depot, pkgs, ... }:
+
+let
+  rustfmt = pkgs.writeShellScript "rustfmt-tazjin" ''
+    ${pkgs.fd}/bin/fd -e rs | \
+      ${pkgs.ripgrep}/bin/rg 'users/tazjin' | \
+      xargs ${pkgs.rustfmt}/bin/rustfmt --check --config-path users/tazjin
+  '';
+
+in
+depot.nix.readTree.drvTargets {
+  rustfmt = rustfmt.overrideAttrs (_: {
+    # rustfmt not respecting config atm, disable
+    meta.ci.skip = true;
+
+    meta.ci.extraSteps.rustfmt = {
+      command = rustfmt;
+    };
+  });
+
+  # Use a screen lock command that resets the keyboard layout
+  # before locking, to avoid locking me out when the layout is
+  # in Russian.
+  screenLock = pkgs.writeShellScriptBin "tazjin-screen-lock" ''
+    ${pkgs.xorg.setxkbmap}/bin/setxkbmap us
+    ${pkgs.xorg.setxkbmap}/bin/setxkbmap -option caps:super
+    exec ${pkgs.xsecurelock}/bin/xsecurelock
+  '';
+}
diff --git a/users/tazjin/dns/default.nix b/users/tazjin/dns/default.nix
new file mode 100644
index 0000000000..6ff6cc06e2
--- /dev/null
+++ b/users/tazjin/dns/default.nix
@@ -0,0 +1,13 @@
+# Performs simple (local-only) validity checks on DNS zones.
+{ depot, pkgs, ... }:
+
+let
+  checkZone = zone: file: pkgs.runCommand "${zone}-check" { } ''
+    ${pkgs.bind}/bin/named-checkzone -i local ${zone} ${file} | tee $out
+  '';
+
+in
+depot.nix.readTree.drvTargets {
+  kontemplate-works = checkZone "kontemplate.works" ./kontemplate.works.zone;
+  tazj-in = checkZone "tazj.in" ./tazj.in.zone;
+}
diff --git a/users/tazjin/cloud-dns/import b/users/tazjin/dns/import
index e79e426b55..8ea1d694c9 100755
--- a/users/tazjin/cloud-dns/import
+++ b/users/tazjin/dns/import
@@ -1,10 +1,11 @@
 #!/bin/sh
 set -ue
 
-# Imports a zone file into a Google Cloud DNS zone of the same name
+# Imports a zone file into Google Cloud DNS
 readonly ZONE="${1}"
+readonly FILE="${2}"
 
-gcloud dns record-sets import "${ZONE}" \
+gcloud dns record-sets import "${FILE}" \
        --project composite-watch-759 \
        --zone-file-format \
        --delete-all-existing \
diff --git a/users/tazjin/cloud-dns/kontemplate-works b/users/tazjin/dns/kontemplate.works.zone
index 326a129d21..326a129d21 100644
--- a/users/tazjin/cloud-dns/kontemplate-works
+++ b/users/tazjin/dns/kontemplate.works.zone
diff --git a/users/tazjin/cloud-dns/root-tazj-in b/users/tazjin/dns/tazj.in.zone
index 43db5834a0..43db5834a0 100644
--- a/users/tazjin/cloud-dns/root-tazj-in
+++ b/users/tazjin/dns/tazj.in.zone
diff --git a/users/tazjin/docs/install-zfs.md b/users/tazjin/docs/install-zfs.md
new file mode 100644
index 0000000000..415af30fd4
--- /dev/null
+++ b/users/tazjin/docs/install-zfs.md
@@ -0,0 +1,116 @@
+Current steps for my NixOS-on-ZFS installs with impermanence.
+
+## Target layout (example from tverskoy):
+
+Partitioning:
+
+```
+nvme0n1     259:0    0 238.5G  0 disk
+โ”œโ”€nvme0n1p1 259:1    0   128M  0 part /boot (type: EFI system)
+โ””โ”€nvme0n1p2 259:2    0 238.3G  0 part       (type: Solaris root)
+```
+
+ZFS layout:
+
+```
+NAME                   USED  AVAIL     REFER  MOUNTPOINT
+zpool                  212G  19.0G      248K  /zpool
+zpool/ephemeral        668M  19.0G      192K  /zpool/ephemeral
+zpool/ephemeral/home   667M  19.0G      667M  legacy
+zpool/local           71.3G  19.0G      192K  /zpool/local
+zpool/local/nix       71.3G  19.0G     71.3G  legacy
+zpool/safe             140G  19.0G      192K  /zpool/safe
+zpool/safe/depot       414M  19.0G      414M  legacy
+zpool/safe/persist     139G  19.0G      139G  legacy
+```
+
+With reset-snapshots:
+
+```
+NAME                                USED  AVAIL     REFER  MOUNTPOINT
+zpool/ephemeral/home@blank          144K      -      192K  -
+zpool/ephemeral/home@tazjin-clean   144K      -      200K  -
+```
+
+Legacy mountpoints are used because the NixOS wiki advises that using
+ZFS own mountpoints might lead to issues with the mount order during
+boot.
+
+## Install steps
+
+1. First, get internet.
+
+2. Use `fdisk` to set up the partition layout above (fwiw, EFI type
+   should be `1`, Solaris root should be `66`).
+
+3. Format the first partition for EFI: `mkfs.fat -F32 -n EFI $part1`
+
+4. Init ZFS stuff:
+
+   ```
+   zpool create \
+     # 2 SSD only settings
+     -o ashift=12 \
+     -o autotrim=on \
+     -R /mnt \
+     -O canmount=off \
+     -O mountpoint=none \
+     -O acltype=posixacl \
+     -O compression=lz4 \
+     -O atime=off \
+     -O xattr=sa \
+     -O encryption=aes-256-gcm \
+     -O keylocation=prompt \
+     -O keyformat=passphrase \
+     zpool $part2
+   ```
+
+   Reserve some space for deletions:
+
+   ```
+   zfs create -o refreservation=1G -o mountpoint=none zpool/reserved
+   ```
+
+   Create the datasets as per the target layout:
+
+   ```
+   # Throwaway datasets
+   zfs create -o canmount=off -o mountpoint=none zpool/ephemeral
+   zfs create -o mountpoint=legacy zpool/ephemeral/root
+   zfs create -o mountpoint=legacy zpool/ephemeral/home
+
+   # Persistent datasets
+   zfs create -o canmount=off -o mountpoint=none zpool/persistent
+   zfs create -o mountpoint=legacy zpool/persistent/nix
+   zfs create -o mountpoint=legacy zpool/persistent/depot
+   zfs create -o mountpoint=legacy zpool/persistent/data
+   ```
+
+   Create completely blank snapshots of the ephemeral datasets:
+
+   ```
+   zfs snapshot zpool/ephemeral/root@blank
+   zfs snapshot zpool/ephemeral/home@blank
+   ```
+
+   The ephemeral home volume needs the user folder already set up with
+   permissions. Mount it and create the folder there:
+
+   ```
+   mount -t zfs zpool/ephemeral/root /mnt
+   mkdir /mnt/home
+   mount -t zfs zpool/ephemeral/home /mnt/home
+   mkdir /mnt/home/tazjin
+   chmod 1000:100 /mnt/home/tazjin
+   zfs snapshot zpool/ephemeral/home@tazjin-clean
+   ```
+
+   Now the persistent Nix store volume can be mounted and installation
+   can begin.
+
+   ```
+   mkdir /mnt/nix
+   mount -t zfs zpool/persistent/nix /mnt/nix
+   ```
+
+4. Configure & install NixOS as usual.
diff --git a/users/tazjin/dotfiles/default.nix b/users/tazjin/dotfiles/default.nix
new file mode 100644
index 0000000000..9b783a9c85
--- /dev/null
+++ b/users/tazjin/dotfiles/default.nix
@@ -0,0 +1,3 @@
+_: {
+  dunstrc = ./dunstrc;
+}
diff --git a/users/tazjin/dotfiles/dunstrc b/users/tazjin/dotfiles/dunstrc
new file mode 100644
index 0000000000..2aa1141b6e
--- /dev/null
+++ b/users/tazjin/dotfiles/dunstrc
@@ -0,0 +1,54 @@
+[global]
+font = Iosevka Term 11
+origin = top-left
+markup = yes
+plain_text = no
+format = "<b>%s</b>\n%b"
+sort = no
+indicate_hidden = yes
+alignment = center
+bounce_freq = 0
+show_age_threshold = -1
+word_wrap = yes
+ignore_newline = no
+stack_duplicates = yes
+hide_duplicate_count = yes
+geometry = "300x50-15+49"
+shrink = no
+transparency = 5
+idle_threshold = 0
+monitor = 0
+follow = keyboard
+sticky_history = yes
+history_length = 15
+show_indicators = no
+line_height = 3
+separator_height = 2
+padding = 6
+horizontal_padding = 6
+separator_color = frame
+startup_notification = false
+dmenu = /usr/bin/dmenu -p dunst:
+browser = /usr/bin/firefox -new-tab
+icon_position = off
+max_icon_size = 80
+frame_width = 3
+frame_color = "#8EC07C"
+
+[urgency_low]
+frame_color = "#3B7C87"
+foreground = "#3B7C87"
+background = "#191311"
+timeout = 4
+
+[urgency_normal]
+frame_color = "#5B8234"
+foreground = "#5B8234"
+background = "#191311"
+timeout = 6
+
+[urgency_critical]
+frame_color = "#B7472A"
+foreground = "#B7472A"
+background = "#191311"
+timeout = 8
diff --git a/users/tazjin/dt/CMakeLists.txt b/users/tazjin/dt/CMakeLists.txt
deleted file mode 100644
index 85b659fea8..0000000000
--- a/users/tazjin/dt/CMakeLists.txt
+++ /dev/null
@@ -1,16 +0,0 @@
-# -*- mode: cmake; -*-
-cmake_minimum_required(VERSION 3.16)
-project(dt)
-add_executable(dt dt.cc)
-find_package(absl REQUIRED)
-
-target_link_libraries(dt
-  absl::flags
-  absl::flags_parse
-  absl::hash
-  absl::time
-  absl::strings
-  farmhash
-)
-
-install(TARGETS dt DESTINATION bin)
diff --git a/users/tazjin/dt/README.md b/users/tazjin/dt/README.md
deleted file mode 100644
index ee43d56064..0000000000
--- a/users/tazjin/dt/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-dt
-==
-
-It's got a purpose.
-
-## Usage:
-
-```
-nix-build -E '(import (builtins.fetchGit "https://git.tazj.in/") {}).fun.dt'
-./result/bin/dt --one ... --two ...
-```
diff --git a/users/tazjin/dt/default.nix b/users/tazjin/dt/default.nix
deleted file mode 100644
index ae9388d187..0000000000
--- a/users/tazjin/dt/default.nix
+++ /dev/null
@@ -1,15 +0,0 @@
-{ depot, pkgs, ... }:
-
-let
-  stdenv = with pkgs; overrideCC clangStdenv clang_10;
-  abseil_cpp = pkgs.abseil_cpp;
-in stdenv.mkDerivation {
-  name = "dt";
-  src = ./.;
-  nativeBuildInputs = [ pkgs.cmake ];
-  buildInputs = with pkgs; [
-    abseil_cpp
-    farmhash
-  ];
-  meta.ci = false;
-}
diff --git a/users/tazjin/dt/dt.cc b/users/tazjin/dt/dt.cc
deleted file mode 100644
index 5c4c3da768..0000000000
--- a/users/tazjin/dt/dt.cc
+++ /dev/null
@@ -1,79 +0,0 @@
-#include <iostream>
-#include <vector>
-
-#include "absl/flags/flag.h"
-#include "absl/flags/parse.h"
-#include "absl/hash/hash.h"
-#include "absl/strings/str_cat.h"
-#include "absl/time/clock.h"
-#include "absl/time/time.h"
-#include "absl/types/optional.h"
-#include "farmhash.h"
-
-ABSL_FLAG(std::vector<std::string>, words, {}, "words to use");
-
-struct Result {
-  std::string a;
-  int ec;
-  absl::optional<std::string> p;
-};
-
-std::string which(const std::vector<std::string>& words) {
-  uint64_t fp;
-  std::string word;
-
-  for (const auto& w : words) {
-    auto nfp = util::Fingerprint64(w);
-    if (nfp > fp) {
-      fp = nfp;
-      word = w;
-    }
-  }
-
-  return word;
-}
-
-Result decide(const std::vector<std::string>& words) {
-  auto input = absl::FormatTime("%Y%m%d", absl::Now(), absl::UTCTimeZone());
-  for (const auto& w : words) {
-    input += w;
-  }
-
-  auto base = util::Fingerprint64(input);
-  Result result = { "nope" };
-
-  if (base % 10 == 0) {
-    result.a = "ca";
-  } else if (base % 8 == 0) {
-    result.a = "c1";
-    result.p = which(words);
-  } else if (base % 6 == 0) {
-    result.a = "skip";
-  } else if (base % 3 == 0) {
-    result.a = "e1";
-    result.ec = base % 10;
-    result.p = which(words);
-  } else if (base % 2 == 0) {
-    result.a = "ea";
-    result.ec = base % 10;
-  }
-
-  return result;
-}
-
-int main(int argc, char *argv[]) {
-  absl::ParseCommandLine(argc, argv);
-
-  auto words = absl::GetFlag(FLAGS_words);
-  if (words.size() < 2) {
-    std::cerr << "needs at least two!" << std::endl;
-    return 1;
-  }
-
-  auto result = decide(words);
-  std::cout << result.a
-            << (result.p.has_value() ? absl::StrCat(" ", "(", result.p.value(), ")")
-                                     : "")
-            << (result.ec > 0 ? absl::StrCat(": ", result.ec) : "")
-            << std::endl;
-}
diff --git a/users/tazjin/elisp-deps/deps.el b/users/tazjin/elisp-deps/deps.el
new file mode 100644
index 0000000000..954d71cfba
--- /dev/null
+++ b/users/tazjin/elisp-deps/deps.el
@@ -0,0 +1,83 @@
+;; Visualise the internal structure of an Emacs Lisp file using
+;; Graphviz.
+;;
+;; Entry point is the function `edeps-analyse-file'.
+
+(require 'map)
+
+(defun edeps-read-defs (file-name)
+  "Stupidly read all definitions from an Emacs Lisp file. This only
+considers top-level forms, where the first element of the form is
+a symbol whose name contains the string `def', and where the
+second element is a symbol.
+
+Returns a hashmap of all these symbols with the remaining forms
+in their bodies."
+
+  (with-temp-buffer
+    (insert-file-contents file-name)
+    (goto-char (point-min))
+
+    (let ((symbols (make-hash-table)))
+      (condition-case _err
+          (while t
+            (let ((form (read (current-buffer))))
+              (when (and (listp form)
+                         (symbolp (car form))
+                         (string-match "def" (symbol-name (car form)))
+                         (symbolp (cadr form)))
+                (when (and (map-contains-key symbols (cadr form))
+                           ;; generic methods have multiple definitions
+                           (not (eq (car form) 'cl-defmethod)))
+                  (error "Duplicate symbol: %s" (symbol-name (cadr form))))
+
+                (map-put! symbols (cadr form)
+                          (cons (car form) (cddr form))))))
+        (end-of-file symbols)))))
+
+(defun edeps-analyse-structure (symbols)
+  "Analyse the internal structure of the symbols found by
+edeps-read-defs, and return a hashmap with the results of the
+analysis. The hashmap uses the symbols as keys, "
+  (let ((deps (make-hash-table)))
+    (map-do
+     (lambda (sym val)
+       (dolist (expr (flatten-list (cdr val)))
+         (when (map-contains-key symbols expr)
+           (map-put! deps expr (cons sym (ht-get deps expr))))))
+     symbols)
+    deps))
+
+(defun edeps-graph-deps (symbols deps)
+  (with-temp-buffer
+    (insert "digraph edeps {\n")
+
+    ;; List all symbols first
+    (insert "  subgraph {\n")
+    (map-do
+     (lambda (sym val)
+       (insert "    " (format "\"%s\" [label=\"%s\\n(%s)\"];\n" sym sym (car val))))
+     symbols)
+    (insert "  }\n\n")
+
+    ;; Then drop all the edges in there ..
+    (insert "  subgraph {\n")
+    (map-do
+     (lambda (sym deps)
+       (dolist (dep deps)
+         (insert "    " (format "\"%s\" -> \"%s\";\n" dep sym))))
+     deps)
+    (insert "  }\n")
+
+    (insert "}\n")
+    (buffer-string)))
+
+(defun edeps-analyse-file (infile outfile)
+  "Produces a dot-graph in OUTFILE from an internal structural
+analysis of INFILE. This can be graphed using the graphviz
+package."
+  (let* ((symbols (edeps-read-defs infile))
+         (deps (edeps-analyse-structure symbols)))
+    (with-temp-buffer
+      (insert (edeps-graph-deps symbols deps))
+      (write-file outfile))))
diff --git a/users/tazjin/emacs/config/bindings.el b/users/tazjin/emacs/config/bindings.el
index 4e1f341e32..d8b63e33e4 100644
--- a/users/tazjin/emacs/config/bindings.el
+++ b/users/tazjin/emacs/config/bindings.el
@@ -1,11 +1,11 @@
+;; Switch buffers reliably in the face of spurious renames.
+(global-set-key (kbd "C-x b") #'reliably-switch-buffer)
+
 ;; Font size
 (define-key global-map (kbd "C-=") 'increase-default-text-scale) ;; '=' because there lies '+'
 (define-key global-map (kbd "C--") 'decrease-default-text-scale)
 (define-key global-map (kbd "C-x C-0") 'set-default-text-scale)
 
-;; What does <tab> do? Well, it depends ...
-(define-key prog-mode-map (kbd "<tab>") #'company-indent-or-complete-common)
-
 ;; imenu instead of insert-file
 (global-set-key (kbd "C-x i") 'imenu)
 
@@ -15,8 +15,6 @@
 ;; Start eshell or switch to it if it's active.
 (global-set-key (kbd "C-x m") 'eshell)
 
-;; Start a new eshell even if one is active.
-(global-set-key (kbd "C-x C-p") 'ivy-browse-repositories)
 (global-set-key (kbd "M-g M-g") 'goto-line-with-feedback)
 
 ;; Miscellaneous editing commands
@@ -24,7 +22,7 @@
 (global-set-key (kbd "C-c a") 'align-regexp)
 (global-set-key (kbd "C-c m") 'mc/mark-dwim)
 
-;; Browse URLs (very useful for Gitlab's SSH output!)
+;; Browse URLs (very useful for Gerrit's push output, etc!)
 (global-set-key (kbd "C-c b p") 'browse-url-at-point)
 (global-set-key (kbd "C-c b b") 'browse-url)
 
@@ -41,8 +39,11 @@
 ;; Insert TODO comments
 (global-set-key (kbd "C-c t") 'insert-todo-comment)
 
-;; Make sharing music easier
-(global-set-key (kbd "s-s w") #'songwhip-lookup-url)
+;; Open the depot
+(global-set-key (kbd "s-s d") #'tvl-depot-status)
+
+;; Open any project through zoxide
+(global-set-key (kbd "s-s r") #'zoxide-open-project)
 
 ;; Add subthread collapsing to notmuch-show.
 ;;
@@ -54,4 +55,50 @@
     (interactive)
     (notmuch-show-open-or-close-subthread t))) ;; open
 
+;; Get rid of the annoying `save-some-buffers' shortcut which I
+;; *NEVER* use intentionally.
+(unbind-key (kbd "C-x s") 'global-map)
+
+;; German keyboard layout with Y and Z in the correct place.
+
+(quail-define-package
+ "german-qwerty" "German" "DE@" t
+ "German (Deutsch) input method with QWERTY keys"
+ nil t t t t nil nil nil nil nil t)
+
+;; 1!  2"  3ยง  4$  5%  6&  7/  8(  9)  0=  รŸ?  [{  ]}
+;;  qQ  wW  eE  rR  tT  yY  uU  iI  oO  pP  รผรœ  +*
+;;   aA  sS  dD  fF  gG  hH  jJ  kK  lL  รถร–  รคร„  #^
+;;    zZ  xX  cC  vV  bB  nN  mM  ,;  .:  -_
+
+(quail-define-rules
+ ("-" ?รŸ)
+ ("=" ?\[)
+ ("`" ?\])
+ ("[" ?รผ)
+ ("]" ?+)
+ (";" ?รถ)
+ ("'" ?รค)
+ ("\\" ?#)
+ ("/" ?-)
+
+ ("@" ?\")
+ ("#" ?ยง)
+ ("^" ?&)
+ ("&" ?/)
+ ("*" ?\()
+ ("(" ?\))
+ (")" ?=)
+ ("_" ??)
+ ("+" ?{)
+ ("~" ?})
+ ("{" ?รœ)
+ ("}" ?*)
+ (":" ?ร–)
+ ("\"" ?ร„)
+ ("|" ?^)
+ ("<" ?\;)
+ (">" ?:)
+ ("?" ?_))
+
 (provide 'bindings)
diff --git a/users/tazjin/emacs/config/custom.el b/users/tazjin/emacs/config/custom.el
index a157c7a5fa..3e9a9dcd06 100644
--- a/users/tazjin/emacs/config/custom.el
+++ b/users/tazjin/emacs/config/custom.el
@@ -6,11 +6,7 @@
  '(ac-auto-show-menu 0.8)
  '(ac-delay 0.2)
  '(avy-background t)
- '(cargo-process--custom-path-to-bin "env CARGO_INCREMENTAL=1 cargo")
  '(cargo-process--enable-rust-backtrace 1)
- '(company-auto-complete (quote (quote company-explicit-action-p)))
- '(company-idle-delay 0.5)
- '(custom-enabled-themes (quote (gruber-darker)))
  '(custom-safe-themes
    (quote
     ("d61fc0e6409f0c2a22e97162d7d151dee9e192a90fa623f8d6a071dbf49229c6" "3c83b3676d796422704082049fc38b6966bcad960f896669dfc21a7a37a748fa" "89336ca71dae5068c165d932418a368a394848c3b8881b2f96807405d8c6b5b6" default)))
@@ -27,26 +23,3 @@
  '(ns-right-command-modifier (quote meta))
  '(require-final-newline (quote visit-save))
  '(tls-program (quote ("gnutls-cli --x509cafile %t -p %p %h"))))
-(custom-set-faces
- ;; custom-set-faces was added by Custom.
- ;; If you edit it by hand, you could mess it up, so be careful.
- ;; Your init file should contain only one such instance.
- ;; If there is more than one, they won't work right.
- '(default ((t (:foreground "#e4e4ef" :background "#181818"))))
- '(rainbow-delimiters-depth-1-face ((t (:foreground "#2aa198"))))
- '(rainbow-delimiters-depth-2-face ((t (:foreground "#b58900"))))
- '(rainbow-delimiters-depth-3-face ((t (:foreground "#268bd2"))))
- '(rainbow-delimiters-depth-4-face ((t (:foreground "#dc322f"))))
- '(rainbow-delimiters-depth-5-face ((t (:foreground "#859900"))))
- '(rainbow-delimiters-depth-6-face ((t (:foreground "#268bd2"))))
- '(rainbow-delimiters-depth-7-face ((t (:foreground "#cb4b16"))))
- '(rainbow-delimiters-depth-8-face ((t (:foreground "#d33682"))))
- '(rainbow-delimiters-depth-9-face ((t (:foreground "#839496"))))
- '(term-color-black ((t (:background "#282828" :foreground "#282828"))))
- '(term-color-blue ((t (:background "#96a6c8" :foreground "#96a6c8"))))
- '(term-color-cyan ((t (:background "#1fad83" :foreground "#1fad83"))))
- '(term-color-green ((t (:background "#73c936" :foreground "#73c936"))))
- '(term-color-magenta ((t (:background "#9e95c7" :foreground "#9e95c7"))))
- '(term-color-red ((t (:background "#f43841" :foreground "#f43841"))))
- '(term-color-white ((t (:background "#f5f5f5" :foreground "#f5f5f5"))))
- '(term-color-yellow ((t (:background "#ffdd33" :foreground "#ffdd33")))))
diff --git a/users/tazjin/emacs/config/desktop.el b/users/tazjin/emacs/config/desktop.el
index 38da8f75bc..aa232fec2f 100644
--- a/users/tazjin/emacs/config/desktop.el
+++ b/users/tazjin/emacs/config/desktop.el
@@ -4,13 +4,27 @@
 ;; window-management (EXWM) as well as additional system-wide
 ;; commands.
 
-(require 's)
-(require 'f)
-(require 'dash)
 (require 'exwm)
 (require 'exwm-config)
 (require 'exwm-randr)
 (require 'exwm-systemtray)
+(require 'exwm-xim )
+(require 'f)
+(require 'ring)
+(require 's)
+(require 'seq)
+
+(defcustom tazjin--screen-lock-command "tazjin-screen-lock"
+  "Command to execute for locking the screen."
+  :group 'tazjin)
+
+(defcustom tazjin--backlight-increase-command "light -A 4"
+  "Command to increase screen brightness."
+  :group 'tazjin)
+
+(defcustom tazjin--backlight-decrease-command "light -U 4"
+  "Command to decrease screen brightness."
+  :group 'tazjin)
 
 (defun pactl (cmd)
   (shell-command (concat "pactl " cmd))
@@ -22,24 +36,14 @@
 
 (defun brightness-up ()
   (interactive)
-  (shell-command "xbacklight -inc 5")
+  (shell-command tazjin--backlight-increase-command)
   (message "Brightness increased"))
 
 (defun brightness-down ()
   (interactive)
-  (shell-command "xbacklight -dec 5")
+  (shell-command tazjin--backlight-decrease-command)
   (message "Brightness decreased"))
 
-(defun lock-screen ()
-  (interactive)
-  ;; A sudoers configuration is in place that lets me execute this
-  ;; particular command without having to enter a password.
-  ;;
-  ;; The reason for things being set up this way is that I want
-  ;; xsecurelock.service to be started as a system-wide service that
-  ;; is tied to suspend.target.
-  (shell-command "/usr/bin/sudo /usr/bin/systemctl start xsecurelock.service"))
-
 (defun set-xkb-layout (layout)
   "Set the current X keyboard layout."
 
@@ -47,6 +51,12 @@
   (shell-command "setxkbmap -option caps:super")
   (message "Set X11 keyboard layout to '%s'" layout))
 
+(defun lock-screen ()
+  (interactive)
+  (set-xkb-layout "us")
+  (deactivate-input-method)
+  (shell-command tazjin--screen-lock-command))
+
 (defun create-window-name ()
   "Construct window names to be used for EXWM buffers by
   inspecting the window's X11 class and title.
@@ -60,39 +70,22 @@
   human-accessible titles."
 
   (pcase (list (or exwm-class-name "unknown") (or exwm-title "unknown"))
-    ;; In Cider windows, rename the class and keep the workspace/file
-    ;; as the title.
-    (`("Google-chrome" ,(and (pred (lambda (title) (s-ends-with? " - Cider" title))) title))
-     (format "Cider<%s>" (s-chop-suffix " - Cider" title)))
+    ;; Yandex.Music -> `ะฏ.Music<... stuff ...>'
+    (`("Chromium-browser" ,(and (pred (lambda (title) (s-starts-with? "Yandex.Music - " title))) title))
+     (format "ะฏ.Music<%s>" (s-chop-prefix "Yandex.Music - " title)))
 
-    ;; Attempt to detect IRCCloud windows via their title, which is a
-    ;; combination of the channel name and network.
-    ;;
-    ;; This is what would often be referred to as a "hack". The regexp
-    ;; will not work if a network connection buffer is selected in
-    ;; IRCCloud, but since the title contains no other indication that
-    ;; we're dealing with an IRCCloud window
-    (`("Google-chrome"
-       ,(and (pred (lambda (title)
-                     (s-matches? "^[\*\+]\s#[a-zA-Z0-9/\-]+\s\|\s[a-zA-Z\.]+$" title)))
-             title))
-     (format "IRCCloud<%s>" title))
-
-    ;; For other Chrome windows, make the title shorter.
-    (`("Google-chrome" ,title)
-     (format "Chrome<%s>" (s-truncate 42 (s-chop-suffix " - Google Chrome" title))))
-
-    ;; Gnome-terminal -> Term
-    (`("Gnome-terminal" ,title)
-     ;; fish-shell buffers contain some unnecessary whitespace and
-     ;; such before the current working directory. This can be
-     ;; stripped since most of my terminals are fish shells anyways.
-     (format "Term<%s>" (s-trim-left (s-chop-prefix "fish" title))))
+    ;; For other Chromium windows, make the title shorter.
+    (`("Chromium-browser" ,title)
+     (format "Chromium<%s>" (s-truncate 42 (s-chop-suffix " - Chromium" title))))
+
+    ;; similarly for Firefox
+    (`("firefox" ,title)
+     (format "FF<%s>" title))
 
     ;; Quassel buffers
     ;;
     ;; These have a title format that looks like:
-    ;; "Quassel IRC - ##tvl (Freenode) โ€” Quassel IRC"
+    ;; "Quassel IRC - #tvl (hackint) โ€” Quassel IRC"
     (`("quassel" ,title)
      (progn
        (if (string-match
@@ -109,9 +102,6 @@
     (`(,class ,title) (format "%s<%s>" class (s-truncate 12 title)))))
 
 ;; EXWM launch configuration
-;;
-;; This used to use use-package, but when something breaks use-package
-;; it doesn't exactly make debugging any easier.
 
 (let ((titlef (lambda ()
                 (exwm-workspace-rename-buffer (create-window-name)))))
@@ -119,23 +109,57 @@
   (add-hook 'exwm-update-title-hook titlef))
 
 (fringe-mode 3)
-(exwm-enable)
 
-;; 's-N': Switch to certain workspace
-(setq exwm-workspace-number 10)
-(dotimes (i 10)
-  (exwm-input-set-key (kbd (format "s-%d" i))
-                      `(lambda ()
-                         (interactive)
-                         (exwm-workspace-switch-create ,i))))
+;; tab-bar related config
+(setq tab-bar-show 1)
+(setq tab-bar-tab-hints t)
 
-;; Launch applications / any command  with completion (dmenu style!)
-(exwm-input-set-key (kbd "s-d") #'counsel-linux-app)
-(exwm-input-set-key (kbd "s-x") #'ivy-run-external-command)
-(exwm-input-set-key (kbd "s-p") #'ivy-password-store)
+(setq tab-bar-format
+      '(tab-bar-format-history
+        tab-bar-format-tabs tab-bar-separator
+        tab-bar-format-align-right tab-bar-format-global))
 
-;; Add X11 terminal selector to a key
-(exwm-input-set-key (kbd "C-x t") #'ts/switch-to-terminal)
+(setq tab-bar-new-tab-choice
+      (lambda () (get-buffer-create "*scratch*")))
+
+(tab-bar-mode 1)
+
+(setq x-no-window-manager t) ;; TODO(tazjin): figure out when to remove this
+(exwm-enable)
+(exwm-randr-enable)
+
+;; Tab-management shortcuts
+
+(defun tab-bar-select-or-return ()
+  "This function behaves like `tab-bar-select-tab', except it calls
+`tab-recent' if asked to jump to the current tab. This simulates
+the back&forth behaviour of i3."
+  (interactive)
+  (let* ((key (event-basic-type last-command-event))
+         (tab (if (and (characterp key) (>= key ?1) (<= key ?9))
+                  (- key ?0)
+                0))
+         (current (1+ (tab-bar--current-tab-index))))
+    (if (eq tab current)
+        (tab-recent)
+      (tab-bar-select-tab tab))))
+
+(dotimes (i 8)
+  (exwm-input-set-key (kbd (format "s-%d" (+ 1 i))) #'tab-bar-select-or-return))
+
+(exwm-input-set-key (kbd "s-9") #'tab-last)
+(exwm-input-set-key (kbd "s-f") #'tab-next)
+(exwm-input-set-key (kbd "s-b") #'tab-recent)
+(exwm-input-set-key (kbd "s-w") #'tab-close)
+(exwm-input-set-key (kbd "s-n") #'tab-new)
+
+;; Launch applications / any command with completion (dmenu style!)
+(exwm-input-set-key (kbd "s-d") #'run-xdg-app)
+(exwm-input-set-key (kbd "s-x") #'run-external-command)
+(exwm-input-set-key (kbd "s-p") #'password-store-lookup)
+
+;; Add vterm selector to a key
+(exwm-input-set-key (kbd "s-v") #'ts/switch-to-terminal)
 
 ;; Toggle between line-mode / char-mode
 (exwm-input-set-key (kbd "C-c C-t C-t") #'exwm-input-toggle-keyboard)
@@ -162,19 +186,19 @@
 (bind-xkb "no" "k n")
 (bind-xkb "ru" "k r")
 (bind-xkb "se" "k s")
+(bind-xkb "us" "ะป ะณ")
+(bind-xkb "de" "ะป ะฒ")
+(bind-xkb "no" "ะป ั‚")
+(bind-xkb "ru" "ะป ะบ")
 
-;; These are commented out because Emacs no longer starts (??) if
-;; they're set at launch.
-;;
-;; (bind-xkb "us" "ะป ะณ")
-;; (bind-xkb "de" "ะป ะฒ")
-;; (bind-xkb "no" "ะป ั‚")
-;; (bind-xkb "ru" "ะป ะบ")
+;; Configuration of EXWM input method handling for X applications
+(exwm-xim-enable)
+(setq default-input-method "russian-computer")
+(push ?\C-\\ exwm-input-prefix-keys)
 
 ;; Line-editing shortcuts
-(exwm-input-set-simulation-keys
- '(([?\C-d] . delete)
-   ([?\C-w] . ?\C-c)))
+(exwm-input-set-simulation-key (kbd "C-d") (kbd "DEL"))
+(exwm-input-set-simulation-key (kbd "C-w") (kbd "C-c"))
 
 ;; Show time & battery status in the mode line
 (display-time-mode)
@@ -183,80 +207,123 @@
 ;; enable display of X11 system tray within Emacs
 (exwm-systemtray-enable)
 
-;; Configure xrandr (multi-monitor setup).
+;; Multi-monitor configuration.
 ;;
-;; This makes some assumptions about how my machines are connected to
-;; my home setup during the COVID19 isolation period.
-
-(defun set-randr-config (screens)
-  (setq exwm-randr-workspace-monitor-plist
-        (-flatten (-map (lambda (screen)
-                          (-map (lambda (screen-id) (list screen-id (car screen))) (cdr screen)))
-                        screens))))
-
-;; Layouts for Vauxhall (laptop)
+;; With tab-bar-mode, each monitor only displays at most one
+;; workspace. Workspaces are only created, never deleted, meaning that
+;; the number of workspaces will be equivalent to the maximum number
+;; of displays that were connected during a session.
+;;
+;; The first workspace is special: It is kept on the primary monitor.
 
-(defun randr-vauxhall-layout-single ()
-  "Laptop screen only!"
+(defun exwm-assign-workspaces ()
+  "Assigns workspaces to the currently existing monitors, putting
+the first one on the primary display and allocating the others
+dynamically if needed in no particular order."
   (interactive)
-  (set-randr-config '(("eDP1" (number-sequence 0 9))))
-  (shell-command "xrandr --output eDP1 --auto --primary")
-  (shell-command "xrandr --output HDMI1 --off")
-  (shell-command "xrandr --output DP2 --off")
-  (exwm-randr-refresh))
-
-(defun randr-vauxhall-layout-all ()
-  "Use all screens at home."
+  (let* ((randr-monitors (exwm-randr--get-monitors))
+         (primary (car randr-monitors))
+         (all-monitors (seq-map #'car (cadr randr-monitors)))
+         (sorted-primary-first (seq-sort (lambda (a b)
+                                           (or (equal a primary)
+                                               (< a b)))
+                                         all-monitors))
+         ;; assign workspace numbers to each monitor ...
+         (workspace-assignments
+          (flatten-list (seq-map-indexed (lambda (monitor idx)
+                                           (list idx monitor))
+                                         sorted-primary-first))))
+    ;; ensure that the required workspaces exist
+    (exwm-workspace-switch-create (- (seq-length all-monitors) 1))
+
+    ;; update randr config
+    (setq exwm-randr-workspace-monitor-plist workspace-assignments)
+    (exwm-randr-refresh)
+
+    ;; leave focus on primary workspace
+    (exwm-workspace-switch 0)))
+
+(defun list-available-monitors ()
+  "List connected, but unused monitors."
+  (let* ((all-connected
+          (seq-map (lambda (line) (car (s-split " " line)))
+                   (s-lines (s-trim (shell-command-to-string "xrandr | grep connected | grep -v disconnected")))))
+         (all-active (seq-map #'car (cadr (exwm-randr--get-monitors)))))
+    (seq-filter (lambda (s) (not (seq-contains-p all-active s)))
+                all-connected)))
+
+(defun exwm-enable-monitor ()
+  "Interactively construct an EXWM invocation that enable the
+given monitor and assigns a workspace to it."
   (interactive)
-  (set-randr-config
-   '(("eDP1" 0)
-     ("HDMI1" 1 2 3 4 5)
-     ("DP2" 6 7 8 9)))
-
-  (shell-command "xrandr --output HDMI1 --right-of eDP1 --auto --primary")
-  (shell-command "xrandr --output DP2 --right-of HDMI1 --auto --rotate left")
-  (exwm-randr-refresh))
 
-(defun randr-vauxhall-layout-wide-only ()
-  "Use only the wide screen at home."
+  (let* ((monitors (list-available-monitors))
+         (primary (car (exwm-randr--get-monitors)))
+         (monitor (pcase (seq-length monitors)
+                    (0 (error "No available monitors."))
+                    (1 (car monitors))
+                    (_
+                     (completing-read "Which monitor? " (list-available-monitors) nil t))))
+
+         (configurations `(("secondary (left)" . ,(format "--left-of %s" primary))
+                           ("secondary (right)" . ,(format "--right-of %s" primary))
+                           ("primary (left)" . ,(format "--left-of %s --primary" primary))
+                           ("primary (right)" . ,(format "--right-of %s --primary" primary))
+                           ("mirror" . ,(format "--same-as %s" primary))))
+
+         (where (completing-read (format "%s should be " monitor)
+                                 (seq-map #'car configurations)
+                                 nil t))
+         (xrandr-pos (cdr (assoc where configurations)))
+         (xrandr-cmd (format "xrandr --output %s --auto %s" monitor xrandr-pos)))
+    (message "Invoking '%s'" xrandr-cmd)
+    (shell-command xrandr-cmd)
+    (exwm-assign-workspaces)))
+
+(defun exwm-disable-monitor ()
+  "Interactively choose a monitor to disable."
   (interactive)
-  (set-randr-config
-   '(("eDP1" 8 9 0)
-     ("HDMI1" 1 2 4 5 6 7)))
 
-  (shell-command "xrandr --output DP2 --off")
-  (shell-command "xrandr --output HDMI1 --right-of eDP1 --auto --primary")
-  (exwm-randr-refresh))
+  (let* ((all (exwm-randr--get-monitors))
+         (active (seq-map #'car (cadr all)))
+         (monitor (if (> (seq-length active) 1)
+                      (completing-read "Disable which monitor? " active nil t)
+                    (error "Only one monitor is active!")))
 
-;; Layouts for frog (desktop)
+         ;; If this monitor was primary, pick another active one instead.
+         (remaining (seq-filter (lambda (s) (not (equal s monitor))) active))
+         (new-primary
+          (when (equal monitor (car all))
+            (pcase (seq-length remaining)
+              (1 (car remaining))
+              (_ (completing-read "New primary? " remaining nil t))))))
 
-(defun randr-frog-layout-right-only ()
-  "Use only the right screen on frog."
-  (interactive)
-  (set-randr-config `(("DisplayPort-0" ,(number-sequence 0 9))))
-  (shell-command "xrandr --output DisplayPort-0 --off")
-  (shell-command "xrandr --output DisplayPort-1 --auto --primary"))
+    (when new-primary
+      (shell-command (format "xrandr --output %s --primary" new-primary)))
 
-(defun randr-frog-layout-both ()
-  "Use the left and right screen on frog."
-  (interactive)
-  (set-randr-config `(("DisplayPort-0" 1 2 3 4 5)
-                      ("DisplayPort-1" 6 7 8 9 0)))
+    (shell-command (format "xrandr --output %s --off" monitor))
+    (exwm-assign-workspaces)))
 
-  (shell-command "xrandr --output DisplayPort-0 --auto --primary --left-of DisplayPort-1")
-  (shell-command "xrandr --output DisplayPort-1 --auto --right-of DisplayPort-0 --rotate left"))
-
-(pcase (s-trim (shell-command-to-string "hostname"))
-  ("vauxhall"
-   (exwm-input-set-key (kbd "s-m s") #'randr-vauxhall-layout-single)
-   (exwm-input-set-key (kbd "s-m a") #'randr-vauxhall-layout-all)
-   (exwm-input-set-key (kbd "s-m w") #'randr-vauxhall-layout-wide-only))
-
-  ("frog"
-   (exwm-input-set-key (kbd "s-m b") #'randr-frog-layout-both)
-   (exwm-input-set-key (kbd "s-m r") #'randr-frog-layout-right-only)))
+(defun exwm-switch-monitor ()
+  "Switch focus to another monitor by name."
+  (interactive)
 
-(exwm-randr-enable)
+  ;; TODO: Filter out currently active? How to determine it?
+  (let* ((target (completing-read "Switch to monitor: "
+                                  (seq-map #'car (cadr (exwm-randr--get-monitors)))
+                                  nil t))
+         (target-workspace
+          (cl-loop for (workspace screen) on exwm-randr-workspace-monitor-plist by #'cddr
+                   when (equal screen target) return workspace)))
+    (exwm-workspace-switch target-workspace)))
+
+(exwm-input-set-key (kbd "s-m e") #'exwm-enable-monitor)
+(exwm-input-set-key (kbd "s-m d") #'exwm-disable-monitor)
+(exwm-input-set-key (kbd "s-m o") #'exwm-switch-monitor)
+
+;; Notmuch shortcuts as EXWM globals
+;; (g m => gmail)
+(exwm-input-set-key (kbd "s-g m") #'notmuch)
 
 ;; Let buffers move seamlessly between workspaces by making them
 ;; accessible in selectors on all frames.
diff --git a/users/tazjin/emacs/config/functions.el b/users/tazjin/emacs/config/functions.el
index 9bb6772a27..68a384d20f 100644
--- a/users/tazjin/emacs/config/functions.el
+++ b/users/tazjin/emacs/config/functions.el
@@ -2,9 +2,7 @@
 (require 'dash)
 (require 'map)
 
-(defun load-file-if-exists (filename)
-  (if (file-exists-p filename)
-      (load filename)))
+(require 'gio-list-apps) ;; native module!
 
 (defun goto-line-with-feedback ()
   "Show line numbers temporarily, while prompting for the line number input"
@@ -17,29 +15,19 @@
           (goto-line target)))
     (setq-local display-line-numbers nil)))
 
-;; These come from the emacs starter kit
-
 (defun esk-add-watchwords ()
   (font-lock-add-keywords
    nil '(("\\<\\(FIX\\(ME\\)?\\|TODO\\|DEBUG\\|HACK\\|REFACTOR\\|NOCOMMIT\\)"
           1 font-lock-warning-face t))))
 
+(add-hook 'prog-mode-hook 'esk-add-watchwords)
+
 (defun esk-sudo-edit (&optional arg)
   (interactive "p")
   (if (or arg (not buffer-file-name))
       (find-file (concat "/sudo:root@localhost:" (read-file-name "File: ")))
     (find-alternate-file (concat "/sudo:root@localhost:" buffer-file-name))))
 
-;; Open Fefes blog
-(defun fefes-blog ()
-  (interactive)
-  (eww "https://blog.fefe.de/"))
-
-;; Open the NixOS man page
-(defun nixos-man ()
-  (interactive)
-  (man "configuration.nix"))
-
 ;; Get the nix store path for a given derivation.
 ;; If the derivation has not been built before, this will trigger a build.
 (defun nix-store-path (derivation)
@@ -59,9 +47,6 @@
            (buffer-name)
            require-final-newline))
 
-;; Helm includes a command to run external applications, which does
-;; not seem to exist in ivy. This implementation uses some of the
-;; logic from Helm to provide similar functionality using ivy.
 (defun list-external-commands ()
   "Creates a list of all external commands available on $PATH
   while filtering NixOS wrappers."
@@ -82,9 +67,9 @@
   '(("google-chrome" . "--force-device-scale-factor=1.4"))
 
   "This setting lets me add additional flags to specific commands
-  that are run interactively via `ivy-run-external-command'.")
+  that are run interactively via `run-external-command'.")
 
-(defun run-external-command (cmd)
+(defun run-external-command--handler (cmd)
   "Execute the specified command and notify the user when it
   finishes."
     (let* ((extra-flags (cdr (assoc cmd external-command-flag-overrides)))
@@ -96,59 +81,46 @@
          (when (string= event "finished\n")
            (message "%s process finished." process))))))
 
-(defun ivy-run-external-command ()
+(defun run-external-command ()
   "Prompts the user with a list of all installed applications and
   lets them select one to launch."
 
   (interactive)
   (let ((external-commands-list (list-external-commands)))
-    (ivy-read "Command:" external-commands-list
-              :require-match t
-              :history 'external-commands-history
-              :action #'run-external-command)))
-
-(defun ivy-password-store (&optional password-store-dir)
-  "Custom version of password-store integration with ivy that
-  actually uses the GPG agent correctly."
+    (run-external-command--handler
+     (completing-read "Command: " external-commands-list
+                      nil                             ;; predicate
+                      t                               ;; require-match
+                      nil                             ;; initial-input
+                      ;; hist
+                      'external-commands-history))))
+
+(defun password-store-lookup (&optional password-store-dir)
+  "Interactive password-store lookup function that actually uses
+the GPG agent correctly."
 
   (interactive)
-  (ivy-read "Copy password of entry: "
-            (password-store-list (or password-store-dir (password-store-dir)))
-            :require-match t
-            :keymap ivy-pass-map
-            :action (lambda (entry)
-                      (let ((password (auth-source-pass-get 'secret entry)))
-                        (password-store-clear)
-                        (kill-new password)
-                        (setq password-store-kill-ring-pointer kill-ring-yank-pointer)
-                        (message "Copied %s to the kill ring. Will clear in %s seconds."
-                                 entry (password-store-timeout))
-                        (setq password-store-timeout-timer
-                              (run-at-time (password-store-timeout)
-                                           nil 'password-store-clear))))))
-
-(defun ivy-browse-repositories ()
-  "Select a git repository and open its associated magit buffer."
 
-  (interactive)
-  (ivy-read "Repository: "
-            (magit-list-repos)
-            :require-match t
-            :sort t
-            :action #'magit-status))
-
-(defun bottom-right-window-p ()
-  "Determines whether the last (i.e. bottom-right) window of the
-  active frame is showing the buffer in which this function is
-  executed."
-  (let* ((frame (selected-frame))
-         (right-windows (window-at-side-list frame 'right))
-         (bottom-windows (window-at-side-list frame 'bottom))
-         (last-window (car (seq-intersection right-windows bottom-windows))))
-    (eq (current-buffer) (window-buffer last-window))))
+  (let* ((entry (completing-read "Copy password of entry: "
+                   (password-store-list (or password-store-dir
+                                            (password-store-dir)))
+                   nil ;; predicate
+                   t   ;; require-match
+                   ))
+         (password (or (let ((epa-suppress-error-buffer t))
+                         (auth-source-pass-get 'secret entry))
+                       (error "failed to decrypt '%s', wrong password?" entry))))
+    (password-store-clear)
+    (kill-new password)
+    (setq password-store-kill-ring-pointer kill-ring-yank-pointer)
+    (message "Copied %s to the kill ring. Will clear in %s seconds."
+             entry (password-store-timeout))
+    (setq password-store-timeout-timer
+          (run-at-time (password-store-timeout)
+                       nil 'password-store-clear))))
 
 (defhydra mc/mark-more-hydra (:color pink)
-  ("<up>" mmlte--up "Mark previous like this")
+  ("<up>" mc/mmlte--up "Mark previous like this")
   ("<down>" mc/mmlte--down "Mark next like this")
   ("<left>" mc/mmlte--left (if (eq mc/mark-more-like-this-extended-direction 'up)
                                "Skip past the cursor furthest up"
@@ -174,26 +146,15 @@
       (mc/mmlte--down)
       (mc/mark-more-hydra/body))))
 
-(defun memespace-region ()
-  "Make a meme out of it."
+(setq mc/cmds-to-run-for-all '(kill-region paredit-newline))
 
-  (interactive)
-  (let* ((start (region-beginning))
-         (end (region-end))
-         (memed
-          (message
-           (s-trim-right
-            (apply #'string
-                   (-flatten
-                    (nreverse
-                     (-reduce-from (lambda (acc x)
-                                     (cons (cons x (-repeat (+ 1 (length acc)) 32)) acc))
-                                   '()
-                                   (string-to-list (buffer-substring-no-properties start end))))))))))
-
-    (save-excursion (delete-region start end)
-                    (goto-char start)
-                    (insert memed))))
+(setq mc/cmds-to-run-once '(mc/mark-dwim
+                            mc/mark-more-hydra/mc/mmlte--down
+                            mc/mark-more-hydra/mc/mmlte--left
+                            mc/mark-more-hydra/mc/mmlte--right
+                            mc/mark-more-hydra/mc/mmlte--up
+                            mc/mark-more-hydra/mmlte--up
+                            mc/mark-more-hydra/nil))
 
 (defun insert-todo-comment (prefix todo)
   "Insert a comment at point with something for me to do."
@@ -237,11 +198,16 @@
   (if prefix (text-scale-adjust 0)
     (set-face-attribute 'default nil :height (or to 120))))
 
-(defun scrot-select ()
+(defun screenshot-select (filename)
   "Take a screenshot based on a mouse-selection and save it to
   ~/screenshots."
-  (interactive)
-  (shell-command "scrot '$a_%Y-%m-%d_%s.png' -s -e 'mv $f ~/screenshots/'"))
+  (interactive "sScreenshot filename: ")
+  (let* ((path (f-join "~/screenshots"
+                       (format "%s-%d.png"
+                               (if (string-empty-p filename) "shot" filename)
+                               (time-convert nil 'integer)))))
+    (shell-command (format "maim --select %s" path))
+    (message "Wrote screenshot to %s" path)))
 
 (defun graph-unread-mails ()
   "Create a bar chart of unread mails based on notmuch tags.
@@ -289,6 +255,17 @@
 
 (add-to-list 'project-find-functions #'find-depot-project)
 
+(defun find-cargo-project (dir)
+  "Attempt to find the current project in `project-find-functions'
+by looking for a `Cargo.toml' file."
+  (when dir
+    (unless (equal "/" dir)
+      (if (f-exists-p (f-join dir "Cargo.toml"))
+          (cons 'transient dir)
+        (find-cargo-project (f-parent dir))))))
+
+(add-to-list 'project-find-functions #'find-cargo-project)
+
 (defun magit-find-file-worktree ()
   (interactive)
   "Find a file in the current (ma)git worktree."
@@ -296,31 +273,80 @@
                              (magit-read-file-from-rev "HEAD" "Find file")
                              #'pop-to-buffer-same-window))
 
-(defun songwhip--handle-result (status &optional cbargs)
-  ;; TODO(tazjin): Inspect status, which looks different in practice
-  ;; than the manual claims.
-  (if-let* ((response (json-parse-string
-                       (buffer-substring url-http-end-of-headers (point-max))))
-            (sw-path (ht-get* response "data" "path"))
-            (link (format "https://songwhip.com/%s" sw-path))
-            (select-enable-clipboard t))
-      (progn
-        (kill-new link)
-        (message "Copied Songwhip link (%s)" link))
-    (warn "Something went wrong while retrieving Songwhip link!")
-    ;; For debug purposes, the buffer is persisted in this case.
-    (setq songwhip--debug-buffer (current-buffer))))
-
-(defun songwhip-lookup-url (url)
-  "Look up URL on Songwhip and copy the resulting link to the clipboard."
-  (interactive "sEnter source URL: ")
-  (let ((songwhip-url "https://songwhip.com/api/")
-        (url-request-method "POST")
-        (url-request-extra-headers '(("Content-Type" . "application/json")))
-        (url-request-data
-         (json-serialize `((country . "GB")
-                           (url . ,url)))))
-    (url-retrieve "https://songwhip.com/api/" #'songwhip--handle-result nil t t)
-    (message "Requesting Songwhip URL ... please hold the line.")))
+(defun zoxide-open-project ()
+  "Query Zoxide for paths, and open the result as appropriate (magit or dired)."
+  (interactive)
+  (zoxide-open-with
+   nil
+   (lambda (path)
+     (condition-case err (magit-status-setup-buffer path)
+       (magit-outside-git-repo (dired path))))))
+
+(defun toggle-nix-test-and-exp ()
+  "Switch between the .nix and .exp file in a Tvix/Nix test."
+  (interactive)
+  (let* ((file (buffer-file-name))
+         (other (if (s-suffix? ".nix" file)
+                    (s-replace-regexp ".nix$" ".exp" file)
+                  (if (s-suffix? ".exp" file)
+                      (s-replace-regexp ".exp$" ".nix" file)
+                    (error "Not a .nix/.exp file!")))))
+    (find-file other)))
+
+(defun reliably-switch-buffer ()
+  "Reliably and interactively switch buffers, without ending up in a
+situation where the buffer was renamed during selection and an
+empty new buffer is created.
+
+This is done by, in contrast to most buffer-switching functions,
+retaining a list of the buffer *objects* and their associated
+names, instead of only their names (which might change)."
+
+  (interactive)
+  (let* ((buffers (seq-map (lambda (b) (cons (buffer-name b) b))
+                           (seq-filter (lambda (b) (not (string-prefix-p " " (buffer-name b))))
+                                       (buffer-list))))
+
+         ;; Annotate buffers that display remote files. I frequently
+         ;; want to see it, because I might have identically named
+         ;; files open locally and remotely at the same time, and it
+         ;; helps with differentiating them.
+         (completion-extra-properties
+          '(:annotation-function
+            (lambda (name)
+              (if-let* ((file (buffer-file-name (cdr (assoc name buffers))))
+                        (remote (file-remote-p file)))
+                  (format " [%s]" remote)))))
+
+         (name (completing-read "Switch to buffer: " (seq-map #'car buffers)))
+         (selected (or (cdr (assoc name buffers))
+                       ;; Allow users to manually select invisible buffers ...
+                       (get-buffer name))))
+    (switch-to-buffer (or selected name) nil 't)))
+
+(defun run-xdg-app ()
+  "Use `//users/tazjin/gio-list-apps' to retrieve a list of
+installed (and visible) XDG apps, and let users launch them."
+  (interactive)
+  (let* ((apps (taz-list-xdg-apps))
+
+         ;; Display the command that will be run as an annotation
+         (completion-extra-properties
+          '(:annotation-function (lambda (app) (format " [%s]" (cdr (assoc app apps)))))))
+
+    (run-external-command--handler (cdr (assoc (completing-read "App: " apps nil t) apps)))))
+
+(defun advice-remove-all (sym)
+  "Remove all advices from symbol SYM."
+  (interactive "aFunction symbol: ")
+  (advice-mapc (lambda (advice _props) (advice-remove sym advice)) sym))
+
+(defun M-x-always-same-window ()
+  "Run `execute-extended-command', but ensure that whatever it does
+always opens in the same window in which the command was invoked."
+  (interactive)
+  (let ((display-buffer-overriding-action
+         '((display-buffer-same-window) . ((inhibit-same-window . nil)))))
+    (call-interactively #'execute-extended-command)))
 
 (provide 'functions)
diff --git a/users/tazjin/emacs/config/init.el b/users/tazjin/emacs/config/init.el
index 9f34c60a8d..ced3bf2ff8 100644
--- a/users/tazjin/emacs/config/init.el
+++ b/users/tazjin/emacs/config/init.el
@@ -1,29 +1,18 @@
 ;;; init.el --- Package bootstrapping. -*- lexical-binding: t; -*-
 
+;; Disable annoying warnings from native compilation.
+(setq native-comp-async-report-warnings-errors nil
+      warning-suppress-log-types '((comp)))
+
 ;; Packages are installed via Nix configuration, this file only
 ;; initialises the newly loaded packages.
 
 (require 'use-package)
 (require 'seq)
 
-
-;; TODO(tazjin): Figure out what's up with vc.
-;;
-;; Leaving vc enabled breaks all find-file operations with messages
-;; about .git folders being absent, but in random places.
-(require 'vc)
-(setq vc-handled-backends nil)
-
 (package-initialize)
 
 ;; Initialise all packages installed via Nix.
-;;
-;; TODO: Generate this section in Nix for all packages that do not
-;; require special configuration.
-
-;;
-;; Packages providing generic functionality.
-;;
 
 (use-package ace-window
   :bind (("C-x o" . ace-window))
@@ -40,23 +29,12 @@
 
 (use-package browse-kill-ring)
 
-(use-package company
-  :hook ((prog-mode . company-mode))
-  :config (setq company-tooltip-align-annotations t))
-
-(use-package counsel
-  :after (ivy)
-  :config (counsel-mode 1)
-  :bind (("C-c r g" . counsel-rg)))
+(use-package consult
+  :bind
+  ("C-c r g" . consult-ripgrep)
+  ("C-s" . consult-line))
 
 (use-package dash)
-(use-package dash-functional)
-
-(use-package dottime
-  :demand
-  :after (notmuch telega)
-  :config (dottime-display-mode t))
-
 (use-package gruber-darker-theme)
 
 (use-package eglot
@@ -69,32 +47,13 @@
 (use-package hydra)
 (use-package idle-highlight-mode :hook ((prog-mode . idle-highlight-mode)))
 
-(use-package ivy
-  :config
-  (ivy-mode 1)
-  (setq enable-recursive-minibuffers t)
-  (setq ivy-use-virtual-buffers t))
-
-(use-package ivy-pass :after (ivy))
-
-(use-package ivy-prescient
-  :after (ivy prescient)
-  :config
-  (ivy-prescient-mode)
-  ;; Fixes an issue with how regexes are passed to ripgrep from counsel,
-  ;; see raxod502/prescient.el#43
-  (setf (alist-get 'counsel-rg ivy-re-builders-alist) #'ivy--regex-plus))
-
 (use-package multiple-cursors)
 
 (use-package notmuch
-  :bind (:map global-map
-              ("s-g m" . notmuch)
-              ("s-g M" . counsel-notmuch)) ;; g m -> gmail
-  :config
-  (setq notmuch-search-oldest-first nil)
-  (setq notmuch-show-all-tags-list t)
-  (setq notmuch-hello-tag-list-make-query "tag:unread"))
+  :custom
+  (notmuch-search-oldest-first nil)
+  (notmuch-show-all-tags-list t)
+  (notmuch-hello-tag-list-make-query "tag:unread"))
 
 (use-package paredit :hook ((lisp-mode . paredit-mode)
                             (emacs-lisp-mode . paredit-mode)))
@@ -105,21 +64,20 @@
   (pinentry-start))
 
 (use-package prescient
-  :after (ivy counsel)
-  :config (prescient-persist-mode))
+  :config
+  (prescient-persist-mode)
+  (setq completion-styles '(basic prescient)))
 
 (use-package rainbow-delimiters :hook (prog-mode . rainbow-delimiters-mode))
 (use-package rainbow-mode)
 (use-package s)
-(use-package string-edit)
+(use-package string-edit-at-point)
+(use-package term-switcher)
 
-(use-package swiper
-  :after (counsel ivy)
-  :bind (("C-s" . swiper)))
+(use-package undo-tree
+  :config (global-undo-tree-mode)
+  :custom (undo-tree-auto-save-history nil))
 
-(use-package telephone-line) ;; configuration happens outside of use-package
-(use-package term-switcher)
-(use-package undo-tree :config (global-undo-tree-mode))
 (use-package uuidgen)
 (use-package which-key :config (which-key-mode t))
 
@@ -132,41 +90,13 @@
   :config (setq magit-repository-directories '(("/home/tazjin/projects" . 2)
                                                ("/home/tazjin" . 1))))
 
-(use-package org-journal
-  ;; Always use my own key to encrypt files. There seems to be no
-  ;; global way to set this, as `epa-file-encrypt-to' only has an
-  ;; effect as a file-local variable (?!)
-  :hook ((org-journal-mode . (lambda ()
-                               (setq-local epa-file-encrypt-to
-                                           "DCF34CFAC1AC44B87E26333136EE34814F6D294A"))))
-
-  :config
-  (setq org-journal-dir "/ssh:camden.tazj.in:/home/tazjin/journal"
-        org-journal-encrypt-journal t
-        org-journal-file-type 'weekly
-        org-journal-date-format "%A, %Y-%m-%d"
-        org-journal-file-format "%Y%m%d-weekly"
-
-        ;; Saturday, because reasons.
-        org-journal-start-on-weekday 6)
-
-  ;; org-journal doesn't actually enter its mode automatically if
-  ;; encryption is used (I'm not sure why), so this teaches Emacs to
-  ;; recognise the files.
-  (add-to-list 'auto-mode-alist '("[0-9]-weekly\\.gpg\\'" . org-journal-mode)))
-
-(use-package org-ql)
-
 (use-package password-store)
-(use-package pg)
 (use-package restclient)
 
 (use-package vterm
-  :config (progn
-            (setq vterm-shell "fish")
-            (setq vterm-exit-functions
-                  (lambda (&rest _) (kill-buffer (current-buffer))))
-            (setq vterm-kill-buffer-on-exit t)))
+  :custom
+  (vterm-shell "fish")
+  (vterm-kill-buffer-on-exit t))
 
 ;; vterm removed the ability to set a custom title generator function
 ;; via the public API, so this overrides its private title generation
@@ -185,9 +115,9 @@
 (use-package cargo
   :hook ((rust-mode . cargo-minor-mode)
          (cargo-process-mode . visual-line-mode))
-  :bind (:map cargo-minor-mode-map ("C-c C-c C-l" . ignore)))
+  :bind (:map cargo-mode-map ("C-c C-c C-l" . ignore)))
 
-(use-package dockerfile-mode)
+(use-package dockerfile-ts-mode)
 
 (use-package erlang
   :hook ((erlang-mode . (lambda ()
@@ -195,7 +125,6 @@
                           (local-set-key ">" 'self-insert-command)))))
 
 (use-package f)
-(use-package geiser)
 
 (use-package go-mode
   :bind (:map go-mode-map ("C-c C-r" . recompile))
@@ -208,9 +137,7 @@
 
 (use-package ielm
   :hook ((inferior-emacs-lisp-mode . (lambda ()
-                                       (paredit-mode)
-                                       (rainbow-delimiters-mode-enable)
-                                       (company-mode)))))
+                                       (rainbow-delimiters-mode-enable)))))
 
 (use-package jq-mode
   :config (add-to-list 'auto-mode-alist '("\\.jq\\'" . jq-mode)))
@@ -219,11 +146,8 @@
   :hook ((kotlin-mode . (lambda ()
                           (setq indent-line-function #'indent-relative)))))
 
-(use-package lsp-mode)
-
 (use-package markdown-mode
   :config
-  (add-to-list 'auto-mode-alist '("\\.txt\\'" . markdown-mode))
   (add-to-list 'auto-mode-alist '("\\.markdown\\'" . markdown-mode))
   (add-to-list 'auto-mode-alist '("\\.md\\'" . markdown-mode)))
 
@@ -240,24 +164,58 @@
 (use-package sly
   :hook ((sly-mrepl-mode . (lambda ()
                              (paredit-mode)
-                             (rainbow-delimiters-mode-enable)
-                             (company-mode))))
+                             (rainbow-delimiters-mode-enable))))
   :config
   (setq common-lisp-hyperspec-root "file:///home/tazjin/docs/lisp/"))
 
 (use-package telega
-  :bind (:map global-map ("s-t" . telega))
-  :config (telega-mode-line-mode 1))
+  :bind (:map global-map ("s-c" . (lambda (p) (interactive "P")
+                                    (if p (call-interactively #'telega-chat-with)
+                                      (telega))))
+         :map telega-chat-button-map ("a" . ignore))
+  :config (telega-mode-line-mode 1)
+  :custom
+  (telega-emoji-use-images nil)
+  (telega-completing-read-function #'completing-read))
 
 (use-package terraform-mode)
-(use-package toml-mode)
+(use-package toml-ts-mode)
 
-(use-package tvl
-  :custom
-  (tvl-gerrit-remote "gerrit"))
+(use-package treecrumbs
+  :hook ((yaml-ts-mode . treecrumbs-mode)))
+
+(use-package tvl)
+
+(use-package vertico
+  :config
+  (vertico-mode))
 
 (use-package web-mode)
-(use-package yaml-mode)
+(use-package yaml-ts-mode)
+(use-package zoxide)
+
+(use-package passively
+  :custom
+  (passively-store-state "/persist/tazjin/known-russian-words.el"))
+
+;; Note taking configuration for deft.
+(use-package deft
+  :custom
+  (deft-directory "/persist/tazjin/deft/")
+  (deft-extensions '("md" "org" "txt"))
+  (deft-default-extension "md"))
+
+(use-package zetteldeft
+  :custom
+  ;; Configure for Markdown
+  (zetteldeft-link-indicator "[[")
+  (zetteldeft-link-suffix "]]")
+  (zetteldeft-title-prefix "# ")
+  (zetteldeft-list-prefix "* "))
+
+;; Initialise midnight.el, which by default automatically cleans up
+;; unused buffers at midnight.
+(require 'midnight)
 
 (defgroup tazjin nil
   "Settings related to my configuration")
@@ -273,7 +231,7 @@
 ;; The way this will work for now is that Emacs will *write*
 ;; configuration to the file tracked in my repository, while not
 ;; actually *reading* it from there (unless Emacs is rebuilt).
-(setq custom-file (expand-file-name "~/depot/tools/emacs/config/custom.el"))
+(setq custom-file (f-join depot-path "users" "tazjin" "emacs" "config" "custom.el"))
 (load-library "custom")
 
 (defvar home-dir (expand-file-name "~"))
@@ -288,10 +246,8 @@
                  look-and-feel
                  functions
                  settings
-                 modes
                  bindings
                  eshell-setup))
-(telephone-line-setup)
 (ace-window-display-mode)
 
 ;; If a local configuration library exists, it should be loaded.
@@ -301,4 +257,6 @@
 (if-let (local-file (locate-library "local"))
     (load local-file))
 
+(require 'dottime)
+
 (provide 'init)
diff --git a/users/tazjin/emacs/config/look-and-feel.el b/users/tazjin/emacs/config/look-and-feel.el
index 8cca6e1bf0..b771b4cd03 100644
--- a/users/tazjin/emacs/config/look-and-feel.el
+++ b/users/tazjin/emacs/config/look-and-feel.el
@@ -11,9 +11,6 @@
 (setq ring-bell-function 'ignore)
 (setq initial-scratch-message "")
 
-;; Remember layout changes
-(winner-mode 1)
-
 ;; Usually emacs will run as a proper GUI application, in which case a few
 ;; extra settings are nice-to-have:
 (when window-system
@@ -22,69 +19,39 @@
   (blink-cursor-mode -1))
 
 ;; Configure Emacs fonts.
-(let ((font (if (equal "frog" (s-trim (shell-command-to-string "hostname")))
-                ;; For unclear reasons, frog refuses to render the
-                ;; regular font weight - everything ends up bold,
-                ;; which makes it hard to distinguish e.g. read/unread
-                ;; emails.
-                ;;
-                ;; Semi-bold looks a little different than on vauxhall
-                ;; and other machines, but it's alright.
-                (format "JetBrains Mono Semi Light-%d" 12)
-              (format "JetBrains Mono-%d" 12))))
+(let ((font (format "JetBrains Mono-%d" 12)))
   (setq default-frame-alist `((font . ,font)))
   (set-frame-font font t t))
 
-;; Configure telephone-line
-(defun telephone-misc-if-last-window ()
-  "Renders the mode-line-misc-info string for display in the
-  mode-line if the currently active window is the last one in the
-  frame.
-
-  The idea is to not display information like the current time,
-  load, battery levels on all buffers."
-
-  (when (bottom-right-window-p)
-    (telephone-line-raw mode-line-misc-info t)))
-
-(defun telephone-line-setup ()
-  (telephone-line-defsegment telephone-line-last-window-segment ()
-    (telephone-misc-if-last-window))
-
-  ;; Display the current EXWM workspace index in the mode-line
-  (telephone-line-defsegment telephone-line-exwm-workspace-index ()
-    (when (bottom-right-window-p)
-      (format "[%s]" exwm-workspace-current-index)))
-
-  ;; Define a highlight font for ~ important ~ information in the last
-  ;; window.
-  (defface special-highlight '((t (:foreground "white" :background "#5f627f"))) "")
-  (add-to-list 'telephone-line-faces
-               '(highlight . (special-highlight . special-highlight)))
-
-  (setq telephone-line-lhs
-        '((nil . (telephone-line-position-segment))
-          (accent . (telephone-line-buffer-segment))))
-
-  (setq telephone-line-rhs
-        '((accent . (telephone-line-major-mode-segment))
-          (nil . (telephone-line-last-window-segment
-                  telephone-line-exwm-workspace-index))
-
-          ;; TODO(tazjin): lets not do this particular thing while I
-          ;; don't actually run notmuch, there are too many things
-          ;; that have a dependency on the modeline drawing correctly
-          ;; (including randr operations!)
-          ;;
-          ;; (highlight . (telephone-line-notmuch-counts))
-          ))
-
-  (setq telephone-line-primary-left-separator 'telephone-line-tan-left
-        telephone-line-primary-right-separator 'telephone-line-tan-right
-        telephone-line-secondary-left-separator 'telephone-line-tan-hollow-left
-        telephone-line-secondary-right-separator 'telephone-line-tan-hollow-right)
-
-  (telephone-line-mode 1))
+;; Configure the modeline
+
+;; Implements a mode-line warning if there are any logged in TTY
+;; sessions apart from the graphical one.
+;;
+;; The status is only updated once every 30 seconds, as it requires
+;; shelling out to some commands (for now).
+(defun list-tty-sessions ()
+  "List all logged in tty sessions, except tty7 (graphical)"
+  (let ((command "who | awk '{print $2}' | grep -v tty7"))
+    (-filter (lambda (s) (not (string-empty-p s)))
+             (s-lines
+              (s-trim (let ((default-directory "/"))
+                        (shell-command-to-string command)))))))
+
+(defvar cached-tty-sessions (cons (time-convert nil 'integer) (list-tty-sessions))
+   "Cached TTY session value to avoid running the command too often.")
+
+;; TODO(tazjin): add this to the modeline
+
+(defun get-cached-tty-sessions ()
+  (let ((time ))
+    (when (< 30
+             (- (time-convert nil 'integer)
+                (car cached-tty-sessions)))
+      (setq cached-tty-sessions
+            (cons (time-convert nil 'integer) (list-tty-sessions)))))
+
+  (cdr cached-tty-sessions))
 
 ;; Auto refresh buffers
 (global-auto-revert-mode 1)
@@ -119,4 +86,13 @@
 ;; Don't wrap around when moving between buffers
 (setq windmove-wrap-around nil)
 
+;; Don't show me all emacs warnings immediately. Unfortunately this is
+;; not very granular, as emacs displays most of its warnings in the
+;; `emacs' "category", but without it every time I
+;; fullscreen/unfullscreen the warning buffer destroys my layout.
+;;
+;; Warnings suppressed by this are still logged to the warnings
+;; buffer.
+(setq warning-suppress-types '((emacs)))
+
 (provide 'look-and-feel)
diff --git a/users/tazjin/emacs/config/mail-setup.el b/users/tazjin/emacs/config/mail-setup.el
index 1167bcadd3..7352c8ba10 100644
--- a/users/tazjin/emacs/config/mail-setup.el
+++ b/users/tazjin/emacs/config/mail-setup.el
@@ -1,8 +1,6 @@
 (require 'notmuch)
-(require 'counsel-notmuch)
 
 ;; (global-set-key (kbd "C-c m") 'notmuch-hello)
-;; (global-set-key (kbd "C-c C-m") 'counsel-notmuch)
 ;; (global-set-key (kbd "C-c C-e n") 'notmuch-mua-new-mail)
 
 (setq notmuch-cache-dir (format "%s/.cache/notmuch" (getenv "HOME")))
@@ -25,8 +23,10 @@
 (setq notmuch-show-empty-saved-searches t)
 
 ;; Mail sending configuration
-(setq send-mail-function 'sendmail-send-it) ;; sendmail provided by MSMTP
-(setq notmuch-always-prompt-for-sender t)
+(setq sendmail-program "gmi") ;; lieer binary supports sendmail emulation
+(setq message-sendmail-extra-arguments
+      '("send" "--quiet" "-t" "-C" "~/mail/account.tazjin"))
+(setq send-mail-function 'sendmail-send-it)
 (setq notmuch-mua-user-agent-function
       (lambda () (format "Emacs %s; notmuch.el %s" emacs-version notmuch-emacs-version)))
 (setq mail-host-address (system-name))
@@ -49,7 +49,7 @@
 ;; handle that gracefully.
 (define-key notmuch-message-mode-map (kbd "C-x C-s") #'ignore)
 
-;; Define a telephone-line segment for displaying the count of unread,
+;; Define a mode-line segment for displaying the count of unread,
 ;; important mails in the last window's mode-line:
 (defvar *last-notmuch-count-redraw* 0)
 (defvar *current-notmuch-count* nil)
@@ -74,10 +74,6 @@
              (not (equal *current-notmuch-count* "I: 0; D: 0")))
     *current-notmuch-count*))
 
-(telephone-line-defsegment telephone-line-notmuch-counts ()
-  "This segment displays the count of unread notmuch messages in
-  the last window's mode-line (if unread messages are present)."
-
-  (update-display-notmuch-counts))
+;; TODO(tazjin): re-add this segment to the modeline
 
 (provide 'mail-setup)
diff --git a/users/tazjin/emacs/config/modes.el b/users/tazjin/emacs/config/modes.el
deleted file mode 100644
index 69fb523d0d..0000000000
--- a/users/tazjin/emacs/config/modes.el
+++ /dev/null
@@ -1,37 +0,0 @@
-;; Initializes modes I use.
-
-(add-hook 'prog-mode-hook 'esk-add-watchwords)
-(add-hook 'prog-mode-hook 'hl-line-mode)
-
-;; Use auto-complete as completion at point
-(defun set-auto-complete-as-completion-at-point-function ()
-  (setq completion-at-point-functions '(auto-complete)))
-
-(add-hook 'auto-complete-mode-hook
-          'set-auto-complete-as-completion-at-point-function)
-
-;; Enable rainbow-delimiters for all things programming
-(add-hook 'prog-mode-hook 'rainbow-delimiters-mode)
-
-;; Enable Paredit & Company in Emacs Lisp mode
-(add-hook 'emacs-lisp-mode-hook 'company-mode)
-
-;; Always highlight matching brackets
-(show-paren-mode 1)
-
-;; Always auto-close parantheses and other pairs
-(electric-pair-mode)
-
-;; Keep track of recent files
-(recentf-mode)
-
-;; Easily navigate sillycased words
-(global-subword-mode 1)
-
-;; Transparently open compressed files
-(auto-compression-mode t)
-
-;; Configure go-mode for Go2 Alpha
-(add-to-list 'auto-mode-alist '("\\.go2$" . go-mode))
-
-(provide 'modes)
diff --git a/users/tazjin/emacs/config/settings.el b/users/tazjin/emacs/config/settings.el
index b895d5e406..6c66ca608d 100644
--- a/users/tazjin/emacs/config/settings.el
+++ b/users/tazjin/emacs/config/settings.el
@@ -1,8 +1,5 @@
 (require 'uniquify)
 
-;; Move files to trash when deleting
-(setq delete-by-moving-to-trash t)
-
 ;; We don't live in the 80s, but we're also not a shitty web app.
 (setq gc-cons-threshold 20000000)
 
@@ -48,4 +45,45 @@
 ;; Show time in 24h format
 (setq display-time-24hr-format t)
 
+;; Use python-mode for Starlark files.
+(add-to-list 'auto-mode-alist '("\\.star\\'" . python-mode))
+
+;; Use cmake-mode for relevant files.
+(add-to-list 'auto-mode-alist '("ya\\.make\\'" . cmake-ts-mode))
+
+;; Use tree-sitter modes for various languages.
+(setq major-mode-remap-alist
+      '((bash-mode . bash-ts-mode)
+        (c++-mode . c++-ts-mode)
+        (c-mode . c-ts-mode)
+        (c-or-c++-mode . c-or-c++-ts-mode)
+        (json-mode . json-ts-mode)
+        (python-mode . python-ts-mode)
+        (rust-mode . rust-ts-mode)
+        (toml-mode . toml-ts-mode)
+        (yaml-mode . yaml-ts-mode)
+        (go-mode . go-ts-mode)
+        (cmake-mode . cmake-ts-mode)))
+
+;; Visually highlight current line in programming buffers
+(add-hook 'prog-mode-hook 'hl-line-mode)
+
+;; Enable rainbow-delimiters for all things programming
+(add-hook 'prog-mode-hook 'rainbow-delimiters-mode)
+
+;; Always highlight matching brackets
+(show-paren-mode 1)
+
+;; Always auto-close parantheses and other pairs
+(electric-pair-mode)
+
+;; Keep track of recent files
+(recentf-mode)
+
+;; Easily navigate sillycased words
+(global-subword-mode 1)
+
+;; Transparently open compressed files
+(auto-compression-mode t)
+
 (provide 'settings)
diff --git a/users/tazjin/emacs/default.nix b/users/tazjin/emacs/default.nix
index df73c05ffb..46843432f1 100644
--- a/users/tazjin/emacs/default.nix
+++ b/users/tazjin/emacs/default.nix
@@ -1,153 +1,189 @@
 # This file builds an Emacs pre-configured with the packages I need
 # and my personal Emacs configuration.
-#
-# On NixOS machines, this Emacs currently does not support
-# Imagemagick, see https://github.com/NixOS/nixpkgs/issues/70631.
-#
-# Forcing Emacs to link against Imagemagick currently causes libvterm
-# to segfault, which is a lot less desirable than not having telega
-# render images correctly.
-{ depot, lib, ... }:
+{ depot, lib, pkgs, ... }:
 
-let
-  inherit (depot) third_party;
+pkgs.makeOverridable
+  ({ emacs ? pkgs.emacs29 }:
+  let
+    emacsPackages = (pkgs.emacsPackagesFor emacs);
+    emacsWithPackages = emacsPackages.emacsWithPackages;
 
-  emacsWithPackages = (third_party.emacsPackagesGen third_party.emacs27).emacsWithPackages;
+    # If switching telega versions, use this variable because it will
+    # keep the version check, binary path and so on in sync.
+    currentTelega = epkgs: epkgs.melpaPackages.telega;
 
-  # Pick telega from unstable channel for recent fixes.
-  unstable = import third_party.nixpkgsSrc {};
-  telegaUnstable = (unstable.emacsPackagesGen third_party.emacs27).telega;
+    # $PATH for binaries that need to be available to Emacs
+    emacsBinPath = lib.makeBinPath [
+      (currentTelega pkgs.emacsPackages)
+      pkgs.libwebp # for dwebp, required by telega
+    ];
 
-  # $PATH for binaries that need to be available to Emacs
-  emacsBinPath = lib.makeBinPath [ telegaUnstable ];
+    identity = x: x;
 
-  identity = x: x;
+    # tree-sitter grammars for various ts-modes
+    customTreesitGrammars = emacs.pkgs.treesit-grammars.with-grammars (g: with g; [
+      tree-sitter-bash
+      tree-sitter-c
+      tree-sitter-cmake
+      tree-sitter-cpp
+      tree-sitter-css
+      tree-sitter-dockerfile
+      tree-sitter-go
+      tree-sitter-gomod
+      tree-sitter-hcl
+      tree-sitter-html
+      tree-sitter-java
+      tree-sitter-json
+      tree-sitter-latex
+      tree-sitter-make
+      tree-sitter-nix
+      tree-sitter-python
+      tree-sitter-rust
+      tree-sitter-sql
+      tree-sitter-toml
+      tree-sitter-yaml
+    ]);
 
-  tazjinsEmacs = pkgfun: (emacsWithPackages(epkgs: pkgfun(
-  # Actual ELPA packages (the enlightened!)
-  (with epkgs.elpaPackages; [
-    ace-window
-    avy
-    flymake
-    pinentry
-    rainbow-mode
-    undo-tree
-    xelb
-  ]) ++
+    tazjinsEmacs = pkgfun: (emacsWithPackages (epkgs: pkgfun (with epkgs; [
+      ace-link
+      ace-window
+      avy
+      bazel
+      browse-kill-ring
+      cargo
+      clojure-mode
+      consult
+      deft
+      direnv
+      elixir-mode
+      elm-mode
+      erlang
+      depotExwm
+      go-mode
+      google-c-style
+      gruber-darker-theme
+      haskell-mode
+      ht
+      hydra
+      idle-highlight-mode
+      inspector
+      jq-mode
+      kotlin-mode
+      kubernetes
+      magit
+      markdown-toc
+      multiple-cursors
+      nginx-mode
+      nix-mode
+      notmuch
+      paredit
+      password-store
+      pinentry
+      prescient
+      protobuf-mode
+      rainbow-delimiters
+      rainbow-mode
+      request
+      restclient
+      rust-mode
+      sly
+      string-edit-at-point
+      terraform-mode
+      undo-tree
+      uuidgen
+      vertico
+      vterm
+      web-mode
+      websocket
+      which-key
+      xelb
+      yasnippet
+      zetteldeft
+      zoxide
 
-  # MELPA packages:
-  (with epkgs.melpaPackages; [
-    ace-link
-    bazel-mode
-    browse-kill-ring
-    cargo
-    clojure-mode
-    cmake-mode
-    counsel
-    counsel-notmuch
-    dash-functional
-    direnv
-    dockerfile-mode
-    eglot
-    elixir-mode
-    elm-mode
-    erlang
-    geiser
-    go-mode
-    gruber-darker-theme
-    haskell-mode
-    ht
-    hydra
-    idle-highlight-mode
-    intero
-    ivy
-    ivy-pass
-    ivy-prescient
-    jq-mode
-    kotlin-mode
-    lispy
-    lsp-mode
-    magit
-    markdown-toc
-    meson-mode
-    multi-term
-    multiple-cursors
-    nginx-mode
-    nix-mode
-    notmuch # this comes from pkgs.third_party
-    org-journal
-    org-ql
-    paredit
-    password-store
-    pg
-    polymode
-    prescient
-    protobuf-mode
-    racket-mode
-    rainbow-delimiters
-    refine
-    request
-    restclient
-    sly
-    string-edit
-    swiper
-    telegaUnstable
-    telephone-line
-    terraform-mode
-    toml-mode
-    transient
-    use-package
-    uuidgen
-    web-mode
-    websocket
-    which-key
-    yaml-mode
-    yasnippet
-  ]) ++
+      # experimental (not otherwise embedded in config yet)
+      orderless
+      corfu
+      eat
 
-  # Custom packages
-  (with depot.tools.emacs-pkgs; [
-    dottime
-    nix-util
-    term-switcher
-    tvl
+      # Wonky stuff
+      (currentTelega epkgs)
+      customTreesitGrammars # TODO(tazjin): how is this *supposed* to work?!
 
-    # patched / overridden versions of packages
-    depot.third_party.emacs.exwm
-    depot.third_party.emacs.rcirc
-    depot.third_party.emacs.vterm
-    depot.third_party.emacs.explain-pause-mode
-  ]))));
-in lib.fix(self: l: f: third_party.writeShellScriptBin "tazjins-emacs" ''
-  export PATH="${emacsBinPath}:$PATH"
-  exec ${tazjinsEmacs f}/bin/emacs \
-    --debug-init \
-    --no-site-file \
-    --no-site-lisp \
-    --no-init-file \
-    --directory ${./config} ${if l != null then "--directory ${l}" else ""} \
-    --eval "(require 'init)" $@
-  '' // {
-    # Call overrideEmacs with a function (pkgs -> pkgs) to modify the
-    # packages that should be included in this Emacs distribution.
-    overrideEmacs = f': self l f';
+      # Custom depot packages (either ours, or overridden ones)
+      tvlPackages.dottime
+      tvlPackages.nix-util
+      tvlPackages.passively
+      tvlPackages.rcirc
+      tvlPackages.term-switcher
+      tvlPackages.treecrumbs
+      tvlPackages.tvl
 
-    # Call withLocalConfig with the path to a *folder* containing a
-    # `local.el` which provides local system configuration.
-    withLocalConfig = confDir: self confDir f;
+      # Dynamic/native modules
+      depot.users.tazjin.gio-list-apps
+    ])));
 
-    # Build a derivation that uses the specified local Emacs (i.e.
-    # built outside of Nix) instead
-    withLocalEmacs = emacsBin: third_party.writeShellScriptBin "tazjins-emacs" ''
+    # Tired of telega.el runtime breakages through tdlib
+    # incompatibility. Target to make that a build failure instead.
+    tdlibCheck =
+      let
+        tgEmacs = emacsWithPackages (epkgs: [ (currentTelega epkgs) ]);
+        verifyTdlibVersion = builtins.toFile "verify-tdlib-version.el" ''
+          (require 'telega)
+          (defvar tdlib-version "${pkgs.tdlib.version}")
+          (when (or (version< tdlib-version
+                              telega-tdlib-min-version)
+                    (and telega-tdlib-max-version
+                          (version< telega-tdlib-max-version
+                                    tdlib-version)))
+             (message "Found TDLib version %s, but require %s to %s"
+                     tdlib-version telega-tdlib-min-version telega-tdlib-max-version)
+            (kill-emacs 1))
+        '';
+      in
+      pkgs.runCommand "tdlibCheck" { } ''
+        export PATH="${emacsBinPath}:$PATH"
+        ${tgEmacs}/bin/emacs --script ${verifyTdlibVersion} && touch $out
+      '';
+  in
+  lib.fix
+    (self: l: f: (pkgs.writeShellScriptBin "tazjins-emacs" ''
       export PATH="${emacsBinPath}:$PATH"
-      export EMACSLOADPATH="${(tazjinsEmacs f).deps}/share/emacs/site-lisp:"
-      exec ${emacsBin} \
+      exec ${tazjinsEmacs f}/bin/emacs \
         --debug-init \
         --no-site-file \
         --no-site-lisp \
         --no-init-file \
-        --directory ${./config} \
-        ${if l != null then "--directory ${l}" else ""} \
+        --directory ${./config} ${if l != null then "--directory ${l}" else ""} \
+        --eval "(add-to-list 'treesit-extra-load-path \"${customTreesitGrammars}/lib\")" \
         --eval "(require 'init)" $@
-    '';
-  }) null identity
+    '').overrideAttrs
+      (_: {
+        passthru = {
+          # Expose original Emacs used for my configuration.
+          inherit emacs;
+
+          # Expose the pure emacs with all packages.
+          inherit emacsPackages;
+          emacsWithPackages = tazjinsEmacs f;
+
+          # Call overrideEmacs with a function (pkgs -> pkgs) to modify the
+          # packages that should be included in this Emacs distribution.
+          overrideEmacs = f': self l f';
+
+          # Call withLocalConfig with the path to a *folder* containing a
+          # `local.el` which provides local system configuration.
+          withLocalConfig = confDir: self confDir f;
+
+          # Expose telega/tdlib version check as a target that is built in
+          # CI.
+          #
+          # TODO(tazjin): uncomment when telega works again
+          inherit tdlibCheck;
+          meta.ci.targets = [ "tdlibCheck" ];
+        };
+      }))
+    null
+    identity
+  )
+{ }
diff --git a/users/tazjin/finito/default.nix b/users/tazjin/finito/default.nix
index e50ac32be4..9a39591eab 100644
--- a/users/tazjin/finito/default.nix
+++ b/users/tazjin/finito/default.nix
@@ -2,4 +2,8 @@
 
 depot.third_party.naersk.buildPackage {
   src = ./.;
+
+  # Got broken by a rustc update (?)
+  # https://buildkite.com/tvl/depot/builds/17910#01841493-dc42-44f8-b904-32bf3d835485
+  meta.ci.skip = true;
 }
diff --git a/users/tazjin/finito/finito-core/src/lib.rs b/users/tazjin/finito/finito-core/src/lib.rs
index 517bfad2bc..aaec03a77b 100644
--- a/users/tazjin/finito/finito-core/src/lib.rs
+++ b/users/tazjin/finito/finito-core/src/lib.rs
@@ -38,8 +38,8 @@
 //!
 //!   * an event type representing all possible events in the machine
 //!
-//!   * an action type representing a description of all possible
-//!     side-effects of the machine
+//!   * an action type representing a description of all possible side-effects
+//!     of the machine
 //!
 //! Using the definition above we can now say that a transition in a
 //! state-machine, involving these three types, takes an initial state
@@ -92,14 +92,13 @@
 //!
 //!   * `finito`: Core components and classes of Finito
 //!
-//!   * `finito-in-mem`: In-memory implementation of state machines
-//!     that do not need to live longer than an application using
-//!     standard library concurrency primitives.
+//!   * `finito-in-mem`: In-memory implementation of state machines that do not
+//!     need to live longer than an application using standard library
+//!     concurrency primitives.
 //!
-//!   * `finito-postgres`: Postgres-backed, persistent implementation
-//!     of state machines that, well, do need to live longer. Uses
-//!     Postgres for concurrency synchronisation, so keep that in
-//!     mind.
+//!   * `finito-postgres`: Postgres-backed, persistent implementation of state
+//!     machines that, well, do need to live longer. Uses Postgres for
+//!     concurrency synchronisation, so keep that in mind.
 //!
 //! Which should cover most use-cases. Okay, enough prose, lets dive
 //! in.
@@ -110,8 +109,8 @@
 
 extern crate serde;
 
-use serde::Serialize;
 use serde::de::DeserializeOwned;
+use serde::Serialize;
 use std::fmt::Debug;
 use std::mem;
 
@@ -120,7 +119,10 @@ use std::mem;
 ///
 /// This trait is used to implement transition logic and to "tie the
 /// room together", with the room being our triplet of types.
-pub trait FSM where Self: Sized {
+pub trait FSM
+where
+    Self: Sized,
+{
     /// A human-readable string uniquely describing what this FSM
     /// models. This is used in log messages, database tables and
     /// various other things throughout Finito.
@@ -166,7 +168,7 @@ pub trait FSM where Self: Sized {
 
     /// `act` interprets and executes FSM actions. This is the only
     /// part of an FSM in which side-effects are allowed.
-    fn act(Self::Action, &Self::State) -> Result<Vec<Self::Event>, Self::Error>;
+    fn act(action: Self::Action, state: &Self::State) -> Result<Vec<Self::Event>, Self::Error>;
 }
 
 /// This function is the primary function used to advance a state
@@ -223,11 +225,13 @@ pub trait FSMBackend<S: 'static> {
     /// Insert a new state-machine into the backend's storage and
     /// return its newly allocated key.
     fn insert_machine<F>(&self, initial: F) -> Result<Self::Key, Self::Error>
-    where F: FSM + Serialize + DeserializeOwned;
+    where
+        F: FSM + Serialize + DeserializeOwned;
 
     /// Retrieve the current state of an FSM by its key.
     fn get_machine<F: FSM>(&self, key: Self::Key) -> Result<F, Self::Error>
-    where F: FSM + Serialize + DeserializeOwned;
+    where
+        F: FSM + Serialize + DeserializeOwned;
 
     /// Advance a state machine by applying an event and persisting it
     /// as well as any resulting actions.
@@ -236,8 +240,9 @@ pub trait FSMBackend<S: 'static> {
     /// on the backend used. Please consult the backend's
     /// documentation for details.
     fn advance<'a, F: FSM>(&'a self, key: Self::Key, event: F::Event) -> Result<F, Self::Error>
-    where F: FSM + Serialize + DeserializeOwned,
-          F::State: From<&'a S>,
-          F::Event: Serialize + DeserializeOwned,
-          F::Action: Serialize + DeserializeOwned;
+    where
+        F: FSM + Serialize + DeserializeOwned,
+        F::State: From<&'a S>,
+        F::Event: Serialize + DeserializeOwned,
+        F::Action: Serialize + DeserializeOwned;
 }
diff --git a/users/tazjin/finito/finito-door/src/lib.rs b/users/tazjin/finito/finito-door/src/lib.rs
index 68542c0bc4..441ab0e3d2 100644
--- a/users/tazjin/finito/finito-door/src/lib.rs
+++ b/users/tazjin/finito/finito-door/src/lib.rs
@@ -27,15 +27,15 @@
 //! The door can only be locked if it is closed. Oh, and it has a few
 //! extra features:
 //!
-//! * whenever the door's state changes, an IRC channel receives a
-//!   message about that
+//! * whenever the door's state changes, an IRC channel receives a message about
+//!   that
 //!
-//! * the door calls the police if the code is intered incorrectly more
-//!   than a specified number of times (mhm, lets say, three)
+//! * the door calls the police if the code is intered incorrectly more than a
+//!   specified number of times (mhm, lets say, three)
 //!
-//! * if the police is called the door can not be interacted with
-//!   anymore (and honestly, for the sake of this example, we don't
-//!   care how its functionality is restored)
+//! * if the police is called the door can not be interacted with anymore (and
+//!   honestly, for the sake of this example, we don't care how its
+//!   functionality is restored)
 //!
 //! ## The Door - Visualized
 //!
@@ -71,7 +71,8 @@
 //!
 //! Alright, enough foreplay, lets dive in!
 
-#[macro_use] extern crate serde_derive;
+#[macro_use]
+extern crate serde_derive;
 
 extern crate failure;
 extern crate finito;
@@ -292,11 +293,13 @@ mod tests {
     use finito::advance;
 
     fn test_fsm<S: FSM>(initial: S, events: Vec<S::Event>) -> (S, Vec<S::Action>) {
-        events.into_iter().fold((initial, vec![]), |(state, mut actions), event| {
-            let (new_state, mut new_actions) = advance(state, event);
-            actions.append(&mut new_actions);
-            (new_state, actions)
-        })
+        events
+            .into_iter()
+            .fold((initial, vec![]), |(state, mut actions), event| {
+                let (new_state, mut new_actions) = advance(state, event);
+                actions.append(&mut new_actions);
+                (new_state, actions)
+            })
     }
 
     #[test]
@@ -313,7 +316,10 @@ mod tests {
         ];
         let (final_state, actions) = test_fsm(initial, events);
 
-        assert_eq!(final_state, DoorState::Locked { code: 4567, attempts: 2 });
+        assert_eq!(final_state, DoorState::Locked {
+            code: 4567,
+            attempts: 2
+        });
         assert_eq!(actions, vec![
             DoorAction::NotifyIRC("door was closed".into()),
             DoorAction::NotifyIRC("door was opened".into()),
diff --git a/users/tazjin/finito/finito-postgres/src/error.rs b/users/tazjin/finito/finito-postgres/src/error.rs
index e130d18361..ed33775cd7 100644
--- a/users/tazjin/finito/finito-postgres/src/error.rs
+++ b/users/tazjin/finito/finito-postgres/src/error.rs
@@ -1,10 +1,9 @@
 //! This module defines error types and conversions for issue that can
 //! occur while dealing with persisted state machines.
 
-use std::result;
-use std::fmt;
-use uuid::Uuid;
 use std::error::Error as StdError;
+use std::{fmt, result};
+use uuid::Uuid;
 
 // errors to chain:
 use postgres::Error as PgError;
@@ -41,20 +40,15 @@ impl fmt::Display for Error {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         use ErrorKind::*;
         let msg = match &self.kind {
-            Serialization(err) =>
-                format!("JSON serialization error: {}", err),
+            Serialization(err) => format!("JSON serialization error: {}", err),
 
-            Database(err) =>
-                format!("PostgreSQL error: {}", err),
+            Database(err) => format!("PostgreSQL error: {}", err),
 
-            DBPool(err) =>
-                format!("Database connection pool error: {}", err),
+            DBPool(err) => format!("Database connection pool error: {}", err),
 
-            FSMNotFound(id) =>
-                format!("FSM with ID {} not found", id),
+            FSMNotFound(id) => format!("FSM with ID {} not found", id),
 
-            ActionNotFound(id) =>
-                format!("Action with ID {} not found", id),
+            ActionNotFound(id) => format!("Action with ID {} not found", id),
         };
 
         match &self.context {
@@ -66,7 +60,7 @@ impl fmt::Display for Error {
 
 impl StdError for Error {}
 
-impl <E: Into<ErrorKind>> From<E> for Error {
+impl<E: Into<ErrorKind>> From<E> for Error {
     fn from(err: E) -> Error {
         Error {
             kind: err.into(),
@@ -99,11 +93,11 @@ pub trait ResultExt<T> {
     fn context<C: fmt::Display>(self, ctx: C) -> Result<T>;
 }
 
-impl <T, E: Into<Error>> ResultExt<T> for result::Result<T, E> {
+impl<T, E: Into<Error>> ResultExt<T> for result::Result<T, E> {
     fn context<C: fmt::Display>(self, ctx: C) -> Result<T> {
         self.map_err(|err| Error {
             context: Some(format!("{}", ctx)),
-            .. err.into()
+            ..err.into()
         })
     }
 }
diff --git a/users/tazjin/finito/finito-postgres/src/lib.rs b/users/tazjin/finito/finito-postgres/src/lib.rs
index ae147f751f..ea63cc9dfd 100644
--- a/users/tazjin/finito/finito-postgres/src/lib.rs
+++ b/users/tazjin/finito/finito-postgres/src/lib.rs
@@ -4,8 +4,10 @@
 //!
 //! TODO: events & actions should have `SERIAL` keys
 
-#[macro_use] extern crate postgres;
-#[macro_use] extern crate postgres_derive;
+#[macro_use]
+extern crate postgres;
+#[macro_use]
+extern crate postgres_derive;
 
 extern crate chrono;
 extern crate finito;
@@ -14,23 +16,25 @@ extern crate serde;
 extern crate serde_json;
 extern crate uuid;
 
-#[cfg(test)] mod tests;
-#[cfg(test)] extern crate finito_door;
+#[cfg(test)]
+mod tests;
+#[cfg(test)]
+extern crate finito_door;
 
 mod error;
-pub use error::{Result, Error, ErrorKind};
+pub use error::{Error, ErrorKind, Result};
 
 use chrono::prelude::{DateTime, Utc};
 use error::ResultExt;
-use finito::{FSM, FSMBackend};
+use finito::{FSMBackend, FSM};
 use postgres::transaction::Transaction;
 use postgres::GenericConnection;
-use serde::Serialize;
+use r2d2_postgres::{r2d2, PostgresConnectionManager};
 use serde::de::DeserializeOwned;
+use serde::Serialize;
 use serde_json::Value;
 use std::marker::PhantomData;
 use uuid::Uuid;
-use r2d2_postgres::{r2d2, PostgresConnectionManager};
 
 type DBPool = r2d2::Pool<PostgresConnectionManager>;
 type DBConn = r2d2::PooledConnection<PostgresConnectionManager>;
@@ -112,15 +116,13 @@ pub struct FinitoPostgres<S> {
     db_pool: DBPool,
 }
 
-impl <S> FinitoPostgres<S> {
+impl<S> FinitoPostgres<S> {
     pub fn new(state: S, db_pool: DBPool, _pool_size: usize) -> Self {
-        FinitoPostgres {
-            state, db_pool,
-        }
+        FinitoPostgres { state, db_pool }
     }
 }
 
-impl <State: 'static> FSMBackend<State> for FinitoPostgres<State> {
+impl<State: 'static> FSMBackend<State> for FinitoPostgres<State> {
     type Key = Uuid;
     type Error = Error;
 
@@ -134,10 +136,11 @@ impl <State: 'static> FSMBackend<State> for FinitoPostgres<State> {
         let fsm = S::FSM_NAME.to_string();
         let state = serde_json::to_value(initial).context("failed to serialise FSM")?;
 
-        self.conn()?.execute(query, &[&id, &fsm, &state]).context("failed to insert FSM")?;
+        self.conn()?
+            .execute(query, &[&id, &fsm, &state])
+            .context("failed to insert FSM")?;
 
         return Ok(id);
-
     }
 
     fn get_machine<S: FSM + DeserializeOwned>(&self, key: Uuid) -> Result<S> {
@@ -156,10 +159,12 @@ impl <State: 'static> FSMBackend<State> for FinitoPostgres<State> {
     /// processing is finished as running actions may result in additional
     /// transitions.
     fn advance<'a, S>(&'a self, key: Uuid, event: S::Event) -> Result<S>
-    where S: FSM + Serialize + DeserializeOwned,
-          S::State: From<&'a State>,
-          S::Event: Serialize + DeserializeOwned,
-          S::Action: Serialize + DeserializeOwned {
+    where
+        S: FSM + Serialize + DeserializeOwned,
+        S::State: From<&'a State>,
+        S::Event: Serialize + DeserializeOwned,
+        S::Action: Serialize + DeserializeOwned,
+    {
         let conn = self.conn()?;
         let tx = conn.transaction().context("could not begin transaction")?;
         let state = get_machine_internal(&tx, key, true)?;
@@ -187,16 +192,18 @@ impl <State: 'static> FSMBackend<State> for FinitoPostgres<State> {
     }
 }
 
-impl <State: 'static> FinitoPostgres<State> {
+impl<State: 'static> FinitoPostgres<State> {
     /// Execute several actions at the same time, each in a separate
     /// thread. Note that actions returning further events, causing
     /// further transitions, returning further actions and so on will
     /// potentially cause multiple threads to get created.
-    fn run_actions<'a, S>(&'a self, fsm_id: Uuid, action_ids: Vec<Uuid>) where
+    fn run_actions<'a, S>(&'a self, fsm_id: Uuid, action_ids: Vec<Uuid>)
+    where
         S: FSM + Serialize + DeserializeOwned,
         S::Event: Serialize + DeserializeOwned,
         S::Action: Serialize + DeserializeOwned,
-        S::State: From<&'a State> {
+        S::State: From<&'a State>,
+    {
         let state: S::State = (&self.state).into();
         let conn = self.conn().expect("TODO");
 
@@ -214,17 +221,19 @@ impl <State: 'static> FinitoPostgres<State> {
 
     /// Retrieve a single connection from the database connection pool.
     fn conn(&self) -> Result<DBConn> {
-        self.db_pool.get().context("failed to retrieve connection from pool")
+        self.db_pool
+            .get()
+            .context("failed to retrieve connection from pool")
     }
 }
 
-
-
 /// Insert a single state-machine into the database and return its
 /// newly allocated, random UUID.
-pub fn insert_machine<C, S>(conn: &C, initial: S) -> Result<Uuid> where
+pub fn insert_machine<C, S>(conn: &C, initial: S) -> Result<Uuid>
+where
     C: GenericConnection,
-    S: FSM + Serialize {
+    S: FSM + Serialize,
+{
     let query = r#"
       INSERT INTO machines (id, fsm, state)
       VALUES ($1, $2, $3)
@@ -240,13 +249,12 @@ pub fn insert_machine<C, S>(conn: &C, initial: S) -> Result<Uuid> where
 }
 
 /// Insert a single event into the database and return its UUID.
-fn insert_event<C, S>(conn: &C,
-                      fsm_id: Uuid,
-                      event: &S::Event) -> Result<Uuid>
+fn insert_event<C, S>(conn: &C, fsm_id: Uuid, event: &S::Event) -> Result<Uuid>
 where
     C: GenericConnection,
     S: FSM,
-    S::Event: Serialize {
+    S::Event: Serialize,
+{
     let query = r#"
       INSERT INTO events (id, fsm, fsm_id, event)
       VALUES ($1, $2, $3, $4)
@@ -254,21 +262,19 @@ where
 
     let id = Uuid::new_v4();
     let fsm = S::FSM_NAME.to_string();
-    let event_value = serde_json::to_value(event)
-        .context("failed to serialize event")?;
+    let event_value = serde_json::to_value(event).context("failed to serialize event")?;
 
     conn.execute(query, &[&id, &fsm, &fsm_id, &event_value])?;
-    return Ok(id)
+    return Ok(id);
 }
 
 /// Insert a single action into the database and return its UUID.
-fn insert_action<C, S>(conn: &C,
-                       fsm_id: Uuid,
-                       event_id: Uuid,
-                       action: &S::Action) -> Result<Uuid> where
+fn insert_action<C, S>(conn: &C, fsm_id: Uuid, event_id: Uuid, action: &S::Action) -> Result<Uuid>
+where
     C: GenericConnection,
     S: FSM,
-    S::Action: Serialize {
+    S::Action: Serialize,
+{
     let query = r#"
       INSERT INTO actions (id, fsm, fsm_id, event_id, content, status)
       VALUES ($1, $2, $3, $4, $5, $6)
@@ -276,23 +282,26 @@ fn insert_action<C, S>(conn: &C,
 
     let id = Uuid::new_v4();
     let fsm = S::FSM_NAME.to_string();
-    let action_value = serde_json::to_value(action)
-        .context("failed to serialize action")?;
+    let action_value = serde_json::to_value(action).context("failed to serialize action")?;
 
-    conn.execute(
-        query,
-        &[&id, &fsm, &fsm_id, &event_id, &action_value, &ActionStatus::Pending]
-    )?;
+    conn.execute(query, &[
+        &id,
+        &fsm,
+        &fsm_id,
+        &event_id,
+        &action_value,
+        &ActionStatus::Pending,
+    ])?;
 
-    return Ok(id)
+    return Ok(id);
 }
 
 /// Update the state of a specified machine.
-fn update_state<C, S>(conn: &C,
-                      fsm_id: Uuid,
-                      state: &S) -> Result<()> where
+fn update_state<C, S>(conn: &C, fsm_id: Uuid, state: &S) -> Result<()>
+where
     C: GenericConnection,
-    S: FSM + Serialize {
+    S: FSM + Serialize,
+{
     let query = r#"
       UPDATE machines SET state = $1 WHERE id = $2
     "#;
@@ -312,23 +321,28 @@ fn update_state<C, S>(conn: &C,
 fn alter_for_update(alter: bool, query: &str) -> String {
     match alter {
         false => query.to_string(),
-        true  => format!("{} FOR UPDATE", query),
+        true => format!("{} FOR UPDATE", query),
     }
 }
 
 /// Retrieve the current state of a state machine from the database,
 /// optionally locking the machine state for the duration of some
 /// enclosing transaction.
-fn get_machine_internal<C, S>(conn: &C,
-                              id: Uuid,
-                              for_update: bool) -> Result<S> where
+fn get_machine_internal<C, S>(conn: &C, id: Uuid, for_update: bool) -> Result<S>
+where
     C: GenericConnection,
-    S: FSM + DeserializeOwned {
-    let query = alter_for_update(for_update, r#"
+    S: FSM + DeserializeOwned,
+{
+    let query = alter_for_update(
+        for_update,
+        r#"
       SELECT state FROM machines WHERE id = $1
-    "#);
+    "#,
+    );
 
-    let rows = conn.query(&query, &[&id]).context("failed to retrieve FSM")?;
+    let rows = conn
+        .query(&query, &[&id])
+        .context("failed to retrieve FSM")?;
 
     if let Some(row) = rows.into_iter().next() {
         Ok(serde_json::from_value(row.get(0)).context("failed to deserialize FSM")?)
@@ -339,20 +353,25 @@ fn get_machine_internal<C, S>(conn: &C,
 
 /// Retrieve an action from the database, optionally locking it for
 /// the duration of some enclosing transaction.
-fn get_action<C, S>(conn: &C, id: Uuid) -> Result<(ActionStatus, S::Action)> where
+fn get_action<C, S>(conn: &C, id: Uuid) -> Result<(ActionStatus, S::Action)>
+where
     C: GenericConnection,
     S: FSM,
-    S::Action: DeserializeOwned {
-    let query = alter_for_update(true, r#"
+    S::Action: DeserializeOwned,
+{
+    let query = alter_for_update(
+        true,
+        r#"
       SELECT status, content FROM actions
       WHERE id = $1 AND fsm = $2
-    "#);
+    "#,
+    );
 
     let rows = conn.query(&query, &[&id, &S::FSM_NAME])?;
 
     if let Some(row) = rows.into_iter().next() {
-        let action = serde_json::from_value(row.get(1))
-            .context("failed to deserialize FSM action")?;
+        let action =
+            serde_json::from_value(row.get(1)).context("failed to deserialize FSM action")?;
         Ok((row.get(0), action))
     } else {
         Err(ErrorKind::ActionNotFound(id).into())
@@ -360,13 +379,17 @@ fn get_action<C, S>(conn: &C, id: Uuid) -> Result<(ActionStatus, S::Action)> whe
 }
 
 /// Update the status of an action after an attempt to run it.
-fn update_action_status<C, S>(conn: &C,
-                              id: Uuid,
-                              status: ActionStatus,
-                              error: Option<String>,
-                              _fsm: PhantomData<S>) -> Result<()> where
+fn update_action_status<C, S>(
+    conn: &C,
+    id: Uuid,
+    status: ActionStatus,
+    error: Option<String>,
+    _fsm: PhantomData<S>,
+) -> Result<()>
+where
     C: GenericConnection,
-    S: FSM {
+    S: FSM,
+{
     let query = r#"
       UPDATE actions SET status = $1, error = $2
       WHERE id = $3 AND fsm = $4
@@ -389,10 +412,16 @@ fn update_action_status<C, S>(conn: &C,
 /// panic), the error will be persisted. Should it fail by panicking
 /// (which developers should never do explicitly in action
 /// interpreters) its status will not be changed.
-fn run_action<S>(tx: Transaction, id: Uuid, state: &S::State, _fsm: PhantomData<S>)
-                 -> Result<Vec<S::Event>> where
+fn run_action<S>(
+    tx: Transaction,
+    id: Uuid,
+    state: &S::State,
+    _fsm: PhantomData<S>,
+) -> Result<Vec<S::Event>>
+where
     S: FSM,
-    S::Action: DeserializeOwned {
+    S::Action: DeserializeOwned,
+{
     let (status, action) = get_action::<Transaction, S>(&tx, id)?;
 
     let result = match status {
@@ -401,29 +430,25 @@ fn run_action<S>(tx: Transaction, id: Uuid, state: &S::State, _fsm: PhantomData<
                 // If the action succeeded, update its status to
                 // completed and return the created events.
                 Ok(events) => {
-                    update_action_status(
-                        &tx, id, ActionStatus::Completed, None, PhantomData::<S>
-                    )?;
+                    update_action_status(&tx, id, ActionStatus::Completed, None, PhantomData::<S>)?;
                     events
-                },
+                }
 
                 // If the action failed, persist the debug message and
                 // return nothing.
                 Err(err) => {
                     let msg = Some(format!("{:?}", err));
-                    update_action_status(
-                        &tx, id, ActionStatus::Failed, msg, PhantomData::<S>
-                    )?;
+                    update_action_status(&tx, id, ActionStatus::Failed, msg, PhantomData::<S>)?;
                     vec![]
-                },
+                }
             }
-        },
+        }
 
         _ => {
             // TODO: Currently only pending actions are run because
             // retryable actions are not yet implemented.
             vec![]
-        },
+        }
     };
 
     tx.commit().context("failed to commit transaction")?;
diff --git a/users/tazjin/finito/finito-postgres/src/tests.rs b/users/tazjin/finito/finito-postgres/src/tests.rs
index b1b5821be3..dd270c3875 100644
--- a/users/tazjin/finito/finito-postgres/src/tests.rs
+++ b/users/tazjin/finito/finito-postgres/src/tests.rs
@@ -16,7 +16,11 @@ fn test_insert_machine() {
     let door = insert_machine(&conn, initial).expect("Failed to insert door");
     let result = get_machine(&conn, &door, false).expect("Failed to fetch door");
 
-    assert_eq!(result, DoorState::Opened, "Inserted door state should match");
+    assert_eq!(
+        result,
+        DoorState::Opened,
+        "Inserted door state should match"
+    );
 }
 
 #[test]
@@ -41,7 +45,10 @@ fn test_advance() {
     }
 
     let result = get_machine(&conn, &door, false).expect("Failed to fetch door");
-    let expected = DoorState::Locked { code: 4567, attempts: 2 };
+    let expected = DoorState::Locked {
+        code: 4567,
+        attempts: 2,
+    };
 
     assert_eq!(result, expected, "Advanced door state should match");
 }
diff --git a/users/tazjin/generator-example/.gitignore b/users/tazjin/generator-example/.gitignore
new file mode 100644
index 0000000000..ea8c4bf7f3
--- /dev/null
+++ b/users/tazjin/generator-example/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/users/tazjin/generator-example/Cargo.lock b/users/tazjin/generator-example/Cargo.lock
new file mode 100644
index 0000000000..a6f25ee394
--- /dev/null
+++ b/users/tazjin/generator-example/Cargo.lock
@@ -0,0 +1,124 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "genawaiter"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0"
+dependencies = [
+ "genawaiter-macro",
+ "genawaiter-proc-macro",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "genawaiter-macro"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc"
+
+[[package]]
+name = "genawaiter-proc-macro"
+version = "0.99.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "generator-example"
+version = "0.1.0"
+dependencies = [
+ "genawaiter",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "syn-mid",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.51"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn-mid"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baa8e7560a164edb1621a55d18a0c59abf49d360f47aa7b821061dd7eea7fac9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
diff --git a/users/tazjin/generator-example/Cargo.toml b/users/tazjin/generator-example/Cargo.toml
new file mode 100644
index 0000000000..faf313973f
--- /dev/null
+++ b/users/tazjin/generator-example/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "generator-example"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+genawaiter = "0.99.1"
diff --git a/users/tazjin/generator-example/README.md b/users/tazjin/generator-example/README.md
new file mode 100644
index 0000000000..0bec13ee9a
--- /dev/null
+++ b/users/tazjin/generator-example/README.md
@@ -0,0 +1,11 @@
+generator-example
+=================
+
+This is an experiment with the [`genawaiter`][] crate, to see if it
+could be suitable for dealing with the execution flattening problem in
+Tvix.
+
+It constructs a dummy example that is similar to some of the problems
+we have in Tvix that require generator-like thunk forcing.
+
+[`genawaiter`]: https://docs.rs/genawaiter/latest/genawaiter/index.html
diff --git a/users/tazjin/generator-example/src/main.rs b/users/tazjin/generator-example/src/main.rs
new file mode 100644
index 0000000000..4aa931caf8
--- /dev/null
+++ b/users/tazjin/generator-example/src/main.rs
@@ -0,0 +1,115 @@
+use genawaiter::rc::{Co, Gen};
+use std::cell::RefCell;
+use std::future::Future;
+use std::pin::Pin;
+use std::rc::Rc;
+
+#[derive(Debug)]
+enum ValueRepr {
+    Int(i64),
+    Thunk((i64, i64)),
+}
+
+#[derive(Clone, Debug)]
+struct Value(Rc<RefCell<ValueRepr>>);
+
+impl Value {
+    fn force(&self) {
+        let mut inner = self.0.borrow_mut();
+        match *inner {
+            ValueRepr::Int(_) => return,
+            ValueRepr::Thunk((a, b)) => {
+                *inner = ValueRepr::Int(a + b);
+            }
+        }
+    }
+
+    fn is_forced(&self) -> bool {
+        matches!(*self.0.borrow(), ValueRepr::Int(_))
+    }
+
+    fn int(&self) -> i64 {
+        match *self.0.borrow() {
+            ValueRepr::Int(i) => i,
+            ValueRepr::Thunk(_) => panic!("unforced thunk!"),
+        }
+    }
+}
+
+impl From<i64> for Value {
+    fn from(value: i64) -> Self {
+        Value(Rc::new(RefCell::new(ValueRepr::Int(value))))
+    }
+}
+
+impl From<(i64, i64)> for Value {
+    fn from(value: (i64, i64)) -> Self {
+        Value(Rc::new(RefCell::new(ValueRepr::Thunk(value))))
+    }
+}
+
+async fn list_maker(values: Vec<Value>, co: Co<Value>) -> Vec<i64> {
+    let mut output: Vec<i64> = vec![];
+
+    for value in values {
+        if !value.is_forced() {
+            co.yield_(value.clone()).await;
+        }
+
+        output.push(value.int());
+    }
+
+    output
+}
+
+async fn list_reverser(values: Vec<Value>, co: Co<Value>) -> Vec<i64> {
+    let mut output = list_maker(values, co).await;
+    output.reverse();
+    output
+}
+
+struct Frame {
+    gen: Gen<Value, (), Pin<Box<dyn Future<Output = Vec<i64>>>>>,
+}
+
+fn pin_future(
+    f: impl Future<Output = Vec<i64>> + 'static,
+) -> Pin<Box<dyn Future<Output = Vec<i64>>>> {
+    Box::pin(f)
+}
+
+fn main() {
+    let mut frames: Vec<Frame> = vec![];
+
+    let values: Vec<Value> = vec![
+        42.into(),
+        (12, 54).into(),
+        4.into(),
+        (40, 2).into(),
+        2.into(),
+    ];
+    let second = values.clone();
+
+    frames.push(Frame {
+        gen: Gen::new(|co| pin_future(list_maker(values, co))),
+    });
+
+    frames.push(Frame {
+        gen: Gen::new(|co| pin_future(list_reverser(second, co))),
+    });
+
+    for (idx, mut frame) in frames.into_iter().enumerate() {
+        loop {
+            match frame.gen.resume() {
+                genawaiter::GeneratorState::Yielded(val) => {
+                    println!("yielded {:?} in frame {}", val, idx);
+                    val.force();
+                }
+                genawaiter::GeneratorState::Complete(list) => {
+                    println!("result {}: {:?}", idx, list);
+                    break;
+                }
+            }
+        }
+    }
+}
diff --git a/users/tazjin/gio-list-apps/.gitignore b/users/tazjin/gio-list-apps/.gitignore
new file mode 100644
index 0000000000..2f7896d1d1
--- /dev/null
+++ b/users/tazjin/gio-list-apps/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/users/tazjin/gio-list-apps/Cargo.lock b/users/tazjin/gio-list-apps/Cargo.lock
new file mode 100644
index 0000000000..b475b35a6c
--- /dev/null
+++ b/users/tazjin/gio-list-apps/Cargo.lock
@@ -0,0 +1,616 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "anyhow"
+version = "1.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "bitflags"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
+
+[[package]]
+name = "cfg-expr"
+version = "0.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b40ccee03b5175c18cde8f37e7d2a33bcef6f8ec8f7cc0d81090d1bb380949c9"
+dependencies = [
+ "smallvec",
+ "target-lexicon",
+]
+
+[[package]]
+name = "ctor"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096"
+dependencies = [
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "darling"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "emacs"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6797a940189d353de79bec32abe717aeeecd79a08236e84404c888354e040665"
+dependencies = [
+ "anyhow",
+ "ctor",
+ "emacs-macros",
+ "emacs_module",
+ "once_cell",
+ "rustc_version",
+ "thiserror",
+]
+
+[[package]]
+name = "emacs-macros"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69656fdfe7c2608b87164964db848b5c3795de7302e3130cce7131552c6be161"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "emacs_module"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3067bc974045ed2c6db333bd4fc30d3bdaafa6421a9a889fa7b2826b6f7f2fa"
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[package]]
+name = "futures-util"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+dependencies = [
+ "futures-core",
+ "futures-macro",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gio"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7884cba6b1c5db1607d970cadf44b14a43913d42bc68766eea6a5e2fe0891524"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "gio-sys",
+ "glib",
+ "libc",
+ "once_cell",
+ "pin-project-lite",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "gio-list-apps"
+version = "0.1.0"
+dependencies = [
+ "emacs",
+ "gio",
+]
+
+[[package]]
+name = "gio-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
+dependencies = [
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "system-deps",
+ "winapi",
+]
+
+[[package]]
+name = "glib"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331156127e8166dd815cf8d2db3a5beb492610c716c03ee6db4f2d07092af0a7"
+dependencies = [
+ "bitflags",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-task",
+ "futures-util",
+ "gio-sys",
+ "glib-macros",
+ "glib-sys",
+ "gobject-sys",
+ "libc",
+ "memchr",
+ "once_cell",
+ "smallvec",
+ "thiserror",
+]
+
+[[package]]
+name = "glib-macros"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "179643c50bf28d20d2f6eacd2531a88f2f5d9747dd0b86b8af1e8bb5dd0de3c0"
+dependencies = [
+ "heck",
+ "proc-macro-crate",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "glib-sys"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
+dependencies = [
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "gobject-sys"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
+dependencies = [
+ "glib-sys",
+ "libc",
+ "system-deps",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
+name = "indexmap"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "memchr"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "semver"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
+dependencies = [
+ "semver-parser",
+]
+
+[[package]]
+name = "semver-parser"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
+
+[[package]]
+name = "serde"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
+
+[[package]]
+name = "strsim"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "system-deps"
+version = "6.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3"
+dependencies = [
+ "cfg-expr",
+ "heck",
+ "pkg-config",
+ "toml",
+ "version-compare",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a"
+
+[[package]]
+name = "thiserror"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "toml"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+
+[[package]]
+name = "version-compare"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winnow"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc"
+dependencies = [
+ "memchr",
+]
diff --git a/users/tazjin/gio-list-apps/Cargo.toml b/users/tazjin/gio-list-apps/Cargo.toml
new file mode 100644
index 0000000000..eb62d1fcaf
--- /dev/null
+++ b/users/tazjin/gio-list-apps/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "gio-list-apps"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+emacs = "0.18.0"
+gio = "0.18.1"
diff --git a/users/tazjin/gio-list-apps/default.nix b/users/tazjin/gio-list-apps/default.nix
new file mode 100644
index 0000000000..c63f4dd487
--- /dev/null
+++ b/users/tazjin/gio-list-apps/default.nix
@@ -0,0 +1,14 @@
+{ depot, pkgs, lib, ... }:
+
+pkgs.rustPlatform.buildRustPackage {
+  name = "gio-list-apps";
+  src = lib.cleanSource ./.;
+  cargoLock.lockFile = ./Cargo.lock;
+  nativeBuildInputs = [ pkgs.pkg-config ];
+  buildInputs = [ pkgs.gtk3 depot.users.tazjin.emacs.emacs ];
+
+  postInstall = ''
+    mkdir -p $out/share/emacs/site-lisp
+    ln -s $out/lib/libgio_list_apps.so $out/share/emacs/site-lisp/gio-list-apps.so
+  '';
+}
diff --git a/users/tazjin/gio-list-apps/src/lib.rs b/users/tazjin/gio-list-apps/src/lib.rs
new file mode 100644
index 0000000000..55eb8dc0be
--- /dev/null
+++ b/users/tazjin/gio-list-apps/src/lib.rs
@@ -0,0 +1,31 @@
+use emacs::{defun, Env, IntoLisp, Result, Value};
+use gio::traits::AppInfoExt;
+use gio::AppInfo;
+
+emacs::plugin_is_GPL_compatible!();
+
+#[emacs::module(defun_prefix = "taz", mod_in_name = false)]
+fn init(_: &Env) -> Result<()> {
+    Ok(())
+}
+
+/// Returns an alist of the currently available XDG applications (through their
+/// `.desktop' shortcuts), and the command line parameters needed to start them.
+///
+/// Hidden applications or applications without specified command-line
+/// parameters are not included.
+#[defun]
+fn list_xdg_apps(env: &Env) -> Result<Value> {
+    let mut visible_apps: Vec<Value> = vec![];
+
+    for app in AppInfo::all().into_iter().filter(AppInfo::should_show) {
+        if let Some(cmd) = app
+            .commandline()
+            .and_then(|p| Some(p.to_str()?.to_string()))
+        {
+            visible_apps.push(env.cons(app.name().as_str().into_lisp(env)?, cmd.into_lisp(env)?)?);
+        }
+    }
+
+    env.list(&visible_apps)
+}
diff --git a/users/tazjin/gruber-darker.qss b/users/tazjin/gruber-darker.qss
new file mode 100644
index 0000000000..16f4c2f329
--- /dev/null
+++ b/users/tazjin/gruber-darker.qss
@@ -0,0 +1,508 @@
+/**
+** Gruber Darker theme for Quassel.
+**
+** This theme derives from multiple different things:
+**
+** - Quassel DarkSolarized (https://gist.github.com/Zren/e91ad5197f9d6b6d410f)
+** - Quassel Dracula (https://github.com/dracula/quassel)
+** - gruber-darker for Emacs (https://github.com/rexim/gruber-darker-theme)
+** - Original Gruber theme for BBEdit (https://daringfireball.net/projects/bbcolors/schemes/)
+**
+** This is a work-in-progress as I haven't figured out the point of
+** all of the colours yet, and what I want them to be instead.
+**
+**/
+
+/**
+** Helpful Links:
+**  - QT:
+**      http://qt-project.org/doc/qt-4.8/stylesheet-syntax.html
+**      http://doc.qt.nokia.com/4.7-snapshot/stylesheet-reference.html
+**      http://doc.qt.nokia.com/4.7-snapshot/stylesheet-examples.html
+**  - Plastique Client Style:
+**      https://qt.gitorious.org/qt/qt/source/src/gui/styles/qplastiquestyle.cpp
+**      https://github.com/mirror/qt/blob/4.8/src/gui/styles/qplastiquestyle.cpp
+**  - Quassel Stylesheet Gallery:
+**      http://bugs.quassel-irc.org/projects/1/wiki/Stylesheet_Gallery
+**      http://bugs.quassel-irc.org/projects/1/wiki/Stylesheet_Gallery#DarkMonokaiqss
+*/
+
+/**
+**  - QSS Notes:
+**      Quassel stylesheets also support Palette { role: color; } for setting the system
+**      palette. See the QPalette docs for available roles, and convert them into qss-style
+**      attributes, so ButtonText would become button-text or see qssparser.cpp In fact,
+**      qssparser.cpp is the authorative source for Quassel's qss syntax that contains all
+**      the extensions over standard Qt qss syntax.
+**      See:
+**          http://qt-project.org/doc/qt-4.8/qpalette.html#ColorRole-enum
+**          https://github.com/quassel/quassel/blob/master/src/uisupport/qssparser.cpp
+**
+*/
+
+Palette {
+    /* Window colors */
+    window: #282828;
+    background: #181818;
+    foreground: #f4f4f4;
+
+    base: #181818;
+    alternate-base: #282828;
+
+    /* Just setting palette(tooltip-base) doesn't work as intended so we set it in
+    ** a QTooltip{} rule as well.
+    */
+    tooltip-base: #282a36; // palette(base) TODO
+    tooltip-text: white; // palette(text) TODO
+
+    /* The following attributes should be done in a scale */
+    light: #444444; // Tab Borders, Scrollbar handle grips, Titled Panel border (Settings)
+    midlight: #333333; // ?
+    button: #292929; // Menu BG, Scrollbar and Button base.
+    mid: #252525; // Titled Panel border (Settings)
+    dark: #202020; // TreeView [-] and ... color (Also various borders in Windows Client Style)
+    shadow: #1d1d1d; // ?
+
+
+    /* Text colors */
+    text: white;
+    button-text: #f8f8f2;
+
+    highlight: #44475a;
+
+    /* Link colors */
+    link: #ff79c6;
+    link-visited: #bd93f9;
+
+    /* Color of the marker line in the chat view. BG Node that is overlayed on the first new ChatLine. */
+    // 0 -> 0.1 (sharp line)
+    marker-line: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #586e75, stop: 0.1 #586e75, stop: 0.1 transparent);
+}
+
+/*
+** Base Object Colors
+*/
+
+/* Tables */
+// QTreeView#settingsTree -> Tree in the Settings popup.
+
+QTreeView, QTableView {
+    alternate-background-color: #282a36;
+    // background-color: palette(shadow);
+    border: 0px;
+}
+
+QTreeView {
+  selection-background-color: transparent;
+}
+
+QTreeView::item {
+  border-left: 2px solid palette(base);
+}
+
+QTreeView::item:focus {
+  border-width: 0 0 0 2px;
+  outline: none;
+}
+
+QTreeView::item:selected {
+  border-width: 0 0 0 2px;
+  color: palette(button-text);
+}
+
+QTreeView::item:hover {
+  background: palette(dark);
+}
+
+
+QTreeView::item:selected:active{
+  color: palette(button-text);
+  background: palette(dark);
+  border-color: palette(highlight);
+}
+
+QTreeView::item:selected:!active {
+  color: palette(button-text);
+  background: palette(dark);
+  border-color: palette(highlight);
+}
+
+/* Scrollbar */
+/* From Quassel Wiki: http://sprunge.us/iZGB */
+QScrollBar {
+    //background: transparent;
+    background: palette(base);
+    margin: 0;
+}
+QScrollBar:hover {
+    /* Optional: Subtle accent of scrolling area on hover */
+    background: #161616; /* base +2 */
+}
+QScrollBar:vertical {
+    width: 8px;
+}
+QScrollBar:horizontal {
+    height: 8px;
+}
+
+QScrollBar::handle {
+    padding: 0;
+    margin: 2px;
+    border-radius: 2px;
+    border: 2px solid palette(midlight);
+    background: palette(midlight);
+}
+
+QScrollBar::handle:vertical {
+    min-height: 20px;
+    min-width: 0px;
+}
+
+QScrollBar::handle:horizontal {
+    min-width: 20px;
+    min-height: 0px;
+}
+QScrollBar::handle:hover {
+    border-color: palette(light);
+    background: palette(light);
+}
+QScrollBar::handle:pressed {
+    background: palette(highlight);
+    border-color: palette(highlight);
+}
+
+QScrollBar::add-line , QScrollBar::sub-line {
+    height: 0px;
+    border: 0px;
+}
+QScrollBar::up-arrow, QScrollBar::down-arrow {
+    border: 0px;
+    width: 0px;
+    height: 0px;
+}
+
+QScrollBar::add-page, QScrollBar::sub-page {
+    background: none;
+}
+
+/* Input Box */
+MultiLineEdit {
+    //background: palette(base);
+    //color: palette(foreground);
+}
+
+/* Widgets */
+/* http://doc.qt.nokia.com/4.7-snapshot/qdockwidget.html */
+//QMainWindow,
+QMainWindow QAbstractScrollArea {
+    //border: 0; // Remove borders.
+    border: 1px solid palette(shadow);
+}
+
+QMainWindow {
+    //background: palette(mid); // Main window trim
+}
+
+/* Splitter */
+/* The splits between QDockWidgets and QMainWindow is a different element. */
+QSplitter::handle,
+QMainWindow::separator {
+	background: palette(dark);
+}
+QSplitter::handle:horizontal:hover,
+QMainWindow::separator:vertical:hover {
+    background: qlineargradient(x1: 0, y1: 0, x2: 1, y2: 0, stop: 0 palette(window), stop: 0.5 palette(light), stop: 1 palette(window));
+}
+
+QSplitter::handle:vertical:hover,
+QMainWindow::separator:horizontal:hover {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(window), stop: 0.5 palette(light), stop: 1 palette(window));
+}
+
+/* Menu Bar / Context Menues */
+QMenu {
+    margin: 5px; // A bit of nice padding around menu items.
+}
+
+/* ToolTip */
+/* Note: You cannot create transparent sections in the popup box without a mask set. Thus the black edges outside the rounded borders. */
+QToolTip {
+    border: 2px solid #202020; // palette(dark)
+    border-radius: 2px;
+    background: #282a36; // palette(base)
+    color: white; // palette(text)
+}
+
+/* Tabs */
+/*
+    The palette is designed for the selected one to be darker. So we need to change it. Decided to do a simple line.
+    tab:bottom and tab:top reverse y1 and y2 on the linear gradients.
+
+    Tab Shadow: #444444 (light)
+    Tab Hover: #666
+    Tab Selected: palette(highlight)
+*/
+
+QTabWidget::tab-bar {
+    alignment: center;
+}
+
+QTabBar::tab {
+    min-width: 30px;
+    height: 20px;
+}
+
+QTabBar::tab:bottom:selected {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(highlight), stop: 0.2 palette(highlight), stop: 0.2 transparent);
+}
+
+QTabBar::tab:top:selected {
+    background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 palette(highlight), stop: 0.2 palette(highlight), stop: 0.2 transparent);
+}
+
+QTabBar::tab:!selected {
+    color: #888;
+}
+
+QTabBar::tab:bottom:!selected {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 palette(light), stop: 0.2 palette(light), stop: 0.2 transparent);
+}
+
+QTabBar::tab:top:!selected {
+    background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 palette(light), stop: 0.2 palette(light), stop: 0.2 transparent);
+}
+
+QTabBar::tab:!selected:hover {
+    color: #aaa;
+}
+
+QTabBar::tab:bottom:!selected:hover {
+    background: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1, stop: 0 #666, stop: 0.2 #666, stop: 0.2 transparent);
+}
+
+QTabBar::tab:top:!selected:hover {
+    background: qlineargradient(x1: 0, y1: 1, x2: 0, y2: 0, stop: 0 #666, stop: 0.2 #666, stop: 0.2 transparent);
+}
+
+/*
+** Quassel CSS
+*/
+
+/* Main Chat Background Override */
+ChatView {
+    background: #181818;
+}
+ChatView QScrollBar {
+    background: #282a36;
+}
+ChatView QScrollBar:hover {
+    background: #282a36;
+}
+
+ChatView QScrollBar::handle {
+    border-color: #44475a;
+    background: #44475a;
+}
+
+ChatView QScrollBar::handle:hover {
+    border-color: #44475a;
+    background: #44475a;
+}
+
+/**/
+QStatusBar {}
+QStatusBar::item {
+    border: none;
+}
+QStatusBar QLabel {
+    color: #888;
+}
+
+/* https://github.com/quassel/quassel/blob/master/src/qtui/ui/msgprocessorstatuswidget.ui */
+QStatusBar MsgProcessorStatusWidget {}
+QStatusBar MsgProcessorStatusWidget QLabel#label {}
+QStatusBar MsgProcessorStatusWidget QProgressBar#progressBar {}
+
+/* https://github.com/quassel/quassel/blob/master/src/qtui/ui/coreconnectionstatuswidget.ui */
+QStatusBar CoreConnectionStatusWidget {}
+QStatusBar CoreConnectionStatusWidget QLabel#messageLabel {}
+QStatusBar CoreConnectionStatusWidget QProgressBar#progressBar {}
+QStatusBar CoreConnectionStatusWidget QLabel#lagLabel {}
+QStatusBar CoreConnectionStatusWidget QLabel#sslLabel {
+    qproperty-pixmap: none; /* Hide the SSL status icon */
+}
+
+
+/* Font */
+// Will not override if selectors are doubled up eg: "ChatLine, MultiLineEdit {}"
+// These will override anything set in Quassel's Settings.
+/**
+ * Don't bold or style MultiLineEdit text in any way otherwise you will be
+ * prone to get weird behaviour in submitting from the Input box.
+ * It will randomly bold your input if you do.
+ */
+ChatLine {
+    //font-family: "MingLiU_HKSCS-ExtB", "Courier New", Courier, Monotype;
+
+    //font-size: 13pt;
+    //font-weight: bold;
+    }
+MultiLineEdit {
+    //font-family: "MingLiU_HKSCS-ExtB", "Courier New", Courier, Monotype;
+
+    //font-size: 20px;
+    //font-weight: normal;
+    }
+ChatLine#plain {
+    //font-weight: bold;
+    }
+
+/* Font: UI Global Font */
+QWidget {
+    //font-family: consolas;
+    }
+ChatListItem {
+    font-family: consolas;
+    }
+NickListItem {
+    font-family: consolas;
+    }
+StyledLabel#topicLabel {
+    font-family: consolas;
+    font-size: 14px;
+    }
+
+
+/* Topic Box */
+StyledLabel#topicLabel { background: palette(base);  font-family: consolas; }
+
+/* Buffer / Channel List */
+/**
+    state: inactive, channel-event, unread-message, highlighted
+    type: query, channel, network
+**/
+ChatListItem { foreground: #f8f8f2; }
+ChatListItem[state="inactive"] { foreground: #44475a; }
+ChatListItem[state="channel-event"] { foreground: #6272a4; } /* palette(button-text) */
+ChatListItem[state="unread-message"] { foreground: #f8f8f2; }
+ChatListItem[state="highlighted"] { foreground: #44475a; }
+
+ChatListItem[type="network", state="unread-message"] { foreground: #44475a; }
+ChatListItem[type="network", state="highlighted"] { foreground: #44475a; }
+ChatListItem[type="query", state="unread-message"] { foreground: #44475a; }
+
+
+/* Nick List */
+/**
+    state: away
+    type: user, category
+**/
+NickListItem[type="category"] { foreground: #6272a4; }
+NickListItem[type="user"] { foreground: #f8f8f2 }
+NickListItem[type="user", state="away"] { foreground: #44475a; }
+
+
+
+/* Chatbox Line Formatting */
+ChatLine[label="highlight"] {
+    foreground: #f5f5f5;
+    background: #282828;
+}
+
+/*
+** Option: Bold highlighted text, but not the timestamp.
+*/
+/*
+ChatLine[label="highlight"] { font-weight: bold; }
+ChatLine::timestamp[label="highlight"]{ font-weight: normal; }
+*/
+
+ChatLine::timestamp[label="highlight"] { foreground: #44475a; }
+
+ChatLine::timestamp {  }
+
+/* ::contents == Message */
+ChatLine::contents {
+    /* Can only set background */
+}
+
+ChatLine#plain { foreground: #f8f8f2; }
+ChatLine#notice { foreground: #44475a; }
+ChatLine#action { foreground: #565f73; font-style: italic; font-weight: bold; }
+ChatLine#nick { foreground: #6272a4; }
+ChatLine#mode { foreground: #6272a4; }
+ChatLine#join { foreground: #6272a4; }
+ChatLine#part { foreground: #6272a4; }
+ChatLine#quit { foreground: #6272a4; }
+ChatLine#kick { foreground: #6272a4; }
+ChatLine#kill { foreground: #6272a4; }
+ChatLine#server { foreground: #44475a; }
+ChatLine#info { foreground: #44475a; }
+ChatLine#error { foreground: #ff5555; }
+ChatLine#daychange { foreground: #44475a; }
+ChatLine#topic { foreground: #f1fa8c; }
+ChatLine#netsplit-join { foreground: #44475a; }
+ChatLine#netsplit-quit { foreground: #44475a; }
+
+ChatLine::timestamp {
+    foreground: #586e75;
+    // Resets the timestemp font during #action and other possible formatting.
+    font-style: normal;
+    font-weight: normal;
+}
+
+ChatLine::url {
+    foreground: palette(link);
+    //font-style: underline; // Uncomment if you always want an underline on links.
+}
+
+/* Sender Colors */
+ChatLine::sender#plain[sender="self"] { foreground: #586e75; }
+
+/**
+ * The following are the sixteen colours used for the senders.
+ * The names are calculated by taking the hash of the nickname.
+ * Then take the modulo (the remainder) when divided by 16.
+ * Preview: http://i.imgur.com/xeRKI4H.png
+ */
+ChatLine::sender#plain[sender="0"] { foreground: #96a6c8; }
+ChatLine::sender#plain[sender="1"] { foreground: #73c936; }
+ChatLine::sender#plain[sender="2"] { foreground: #ffdd33; }
+ChatLine::sender#plain[sender="3"] { foreground: #cc8c3c; }
+ChatLine::sender#plain[sender="4"] { foreground: #ff4f58; }
+ChatLine::sender#plain[sender="5"] { foreground: #9e95c7; }
+ChatLine::sender#plain[sender="6"] { foreground: #95a99f; }
+ChatLine::sender#plain[sender="7"] { foreground: #8be9fd; }
+
+/* +32 */
+ChatLine::sender#plain[sender="8"] { foreground: #96a6c8; }
+ChatLine::sender#plain[sender="9"] { foreground: #73c936; }
+ChatLine::sender#plain[sender="a"] { foreground: #ffdd33; }
+ChatLine::sender#plain[sender="b"] { foreground: #cc8c3c; }
+ChatLine::sender#plain[sender="c"] { foreground: #ff4f58; }
+ChatLine::sender#plain[sender="d"] { foreground: #9e95c7; }
+ChatLine::sender#plain[sender="e"] { foreground: #95a99f; }
+ChatLine::sender#plain[sender="f"] { foreground: #8be9fd; }
+
+/*
+** mIRC formats
+*/
+ChatLine[format="bold"] { font-weight: bold;}
+ChatLine[format="italic"] { font-style: italic; }
+ChatLine[format="underline"] { font-style: underline; }
+
+/* Blues are hard to read. */
+ChatLine[fg-color="2"] { foreground: #15a; }
+ChatLine[bg-color="2"] { background: #15a; }
+ChatLine[fg-color="c"] { foreground: #15f; }
+ChatLine[bg-color="c"] { background: #15f; }
+
+/*
+** Experimental
+*/
+BufferViewDock[active=true] {
+    /* The circle is hardcoded into the title. */
+    /* Color only changes on a refresh (F5) (so it's pointless). */
+    /* Also colors the border in Breeze. */
+    //color: palette(highlight);
+}
diff --git a/users/tazjin/hanebuschtag.txt b/users/tazjin/hanebuschtag.txt
new file mode 100644
index 0000000000..daeb41c9aa
--- /dev/null
+++ b/users/tazjin/hanebuschtag.txt
@@ -0,0 +1,66 @@
+bazurschnaburkini
+buchweizengrรผtze
+burkischnurkischnurzelwutz
+burwurgurken
+burwurka
+gaschnurzel
+gezwurkel
+grunzelgewunzel
+gurzelschnurzelgurke
+hanemazurka
+hanemazurkelgurkel
+haneschlawitzka
+haneschnaburkeln
+haneschnawurkagurka
+haneschnawurkel
+haneschnuren
+haneschnurkissima
+hanewurka
+hanewurkini
+hanewurzeln
+ronzelschlawonzel
+ronzelwonzel
+schabernackel
+schabernackensteak
+schlagurkelwini
+schlaraffenwurburzel
+schlawiburschnurschlakini
+schlawonzel
+schlawurkinischnagurka
+schlawurzelgegurkel
+schlawurzeltrollurzel
+schlunzelgarfunzel
+schmonzelgafonzel
+schmotzrotzel
+schnaburka
+schnaburkel
+schnaburkini
+schnackel
+schnarkelbarkel
+schnarwurzelka
+schnawurkeln
+schnawurzelgackschnurschnacksschnicks
+schnawurzini
+schniepel
+schnirkelschini
+schnรถckel
+schnockelgockel
+schnorchel
+schnรถrk
+schnorkelbusch
+schnรถrkelknรถrkel
+schnorkelorgel
+schnรถrks
+schnotzelgekrotzel
+schnudelwurkini
+schnurburka
+schnurkini
+schnurkinihanfini
+schnurzelgawurzel
+schnurzelwurzelwutz
+schnurzelwutz
+strazurkeln
+wazurka
+wurkelgurkel
+wurkelschnurrini
+wurzelchakramahurka
diff --git a/users/tazjin/home/khamovnik.nix b/users/tazjin/home/khamovnik.nix
new file mode 100644
index 0000000000..6bac67eb1c
--- /dev/null
+++ b/users/tazjin/home/khamovnik.nix
@@ -0,0 +1,10 @@
+# Home manage configuration for zamalek.
+
+{ depot, pkgs, ... }: # readTree
+{ config, lib, ... }: # home-manager
+
+{
+  imports = [
+    depot.users.tazjin.home.shared
+  ];
+}
diff --git a/users/tazjin/home/persistence.nix b/users/tazjin/home/persistence.nix
new file mode 100644
index 0000000000..9ea5ca8eb9
--- /dev/null
+++ b/users/tazjin/home/persistence.nix
@@ -0,0 +1,42 @@
+# Persistence configuration for machines with throw-away setups.
+
+{ depot, pkgs, ... }: # readTree
+{ config, lib, ... }: # home-manager
+
+{
+  imports = [ (depot.third_party.sources.impermanence + "/home-manager.nix") ];
+
+  home.persistence."/persist/tazjin/home" = {
+    allowOther = true;
+
+    directories = [
+      ".cargo"
+      ".config/audacity"
+      ".config/chromium"
+      ".config/google-chrome"
+      ".config/quassel-irc.org"
+      ".config/syncthing"
+      ".config/unity3d"
+      ".electrum"
+      ".gnupg"
+      ".local/share/audacity"
+      ".local/share/direnv"
+      ".local/share/fish"
+      ".local/share/keyrings"
+      ".local/share/zoxide"
+      ".mozilla/firefox"
+      ".password-store"
+      ".rustup"
+      ".ssh"
+      ".steam"
+      ".telega"
+      ".thunderbird"
+      "go"
+      "mail"
+    ];
+
+    files = [
+      ".notmuch-config"
+    ];
+  };
+}
diff --git a/users/tazjin/home/shared.nix b/users/tazjin/home/shared.nix
new file mode 100644
index 0000000000..38d8add4ac
--- /dev/null
+++ b/users/tazjin/home/shared.nix
@@ -0,0 +1,91 @@
+# Shared home configuration for all machines.
+
+{ depot, pkgs, ... }: # readTree
+{ config, lib, ... }: # home-manager
+
+
+let
+  # URL handler to open `tg://` URLs in telega.el
+  telega-launcher = pkgs.writeShellScriptBin "telega-launcher" ''
+    echo "Opening ''${1} in telega.el ..."
+    ${depot.users.tazjin.emacs.emacs}/bin/emacsclient -e "(telega-browse-url \"''${1}\")"
+  '';
+in
+{
+  home.activation.screenshots = lib.hm.dag.entryAnywhere ''
+    $DRY_RUN_CMD mkdir -p $HOME/screenshots
+  '';
+
+  programs.git = {
+    enable = true;
+    userName = "Vincent Ambo";
+    userEmail = "mail@tazj.in";
+    extraConfig = {
+      pull.rebase = true;
+      init.defaultBranch = "canon";
+      safe.directory = [ "/depot" ];
+    };
+  };
+
+  programs.fish = {
+    enable = true;
+    interactiveShellInit = ''
+      # emacs vterm integration
+      source (find '${pkgs.emacsPackages.vterm}' -name 'emacs-vterm.fish')
+
+      # z
+      ${pkgs.zoxide}/bin/zoxide init fish | source
+    '';
+  };
+
+  services.screen-locker = {
+    enable = true;
+    inactiveInterval = 10; # minutes
+    lockCmd = "${depot.users.tazjin.screenLock}/bin/tazjin-screen-lock";
+  };
+
+  home.packages = [ telega-launcher ];
+
+  xdg.desktopEntries.telega-launcher = {
+    name = "Telega Launcher";
+    exec = "${telega-launcher}/bin/telega-launcher";
+    terminal = false;
+    mimeType = [ "x-scheme-handler/tg" ];
+  };
+
+  xdg.mimeApps = {
+    enable = true;
+    defaultApplications = {
+      "x-scheme-handler/tg" = [ "telega-launcher.desktop" ];
+      "text/html" = [ "firefox.desktop" ];
+      "x-scheme-handler/http" = [ "firefox.desktop" ];
+      "x-scheme-handler/https" = [ "firefox.desktop" ];
+      "x-scheme-handler/about" = [ "firefox.desktop" ];
+      "x-scheme-handler/unknown" = [ "firefox.desktop" ];
+    };
+  };
+
+  services.picom = {
+    enable = true;
+    vSync = true;
+    backend = "glx";
+  };
+
+  services.syncthing.enable = true;
+
+  # Enable the dunst notification daemon, but force the
+  # configuration file separately instead of going via the strange
+  # Nix->dunstrc encoding route.
+  services.dunst.enable = true;
+  xdg.configFile."dunst/dunstrc" = {
+    source = depot.users.tazjin.dotfiles.dunstrc;
+    onChange = ''
+      ${pkgs.procps}/bin/pkill -u "$USER" ''${VERBOSE+-e} dunst || true
+    '';
+  };
+
+  systemd.user.startServices = true;
+
+  # Previous default version, see https://github.com/nix-community/home-manager/blob/master/docs/release-notes/rl-2211.adoc
+  home.stateVersion = "18.09";
+}
diff --git a/users/tazjin/home/tverskoy.nix b/users/tazjin/home/tverskoy.nix
new file mode 100644
index 0000000000..6f1116340c
--- /dev/null
+++ b/users/tazjin/home/tverskoy.nix
@@ -0,0 +1,18 @@
+# Home manage configuration for tverskoy.
+
+{ depot, pkgs, ... }: # readTree
+{ config, lib, ... }: # home-manager
+
+{
+  imports = [
+    depot.users.tazjin.home.shared
+    depot.users.tazjin.home.persistence
+  ];
+
+  home.persistence."/persist/tazjin/home" = {
+    directories = [
+      ".config/spotify"
+      ".local/share/Steam"
+    ];
+  };
+}
diff --git a/users/tazjin/home/zamalek.nix b/users/tazjin/home/zamalek.nix
new file mode 100644
index 0000000000..d24de945bb
--- /dev/null
+++ b/users/tazjin/home/zamalek.nix
@@ -0,0 +1,11 @@
+# Home manage configuration for zamalek.
+
+{ depot, pkgs, ... }: # readTree
+{ config, lib, ... }: # home-manager
+
+{
+  imports = [
+    depot.users.tazjin.home.shared
+    depot.users.tazjin.home.persistence
+  ];
+}
diff --git a/users/tazjin/homepage/default.nix b/users/tazjin/homepage/default.nix
index 8f53eba67e..b46f9d4917 100644
--- a/users/tazjin/homepage/default.nix
+++ b/users/tazjin/homepage/default.nix
@@ -5,60 +5,79 @@
 # elements for things such as blog posts and projects.
 #
 # Content for the blog is in //users/tazjin/blog instead of here.
-{ depot, lib, ... }@args:
+{ depot, lib, pkgs, ... }@args:
 
 with depot;
 with nix.yants;
 
 let
   inherit (builtins) readFile replaceStrings sort;
-  inherit (third_party) writeFile runCommandNoCC;
+  inherit (pkgs) writeFile runCommand;
 
   # The different types of entries on the homepage.
-  entryClass = enum "entryClass" [ "blog" "project" "misc" ];
+  entryClass = enum "entryClass" [
+    "blog"
+    "project"
+    "note"
+    "misc"
+  ];
 
   # The definition of a single entry.
   entry = struct "entry" {
     class = entryClass;
-    title = string;
-    url = string;
+    title = option string;
+    url = option string;
     date = int; # epoch
     description = option string;
   };
 
   escape = replaceStrings [ "<" ">" "&" "'" ] [ "&lt;" "&gt;" "&amp;" "&#39;" ];
 
-  postToEntry = defun [ users.tazjin.blog.post entry ] (post: {
+  postToEntry = defun [ web.blog.post entry ] (post: {
     class = "blog";
     title = post.title;
     url = "/blog/${post.key}";
     date = post.date;
+    description = post.description or "Blog post from ${formatDate post.date}";
   });
 
-  formatDate = defun [ int string ] (date: readFile (runCommandNoCC "date" {} ''
-    date --date='@${toString date}' '+%Y-%m-%d' > $out
+  formatDate = defun [ int string ] (date: readFile (runCommand "date" { } ''
+    date --date='@${toString date}' '+%Y-%m-%d' | tr -d '\n' > $out
   ''));
 
-  formatEntryDate = defun [ entry string ] (entry: entryClass.match entry.class {
-    blog = "Blog post from ${formatDate entry.date}";
-    project = "Project from ${formatDate entry.date}";
-    misc = "Posted on ${formatDate entry.date}";
-  });
+  entryUrl = defun [ entry string ] (entry:
+    if entry.class == "note"
+    then "#${toString entry.date}"
+    else entry.url
+  );
+
+  hasDescription = defun [ entry bool ] (entry:
+    ((entry ? description) && (entry.description != null))
+  );
+
+  entryTitle = defun [ entry string ] (entry:
+    let
+      optionalColon = lib.optionalString (hasDescription entry) ":";
+      titleText =
+        if (!(entry ? title) && (entry.class == "note"))
+        then "[${formatDate entry.date}]"
+        else lib.optionalString (entry ? title) ((escape entry.title) + optionalColon);
+    in
+    lib.optionalString (titleText != "")
+      ''<span class="entry-title ${entry.class}">${titleText}</span>''
+  );
 
   entryToDiv = defun [ entry string ] (entry: ''
-    <a href="${entry.url}" class="entry ${entry.class}">
-      <div>
-        <p class="entry-title">${escape entry.title}</p>
-        ${
-          lib.optionalString ((entry ? description) && (entry.description != null))
-          "<p class=\"entry-description\">${escape entry.description}</p>"
-        }
-        <p class="entry-date">${formatEntryDate entry}</p>
-      </div>
+    <a href="${entryUrl entry}" id="${toString entry.date}" class="entry">
+      ${entryTitle entry}
+      ${
+        lib.optionalString (hasDescription entry)
+        "<span class=\"entry-description\">${escape entry.description}</span>"
+      }
     </a>
   '');
 
-  index = entries: third_party.writeText "index.html" (lib.concatStrings (
+  index = entries: pkgs.writeText "index.html" (lib.concatStrings (
     [ (builtins.readFile ./header.html) ]
     ++ (map entryToDiv (sort (a: b: a.date > b.date) entries))
     ++ [ (builtins.readFile ./footer.html) ]
@@ -67,9 +86,12 @@ let
   pageEntries = import ./entries.nix;
   homepage = index ((map postToEntry users.tazjin.blog.posts) ++ pageEntries);
   atomFeed = import ./feed.nix (args // { inherit entry pageEntries; });
-in runCommandNoCC "website" {} ''
+in
+runCommand "website" { } ''
   mkdir $out
   cp ${homepage} $out/index.html
   cp ${atomFeed} $out/feed.atom
-  cp -r ${./static} $out/static
+  mkdir $out/static
+  cp -r ${depot.web.static}/* $out/static
+  cp -rf ${./static}/* $out/static
 ''
diff --git a/users/tazjin/homepage/entries.nix b/users/tazjin/homepage/entries.nix
index 1e2b0b03df..0e98c073ef 100644
--- a/users/tazjin/homepage/entries.nix
+++ b/users/tazjin/homepage/entries.nix
@@ -1,12 +1,52 @@
+let
+  note = date: description: {
+    class = "note";
+    inherit description date;
+  };
+in
 [
   {
+    class = "project";
+    title = "VolgaSprint - Nix hacking in Kazan";
+    url = "https://volgasprint.org/";
+    date = 1712307024;
+    description = ''
+      Hacking on Nix projects for a week in Kazan, Russia, in August
+      2024. Come join us!
+    '';
+  }
+  {
+    class = "misc";
+    title = "@tazlog on Telegram";
+    url = "https://t.me/tazlog";
+    date = 1643321164;
+    description = ''
+      My Telegram channel with occasional random life updates and musings.
+    '';
+  }
+  {
+    class = "project";
+    title = "Ship It! #37";
+    url = "https://changelog.com/shipit/37";
+    date = 1641819600;
+    description = ''
+      Podcast episode about TVL, Nix, monorepos and all sorts of related things.
+    '';
+  }
+  {
+    class = "project";
+    title = "Tvix";
+    url = "https://tvl.fyi/blog/rewriting-nix";
+    date = 1638381387;
+    description = "TVL is rewriting Nix with funding from NLNet.";
+  }
+  {
     class = "misc";
     title = "Interview with Joscha Bach";
     url = "https://www.youtube.com/watch?v=P-2P3MSZrBM";
     date = 1594594800;
     description = ''
-      A fascinating, mind-bending interview by Lex Fridman with Joscha
-      Bach about the Nature of the Universe.
+      Mind-bending discussion with philosopher Joscha Bach.
     '';
   }
   {
@@ -14,7 +54,7 @@
     title = "The Virus Lounge";
     url = "https://tvl.fyi";
     date = 1587435629;
-    description = "A daily social video call in these trying pandemic times. Join us!";
+    description = "A community around Nix, monorepos, build tooling and more!";
   }
   {
     class = "project";
@@ -42,7 +82,7 @@
     title = "dottime";
     url = "https://dotti.me/";
     date = 1560898800;
-    description = "A universal convention for conveying time (by edef <3)";
+    description = "A universal convention for conveying time";
   }
   {
     class = "project";
@@ -57,18 +97,63 @@
     url = "https://principiadiscordia.com/book/1.php";
     date = 1495494000;
     description = ''
-      The Principia is a short book I read as a child, and didn't
-      understand until much later. It shaped much of my world view.
+      A short book about everything that everyone should read.
     '';
   }
   {
     class = "misc";
-    title = "This Week in Virology";
-    url = "http://www.microbe.tv/twiv/";
-    date = 1585517557;
-    description = ''
-      Podcast with high-quality information about virology,
-      epidemiology and so on. Highly relevant to COVID19.
-    '';
+    title = "Nix โ€” ะฝะต ั‚ะพะปัŒะบะพ ะฟะฐะบะตั‚ะฝั‹ะน ะผะตะฝะตะดะถะตั€";
+    date = 1663923600;
+    url = "https://www.youtube.com/watch?v=0Lhahzs-Wos";
+    description = "ะ”ะฒัƒั…ั‡ะฐัะพะฒะพะน (!) ั€ะฐะทะณะพะฒะพั€ ั ะฒะฒะตะดะตะฝะธะตะผ ะฒ Nix, NixOS ะธ ั‚ะฐะบ ะดะฐะปะตะต";
+  }
+  {
+    class = "project";
+    title = "yandex-cloud-rs";
+    date = 1650877200;
+    url = "https://docs.rs/yandex-cloud";
+    description = "ะŸั€ะพัั‚ะพะน SDK ะฝะฐ Rust ะดะปั ั€ะฐะฑะพั‚ั‹ ั API Yandex Cloud.";
+  }
+  {
+    class = "project";
+    title = "nix-1p";
+    date = 1564650000;
+    url = "https://code.tvl.fyi/about/nix/nix-1p";
+    description = "A (more or less) one-page introduction to the Nix language.";
+  }
+  {
+    class = "misc";
+    title = "ะกั‚ะฐะฒะธะผ NixOS!";
+    date = 1678784400;
+    url = "https://progmsk.timepad.ru/event/2358560/";
+    description = "ะ’ัั‚ั€ะตั‡ะฐ ะฒ undef.space ะดะปั ะฟะพะผะพั‰ะธ ะฒ ะฝะฐั‡ะฐะปะต ั€ะฐะฑะพั‚ั‹ ั Nix/NixOS";
+  }
+  {
+    class = "misc";
+    title = "Tvix - September '22";
+    date = 1662973200;
+    url = "https://tvl.fyi/blog/tvix-status-september-22";
+    description = "Tvix update blog post over on TVL";
+  }
+  {
+    class = "project";
+    title = "Tvixbolt";
+    date = 1667293200;
+    url = "https://bolt.tvix.dev/";
+    description = "In-browser language evaluator for Nix, based on Tvix";
+  }
+  {
+    class = "project";
+    title = "ะžะžะž ะขะ’ะ›";
+    date = 1609491600;
+    url = "https://tvl.su/ru/";
+    description = "ะžั„ะธั†ะธะฐะปัŒะฝั‹ะน ัะฐะนั‚ ะผะพะตะน ะบะพะผะฟะฐะฝะธะธ ะฟะพ IT-ะบะพะฝัะฐะปั‚ะธะฝะณัƒ.";
   }
+
+  # Notes.
+  (note 1676106000 "If you have a Huawei device that sometimes struggles on public Wi-Fi networks, try enabling MAC-address randomisation. Huawei devices often get pushed onto management networks!")
+  (note 1686868637 "I moved some of my pages (including this one) to a machine in my flat in Moscow. If you end up having access trouble because your ISP blocks Russian resources, please let me know.")
+  (note 1686868636 "Protip: Use the Reddit blackout to click the 'Logout' button, and never come back.")
+  (note 1486550941 "โ†“ I no longer recommend people to use this. Generate your configuration from a language like Nix instead.")
+  (note 1576800001 "โ†“ No longer just my projects, it's all of TVL! Go check it out.")
 ]
diff --git a/users/tazjin/homepage/feed.nix b/users/tazjin/homepage/feed.nix
index 5e1fa15da6..8043d7ff30 100644
--- a/users/tazjin/homepage/feed.nix
+++ b/users/tazjin/homepage/feed.nix
@@ -4,23 +4,10 @@
 with depot.nix.yants;
 
 let
-  inherit (builtins) map readFile sort;
-  inherit (lib) singleton;
+  inherit (builtins) filter map readFile;
+  inherit (lib) max singleton;
   inherit (pkgs) writeText;
-  inherit (depot.users.tazjin) atom-feed blog renderMarkdown;
-
-  postToEntry = defun [ blog.post atom-feed.entry ] (post: rec {
-    id = "https://tazj.in/blog/${post.key}";
-    title = post.title;
-    content = readFile (renderMarkdown post.content);
-    published = post.date;
-    updated = post.date; # TODO(tazjin): this should be distinct from published
-
-    links = singleton {
-      rel = "alternate";
-      href = id;
-    };
-  });
+  inherit (depot.web) blog atom-feed;
 
   pageEntryToEntry = defun [ entry atom-feed.entry ] (e: {
     id = "tazjin:${e.class}:${toString e.date}";
@@ -35,14 +22,13 @@ let
     };
   });
 
-  allEntries = (map postToEntry blog.posts) ++ (map pageEntryToEntry pageEntries);
+  allEntries = (with depot.users.tazjin.blog; map (blog.toFeedEntry config) posts)
+    ++ (map pageEntryToEntry (filter (e: e.class != "note") pageEntries));
 
   feed = {
     id = "https://tazj.in/";
     title = "tazjin's interblag";
     subtitle = "my posts, projects and other interesting things";
-    # TODO(tazjin): Take the most recently updated entry time instead.
-    updated = builtins.currentTime;
     rights = "ยฉ 2020 tazjin";
     authors = [ "tazjin" ];
 
@@ -51,6 +37,7 @@ let
       href = "https://tazjin/feed.atom";
     };
 
-    entries = sort (a: b: a.published > b.published) allEntries;
+    entries = allEntries;
   };
-in writeText "feed.atom" (atom-feed.renderFeed feed)
+in
+writeText "feed.atom" (atom-feed.renderFeed feed)
diff --git a/users/tazjin/homepage/header.html b/users/tazjin/homepage/header.html
index 7cd106a497..320b5ded8c 100644
--- a/users/tazjin/homepage/header.html
+++ b/users/tazjin/homepage/header.html
@@ -2,6 +2,7 @@
 <head><meta charset="utf-8">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <meta name="description" content="tazjin&#39;s blog">
+  <link rel="stylesheet" type="text/css" href="static/tvl.css" media="all">
   <link rel="stylesheet" type="text/css" href="static/tazjin.css" media="all">
   <link rel="icon" type="image/webp" href="/static/favicon.webp">
   <link rel="alternate" type="application/atom+xml" href="/feed.atom">
@@ -15,22 +16,14 @@
     <hr>
   </header>
   <div class="introduction">
-    <p>Hello, illuminated visitor.</p>
     <p>
-      I'm tazjin. Usually you can find
-      me <a class="dark-link" href="https://git.tazj.in/about">programming computers</a>
-      using tools such as <a class="dark-link" href="https://nixos.org/nix">Nix</a>
-      and <a class="dark-link" href="https://www.gnu.org/software/emacs/">Emacs</a>,
-      cuddling <a class="dark-link" href="https://twitter.com/edefic">people I love</a>
-      or posting nonsense <a class="dark-link" href="https://twitter.com/tazjin">on the
-      internet</a>.
-    </p>
-    <p>
-      Below is a collection of
+      Below are some of
       my <span class="project">projects</span>, <span class="blog">blog
-      posts</span> and some <span class="misc">random things</span> by
-      me or others. If you'd like to get in touch about anything, send
-      me a mail at mail@[this domain] or ping me on IRC or Twitter.
+      posts</span>, <span class="note">notes</span> and some
+      other <span class="misc">random things</span>. If you'd like to
+      get in touch, email me at mail@[this domain] or ping me
+      on <a class="dark-link" href="https://tvl.fyi">TVL</a> IRC.
     </p>
+    <hr>
   </div>
   <div class="entry-container">
diff --git a/users/tazjin/homepage/static/jetbrains-mono-bold-italic.woff2 b/users/tazjin/homepage/static/jetbrains-mono-bold-italic.woff2
deleted file mode 100644
index 34b5c69ae1..0000000000
--- a/users/tazjin/homepage/static/jetbrains-mono-bold-italic.woff2
+++ /dev/null
Binary files differdiff --git a/users/tazjin/homepage/static/jetbrains-mono-bold.woff2 b/users/tazjin/homepage/static/jetbrains-mono-bold.woff2
deleted file mode 100644
index 84a008af7e..0000000000
--- a/users/tazjin/homepage/static/jetbrains-mono-bold.woff2
+++ /dev/null
Binary files differdiff --git a/users/tazjin/homepage/static/jetbrains-mono-italic.woff2 b/users/tazjin/homepage/static/jetbrains-mono-italic.woff2
deleted file mode 100644
index 85fd468789..0000000000
--- a/users/tazjin/homepage/static/jetbrains-mono-italic.woff2
+++ /dev/null
Binary files differdiff --git a/users/tazjin/homepage/static/jetbrains-mono.woff2 b/users/tazjin/homepage/static/jetbrains-mono.woff2
deleted file mode 100644
index d5b94cb9e7..0000000000
--- a/users/tazjin/homepage/static/jetbrains-mono.woff2
+++ /dev/null
Binary files differdiff --git a/users/tazjin/homepage/static/tazjin.css b/users/tazjin/homepage/static/tazjin.css
index aea4d426ea..f921b562ee 100644
--- a/users/tazjin/homepage/static/tazjin.css
+++ b/users/tazjin/homepage/static/tazjin.css
@@ -1,48 +1,3 @@
-/* Jetbrains Mono font from https://www.jetbrains.com/lp/mono/
-   licensed under Apache 2.0. Thanks, Jetbrains! */
-@font-face {
-    font-family: jetbrains-mono;
-    src: url(jetbrains-mono.woff2);
-}
-
-@font-face {
-    font-family: jetbrains-mono;
-    font-weight: bold;
-    src: url(jetbrains-mono-bold.woff2);
-}
-
-@font-face {
-    font-family: jetbrains-mono;
-    font-style: italic;
-    src: url(jetbrains-mono-italic.woff2);
-}
-
-@font-face {
-    font-family: jetbrains-mono;
-    font-weight: bold;
-    font-style: italic;
-    src: url(jetbrains-mono-bold-italic.woff2);
-}
-
-/* Generic-purpose styling */
-
-body {
-    max-width: 800px;
-    margin: 40px auto;
-    line-height: 1.6;
-    font-size: 18px;
-    padding: 0 10px;
-    font-family: jetbrains-mono, monospace;
-}
-
-p, a :not(.uncoloured-link) {
-    color: inherit;
-}
-
-h1, h2, h3 {
-    line-height: 1.2
-}
-
 /* Homepage styling */
 
 .dark {
@@ -54,26 +9,31 @@ h1, h2, h3 {
     color: #96a6c8;
 }
 
+
+.interblag-title {
+    text-decoration: none;
+}
+
 .entry-container {
     display: flex;
-    flex-direction: row;
-    flex-wrap: wrap;
+    flex-direction: column;
+    flex-wrap: nowrap;
     justify-content: flex-start;
 }
 
-.interblag-title {
+.entry {
+    margin-top: 5px;
+    margin-bottom: 5px;
+    padding-left: 5px;
     text-decoration: none;
 }
 
-.entry {
-    width: 42%;
-    margin: 5px;
-    padding-left: 7px;
-    padding-right: 5px;
-    border: 2px solid;
-    border-radius: 5px;
-    flex-grow: 1;
-    text-decoration: none;
+.entry:nth-child(odd) {
+    background: #282828;
+}
+
+.entry-description {
+    color: #e4e4ef;
 }
 
 .misc {
@@ -91,93 +51,7 @@ h1, h2, h3 {
     border-color: #ff4f58;
 }
 
-.entry-title {
-    color: inherit !important;
-    font-weight: bold;
-    text-decoration: none;
-}
-
-.entry-date {
-    font-style: italic;
-}
-
-/* Blog styling */
-
-.light {
-    color: #383838;
-}
-
-.blog-title {
-    color: inherit;
-    text-decoration: none;
-}
-
-.footer {
-    text-align: right;
-}
-
-.date {
-    text-align: right;
-    font-style: italic;
-    float: right;
-}
-
-.inline {
-    display: inline;
-}
-
-.lod {
-    text-align: center;
-}
-
-.uncoloured-link {
-    color: inherit;
-}
-
-pre {
-    width: 100%;
-    overflow: auto;
-}
-
-img {
-    max-width: 100%;
-}
-
-.cheddar-callout {
-    display: block;
-    padding: 10px;
-}
-
-.cheddar-question {
-    color: #3367d6;
-    background-color: #e8f0fe;
-}
-
-.cheddar-todo {
-    color: #616161;
-    background-color: #eeeeee;
-}
-
-.cheddar-tip {
-    color: #00796b;
-    background-color: #e0f2f1;
-}
-
-.cheddar-warning {
-    color: #a52714;
-    background-color: #fbe9e7;
-}
-
-kbd {
-    background-color: #eee;
-    border-radius: 3px;
-    border: 1px solid #b4b4b4;
-    box-shadow: 0 1px 1px rgba(0, 0, 0, .2), 0 2px 0 0 rgba(255, 255, 255, .7) inset;
-    color: #333;
-    display: inline-block;
-    font-size: .85em;
-    font-weight: 700;
-    line-height: 1;
-    padding: 2px 4px;
-    white-space: nowrap;
+.note {
+    color: #ffdd33;
+    border-color: #ffdd33;
 }
diff --git a/users/tazjin/keys.nix b/users/tazjin/keys.nix
deleted file mode 100644
index 6e66cb81c5..0000000000
--- a/users/tazjin/keys.nix
+++ /dev/null
@@ -1,10 +0,0 @@
-# My SSH public keys
-{ ... }:
-
-rec {
-  frog = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKMZzRdcrHTuCPoaFy36MPr5IW/hnImlse/OBOn6udL/ tazjin@frog";
-  vauxhall = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBHs+9QfZTD5qGsBQaWqp5whmXJ9qy/m9swE2M9QBaIQVoIYGemq3HXTzrQ6XekwudJCltP4EpM7h/Qc+Or309Yw=";
-  s10e = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDf7CNlYoauHcSYsMNnCZt5h9QSYH/7keYkg8g3hT32+";
-
-  all = [ frog vauxhall s10e ];
-}
diff --git a/users/tazjin/keys/default.nix b/users/tazjin/keys/default.nix
new file mode 100644
index 0000000000..16b232b094
--- /dev/null
+++ b/users/tazjin/keys/default.nix
@@ -0,0 +1,12 @@
+# My SSH public keys
+{ ... }:
+
+let withAll = keys: keys // { all = builtins.attrValues keys; };
+in withAll {
+  tverskoy = "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBAWvA3RpXpMAqruUbB+eVgvvHCzhs5R9khFRza3YSLeFiIqOxVVgyhzW/BnCSD9t/5JrqRdJIGQLnkQU9m4REhUAAAAEc3NoOg== tazjin@tverskoy";
+  tverskoy_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIM1fGWz/gsq+ZeZXjvUrV+pBlanw1c3zJ9kLTax9FWQy tazjin@tverskoy";
+  zamalek_sk = "sk-ssh-ed25519@openssh.com AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIOAw3OaPAjnC6hArGYEmBoXhPf7aZdRGlDZcSqm6gbB8AAAABHNzaDo= tazjin@zamalek";
+  zamalek_ed25519 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDBRXeb8EuecLHP0bW4zuebXp4KRnXgJTZfeVWXQ1n1R tazjin@zamalek";
+  khamovnik_yk = "ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPgOyR4rRM8IaVGgN2ZxGlKtd7GLYbxdRTRa3u9EhRNSkHAvRTN9sgw7mm0iPLnHChPy10anKV43vTaIm906Gm8=";
+  khamovnik_agenix = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG4YSl5+DHQR3rOoBJLQfQ840U0CrYkByMKdzu/LDxoT tazjin@khamovnik";
+}
diff --git a/users/tazjin/kinesis/README.md b/users/tazjin/kinesis/README.md
new file mode 100644
index 0000000000..7cd95a5e5f
--- /dev/null
+++ b/users/tazjin/kinesis/README.md
@@ -0,0 +1,10 @@
+Kinesis configuration
+=====================
+
+This folder backs up the configuration for my Kinesis keyboards.
+Configuration is not mutually compatible between the Advantage 2 and
+the Advantage 360, so they are stored in different folders and
+(mostly) programmed on-board.
+
+I keep these around in case I get a new keyboard and want to bootstrap
+it to behave the same way as the previous one.
diff --git a/users/tazjin/kinesis/advantage2/qwerty.txt b/users/tazjin/kinesis/advantage2/qwerty.txt
new file mode 100755
index 0000000000..624e809c22
--- /dev/null
+++ b/users/tazjin/kinesis/advantage2/qwerty.txt
@@ -0,0 +1,6 @@
+[caps]>[rwin]

+[lctrl]>[lalt]

+[delete]>[lctrl]

+[rctrl]>[rwin]

+{pup}>{-rwin}{b}{+rwin}

+{pdown}>{-rwin}{f}{+rwin}

diff --git a/users/tazjin/nisp/transform.el b/users/tazjin/nisp/transform.el
new file mode 100644
index 0000000000..89b2bb104d
--- /dev/null
+++ b/users/tazjin/nisp/transform.el
@@ -0,0 +1,137 @@
+;; Nix as a Lisp
+
+(require 'cl-lib)
+(require 'json)
+(require 's)
+(require 'dash)
+
+(defun nisp/expr (form)
+  "Entrypoint for Nisp->Nix transformation. Will translate FORM
+into Nix code, if it is a valid Nisp expression.
+
+To make code generation slightly easier, each
+expression (including literals) is wrapped in an extra pair of
+parens."
+  (concat
+   "("
+   (pcase form
+     ;; Special keywords
+     ('() "null")
+     (`(let . ,rest) (nisp/let form))
+     (`(fn . ,rest) (nisp/fn form))
+     (`(if ,cond ,then ,else) (nisp/if cond then else))
+
+     ;; Nix operators & builtins that need special handling
+     (`(or  ,lhs ,rhs) (nisp/infix "||" lhs rhs))
+     (`(and ,lhs ,rhs) (nisp/infix "&&" lhs rhs))
+     (`(> ,lhs ,rhs) (nisp/infix ">" lhs rhs))
+     (`(< ,lhs ,rhs) (nisp/infix "<" lhs rhs))
+     (`(>= ,lhs ,rhs) (nisp/infix ">=" lhs rhs))
+     (`(<= ,lhs ,rhs) (nisp/infix "<=" lhs rhs))
+     (`(+ ,lhs ,rhs) (nisp/infix "+" lhs rhs))
+     (`(- ,lhs ,rhs) (nisp/infix "-" lhs rhs))
+     (`(* ,lhs ,rhs) (nisp/infix "*" lhs rhs))
+     (`(/ ,lhs ,rhs) (nisp/infix "/" lhs rhs))
+     (`(-> ,lhs ,rhs) (nisp/infix "->" lhs rhs))
+     (`(? ,lhs ,rhs) (nisp/infix "?" lhs rhs))
+     (`(// ,lhs ,rhs) (nisp/infix "//" lhs rhs))
+     (`(++ ,lhs ,rhs) (nisp/infix "++" lhs rhs))
+     (`(== ,lhs ,rhs) (nisp/infix "==" lhs rhs))
+     (`(!= ,lhs ,rhs) (nisp/infix "!=" lhs rhs))
+     (`(! ,term) (concat "!" (nisp/expr term)))
+     (`(- ,term) (concat "-" (nisp/expr term)))
+
+     ;; Attribute sets
+     (`(attrs . ,rest) (nisp/attribute-set form))
+
+     ;; Function calls
+     ((and `(,func . ,args)
+           (guard (symbolp func)))
+      (nisp/funcall func args))
+
+     ;; Primitives
+     ((pred stringp) (json-encode-string form))
+     ((pred numberp) (json-encode-number form))
+     ((pred keywordp) (substring (symbol-name form) 1))
+     ((pred symbolp) (symbol-name form))
+
+     ;; Lists
+     ((pred arrayp) (nisp/list form))
+
+     (other (error "Encountered unhandled form: %s" other)))
+   ")"))
+
+(defun nisp/infix (op lhs rhs)
+  (concat (nisp/expr lhs) " " op " " (nisp/expr rhs)))
+
+(defun nisp/funcall (func args)
+  (concat (symbol-name func) " " (s-join " " (-map #'nisp/expr args))))
+
+(defun nisp/let (form)
+  (pcase form
+    (`(let . (,bindings . (,body . ()))) (concat "let "
+                                                 (nisp/let bindings)
+                                                 (nisp/expr body)))
+    (`((:inherit . ,inherits) . ,rest) (concat (nisp/inherit (car form))
+                                               " "
+                                               (nisp/let rest)))
+    (`((,name . (,value . ())) .,rest) (concat (symbol-name name) " = "
+                                               (nisp/expr value) "; "
+                                               (nisp/let rest)))
+    ('() "in ")
+    (other (error "malformed form '%s' in let expression" other))))
+
+(defun nisp/inherit (form)
+  (pcase form
+    (`(:inherit . ,rest) (concat "inherit " (nisp/inherit rest)))
+    (`((,source) . ,rest) (concat "(" (symbol-name source) ") " (nisp/inherit rest)))
+    (`(,item . ,rest) (concat (symbol-name item) " " (nisp/inherit rest)))
+    ('() ";")))
+
+(defun nisp/if (cond then else)
+  (concat "if " (nisp/expr cond)
+          " then " (nisp/expr then)
+          " else " (nisp/expr else)))
+
+(defun nisp/list (form)
+  (cl-check-type form array)
+  (concat "[ "
+          (mapconcat #'nisp/expr form " ")
+          "]"))
+
+
+(defun nisp/attribute-set (form)
+  "Attribute sets have spooky special handling because they are
+not supported by the reader."
+  (pcase form
+    (`(attrs . ,rest) (concat "{ " (nisp/attribute-set rest)))
+    ((and `(,name . (,value . ,rest))
+          (guard (keywordp name)))
+     (concat (substring (symbol-name name) 1) " = "
+             (nisp/expr value) "; "
+             (nisp/attribute-set rest)))
+    ('() "}")))
+
+(defun nisp/fn (form)
+  (pcase form
+    (`(fn ,args ,body) (concat
+                              (cl-loop for arg in args
+                                       concat (format "%s: " arg))
+                              (nisp/expr body)))))
+
+;; The following functions are not part of the transform.
+
+(defun nisp/eval (form)
+  (interactive "sExpression: ")
+  (when (stringp form)
+    (setq form (read form)))
+
+  (message
+   ;; TODO(tazjin): Construct argv manually to avoid quoting issues.
+   (s-chomp
+    (shell-command-to-string
+     (concat "nix-instantiate --eval -E '" (nisp/expr form) "'")))))
+
+(defun nisp/eval-last-sexp ()
+  (interactive)
+  (nisp/eval (edebug-last-sexp)))
diff --git a/users/tazjin/nix.svg b/users/tazjin/nix.svg
new file mode 100644
index 0000000000..d2ef7c81aa
--- /dev/null
+++ b/users/tazjin/nix.svg
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns="http://www.w3.org/2000/svg"
+   width="141.5919mm"
+   height="122.80626mm"
+   viewBox="0 0 501.70361 435.14028"
+   id="svg2"
+   version="1.1">
+  <g transform="translate(-156.33871,933.1905)" visibility="hidden">
+    <path
+       id="lambda-path"
+       d="m 309.54892,-710.38827 122.19683,211.67512 -56.15706,0.5268 -32.6236,-56.8692 -32.85645,56.5653 -27.90237,-0.011 -14.29086,-24.6896 46.81047,-80.4901 -33.22946,-57.8257 z"
+    />
+    <use
+       id="lambda-1"
+       href="#lambda-path"
+       visibility="visible"
+       transform="rotate(180,407.41868,-715.7565)"
+       fill="#f8f8ff" />
+    <use
+       id="lambda-2"
+       visibility="visible"
+       transform="rotate(-120,407.28823,-715.86995)"
+       href="#lambda-path"
+       fill="#0039a6" />
+    <use
+       id="lambda-3"
+       transform="rotate(-60,407.31177,-715.70016)"
+       href="#lambda-path"
+       visibility="visible"
+       fill="#d52b1e" />
+    <use
+       id="lambda-4"
+       href="#lambda-path"
+       visibility="visible"
+       fill="#d52b1e" />
+    <use
+       id="lambda-5"
+       transform="rotate(60,407.11155,-715.78724)"
+       href="#lambda-path"
+       visibility="visible"
+       fill="#0039a6" />
+    <use
+       transform="rotate(120,407.33916,-716.08356)"
+       id="lambda-6"
+       href="#lambda-path"
+       visibility="visible"
+       fill="#f8f8ff" />
+  </g>
+</svg>
diff --git a/users/tazjin/nixos/.gitignore b/users/tazjin/nixos/.gitignore
new file mode 100644
index 0000000000..212d3ad270
--- /dev/null
+++ b/users/tazjin/nixos/.gitignore
@@ -0,0 +1 @@
+local-config.nix
diff --git a/users/tazjin/nixos/README.md b/users/tazjin/nixos/README.md
index 0093f4ac65..662f2a36ac 100644
--- a/users/tazjin/nixos/README.md
+++ b/users/tazjin/nixos/README.md
@@ -1,20 +1,17 @@
 NixOS configuration
 ===================
 
-My NixOS configuration! It configures most of the packages I require
+My NixOS configurations! It configures most of the packages I require
 on my systems, sets up Emacs the way I need and does a bunch of other
 interesting things.
 
-System configuration lives in folders for each machine and a custom
-fixed point evaluation (similar to standard NixOS module
-configuration) is used to combine configuration together.
+System configuration lives in folders, and some of the modules stem
+from `//ops/modules`.
 
-Building `ops.nixos.rebuilder` yields a script that will automatically
-build and activate the newest configuration based on the current
-hostname.
+Machines are deployed with the script at `ops.nixos.rebuild-system`.
 
 ## Configured hosts:
 
-* `frog` - weapon of mass computation at home
-* `camden` - NUC serving tazj.in, tvl.fyi & co
-* ~~`urdhva` - T470s~~ (currently with edef)
+* `tverskoy` - X13 AMD that's travelling around with me
+* `frog` - weapon of mass computation (in storage in London)
+* `camden` - NUC formerly serving tazj.in (in storage in London)
diff --git a/users/tazjin/nixos/camden/default.nix b/users/tazjin/nixos/camden/default.nix
index d8c439b4f4..130b51dd38 100644
--- a/users/tazjin/nixos/camden/default.nix
+++ b/users/tazjin/nixos/camden/default.nix
@@ -1,11 +1,8 @@
 # This file configures camden.tazj.in, my homeserver.
 { depot, pkgs, lib, ... }:
 
-config: let
-  nixpkgs = import depot.third_party.nixpkgsSrc {
-    config.allowUnfree = true;
-  };
-
+config:
+let
   nginxRedirect = { from, to, acmeHost }: {
     serverName = from;
     useACMEHost = acmeHost;
@@ -13,25 +10,13 @@ config: let
 
     extraConfig = "return 301 https://${to}$request_uri;";
   };
-in lib.fix(self: {
-  depot = depot;
-
-  # Disable the current ACME module and use the old one from 19.09
-  # instead, until the various regressions have been sorted out.
-  # TODO(tazjin): Remove this once the new ACME module works.
-  disabledModules = [ "security/acme.nix" ];
-  imports =
-    let oldChannel = fetchTarball {
-      # NixOS 19.09 on 2020-10-04
-      url = "https://github.com/NixOS/nixpkgs-channels/archive/75f4ba05c63be3f147bcc2f7bd4ba1f029cedcb1.tar.gz";
-      sha256 = "157c64220lf825ll4c0cxsdwg7cxqdx4z559fdp7kpz0g6p8fhhr";
-    };
-    in [
-      "${depot.depotPath}/ops/nixos/depot.nix"
-      "${depot.depotPath}/ops/nixos/quassel.nix"
-      "${depot.depotPath}/ops/nixos/smtprelay.nix"
-      "${oldChannel}/nixos/modules/security/acme.nix"
-    ];
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+in
+lib.fix (self: {
+  imports = [
+    (mod "quassel.nix")
+    (mod "smtprelay.nix")
+  ];
 
   # camden is intended to boot unattended, despite having an encrypted
   # root partition.
@@ -44,8 +29,14 @@ in lib.fix(self: {
   boot = {
     initrd = {
       availableKernelModules = [
-        "ahci" "xhci_pci" "usbhid" "usb_storage" "sd_mod" "sdhci_pci"
-        "rtsx_usb_sdmmc" "r8169"
+        "ahci"
+        "xhci_pci"
+        "usbhid"
+        "usb_storage"
+        "sd_mod"
+        "sdhci_pci"
+        "rtsx_usb_sdmmc"
+        "r8169"
       ];
 
       kernelModules = [ "dm-snapshot" ];
@@ -63,7 +54,7 @@ in lib.fix(self: {
       efi.canTouchEfiVariables = true;
     };
 
-    cleanTmpDir = true;
+    tmp.cleanOnBoot = true;
   };
 
   fileSystems = {
@@ -83,25 +74,17 @@ in lib.fix(self: {
     };
   };
 
-  nix = {
-    maxJobs = lib.mkDefault 4;
-
-    nixPath = [
-      "depot=/home/tazjin/depot"
-      "nixpkgs=${depot.third_party.nixpkgsSrc}"
-    ];
-
-    trustedUsers = [ "root" "tazjin" ];
-
-    binaryCaches = [
+  nix.settings = {
+    max-jobs = lib.mkDefault 4;
+    trusted-users = [ "root" "tazjin" ];
+    substituters = [
       "https://tazjin.cachix.org"
     ];
 
-    binaryCachePublicKeys = [
+    trusted-public-keys = [
       "tazjin.cachix.org-1:IZkgLeqfOr1kAZjypItHMg1NoBjm4zX9Zzep8oRSh7U="
     ];
   };
-  nixpkgs.pkgs = nixpkgs;
 
   powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
 
@@ -125,7 +108,7 @@ in lib.fix(self: {
   programs.mosh.enable = true;
 
   fonts = {
-    fonts = [ nixpkgs.jetbrains-mono ];
+    packages = [ pkgs.jetbrains-mono ];
     fontconfig.defaultFonts.monospace = [ "JetBrains Mono" ];
   };
 
@@ -134,15 +117,14 @@ in lib.fix(self: {
     (with depot; [
       fun.idual.script
       fun.idual.setAlarm
-      third_party.pounce
     ]) ++
 
     # programs from nixpkgs
-    (with nixpkgs; [
+    (with pkgs; [
       bat
       curl
       direnv
-      emacs26-nox
+      emacs28-nox
       fswebcam
       git
       gnupg
@@ -162,14 +144,14 @@ in lib.fix(self: {
       isNormalUser = true;
       uid = 1000;
       extraGroups = [ "git" "wheel" "quassel" "video" ];
-      shell = nixpkgs.fish;
+      shell = pkgs.fish;
     };
 
     # Set up a user & group for general git shenanigans
-    groups.git = {};
+    groups.git = { };
     users.git = {
       group = "git";
-      isNormalUser = false;
+      isSystemUser = true;
     };
   };
 
@@ -181,13 +163,13 @@ in lib.fix(self: {
   services.tailscale.enable = true;
 
   # Allow sudo-ing via the forwarded SSH agent.
-  security.pam.enableSSHAgentAuth = true;
+  security.pam.sshAgentAuth.enable = true;
 
   # NixOS 20.03 broke nginx and I can't be bothered to debug it
   # anymore, all solution attempts have failed, so here's a
   # brute-force fix.
   systemd.services.fix-nginx = {
-    script = "${nixpkgs.coreutils}/bin/chown -R nginx: /var/spool/nginx /var/cache/nginx";
+    script = "${pkgs.coreutils}/bin/chown -R nginx: /var/spool/nginx /var/cache/nginx";
 
     serviceConfig = {
       User = "root";
@@ -205,38 +187,36 @@ in lib.fix(self: {
   # Provision a TLS certificate outside of nginx to avoid
   # nixpkgs#38144
   security.acme = {
-    # acceptTerms = true;
+    acceptTerms = true;
 
     certs."tazj.in" = {
       email = "mail@tazj.in";
-      user = "nginx";
       group = "nginx";
       webroot = "/var/lib/acme/acme-challenge";
-      extraDomains = {
-        "cs.tazj.in" = null;
-        "git.tazj.in" = null;
-        "www.tazj.in" = null;
+      postRun = "systemctl reload nginx";
+
+      extraDomainNames = [
+        "cs.tazj.in"
+        "git.tazj.in"
+        "www.tazj.in"
 
         # Local domains (for this machine only)
-        "camden.tazj.in" = null;
-      };
-      postRun = "systemctl reload nginx";
+        "camden.tazj.in"
+      ];
     };
 
     certs."quassel.tazj.in" = {
       email = "mail@tazj.in";
       webroot = "/var/lib/acme/challenge-quassel";
-      user = "nginx"; # required because of a bug in the ACME module
       group = "quassel";
-      allowKeysForGroup = true;
     };
   };
 
   # Forward logs to Google Cloud Platform
   services.journaldriver = {
-    enable                 = true;
-    logStream              = "home";
-    googleCloudProject     = "tazjins-infrastructure";
+    enable = true;
+    logStream = "home";
+    googleCloudProject = "tazjins-infrastructure";
     applicationCredentials = "/etc/gcp/key.json";
   };
 
@@ -249,7 +229,7 @@ in lib.fix(self: {
   };
 
   services.bitlbee = {
-    enable = true;
+    enable = false;
     portNumber = 2337; # bees
   };
 
@@ -257,7 +237,7 @@ in lib.fix(self: {
   services.nginx = {
     enable = true;
     enableReload = true;
-    package = with nixpkgs; nginx.override {
+    package = with pkgs; nginx.override {
       modules = [ nginxModules.rtmp ];
     };
 
diff --git a/users/tazjin/nixos/default.nix b/users/tazjin/nixos/default.nix
index d4576bd3c7..8f82c39ea1 100644
--- a/users/tazjin/nixos/default.nix
+++ b/users/tazjin/nixos/default.nix
@@ -1,46 +1,12 @@
-# TODO(tazjin): Generalise this and move to //ops/nixos
 { depot, lib, ... }:
 
-let
-  inherit (builtins) foldl';
-
-  systemFor = configs: (depot.third_party.nixos {
-    configuration = lib.fix(config:
-      foldl' lib.recursiveUpdate {} (map (c: c config) configs)
-    );
-  }).system;
-
-  caseFor = hostname: ''
-    ${hostname})
-      echo "Rebuilding NixOS for //users/tazjin/nixos/${hostname}"
-      system=$(nix-build -E '(import <depot> {}).users.tazjin.nixos.${hostname}System' --no-out-link)
-      ;;
-  '';
-
-  rebuilder = depot.third_party.writeShellScriptBin "rebuilder" ''
-    set -ue
-    if [[ $EUID -ne 0 ]]; then
-      echo "Oh no! Only root is allowed to rebuild the system!" >&2
-      exit 1
-    fi
-
-    case $HOSTNAME in
-    ${caseFor "camden"}
-    ${caseFor "frog"}
-    *)
-      echo "$HOSTNAME is not a known NixOS host!" >&2
-      exit 1
-      ;;
-    esac
-
-    nix-env -p /nix/var/nix/profiles/system --set $system
-    $system/bin/switch-to-configuration switch
-  '';
-in {
-  inherit rebuilder;
-
-  camdenSystem = systemFor [ depot.users.tazjin.nixos.camden ];
-  frogSystem = systemFor [ depot.users.tazjin.nixos.frog ];
-
-  meta.targets = [ "camdenSystem" "frogSystem" ];
+let systemFor = sys: (depot.ops.nixos.nixosFor sys).system;
+in depot.nix.readTree.drvTargets {
+  camdenSystem = systemFor depot.users.tazjin.nixos.camden;
+  frogSystem = systemFor depot.users.tazjin.nixos.frog;
+  tverskoySystem = systemFor depot.users.tazjin.nixos.tverskoy;
+  zamalekSystem = systemFor depot.users.tazjin.nixos.zamalek;
+  koptevoRaw = depot.ops.nixos.nixosFor depot.users.tazjin.nixos.koptevo;
+  koptevoSystem = systemFor depot.users.tazjin.nixos.koptevo;
+  khamovnikSystem = systemFor depot.users.tazjin.nixos.khamovnik;
 }
diff --git a/users/tazjin/nixos/frog/default.nix b/users/tazjin/nixos/frog/default.nix
index 9765554319..dfb6b46d5a 100644
--- a/users/tazjin/nixos/frog/default.nix
+++ b/users/tazjin/nixos/frog/default.nix
@@ -1,30 +1,18 @@
-{ depot, lib, ... }:
+{ depot, lib, pkgs, ... }:
 
-config: let
-  nixpkgs = import depot.third_party.nixpkgsSrc {
-    config.allowUnfree = true;
-  };
-
-  lieer = depot.third_party.lieer {};
-
-  # add google-c-style here because other machines get it from, eh,
-  # elsewhere.
-  frogEmacs = (depot.users.tazjin.emacs.overrideEmacs(epkgs: epkgs ++ [
-    depot.third_party.emacsPackages.google-c-style
-  ]));
+config:
+let
+  inherit (pkgs) lieer;
 
-  quasselClient = depot.third_party.quassel.override {
+  quasselClient = pkgs.quassel.override {
     client = true;
     enableDaemon = false;
     monolithic = false;
   };
-in depot.lib.fix(self: {
-  imports = [
-    "${depot.depotPath}/ops/nixos/v4l2loopback.nix"
-  ];
-
+in
+lib.fix (self: {
   boot = {
-    tmpOnTmpfs = true;
+    tmp.useTmpfs = true;
     kernelModules = [ "kvm-amd" ];
 
     loader = {
@@ -38,14 +26,16 @@ in depot.lib.fix(self: {
       kernelModules = [ "dm-snapshot" ];
     };
 
-    kernelPackages = nixpkgs.linuxPackages_latest;
+    kernelPackages = pkgs.linuxPackages_latest;
     kernel.sysctl = {
       "kernel.perf_event_paranoid" = -1;
     };
 
-    kernelPatches = [
-      depot.third_party.kernelPatches.trx40_usb_audio
-    ];
+    # Enable this again if frog is put back into use ...
+    #
+    # kernelPatches = [
+    #   depot.third_party.kernelPatches.trx40_usb_audio
+    # ];
   };
 
   hardware = {
@@ -59,7 +49,7 @@ in depot.lib.fix(self: {
 
     pulseaudio = {
       enable = true;
-      package = nixpkgs.pulseaudioFull;
+      package = pkgs.pulseaudioFull;
     };
 
     bluetooth = {
@@ -67,19 +57,11 @@ in depot.lib.fix(self: {
     };
   };
 
-  nix = {
-    maxJobs = 48;
-    nixPath = [
-      "depot=/depot"
-      "nixpkgs=${depot.third_party.nixpkgsSrc}"
-    ];
-
-    binaryCaches = ["ssh://nix-ssh@whitby.tvl.fyi"];
-    binaryCachePublicKeys = ["cache.tvl.fyi:fd+9d1ceCPvDX/xVhcfv8nAa6njEhAGAEe+oGJDEeoc="];
+  nix.settings = {
+    max-jobs = 48;
+    substituters = [ "ssh://nix-ssh@whitby.tvl.fyi" ];
   };
 
-  nixpkgs.pkgs = nixpkgs;
-
   networking = {
     hostName = "frog";
     useDHCP = true;
@@ -96,7 +78,7 @@ in depot.lib.fix(self: {
   # Generate an immutable /etc/resolv.conf from the nameserver settings
   # above (otherwise DHCP overwrites it):
   environment.etc."resolv.conf" = with lib; {
-    source = depot.third_party.writeText "resolv.conf" ''
+    source = pkgs.writeText "resolv.conf" ''
       ${concatStringsSep "\n" (map (ns: "nameserver ${ns}") self.networking.nameservers)}
       options edns0
     '';
@@ -115,7 +97,7 @@ in depot.lib.fix(self: {
     extraGroups = [ "wheel" "audio" "docker" ];
     isNormalUser = true;
     uid = 1000;
-    shell = nixpkgs.fish;
+    shell = pkgs.fish;
   };
 
   security.sudo = {
@@ -124,7 +106,7 @@ in depot.lib.fix(self: {
   };
 
   fonts = {
-    fonts = with nixpkgs; [
+    packages = with pkgs; [
       corefonts
       dejavu_fonts
       jetbrains-mono
@@ -159,7 +141,7 @@ in depot.lib.fix(self: {
   # Required for Yubikey usage as smartcard
   services.pcscd.enable = true;
   services.udev.packages = [
-    nixpkgs.yubikey-personalization
+    pkgs.yubikey-personalization
   ];
 
   # Enable Docker for Nixery testing
@@ -170,13 +152,13 @@ in depot.lib.fix(self: {
 
   services.xserver = {
     enable = true;
-    layout = "us";
-    xkbOptions = "caps:super";
+    xkb.layout = "us";
+    xkb.options = "caps:super";
     exportConfiguration = true;
     videoDrivers = [ "amdgpu" ];
     displayManager = {
       # Give EXWM permission to control the session.
-      sessionCommands = "${nixpkgs.xorg.xhost}/bin/xhost +SI:localuser:$USER";
+      sessionCommands = "${pkgs.xorg.xhost}/bin/xhost +SI:localuser:$USER";
 
       lightdm.enable = true;
       lightdm.greeters.gtk.clock-format = "%Hยท%M"; # TODO(tazjin): TZ?
@@ -184,7 +166,7 @@ in depot.lib.fix(self: {
 
     windowManager.session = lib.singleton {
       name = "exwm";
-      start = "${frogEmacs}/bin/tazjins-emacs";
+      start = "${depot.users.tazjin.emacs}/bin/tazjins-emacs";
     };
   };
 
@@ -219,32 +201,28 @@ in depot.lib.fix(self: {
   environment.systemPackages =
     # programs from the depot
     (with depot; [
-      frogEmacs
       fun.idual.script
       fun.uggc
       lieer
       ops.kontemplate
       quasselClient
-      third_party.ffmpeg
       third_party.git
-      third_party.lutris
-      third_party.rr
       tools.nsfv-setup
+      users.tazjin.emacs
     ]) ++
 
     # programs from nixpkgs
-    (with nixpkgs; [
+    (with pkgs; [
       age
       bat
       chromium
       clang-manpages
       clang-tools
-      clang_10
+      clang
       curl
       direnv
       dnsutils
-      emacs26 # mostly for emacsclient
-      exa
+      emacs28 # mostly for emacsclient
       fd
       file
       gdb
@@ -260,13 +238,12 @@ in depot.lib.fix(self: {
       jq
       kubectl
       linuxPackages.perf
-      manpages
+      man-pages
       miller
       msmtp
       nix-prefetch-github
       notmuch
       obs-studio
-      obs-v4l2sink
       openssh
       openssl
       pass
@@ -279,10 +256,7 @@ in depot.lib.fix(self: {
       ripgrep
       rustup
       screen
-      scrot
-      sourcetrail
       spotify
-      steam
       tokei
       transmission
       tree
@@ -295,6 +269,10 @@ in depot.lib.fix(self: {
       yubico-piv-tool
       yubikey-personalization
       zoxide
+
+      # Commented out because of interim breakage:
+      # steam
+      # lutris
     ]);
 
   # ... and other nonsense.
diff --git a/users/tazjin/nixos/khamovnik/default.nix b/users/tazjin/nixos/khamovnik/default.nix
new file mode 100644
index 0000000000..8ea925c90d
--- /dev/null
+++ b/users/tazjin/nixos/khamovnik/default.nix
@@ -0,0 +1,133 @@
+# Yandex work laptop
+#
+# Some of the configuration for this machine is not public.
+{ depot, lib, pkgs, ... }:
+
+config:
+let
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+  usermod = name: depot.path.origSrc + ("/users/tazjin/nixos/modules/" + name);
+  private = /arc/junk/tazjin;
+
+  zdevice = device: {
+    inherit device;
+    fsType = "zfs";
+  };
+in
+{
+  imports = [
+    (usermod "chromium.nix")
+    (usermod "desktop.nix")
+    (usermod "fonts.nix")
+    (usermod "home-config.nix")
+    (usermod "laptop.nix")
+    (usermod "physical.nix")
+    (pkgs.home-manager.src + "/nixos")
+  ] ++ (if (builtins.pathExists private) then [
+    (private + "/nixos/yandex.nix")
+    (private + "/emacs/module.nix")
+  ] else [ ]);
+
+  # from hardware-configuration.nix
+  boot = {
+    initrd.luks.devices."luks-9c3cd590-a648-450d-ae42-ed3859d4c717".device =
+      "/dev/disk/by-uuid/9c3cd590-a648-450d-ae42-ed3859d4c717";
+
+    initrd.availableKernelModules = [
+      "xhci_pci"
+      "thunderbolt"
+      "ahci"
+      "nvme"
+      "usb_storage"
+      "sd_mod"
+      "rtsx_pci_sdmmc"
+    ];
+    kernelModules = [ "kvm-intel" ];
+  };
+
+  fileSystems = {
+    "/" = {
+      device = "/dev/disk/by-uuid/1f783029-c4f9-4192-b893-84f4f0c2a493";
+      fsType = "ext4";
+    };
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/DD01-2B3E";
+      fsType = "vfat";
+    };
+  };
+
+  swapDevices = [{
+    device = "/dev/disk/by-uuid/9b9049c5-5975-441d-9ac6-2f9150775fd6";
+  }];
+
+  tvl.cache.enable = true;
+
+  networking.hostName = "khamovnik";
+
+  nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux";
+  powerManagement.cpuFreqGovernor = lib.mkDefault "powersave";
+  hardware.cpu.intel.updateMicrocode = true;
+  hardware.enableRedistributableFirmware = true;
+  hardware.opengl.extraPackages = with pkgs; [
+    intel-compute-runtime
+    intel-media-driver
+    intel-vaapi-driver
+  ];
+
+  # from generated configuration.nix
+  # Bootloader.
+  boot.loader.systemd-boot.enable = true;
+  boot.loader.efi.canTouchEfiVariables = true;
+
+  # Setup keyfile
+  boot.initrd.secrets = {
+    "/crypto_keyfile.bin" = null;
+  };
+
+  # Enable swap on luks
+  boot.initrd.luks.devices."luks-e9a4b4dc-ade2-45bf-8ed0-0ed5c4c392c9".device = "/dev/disk/by-uuid/e9a4b4dc-ade2-45bf-8ed0-0ed5c4c392c9";
+  boot.initrd.luks.devices."luks-e9a4b4dc-ade2-45bf-8ed0-0ed5c4c392c9".keyFile = "/crypto_keyfile.bin";
+
+  # Select internationalisation properties.
+  i18n.defaultLocale = "en_US.UTF-8";
+  i18n.extraLocaleSettings = {
+    LC_ADDRESS = "ru_RU.UTF-8";
+    LC_IDENTIFICATION = "ru_RU.UTF-8";
+    LC_MEASUREMENT = "ru_RU.UTF-8";
+    LC_MONETARY = "ru_RU.UTF-8";
+    LC_NAME = "ru_RU.UTF-8";
+    LC_NUMERIC = "ru_RU.UTF-8";
+    LC_PAPER = "ru_RU.UTF-8";
+    LC_TELEPHONE = "ru_RU.UTF-8";
+    LC_TIME = "ru_RU.UTF-8";
+  };
+
+  # Enable sound with pipewire.
+  sound.enable = true;
+  hardware.pulseaudio.enable = false;
+  security.rtkit.enable = true;
+  services.pipewire = {
+    enable = true;
+    alsa.enable = true;
+    alsa.support32Bit = true;
+    pulse.enable = true;
+  };
+
+  # Try to work around Intel CPU throttling bugs
+  services.throttled.enable = true;
+
+  virtualisation.docker.enable = true;
+
+  hardware.bluetooth.enable = true;
+  users.users.tazjin.extraGroups = [ "tss" ];
+
+  environment.systemPackages = with pkgs; [
+    tdesktop
+    linuxPackages.perf
+    hotspot
+    protobuf
+  ];
+
+  system.stateVersion = "23.05"; # Did you read the comment?
+}
diff --git a/users/tazjin/nixos/koptevo/default.nix b/users/tazjin/nixos/koptevo/default.nix
new file mode 100644
index 0000000000..ea8dfd4bd8
--- /dev/null
+++ b/users/tazjin/nixos/koptevo/default.nix
@@ -0,0 +1,187 @@
+# NUC in my closet.
+_: # ignore readTree options
+
+{ config, depot, lib, pkgs, ... }:
+
+let
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+  usermod = name: depot.path.origSrc + ("/users/tazjin/nixos/modules/" + name);
+in
+{
+  imports = [
+    (mod "quassel.nix")
+    (mod "www/base.nix")
+    (mod "www/tazj.in.nix")
+    (usermod "airsonic.nix")
+    (usermod "geesefs.nix")
+    (usermod "predlozhnik.nix")
+    (usermod "tgsa.nix")
+    (usermod "miniflux.nix")
+    (depot.third_party.agenix.src + "/modules/age.nix")
+  ];
+
+  boot = {
+    loader.systemd-boot.enable = true;
+    loader.efi.canTouchEfiVariables = true;
+    initrd.availableKernelModules = [ "ahci" "xhci_pci" "usb_storage" "sd_mod" "sdhci_pci" ];
+    kernelModules = [ "kvm-intel" ];
+    kernelParams = [ "nomodeset" ];
+  };
+
+  nix.settings.trusted-users = [ "tazjin" ];
+
+  fileSystems = {
+    "/" = {
+      device = "rpool/root";
+      fsType = "zfs";
+    };
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/E214-E6B3";
+      fsType = "vfat";
+    };
+
+    "/var" = {
+      device = "rpool/var";
+      fsType = "zfs";
+    };
+
+    "/home" = {
+      device = "rpool/home";
+      fsType = "zfs";
+    };
+  };
+
+  hardware.cpu.intel.updateMicrocode = true;
+  hardware.enableRedistributableFirmware = true;
+  services.fwupd.enable = true;
+
+  networking = {
+    hostName = "koptevo";
+    hostId = "07bbbf4f";
+    domain = "tazj.in";
+    useDHCP = true;
+    firewall.enable = true;
+    firewall.allowedTCPPorts = [ 22 80 443 ];
+
+    wireless.enable = true;
+    wireless.networks."How do I computer fast?" = {
+      psk = "washyourface";
+    };
+  };
+
+  time.timeZone = "UTC";
+
+  security.acme.acceptTerms = true;
+  security.acme.defaults.email = lib.mkForce "acme@tazj.in";
+
+  programs.fish.enable = true;
+
+  users.users.tazjin = {
+    isNormalUser = true;
+    extraGroups = [ "wheel" "docker" "systemd-journal" ];
+    shell = pkgs.fish;
+    openssh.authorizedKeys.keys = depot.users.tazjin.keys.all;
+  };
+
+  age.secrets =
+    let
+      secretFile = name: depot.users.tazjin.secrets."${name}.age";
+    in
+    {
+      tgsa-yandex.file = secretFile "tgsa-yandex";
+    };
+
+  security.sudo.wheelNeedsPassword = false;
+
+  services.openssh.enable = true;
+
+  services.depot.quassel = {
+    enable = true;
+    acmeHost = "koptevo.tazj.in";
+    bindAddresses = [
+      "0.0.0.0"
+    ];
+  };
+
+  services.tailscale = {
+    enable = true;
+    useRoutingFeatures = "server"; # for exit-node usage
+  };
+
+  # Automatically collect garbage from the Nix store.
+  services.depot.automatic-gc = {
+    enable = true;
+    interval = "daily";
+    diskThreshold = 15; # GiB
+    maxFreed = 10; # GiB
+    preserveGenerations = "14d";
+  };
+
+  services.nginx.virtualHosts."koptevo.tazj.in" = {
+    addSSL = true;
+    enableACME = true;
+
+    extraConfig = ''
+      location = / {
+        return 302 https://at.tvl.fyi/?q=%2F%2Fusers%2Ftazjin%2Fnixos%2Fkoptevo%2Fdefault.nix;
+      }
+    '';
+  };
+
+  # I don't use the podcast nor playlist feature,
+  # but I *have to* supply podcasts to gonic ...
+  systemd.tmpfiles.rules = [
+    "d /tmp/fake-podcasts 0555 nobody nobody -"
+    "d /tmp/fake-playlists 0555 nobody nobody -"
+  ];
+
+  services.gonic = {
+    enable = true;
+    settings = {
+      listen-addr = "0.0.0.0:4747";
+      scan-interval = 5;
+      scan-at-start-enabled = true;
+      podcast-path = [ "/tmp/fake-podcasts" ];
+      playlists-path = [ "/tmp/fake-playlists" ];
+      music-path = [ "/var/lib/geesefs/tazjins-files/music" ];
+    };
+  };
+
+  # hack to work around the strict sandboxing of the gonic module
+  # breaking DNS resolution
+  systemd.services.gonic.serviceConfig.BindReadOnlyPaths = [
+    "-/etc/resolv.conf"
+  ];
+
+  # add a hard dependency on the FUSE mount
+  systemd.services.gonic.requires = [ "geesefs.service" ];
+
+  services.nginx.virtualHosts."music.tazj.in" = {
+    addSSL = true;
+    enableACME = true;
+
+    locations."/" = {
+      proxyPass = "http://127.0.0.1:4747";
+    };
+  };
+
+  # List packages installed in system profile. To search, run:
+  # $ nix search wget
+  environment.systemPackages = with pkgs; [
+    curl
+    htop
+    jq
+    nmap
+    bat
+    emacs-nox
+    nano
+    wget
+  ];
+
+  programs.mtr.enable = true;
+  programs.mosh.enable = true;
+  zramSwap.enable = true;
+
+  system.stateVersion = "23.05";
+}
diff --git a/users/tazjin/nixos/modules/airsonic.nix b/users/tazjin/nixos/modules/airsonic.nix
new file mode 100644
index 0000000000..815f183778
--- /dev/null
+++ b/users/tazjin/nixos/modules/airsonic.nix
@@ -0,0 +1,32 @@
+# airsonic is a decent, web-based player UI for subsonic
+{ pkgs, ... }:
+
+let
+  env = builtins.toFile "env.js" ''
+    window.env = {
+      SERVER_URL: "https://music.tazj.in",
+    }
+  '';
+
+  airsonicDist = pkgs.fetchzip {
+    name = "airsonic-refix";
+
+    # from master CI @ f894d5eacebec2f47486f340c8610f446d4f64b3
+    # https://github.com/tamland/airsonic-refix/actions/runs/6150155527
+    url = "https://storage.yandexcloud.net/tazjin-public/airsonic-refix-f894d5ea.zip";
+    sha256 = "02rnh9h7rh22wkghays389yddwbwg7sawmczdxdmjrcnkc7mq2jz";
+
+    stripRoot = false;
+    postFetch = "cp ${env} $out/env.js";
+  };
+in
+{
+  services.nginx.virtualHosts."player.tazj.in" = {
+    enableACME = true;
+    forceSSL = true;
+    root = "${airsonicDist}";
+
+    # deal with SPA routing requirements
+    locations."/".extraConfig = "try_files $uri /index.html;";
+  };
+}
diff --git a/users/tazjin/nixos/modules/chromium.nix b/users/tazjin/nixos/modules/chromium.nix
new file mode 100644
index 0000000000..22f1c8d362
--- /dev/null
+++ b/users/tazjin/nixos/modules/chromium.nix
@@ -0,0 +1,30 @@
+# Configure the Chromium browser with various useful things.
+{ pkgs, ... }:
+
+{
+  environment.systemPackages = [
+    (pkgs.chromium.override {
+      enableWideVine = true; # DRM support (for ะšะธะฝะพะฟะพะธัะบ)
+    })
+  ];
+
+  programs.chromium = {
+    enable = true;
+    homepageLocation = "about:blank";
+
+    extensions = [
+      "dbepggeogbaibhgnhhndojpepiihcmeb" # Vimium
+      "cjpalhdlnbpafiamejdnhcphjbkeiagm" # uBlock Origin
+      "mohaicophfnifehkkkdbcejkflmgfkof" # nitter redirect
+      "lhdifindchogekmjooeiolmjdlheilae" # Huruf
+    ];
+
+    extraOpts = {
+      SpellcheckEnabled = true;
+      SpellcheckLanguage = [
+        "ru"
+        "en-GB"
+      ];
+    };
+  };
+}
diff --git a/users/tazjin/nixos/modules/default.nix b/users/tazjin/nixos/modules/default.nix
new file mode 100644
index 0000000000..d747e8e131
--- /dev/null
+++ b/users/tazjin/nixos/modules/default.nix
@@ -0,0 +1,2 @@
+# Make readTree happy at this level.
+_: { }
diff --git a/users/tazjin/nixos/modules/desktop.nix b/users/tazjin/nixos/modules/desktop.nix
new file mode 100644
index 0000000000..12a42b8faa
--- /dev/null
+++ b/users/tazjin/nixos/modules/desktop.nix
@@ -0,0 +1,55 @@
+# EXWM and other desktop configuration.
+{ config, depot, lib, pkgs, ... }:
+
+{
+  services = {
+    pipewire = {
+      enable = true;
+      alsa.enable = true;
+      alsa.support32Bit = true;
+      pulse.enable = true;
+    };
+
+    redshift.enable = true;
+    blueman.enable = true;
+
+    xserver = {
+      enable = true;
+      xkb.layout = "us";
+      xkb.options = "caps:super";
+
+      libinput.enable = true;
+
+      displayManager = {
+        # Give EXWM permission to control the session.
+        sessionCommands = "${pkgs.xorg.xhost}/bin/xhost +SI:localuser:$USER";
+        lightdm.enable = true;
+        # lightdm.greeters.gtk.clock-format = "%H:%M"; # TODO(tazjin): TZ?
+      };
+
+      windowManager.session = lib.singleton {
+        name = "exwm";
+        start = "${config.tazjin.emacs}/bin/tazjins-emacs --internal-border=0 --border-width=0";
+      };
+    };
+  };
+
+  # Set variables to enable EXWM-XIM and other Emacs features.
+  environment.sessionVariables = {
+    XMODIFIERS = "@im=exwm-xim";
+    GTK_IM_MODULE = "xim";
+    QT_IM_MODULE = "xim";
+    CLUTTER_IM_MODULE = "xim";
+    EDITOR = "emacsclient";
+    _JAVA_AWT_WM_NONREPARENTING = "1";
+  };
+
+  # Do not restart the display manager automatically
+  systemd.services.display-manager.restartIfChanged = lib.mkForce false;
+
+  # If something needs more than 10s to stop it should probably be
+  # killed.
+  systemd.extraConfig = ''
+    DefaultTimeoutStopSec=10s
+  '';
+}
diff --git a/users/tazjin/nixos/modules/fonts.nix b/users/tazjin/nixos/modules/fonts.nix
new file mode 100644
index 0000000000..ee1b84e581
--- /dev/null
+++ b/users/tazjin/nixos/modules/fonts.nix
@@ -0,0 +1,24 @@
+# Attempt at configuring reasonable font-rendering.
+
+{ pkgs, ... }:
+
+{
+  fonts = {
+    packages = with pkgs; [
+      corefonts
+      dejavu_fonts
+      jetbrains-mono
+      noto-fonts-cjk
+      noto-fonts-emoji
+    ];
+
+    fontconfig = {
+      hinting.enable = true;
+      subpixel.lcdfilter = "light";
+
+      defaultFonts = {
+        monospace = [ "JetBrains Mono" ];
+      };
+    };
+  };
+}
diff --git a/users/tazjin/nixos/modules/geesefs.nix b/users/tazjin/nixos/modules/geesefs.nix
new file mode 100644
index 0000000000..c45ee528f6
--- /dev/null
+++ b/users/tazjin/nixos/modules/geesefs.nix
@@ -0,0 +1,38 @@
+{ depot, pkgs, ... }:
+
+{
+  imports = [
+    (depot.third_party.agenix.src + "/modules/age.nix")
+  ];
+
+  age.secrets.geesefs-tazjins-files.file = depot.users.tazjin.secrets."geesefs-tazjins-files.age";
+  programs.fuse.userAllowOther = true;
+
+  systemd.services.geesefs = {
+    description = "geesefs @ tazjins-files";
+    wantedBy = [ "multi-user.target" ];
+    path = [ pkgs.fuse ];
+
+    serviceConfig = {
+      # TODO: can't get fusermount to work for non-root users (e.g. DynamicUser) here, why?
+
+      Restart = "always";
+      LoadCredential = "geesefs-tazjins-files:/run/agenix/geesefs-tazjins-files";
+      StateDirectory = "geesefs";
+      ExecStartPre = "/run/wrappers/bin/umount -a -t fuse.geesefs";
+    };
+
+    script = ''
+      set -u # bail out if systemd is misconfigured ...
+      set -x
+
+      mkdir -p $STATE_DIRECTORY/tazjins-files $STATE_DIRECTORY/cache
+
+      ${depot.third_party.geesefs}/bin/geesefs \
+        -f -o allow_other \
+        --cache $STATE_DIRECTORY/cache \
+        --shared-config $CREDENTIALS_DIRECTORY/geesefs-tazjins-files \
+        tazjins-files $STATE_DIRECTORY/tazjins-files
+    '';
+  };
+}
diff --git a/users/tazjin/nixos/modules/hidpi.nix b/users/tazjin/nixos/modules/hidpi.nix
new file mode 100644
index 0000000000..2ff61d499a
--- /dev/null
+++ b/users/tazjin/nixos/modules/hidpi.nix
@@ -0,0 +1,19 @@
+# Configuration for machines with HiDPI displays, which are a total
+# mess, of course.
+{ ... }:
+
+{
+  # Expose a variable to all programs that might be interested in the
+  # screen settings to do conditional initialisation (mostly for Emacs).
+  environment.variables.HIDPI_SCREEN = "true";
+
+  # TODO(tazjin): this option has been removed and needs to be replaced
+  # by manual configuration: https://github.com/NixOS/nixpkgs/issues/222805
+  # Ensure a larger font size in early boot stage.
+  # hardware.video.hidpi.enable = true;
+
+  # Bump DPI across the board.
+  # TODO(tazjin): This should actually be set per monitor, but I
+  # haven't yet figured out the right interface for doing that.
+  services.xserver.dpi = 161;
+}
diff --git a/users/tazjin/nixos/modules/home-config.nix b/users/tazjin/nixos/modules/home-config.nix
new file mode 100644
index 0000000000..bda8f7a440
--- /dev/null
+++ b/users/tazjin/nixos/modules/home-config.nix
@@ -0,0 +1,19 @@
+# Inject the right home-manager config for the machine.
+
+{ config, depot, pkgs, ... }:
+
+{
+  users.users.tazjin = {
+    isNormalUser = true;
+    createHome = true;
+    extraGroups = [ "wheel" "networkmanager" "video" "adbusers" ];
+    uid = 1000;
+    shell = pkgs.fish;
+    initialHashedPassword = "$2b$05$1eBPdoIgan/C/L8JFqIHBuVscQyTKw1L/4VBlzlLvLBEf6CXS3EW6";
+  };
+
+  nix.settings.trusted-users = [ "tazjin" ];
+
+  home-manager.useGlobalPkgs = true;
+  home-manager.users.tazjin = depot.users.tazjin.home."${config.networking.hostName}";
+}
diff --git a/users/tazjin/nixos/modules/laptop.nix b/users/tazjin/nixos/modules/laptop.nix
new file mode 100644
index 0000000000..e0d67dc259
--- /dev/null
+++ b/users/tazjin/nixos/modules/laptop.nix
@@ -0,0 +1,15 @@
+# Configuration specifically for laptops that move around.
+{ ... }:
+
+{
+  time.timeZone = "Europe/Moscow";
+
+  # Automatically detect location for redshift & so on ...
+  services.geoclue2.enable = true;
+  location.provider = "geoclue2";
+
+  # Enable power-saving features.
+  services.tlp.enable = true;
+
+  programs.light.enable = true;
+}
diff --git a/users/tazjin/nixos/modules/miniflux.nix b/users/tazjin/nixos/modules/miniflux.nix
new file mode 100644
index 0000000000..72089bfb3d
--- /dev/null
+++ b/users/tazjin/nixos/modules/miniflux.nix
@@ -0,0 +1,22 @@
+{ config, depot, lib, pkgs, ... }:
+
+{
+  age.secrets.miniflux.file = depot.users.tazjin.secrets."miniflux.age";
+
+  services.miniflux = {
+    enable = true;
+    adminCredentialsFile = "/run/agenix/miniflux";
+    config.LISTEN_ADDR = "127.0.0.1:6359";
+    config.BASE_URL = "https://feeds.tazj.in";
+  };
+
+  services.nginx.virtualHosts."feeds" = {
+    serverName = "feeds.tazj.in";
+    enableACME = true;
+    forceSSL = true;
+
+    locations."/" = {
+      proxyPass = "http://127.0.0.1:6359";
+    };
+  };
+}
diff --git a/users/tazjin/nixos/modules/persistence.nix b/users/tazjin/nixos/modules/persistence.nix
new file mode 100644
index 0000000000..b864d13a8d
--- /dev/null
+++ b/users/tazjin/nixos/modules/persistence.nix
@@ -0,0 +1,26 @@
+# Configuration for persistent (non-home) data.
+{ config, depot, pkgs, lib, ... }:
+
+{
+  imports = [
+    (depot.third_party.sources.impermanence + "/nixos.nix")
+  ];
+
+  environment.persistence."/persist" = {
+    directories = [
+      "/etc/NetworkManager/system-connections"
+      "/etc/mullvad-vpn"
+      "/var/cache/mullvad-vpn"
+      "/var/lib/bluetooth"
+      "/var/lib/systemd/coredump"
+      "/var/lib/tailscale"
+      "/var/log"
+    ];
+
+    files = lib.optional (builtins.isNull config.networking.hostId) [
+      "/etc/machine-id"
+    ];
+  };
+
+  programs.fuse.userAllowOther = true;
+}
diff --git a/users/tazjin/nixos/modules/physical.nix b/users/tazjin/nixos/modules/physical.nix
new file mode 100644
index 0000000000..bb85c6fb98
--- /dev/null
+++ b/users/tazjin/nixos/modules/physical.nix
@@ -0,0 +1,105 @@
+# Default configuration settings for physical machines that I use.
+{ lib, pkgs, config, depot, ... }:
+
+let
+  pass-otp = pkgs.pass.withExtensions (e: [ e.pass-otp ]);
+in
+{
+  options = with lib; {
+    tazjin.emacs = mkOption {
+      type = types.package;
+      default = depot.users.tazjin.emacs;
+      description = ''
+        Derivation with my Emacs package, with configuration included.
+      '';
+    };
+  };
+
+  config = {
+    # Install all the default software.
+    environment.systemPackages =
+      # programs from the depot
+      (with depot; [
+        users.tazjin.screenLock
+        users.tazjin.chase-geese
+        config.tazjin.emacs
+        third_party.agenix.cli
+      ]) ++
+
+      # programs from nixpkgs
+      (with pkgs; [
+        (aspellWithDicts (d: [ d.ru ]))
+        amber
+        bat
+        curl
+        ddcutil
+        direnv
+        dnsutils
+        electrum
+        firefox
+        config.tazjin.emacs.emacs # emacsclient
+        expect
+        fd
+        file
+        gdb
+        git
+        gnupg
+        gtk3 # for gtk-launch
+        htop
+        hyperfine
+        iftop
+        imagemagick
+        josh
+        jq
+        lieer
+        maim
+        man-pages
+        moreutils
+        mosh
+        msmtp
+        networkmanagerapplet
+        nix-prefetch-github
+        nmap
+        notmuch
+        openssh
+        openssl
+        pass-otp
+        pavucontrol
+        pinentry
+        pinentry-emacs
+        pulseaudio # for pactl
+        pwgen
+        quasselClient
+        rink
+        ripgrep
+        rustup
+        screen
+        tig
+        tokei
+        tree
+        unzip
+        vlc
+        volumeicon
+        whois
+        xclip
+        xsecurelock
+        zoxide
+      ]);
+
+    # Run services & configure programs for all machines.
+    services.fwupd.enable = true;
+
+    # Disable the broken NetworkManager-wait-online.service
+    systemd.services.NetworkManager-wait-online.enable = lib.mkForce false;
+
+    # Disable the thing that prints annoying warnings when trying to
+    # run manually patchelfed binaries
+    environment.stub-ld.enable = false;
+
+    programs = {
+      fish.enable = true;
+      mosh.enable = true;
+      ssh.startAgent = true;
+    };
+  };
+}
diff --git a/users/tazjin/nixos/modules/predlozhnik.nix b/users/tazjin/nixos/modules/predlozhnik.nix
new file mode 100644
index 0000000000..db20963df1
--- /dev/null
+++ b/users/tazjin/nixos/modules/predlozhnik.nix
@@ -0,0 +1,10 @@
+# Host predlozhnik.ru, serving //users/tazjin/predlozhnik
+{ depot, ... }:
+
+{
+  services.nginx.virtualHosts."predlozhnik.ru" = {
+    root = depot.corp.russian.predlozhnik;
+    enableACME = true;
+    forceSSL = true;
+  };
+}
diff --git a/users/tazjin/nixos/modules/tgsa.nix b/users/tazjin/nixos/modules/tgsa.nix
new file mode 100644
index 0000000000..e162e0d822
--- /dev/null
+++ b/users/tazjin/nixos/modules/tgsa.nix
@@ -0,0 +1,29 @@
+{ config, depot, lib, pkgs, ... }:
+
+{
+  systemd.services.tgsa = {
+    description = "telegram -> SA bbcode thing";
+    wantedBy = [ "multi-user.target" ];
+
+    serviceConfig = {
+      DynamicUser = true;
+      Restart = "always";
+      LoadCredential = "tgsa-yandex.json:/run/agenix/tgsa-yandex";
+    };
+
+    script = ''
+      export YANDEX_KEY_FILE="''${CREDENTIALS_DIRECTORY}/tgsa-yandex.json"
+      ${depot.users.tazjin.tgsa}/bin/tgsa
+    '';
+  };
+
+  services.nginx.virtualHosts."tgsa" = {
+    serverName = "tgsa.tazj.in";
+    enableACME = true;
+    forceSSL = true;
+
+    locations."/" = {
+      proxyPass = "http://127.0.0.1:8472";
+    };
+  };
+}
diff --git a/users/tazjin/nixos/tverskoy/default.nix b/users/tazjin/nixos/tverskoy/default.nix
new file mode 100644
index 0000000000..733929219a
--- /dev/null
+++ b/users/tazjin/nixos/tverskoy/default.nix
@@ -0,0 +1,175 @@
+# tverskoy is my Thinkpad X13 AMD 1st gen
+{ depot, lib, pkgs, ... }:
+
+config:
+let
+  quasselClient = pkgs.quassel.override {
+    client = true;
+    enableDaemon = false;
+    monolithic = false;
+  };
+
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+  usermod = name: depot.path.origSrc + ("/users/tazjin/nixos/modules/" + name);
+in
+lib.fix (self: {
+  imports = [
+    (mod "open_eid.nix")
+    (usermod "chromium.nix")
+    (usermod "desktop.nix")
+    (usermod "fonts.nix")
+    (usermod "home-config.nix")
+    (usermod "laptop.nix")
+    (usermod "persistence.nix")
+    (usermod "physical.nix")
+
+    (pkgs.home-manager.src + "/nixos")
+  ] ++ lib.optional (builtins.pathExists ./local-config.nix) ./local-config.nix;
+
+  tvl.cache.enable = true;
+
+  boot = rec {
+    initrd.availableKernelModules = [ "nvme" "ehci_pci" "xhci_pci" "usb_storage" "sd_mod" "rtsx_pci_sdmmc" ];
+    initrd.kernelModules = [ ];
+
+    # Restore /home to the blank snapshot, erasing all ephemeral data.
+    initrd.postDeviceCommands = lib.mkAfter ''
+      zfs rollback -r zpool/ephemeral/home@tazjin-clean
+    '';
+
+    # Install thinkpad modules for TLP
+    extraModulePackages = [ kernelPackages.acpi_call ];
+
+    kernelModules = [ "kvm-amd" "i2c_dev" ];
+    kernelPackages = pkgs.zfsUnstable.latestCompatibleLinuxPackages;
+    loader.systemd-boot.enable = true;
+    loader.efi.canTouchEfiVariables = true;
+  };
+
+  virtualisation.docker.enable = true;
+  users.users.tazjin.extraGroups = [ "docker" "vboxusers" "adbusers" ];
+
+  fileSystems = {
+    "/" = {
+      device = "zpool/ephemeral/root";
+      fsType = "zfs";
+    };
+
+    "/home" = {
+      device = "zpool/ephemeral/home";
+      fsType = "zfs";
+    };
+
+    "/nix" = {
+      device = "zpool/local/nix";
+      fsType = "zfs";
+    };
+
+    "/depot" = {
+      device = "zpool/safe/depot";
+      fsType = "zfs";
+    };
+
+    "/persist" = {
+      device = "zpool/safe/persist";
+      fsType = "zfs";
+      neededForBoot = true;
+    };
+
+    # SD card
+    "/mnt" = {
+      device = "/dev/disk/by-uuid/c602d703-f1b9-4a44-9e45-94dfe24bdaa8";
+      fsType = "ext4";
+    };
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/BF4F-388B";
+      fsType = "vfat";
+    };
+  };
+
+  hardware = {
+    cpu.amd.updateMicrocode = true;
+    enableRedistributableFirmware = true;
+    bluetooth.enable = true;
+
+    opengl = {
+      enable = true;
+      driSupport32Bit = true;
+
+      extraPackages = with pkgs; [
+        vaapiVdpau
+        libvdpau-va-gl
+      ];
+    };
+  };
+
+  networking = {
+    hostName = "tverskoy";
+    hostId = "3c91827f";
+    domain = "tvl.su";
+    useDHCP = false;
+    networkmanager.enable = true;
+    firewall.enable = false;
+
+    nameservers = [
+      "8.8.8.8"
+      "8.8.4.4"
+    ];
+  };
+
+  security.rtkit.enable = true;
+
+  services = {
+    tailscale.enable = true;
+    printing.enable = true;
+
+    # expose i2c device as /dev/i2c-amdgpu-dm and make it user-accessible
+    # this is required for sending control commands to the Dasung screen.
+    udev.extraRules = ''
+      SUBSYSTEM=="i2c-dev", ACTION=="add", DEVPATH=="/devices/pci0000:00/0000:00:08.1/0000:06:00.0/i2c-5/i2c-dev/i2c-5", SYMLINK+="i2c-amdgpu-dm", TAG+="uaccess"
+    '';
+
+    xserver.videoDrivers = [ "amdgpu" ];
+
+    # Automatically collect garbage from the Nix store.
+    depot.automatic-gc = {
+      enable = true;
+      interval = "1 hour";
+      diskThreshold = 16; # GiB
+      maxFreed = 10; # GiB
+      preserveGenerations = "14d";
+    };
+  };
+
+  systemd.user.services.lieer-tazjin = {
+    description = "Synchronise mail@tazj.in via lieer";
+    script = "${pkgs.lieer}/bin/gmi sync";
+
+    serviceConfig = {
+      WorkingDirectory = "%h/mail/account.tazjin";
+      Type = "oneshot";
+    };
+  };
+
+  systemd.user.timers.lieer-tazjin = {
+    wantedBy = [ "timers.target" ];
+
+    timerConfig = {
+      OnActiveSec = "1";
+      OnUnitActiveSec = "180";
+    };
+  };
+
+  # android stuff for hacking on Awful.apk
+  programs.adb.enable = true;
+
+  # systemd-oomd seems to have been enabled by default around ~
+  # December 2022, and it's really into killing my X session as soon
+  # as I do anything stressful to the machine
+  systemd.services.systemd-oomd.enable = lib.mkForce false;
+
+  environment.systemPackages = [ pkgs.vulkan-tools ];
+
+  system.stateVersion = "20.09";
+})
diff --git a/users/tazjin/nixos/zamalek/default.nix b/users/tazjin/nixos/zamalek/default.nix
new file mode 100644
index 0000000000..a340e8a3e8
--- /dev/null
+++ b/users/tazjin/nixos/zamalek/default.nix
@@ -0,0 +1,88 @@
+# zamalek is my Huawei MateBook X (unknown year)
+{ depot, lib, pkgs, ... }:
+
+config:
+let
+  mod = name: depot.path.origSrc + ("/ops/modules/" + name);
+  usermod = name: depot.path.origSrc + ("/users/tazjin/nixos/modules/" + name);
+
+  zdevice = device: {
+    inherit device;
+    fsType = "zfs";
+  };
+in
+{
+  imports = [
+    (usermod "chromium.nix")
+    (usermod "desktop.nix")
+    (usermod "fonts.nix")
+    (usermod "hidpi.nix")
+    (usermod "home-config.nix")
+    (usermod "laptop.nix")
+    (usermod "persistence.nix")
+    (usermod "physical.nix")
+
+    (pkgs.home-manager.src + "/nixos")
+  ] ++ lib.optional (builtins.pathExists ./local-config.nix) ./local-config.nix;
+
+  tvl.cache.enable = true;
+
+  boot = {
+    initrd.availableKernelModules = [ "nvme" "xhci_pci" ];
+    loader.systemd-boot.enable = true;
+    loader.efi.canTouchEfiVariables = true;
+    supportedFilesystems = [ "zfs" ];
+    zfs.devNodes = "/dev/";
+
+    extraModprobeConfig = ''
+      options snd_hda_intel power_save=1
+      options iwlwifi power_save=1
+      options iwldvm force_cam=0
+      options i915 enable_guc=3 enable_fbc=1
+    '';
+  };
+
+  fileSystems = {
+    "/" = zdevice "zpool/ephemeral/root";
+    "/home" = zdevice "zpool/ephemeral/home";
+    "/persist" = zdevice "zpool/persistent/data" // { neededForBoot = true; };
+    "/nix" = zdevice "zpool/persistent/nix";
+    "/depot" = zdevice "zpool/persistent/depot";
+
+    "/boot" = {
+      device = "/dev/disk/by-uuid/2487-3908";
+      fsType = "vfat";
+    };
+  };
+
+  networking = {
+    hostName = "zamalek";
+    domain = "tvl.su";
+    hostId = "ee399356";
+    networkmanager.enable = true;
+
+    extraHosts = ''
+      10.101.240.1 wifi.silja.fi
+    '';
+
+    nameservers = [
+      "8.8.8.8"
+      "8.8.4.4"
+    ];
+  };
+
+  hardware = {
+    cpu.intel.updateMicrocode = true;
+    bluetooth.enable = true;
+    enableRedistributableFirmware = true;
+    opengl.enable = true;
+  };
+
+  services.xserver.libinput.touchpad.clickMethod = "clickfinger";
+  services.xserver.libinput.touchpad.tapping = false;
+  services.avahi.enable = true;
+  services.tailscale.enable = true;
+  powerManagement.powertop.enable = true;
+
+  system.stateVersion = "21.11";
+}
diff --git a/users/tazjin/presentations/bootstrapping-2018/default.nix b/users/tazjin/presentations/bootstrapping-2018/default.nix
index 0dff14b2a1..2775d0b3fb 100644
--- a/users/tazjin/presentations/bootstrapping-2018/default.nix
+++ b/users/tazjin/presentations/bootstrapping-2018/default.nix
@@ -4,24 +4,26 @@
 
 with pkgs;
 
-let tex = texlive.combine {
-  inherit (texlive)
-    beamer
-    beamertheme-metropolis
-    etoolbox
-    euenc
-    extsizes
-    fontspec
-    lualibs
-    luaotfload
-    luatex
-    minted
-    ms
-    pgfopts
-    scheme-basic
-    translator;
-};
-in stdenv.mkDerivation {
+let
+  tex = texlive.combine {
+    inherit (texlive)
+      beamer
+      beamertheme-metropolis
+      etoolbox
+      euenc
+      extsizes
+      fontspec
+      lualibs
+      luaotfload
+      luatex
+      minted
+      ms
+      pgfopts
+      scheme-basic
+      translator;
+  };
+in
+stdenv.mkDerivation {
   name = "nuug-bootstrapping-slides";
   src = ./.;
 
diff --git a/users/tazjin/presentations/tvix-eval-2023/README.md b/users/tazjin/presentations/tvix-eval-2023/README.md
new file mode 100644
index 0000000000..b14ba8ff50
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/README.md
@@ -0,0 +1,12 @@
+These are the slides for a talk at the Moscow Rust User Group /
+ProgMSK on 2023-09-07.
+
+After building, the presentation can be launched with `pdfpc`
+(available in `nixpkgs`), like this:
+
+```
+pdfpc --windowed=both result/presentation.pdf -R presentation.pdfpc -d 40
+```
+
+I keep the JSON file formatted using `jq . presentation.pdfpc | sponge
+presentation.pdfpc` for easier diffs.
diff --git a/users/tazjin/presentations/tvix-eval-2023/cppnix-example-lexer.cpp b/users/tazjin/presentations/tvix-eval-2023/cppnix-example-lexer.cpp
new file mode 100644
index 0000000000..7c52bce8b6
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/cppnix-example-lexer.cpp
@@ -0,0 +1,13 @@
+attrpath
+  : attrpath '.' attr {
+    $$ = $1; $1->push_back(AttrName(data->symbols.create($3)));
+  }
+  | attrpath '.' string_attr
+    { $$ = $1;
+      ExprString * str = dynamic_cast<ExprString *>($3);
+      if (str) {
+          $$->push_back(AttrName(data->symbols.create(str->s)));
+          delete str;
+      } else
+          $$->push_back(AttrName($3));
+    }
diff --git a/users/tazjin/presentations/tvix-eval-2023/cppnix-example-smuggling.cpp b/users/tazjin/presentations/tvix-eval-2023/cppnix-example-smuggling.cpp
new file mode 100644
index 0000000000..37b9219b2e
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/cppnix-example-smuggling.cpp
@@ -0,0 +1,12 @@
+struct Env {
+  // ... some struct fields ...
+  Value* values[0];
+};
+
+// ....
+
+if (env->type == Env::HasWithExpr) {
+  // ...
+  evalAttrs(*env->up, (Expr *) env->values[0], *v, noPos, "<borked>");
+  //                  ^^^^^^^^^^^^^^^^^^^^^^^
+}
diff --git a/users/tazjin/presentations/tvix-eval-2023/default.nix b/users/tazjin/presentations/tvix-eval-2023/default.nix
new file mode 100644
index 0000000000..a4d855197c
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/default.nix
@@ -0,0 +1,63 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (pkgs) fontconfig texlive stdenv imagemagick runCommand qrencode;
+
+  tex = texlive.combine {
+    inherit (texlive)
+      babel
+      babel-russian
+      beamer
+      beamertheme-metropolis
+      etoolbox
+      euenc
+      extsizes
+      fontspec
+      listings
+      xetex
+      minted
+      ms
+      pgfopts
+      scheme-basic
+      translator;
+  };
+
+  linksQrCode = runCommand "qrcode.png" { } ''
+    ${qrencode}/bin/qrencode -o code.png -s 8 \
+      --background=fafafa \
+      --foreground=000000 \
+      'https://tazj.in/blog/tvix-eval-talk-2023'
+
+    # latex has trouble with the PDF produced by qrencode
+    ${imagemagick}/bin/convert code.png $out
+  '';
+in
+stdenv.mkDerivation {
+  name = "progmsk-tvix-eval";
+  src = ./.;
+
+  nativeBuildInputs = [ tex imagemagick fontconfig ];
+
+  FONTCONFIG_FILE = pkgs.makeFontsConf {
+    fontDirectories = with pkgs; [ jetbrains-mono fira fira-code fira-mono ];
+  };
+
+  buildPhase = ''
+    # LaTeX needs a cache folder in /home/ ...
+    mkdir home
+    export HOME=$PWD/home
+
+    cp ${depot.tvix.logo}/logo.png tvix-logo.png
+    cp ${linksQrCode} qrcode.png
+
+    # As usual, TeX needs to be run twice ...
+    ${tex}/bin/xelatex presentation.tex
+    ${tex}/bin/xelatex presentation.tex
+  '';
+
+  installPhase = ''
+    mkdir -p $out
+    cp presentation.pdf $out/
+    cp $src/presentation.pdfpc $out/
+  '';
+}
diff --git a/users/tazjin/presentations/tvix-eval-2023/presentation.pdfpc b/users/tazjin/presentations/tvix-eval-2023/presentation.pdfpc
new file mode 100644
index 0000000000..ab5cba68bf
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/presentation.pdfpc
@@ -0,0 +1,98 @@
+{
+  "pdfpcFormat": 2,
+  "duration": 40,
+  "disableMarkdown": false,
+  "noteFontSize": 20,
+  "pages": [
+    {
+      "idx": 1,
+      "label": "2",
+      "overlay": 0,
+      "note": "ะŸั€ะธะฒะตั‚, ะผะตะฝั ะทะพะฒัƒั‚ .... ะฏ ัƒะถะต ะผะฝะพะณะพ ะปะตั‚, ั 2016ะณ. ะฟั€ะธะผะตั€ะฝะพ, ะฟะธัˆัƒ ะฝะฐ ะ ะฐัั‚ะต, ะธ ั…ะพั‚ั ะฝะฐ ั€ะฐะฑะพั‚ะต ัƒ ะผะตะฝั ั‡ะฐัั‚ะพ ะฑั‹ะฒะฐัŽั‚ ะดั€ัƒะณะธะต ัะทั‹ะบะธ, ะ ะฐัั‚ - ะปัŽะฑะธะผั‹ะน ะผะพะน ัะทั‹ะบ.\n\nะŸะฐั€ัƒ ะปะตั‚ ะฝะฐะทะฐะด, ะฒะพ ะฒั€ะตะผั ะšะพะดะธะดะฐ, ั ัะพะทะดะฐะป ะพะฝะปะฐะนะฝ-ะบะพะผะผัŽะฝะธั‚ะธ ะขะ’ะ›, ะธ ัะตะณะพะดะฝั ั…ะพั‡ัƒ ะฒะฐะผ ะพะฑ ะพะดะฝะพะผ ะธะท ะฝะฐัˆะธั… ะฟั€ะพะตะบั‚ะพะฒ ั€ะฐััะบะฐะทะฐั‚ัŒ."
+    },
+    {
+      "idx": 2,
+      "label": "3",
+      "overlay": 0,
+      "note": "ะผะพะฝะพั€ะตะฟะพ: ะพะฑัŠััะฝะธั‚ัŒ. ะ’ะตััŒ ะบะพะด ะพั€ะณะฐ ะฒ ะพะดะฝะพะผ ะผะตัั‚ะต. ะ•ะดะธะฝะฝั‹ะน ั‚ัƒะปะธะฝะณ. ะœะฝะพะณะพ ะธะท ะฝะฐั ั€ะฐะฝัŒัˆะต ั€ะฐะฑะพั‚ะฐะปะธ ะฒ ะบะพะผะฟะฐะฝะธัั…, ะณะดะต ั‚ะฐะบ ะดะตะปะฐัŽั‚ (ะฝะฟ ะ“ัƒะณะป).\n\nะœั‹ ั…ะพั‚ะตะปะธ ัะพะทะดะฐั‚ัŒ ั‚ะฐะบะพะน ะถะต ั‚ัƒะปะธะฝะณ, ะฝะพ ะพั‚ะบั€ั‹ั‚ะพ. ะะพ ัƒ ะฝะฐั ะผะตะฝัŒัˆะต ั€ะตััƒั€ัะพะฒ ั‡ะตะผ ัƒ ะ“ัƒะณะปะฐ, ะฝะฐะผะฝะพะณะพ ะผะตะฝัŒัˆะต. ะŸั€ะธัˆะปะพััŒ ะฒั‹ะฑั€ะฐั‚ัŒ ัั„ั„. ัะฟะพัะพะฑ.\nะคะพะบัƒัะธั€ัƒะตะผ ะฝะฐ ะะธะบั, ะฟะพั‚ะพะผัƒ ั‡ั‚ะพ (...). ะ•ัั‚ัŒ ะดะพะบะปะฐะด.\n\nะœั‹ ะฝะฐั‡ะฐะปะธ ะตะณะพ ะฝะต ั‚ะพะปัŒะบะพ ะดะปั ะฟะฐะบะตั‚ะพะฒ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ (ะบะพะฝั„ะธะณ ะธ ั‚ะด.), ั…ะพั‚ะตะปะพััŒ ั€ะตัˆะตะฝะธะต, ะบะพั‚ะพั€ะพะต ั€ะฐะฑะพั‚ะฐะตั‚ ะฒะตะทะดะต."
+    },
+    {
+      "idx": 3,
+      "label": "4",
+      "overlay": 0,
+      "note": "ะ‘ัƒะดะตะผ ั„ะพะบัƒัะธั€ะพะฒะฐั‚ัŒ ั‚ะพะปัŒะบะพ ะฝะฐ ัะทั‹ะบ ัะตะนั‡ะฐั. ะžัั‚ะฐะปัŒะฝั‹ะต ั‡ะฐัั‚ะธ ะธะฝั‚ะตั€ะตัะฝั‹ะต, ะฝะพ ะฝะต ัะตะณะพะดะฝั.\nะฏะทั‹ะบ ะปะตะฝะธะฒั‹ะน, ะทะฝะฐั‡ะธั‚ ั‚ะพะปัŒะบะพ ะฒั‹ั‡ะธัะปัะตะผ ะบะพะด, ะบะพะณะดะฐ ะตะณะพ ั€ะตะทัƒะปัŒั‚ะฐั‚ ะฝัƒะถะตะฝ ะณะดะต-ั‚ะพ.\nะ—ะฝะฐั‡ะธั‚, ะฝะฐะผ ะฝัƒะถะตะฝ ั€ะฐะฝั‚ะฐะนะผ-ะฟั€ะตะดัั‚ะฐะฒะปะตะฝะธะต ะพั‚ะปะพะถะตะฝะฝั‹ั… ะฒั‹ั‡ะธัะปะตะฝะธะน.\nะžั€ะณะฐะฝะธั‡ะฝะพ ั€ะฐะทะฒะธะฒะฐะปัั: ะดะพะฑะฐะฒะธะปะธ ั„ะธั‡ะธ, ะบะพะณะดะฐ ะฝัƒะถะดะฐะปะธััŒ. ะœะฝะพะณะพ ั„ัƒะฝะบั‚ั†ะธะธ ั€ะฐะฑะพั‚ะฐัŽั‚ ั‚ะพะปัŒะบะพ ัะปัƒั‡ะฐะนะฝะพ, ะบะพะผะฑะธะฝะฐั†ะธะธ ั„ะธั‡ - ั‡ะฐัั‚ะพ ัั‚ั€ะฐะฝะฝะพ. (ัˆัƒั‚ะบะฐ ะฟั€ะพ ะก++?)\nะะพ ะตัั‚ัŒ ั…ะพั€ะพัˆะธะน ั„ะฐะบั‚ะพั€: ะฒะตััŒ ะฟัƒะฑะปะธั‡ะฝั‹ะน ะบะพะด ะฒ ะฟั€ะธะฝั†ะธะฟะต ะฒ ะพะดะฝะพะผ ั€ะตะฟะพ. ะžะฑัŠััะฝัั‚ัŒ nixpkgs."
+    },
+    {
+      "idx": 4,
+      "label": "5",
+      "overlay": 0,
+      "note": "ะขะตะบัƒัˆะฐั ะธะผะฟะปะตะผะตะฝั‚ะฐั†ะธั ะฝะฐ ะก++. ะ’ะพั‚ ะฟั€ะธะผะตั€. ะšั‚ะพ-ั‚ะพ ะทะดะตััŒ ะฟะพะฝะธะผะฐะตั‚, ั‡ั‚ะพ ะผั‹ ะฒะธะดะตะผ? ะญั‚ะพ ั‡ะฐัั‚ัŒ ะฟะฐั€ัะตั€ะฐ ะฒ ัะบ, ะฝะพ ั‚ัƒั‚ ัะพะทะดะฐัŽั‚ ั€ะฐะฝั‚ะฐะนะผ-ะทะฝะฐั‡ะตะฝะธั ะฒะพ ะฒั€ะตะผั ะฟะฐั€ัะธะฝะณะฐ. ะžั‡ะตะฝัŒ ัะปะพะถะฝะพ ะฟะพะฝะธะผะฐั‚ัŒ, ั‡ะธั‚ะฐั‚ัŒ, ะดะตะฑะฐะถะธั‚ัŒ ะธ ั‚ะฐะบ ะดะฐะปะตะต.\nะงะธั‚ะฐะปะธ ะฟะฐั€ัะตั€, ะธ ะดะฐะถะต ะฝะฐัˆะปะธ ั‚ะฐะผ ะฝะตะธะทะฒะตัั‚ะฝั‹ะต ั„ะธั‡ะธ ัะทั‹ะบะฐ."
+    },
+    {
+      "idx": 5,
+      "label": "6",
+      "overlay": 0,
+      "note": "ะ’ั‚ะพั€ะพะน ะฟั€ะธะผะตั€. ะ•ัั‚ัŒ ัั‚ั€ะฐะบั‚ Env, ะบะพั‚ะพั€ะฐั ะธัะฟะพะปัŒะทัƒะตั‚ัั ะฒะพ ะผะฝะพะณะธั… ะผะตัั‚ะฐั… ะฒ ะบะพะดะต. ะขะฐะผ ะผะฐััะธะฒ ั‚ะธะฟะฐ Value.\nะ’ะพั‚ ะธัะฟะพะปัŒะทะพะฒะฐะฝะธะต ัั‚ะพะณะพ ะผะฐััะธะฒะฐ. ะงั‚ะพ ะผั‹ ะฒะธะดะตะผ? ะšั‚ะพ ะฟะพะฝะธะผะฐะตั‚?\nะ”ะฐ, ั‚ะฐะผ ะฝะฐ ัะฐะผะพะผ ะดะตะปะต ะฟั€ะพะธัั…ะพะดะธั‚ ะบะฐัั‚ ะฝะฐ ะดั€ัƒะณะพะน ั‚ะธะฟ. ะ—ะฝะฐั‡ะธั‚, ะฒ ัั‚ั€ัƒะบั‚ัƒั€ะต ะดะพะฑะฐะฒะปััŽั‚ ะดะฐะฝะฝั‹ะต, ะบะพั‚ะพั€ั‹ะต ะฝะต ะฟะพะดะพะนะดะตั‚. ะžั‡ะตะฝัŒ unsafe!\n\nะ”ะฐ, ั‡ั‚ะพ ะถะต ะดะตะปะฐั‚ัŒ? ะŸั‹ั‚ะฐะปะธััŒ ะฟะพั‡ะธัั‚ะธั‚ัŒ ะบะพะด, ะฝะพ ัะปัƒั‡ะธะปะพััŒ burnout ะพั‡ะตะฝัŒ ะฑั‹ัั‚ั€ะพ. ะœะตะฝัะตัˆัŒ ะพะดะฝัƒ ะผะฐะปะตะฝัŒะบัƒัŽ ัˆั‚ัƒะบัƒ -> segfaults.\nะŸะพั‡ะตะผัƒ ะบะพะด ะฒะพั‚ ั‚ะฐะบะพะน? -> ะพะฑัŠััะฝัั‚ัŒ.\nะŸั€ะธัˆะปะฐััŒ ะพั‡ะตะฒะธะดะฝะฐั ะธะดะตั."
+    },
+    {
+      "idx": 6,
+      "label": "6",
+      "overlay": 1,
+      "note": "ะŸะตั€ะตะฟะธัะฐั‚ัŒ ะฟั€ะพะตะบั‚ั‹ ะฟะพะปะฝะพัั‚ัŒัŽ, ะพะฑั‹ั‡ะฝะพ ะพั‡ะตะฝัŒ ัะปะพะถะฝะพ. ะะพ ะผั‹ ะผะพะถะตะผ ะผะตะฝัั‚ัŒ ะฐั€ั…., ะธ ะพะฑั…ะพะดะธั‚ัŒ ะฟะตั€ะตะฟะธัะธะฒะฐะฝะธะต ะฝะตะบะพั‚ะพั€ั‹ั… ั‡ะฐัั‚ะตะน.\nะ ะฐัั‚ - ะฝะฐะผ ะพั‡ะตะฒะธะดะฝั‹ะน ะฒั‹ะฑะพั€ ะดะปั ะธะผะฟะปะตะผะตะฝั‚ะฐั†ะธั ัะทั‹ะบะฐ. ะœะฝะพะณะพ ะฝะฐั ะทะฝะฐัŽั‚ ะ ะฐัั‚, ะธ ะฒ ั†ะตะปะพะผ, ะฟะพั‡ะตะผัƒ ะธะผะตะฝะฝะพ ะ ะฐัั‚, ะฒั‹ ัƒะถะต ัะฐะผะธ ะฟะพะฝะธะผะฐะตั‚ะต.\n\nะœั‹ ะพั‚ NLNet, ะพั€ะณะฐะฝะธะทะฐั†ะธั, ..., ะฟะพะปัƒั‡ะธะปะธ ะดะตะฝะณะธ ะทะฐ ัั‚ะพะณะพ ะธ ะฝะฐั‡ะฐะปะธ ั ัะทั‹ะบะพะผ. ะญั‚ะพั‚ ะฟั€ะพะตะบั‚ ะฝะฐะทั‹ะฒะฐะตะผ tvix-eval.\n\nะ•ัั‚ัŒ ะตั‰ะต ะพะดะฝะฐ ะฒะฐะถะฝะฐั ะฟั€ะธั‡ะธะฝะฐ ะดะปั ะฒั‹ะฑะพั€ะฐ ะ ะฐัั‚ะฐ."
+    },
+    {
+      "idx": 7,
+      "label": "6",
+      "overlay": 2,
+      "note": "ะŸะฐั€ัƒ ะปะตั‚ ะฝะฐะทะฐะด, ัˆะฒะตะดัะบะธะน ะฟะฐั€ะตะฝัŒ ัŽะทะตั€ะฝะตะนะผะพะผ, ะบะพั‚ะพั€ั‹ะน ะฝะตะปัŒะทั ะฟั€ะพะธัะฝะพัะธั‚ัŒ, ะฝะฐะฟะธัะฐะป ะฝะฐ ะ ะฐัั‚ะต ะพั‡ะตะฝัŒ ะฑั‹ัั‚ั€ั‹ะน ะธ ะฒ ั†ะตะปะพะผ ั…ะพั€ะพัˆะธะน ะฟะฐั€ัะตั€ ะดะปั ะะธะบัะฐ.\nะญั‚ะพั‚ ะฟะฐั€ัะตั€ ัƒะถะต ะธัะฟะพะปัŒะทัƒะตั‚ัั ะฒ ั€ะฐะทะฝั‹ั… ั ะะธะบัะพะผ ัะฒัะทะฐะฝะฝั‹ั… ะฟั€ะพะตะบั‚ะฐั…. ะžะฝ ัะบะพั€ะตะต ะฒัะตะณะพ ะฒ ะฟัƒั‚ะธ ัั‚ะฐั‚ัŒ ะดะตั„ะพะปัŒั‚ะฝะธะผ ะฟะฐั€ัะตั€ะพะผ ะะธะบัะฐ.\nะšะพะฝะตั‡ะฝะพ, ะฝะตะฟะปะพั…ะพ ะตัะปะธ ะผั‹ ะตะณะพ ั‚ะพะถะต ะธัะฟะพะปัŒะทัƒะตะผ.\nะš ัะพะถะฐะปะตะฝะธัŽ, ะฐะฒั‚ะพั€ ั€ะฝะธะบัะฐ ัƒะผะตั€ะป ะฒ 2021 ะณะพะดัƒ. ะœะฐะปะธ ะธัะฒะตัั‚ะฝะพ ะพ ั‚ะพะผ, ั‡ั‚ะพ ัะปัƒั‡ะธะปะพััŒ. ะœั‹ ะตะผัƒ ะพั‡ะตะฝัŒ ะฑะปะฐะณะพะดะฐั€ะฝั‹ะต, ะธ ั ะฟั€ะพัั‚ะพ ั…ะพั‚ะตะป ะตะณะพ ะทะดะตััŒ ัƒะฟะพะผัะฝัƒั‚ัŒ."
+    },
+    {
+      "idx": 9,
+      "label": "8",
+      "overlay": 0,
+      "note": "ะฟะพะบะฐะทะฐั‚ัŒ opcode.rs, compiler/mod (compile_binop)\n\nั‡ั‚ะพะฑั‹ ะพะฝ ะฝะต ั€ะฐะทะถะธั€ะตะป (ะฟั€ะพ variant_size_differences)\n"
+    },
+    {
+      "idx": 10,
+      "label": "9",
+      "overlay": 0,
+      "note": "ะฟะพะบะฐะทะฐั‚ัŒ value/mod.rs, ะฟะพั‚ะพะผ value/list.rs\n\nะบะพั€ะพั‚ะบะฐั ะพะฑัŠััะฝะตะฝะธะต ัะธั‚ัƒะฐั†ะธะธ ั Gc<...> vs. Rc<...>"
+    },
+    {
+      "idx": 11,
+      "label": "10",
+      "overlay": 0,
+      "note": "ะฟะพะบะฐะทะฐั‚ัŒ vm/mod.rs\n\nะฟะพัะปะตะดะพะฒะฐั‚ะตะปัŒะฝะพ ะฒั‹ะฟะพะปัŒะฝัะตั‚ ะธะฝัั‚ั€ัƒะบั‚ั†ะธะธ ะฒ execute_bytecode\n\nัะฝะฐั‡ะฐะปะฐ ะฝะฐ ะฐะปั„ะฐะฒะธั‚ะฝั‹ะผ ะฟะพั€ัะดะบะต, ะฟะพั‚ะพะผ ั ะฟะพะผะพั‰ัŒัŽ ะฟั€ะพั„ะฐะนะปะตั€ะฐ ะผะตะฝัะปะธ ัั‚ะพ"
+    },
+    {
+      "idx": 12,
+      "label": "10",
+      "overlay": 1,
+      "note": "ะฟะพะบะฐะทะฐั‚ัŒ ะดะธะฐะณั€ะฐะผะผัƒ\n\nะณะตะฝะตั€ะฐั‚ะพั€ั‹ะน ะผะพะถะฝะพ ะฟั€ะธะพัั‚ะฐะฝะพะฒะธั‚ัŒ\n\nTCO - ั…ะฒะพัั‚ะพะฒั‹ะน ะฒั‹ะทะพะฒ\n\nasync - ะพั‡ะตะฝัŒ ะฝะฐัะทั‡ะธะฒะฝั‹ะน (intrusive), ะฝะฐะดะพ ะฑั‹ะปะพ ะตะณะพ ะฒะตะทะดะต ะดะพะฑะฐะฒะธั‚ัŒ, ะฝะตัƒะดะพะฑ"
+    },
+    {
+      "idx": 13,
+      "label": "10",
+      "overlay": 2,
+      "note": "ะ—ะฐะฒะธัะธะผะพ ะพั‚ ะฒั€ะตะผะตะฝะธ, ะผะพะถะฝะพ ะปะธะฑะพ ั‚ะพะปัŒะบะพ ะฟั€ะพ tvixbolt, ะปะธะฑะพ ั‚ะพะถะต ะฟั€ะพ ั‚ะตัั‚ั‹ ะธะท cppnix"
+    },
+    {
+      "idx": 14,
+      "label": "11",
+      "overlay": 0,
+      "note": "ะฝะฐ ัะฐะผะพะผ ะดะตะปะต ัƒะดะธะฒะธั‚ะตะปัŒะฝะพ ะปะตะณะบะพ, ะฝะพ ัั‚ะฐะปะบะธะฒะฐะปะธััŒ ั ะฟั€ะพะฑะปะตะผะพะน, ั‡ั‚ะพ ะพะฝ ะธะฝะพะณะดะฐ ะฟะตั€ะตัั‚ะฐะป ั€ะฐะฑะพั‚ะฐั‚ัŒ\n\nะฟะพะบะฐะทะฐั‚ัŒ ะฟั€ะธะผะตั€ ั SystemTime::now\n\nะตัั‚ัŒ ะบะพะต-ะบะฐะบะธะต ะฑะธะฑะปะธะพั‚ะตะบะธ, ะบะพั‚ะพั€ั‹ะต ะผะฑ ะฟะพะผะพะณัƒั‚, ะฝะพ ะผั‹ ะธั… ะฟะพะบะฐ ะฝะต ะฟั€ะพะฒะตั€ะธะปะธ\n\nะฒ ั†ะตะปะพะผ, wasm ะฝะฐ ั€ะฐัั‚ะต ะดะพะฒะพะปัŒะฝะพ ัƒะดะพะฑะฝะพ"
+    },
+    {
+      "idx": 15,
+      "label": "12",
+      "overlay": 0,
+      "note": "ะพั‚ะบั€ั‹ั‚ั‹ะน ะฟั€ะพะตะบั‚, ะฟั€ะธะฝะธะผะฐะตะผ ะบะพะผะผะธั‚ั‹ ะพั‚ ะฒัะตั…\n\nะตัั‚ัŒ ะตั‰ะต ะฑะฐะณะธ, TODOs, ะธ ั‚ะด ะฒ tvix-eval\n\nะฝะพ ะตัั‚ัŒ ั‚ะพะถะต ะพัั‚ะฐะปัŒะฝั‹ะต ั‡ะฐัั‚ะธ ั‚ะฒะธะบัะฐ, ั‡ั‚ะพ-ั‚ะพ ะฝะฐะนะดะตั‚ัั"
+    },
+    {
+      "idx": 16,
+      "label": "13",
+      "overlay": 0,
+      "note": "ัะฟะฐัะธะฑะพ ะฒัะตะผ, ะฒะพั‚ ััั‹ะปะบะธ, ะฝะฐ QR-ะบะพะดะต ะตัั‚ัŒ ะฒัะต ะฒะพั‚ ัั‚ะพั‚ ะฒะพั‚, ะธ ั‚ะฐะผ ั‚ะพะถะต ะฟะพั‚ะพะผ ะดะพะฑะฐะฒะปัŽ ัะฐะผ ะดะพะบะปะฐะด\n\nะตั‰ะต ะทะฐะฒั‚ั€ะฐ ะฝะฐั‡ะธะฝะฐะตั‚ัั NixCon, ะตัะปะธ ะฒะฐะผ ะฒะดั€ัƒะณ ะธะฝั‚ะตั€ะตัะฝะพ, ะผะพะถะฝะพ ะพะฝะปะฐะนะฝ ะฟะพัะผะพั‚ั€ะตั‚ัŒ. ะขะฐะผ ะฑัƒะดะตั‚ ะดะพะบะปะฐะด ะฟั€ะพ tvix ั‚ะพะถะต, ะฝะพ ะพะฑ ะพัั‚ะฐะปัŒะฝั‹ั… ั‡ะฐัั‚ัั…."
+    }
+  ]
+}
diff --git a/users/tazjin/presentations/tvix-eval-2023/presentation.tex b/users/tazjin/presentations/tvix-eval-2023/presentation.tex
new file mode 100644
index 0000000000..294dad7942
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/presentation.tex
@@ -0,0 +1,148 @@
+\documentclass[12pt]{beamer}
+
+\usepackage[utf8]{inputenc}
+\usepackage[main=russian,english]{babel}
+\usepackage{fontspec}
+\usepackage{listings}
+
+\setmainfont{JetBrains Mono}
+\setsansfont{JetBrains Mono}
+
+\usetheme{metropolis}
+\newenvironment{code}{\ttfamily}{\par}
+\title{tvix-eval \\ ะบะพะผะฟะธะปัั‚ะพั€ ะธ ั€ะฐะฝั‚ะฐะนะผ ะดะปั Nix, ะฝะฐ Rust}
+
+\titlegraphic{\vspace{4.8cm}\flushright\includegraphics[width=6cm,keepaspectratio=true]{tvix-logo.png}}
+
+\date{2023-09-07}
+\author{ะ’ะธะฝัะตะฝั‚ ะะผะฑะพ}
+\institute{TVL}
+
+\begin{document}
+  %% Slide -1 (before counter):
+  \begin{frame}
+    \begin{center}
+      \titlepage
+    \end{center}
+  \end{frame}
+
+  %% Slide 0 (title):
+  \begin{frame}
+    \begin{center}
+      \titlepage
+    \end{center}
+  \end{frame}
+
+  %% Slide 1:
+  \begin{frame}{\textbf{ะข}he \textbf{V}irus \textbf{L}ounge}
+    \begin{itemize}
+    \item ะพะฝะปะฐะนะฝ-ะบะพะผัŒัŽะฝะธั‚ะธ, ะทะฐะฝะธะผะฐัŽั‰ะตะตัั ั‚ัƒะปะธะฝะณะพะผ ะดะปั ะผะพะฝะพั€ะตะฟะพ
+    \item ะพัะฝะพะฒะฝะพะน ั„ะพะบัƒั ะฝะฐ Nix
+    \item Nix ะฝะต ั‚ะพะปัŒะบะพ ะดะปั ัะฑะพั€ะบะธ ะฟะฐะบะตั‚ะพะฒ
+    \item ะฅะพั‚ะตะปะพััŒ ั€ะตัˆะตะฝะธะต, ั‡ั‚ะพะฑั‹ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ Nix ะฒะตะทะดะต
+    \end{itemize}
+  \end{frame}
+
+  %% Slide 2:
+  \begin{frame}{ะžัะพะฑะตะฝะฝะพัั‚ะธ ัะทั‹ะบะฐ Nix}
+    \begin{itemize}
+    \item ะ›ะตะฝะธะฒั‹ะน ัะทั‹ะบ. ะ’ั‹ั‡ะธัะปัั‚ัŒ ะฒัะต ัั€ะฐะทัƒ ะฝะตะปัŒะทั.
+    \item ะฏะทั‹ะบ ั€ะฐะทะฒะธะฒะฐะปัั ะพั€ะณะฐะฝะธั‡ะฝะพ.
+    \item ะ‘ะพะปัŒัˆะธะฝัั‚ะฒะพ ะบะพะดะฐ ะฝะฐ Nix --- ะฒ ะพะดะฝะพะผ ะผะตัั‚ะต: \begin{code}nixpkgs\end{code}
+    \end{itemize}
+  \end{frame}
+
+  %% Slide 3:
+  \begin{frame}{ะขะตะบัƒั‰ะฐั ะธะผะฟะปะตะผะตะฝั‚ะฐั†ะธั: C++ Nix}
+    \lstinputlisting[
+      language=c++,
+      basicstyle={\scriptsize}
+    ]{cppnix-example-lexer.cpp}
+  \end{frame}
+
+  %% Slide 4:
+  \begin{frame}{ะขะตะบัƒั‰ะฐั ะธะผะฟะปะตะผะตะฝั‚ะฐั†ะธั: C++ Nix}
+    \lstinputlisting[
+      language=c++,
+      basicstyle={\scriptsize}
+    ]{cppnix-example-smuggling.cpp}
+  \end{frame}
+
+  %% Slide 5:
+  \section{``Let's rewrite it in Rust!''}
+
+  %% Slide 6:
+  \section*{ะกะฟะฐัะธะฑะพ, jD91mZM2!\\\normalsize{ะฐะฒั‚ะพั€ ``rnix-parser''; *2002 - \textdagger 2021}}
+
+  %% Slide 7:
+  \begin{frame}{tvix-eval, - (ัะทั‹ะบ) Nix, ะฝะฐ Rust}
+    \begin{itemize}
+    \item ะฝะฐะฟะธัะฐะฝะพ ั ััƒั‰ะตัั‚ะฒัƒัŽั‰ะธะผ ะฟะฐั€ัะตั€ะพะผ
+    \item bytecode-ะธะฝั‚ะตั€ะฟั€ะตั‚ะฐั‚ะพั€, ะฒะผะตัั‚ะพ tree-walk
+    \item ะดะพะปะถะฝะฐ ั€ะฐะฑะพั‚ะฐั‚ัŒ ะฝะต ั‚ะพะปัŒะบะพ ะดะปั ะพัั‚ะฐะปัŒะฝั‹ั… ั‡ะฐัั‚ะตะน tvix
+    \end{itemize}
+  \end{frame}
+
+  %% Slide 8:
+  \begin{frame}{tvix-eval, ะพัะฝะพะฒะฝั‹ะต ั‡ะฐัั‚ะธ}
+    \begin{enumerate}
+    \item ัะพะฑัั‚ะฒะตะฝะฝั‹ะน ะฑะฐะนั‚ะบะพะด ะธ ะบะพะผะฟะธะปัั‚ะพั€
+    \end{enumerate}
+  \end{frame}
+
+  %% ะฟะพะบะฐะทะฐั‚ัŒ opcode.rs, ะฑั‹ัั‚ั€ะพ ะฟะพะบะฐะทะฐั‚ัŒ compiler/mod.rs
+
+  %% Slide 9:
+  \begin{frame}{tvix-eval, ะพัะฝะพะฒะฝั‹ะต ั‡ะฐัั‚ะธ}
+    \begin{enumerate}
+    \item ัะพะฑัั‚ะฒะตะฝะฝั‹ะน ะฑะฐะนั‚ะบะพะด ะธ ะบะพะผะฟะธะปัั‚ะพั€
+    \item ะฟั€ะตะดัั‚ะฐะฒะปะตะฝะธะต ะทะฝะฐั‡ะตะฝะธะน ัะทั‹ะบะฐ ะฒ ั€ะฐะฝั‚ะฐะนะผะต
+    \end{enumerate}
+  \end{frame}
+
+  %% ะฟะพะบะฐะทะฐั‚ัŒ Value
+
+  %% Slide 10:
+  \begin{frame}{tvix-eval, ะพัะฝะพะฒะฝั‹ะต ั‡ะฐัั‚ะธ}
+    \begin{enumerate}
+    \item ัะพะฑัั‚ะฒะตะฝะฝั‹ะน ะฑะฐะนั‚ะบะพะด ะธ ะบะพะผะฟะธะปัั‚ะพั€
+    \item ะฟั€ะตะดัั‚ะฐะฒะปะตะฝะธะต ะทะฝะฐั‡ะตะฝะธะธ ัะทั‹ะบะฐ ะฒ ั€ะฐะฝั‚ะฐะนะผะต
+    \item ... ะธ ัะฐะผ ั€ะฐะฝั‚ะฐะนะผ!
+    \end{enumerate}
+  \end{frame}
+
+  %% ะฟะพะบะฐะทะฐั‚ัŒ VM
+
+  \section{``ะŸะพะดะพะถะดะธ, ะฝะฐะฟะธัะฐั‚ัŒ ั€ะฐะฝั‚ะฐะนะผ ะถะต ะฝะต ั‚ะฐะบ ะฟั€ะพัั‚ะพ?''}
+
+  %% ะพะฑัŠััะฝะธั‚ัŒ ะฟั€ะพะฑะปะตะผัƒ ัะพ ัั‚ะตะบะพะผ ะธ ั€ะตัˆะตะฝะธะต, ะฟะพะบะฐะทะฐั‚ัŒ ะดะธะฐะณั€ะฐะผะผัƒ
+
+  \section{``ะ ะพั‚ะบัƒะดะฐ ะทะฝะฐะตัˆัŒ, ั‡ั‚ะพ ัั‚ะพ ะฒัะต ะฟั€ะฐะฒะธะปัŒะฝะพ ั€ะฐะฑะพั‚ะฐะตั‚?''}
+
+  %% ะฟะพะบะฐะทะฐั‚ัŒ ะบะฐะบ ั‚ะตัั‚ั‹ ั€ะฐะฑะพั‚ะฐัŽั‚
+  %% ะพะฑัŠััะฝะธั‚ัŒ ะดะตะฑะฐะณะธะฝะณ, ะขะฒะธะบัะฑะพะปั‚ ะธ ั‚ะด
+
+  %% Slide 10:
+  \begin{frame}{tvix-eval, ะฒ ะฑั€ะฐัƒะทะตั€ะต}
+    \begin{itemize}
+    \item ัƒะดะธะฒะธั‚ะตะปัŒะฝะพ ะปะตะณะบะพ ะดะตะปะฐั‚ัŒ
+    \item ะฝะพ ะตัั‚ัŒ ัะปะพะถะฝะพัั‚ะธ ะฒ \begin{code}std::\end{code}
+      % ะฟะพะบะฐะทะฐั‚ัŒ ะฟั€ะธะผะตั€
+    \end{itemize}
+  \end{frame}
+
+  %% Slide 11:
+  \begin{frame}{ะ ั‡ั‚ะพ ะดะฐะปัŒัˆะต?}
+    ะ’ tvix-eval ะตัั‚ัŒ ะตั‰ะต ะบะพะต-ะบะฐะบะธะต ะธะฝั‚ะตั€ะตัะฝั‹ะต ะฟั€ะพะฑะปะตะผั‹. ะœะพะถะตั‚ ั‚ั‹ ะธั…
+    ั€ะตัˆะธัˆัŒ?
+  \end{frame}
+
+  \begin{frame}{ะกะฟะฐัะธะฑะพ!}
+    \begin{center}
+      \includegraphics[width=6cm,keepaspectratio=true]{qrcode.png}
+
+      https://tazj.in/blog/tvix-eval-talk-2023 \\
+      t.me/tazjin | t.me/tazlog
+    \end{center}
+  \end{frame}
+\end{document}
diff --git a/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/.gitignore b/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/.gitignore
new file mode 100644
index 0000000000..73b9c106db
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/.gitignore
@@ -0,0 +1,2 @@
+target/
+dist/
diff --git a/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/Cargo.lock b/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/Cargo.lock
new file mode 100644
index 0000000000..ef879254cb
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/Cargo.lock
@@ -0,0 +1,899 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "anymap2"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d301b3b94cb4b2f23d7917810addbbaff90738e0ca2be692bd027e70d7e0330c"
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "boolinator"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
+
+[[package]]
+name = "bumpalo"
+version = "3.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
+
+[[package]]
+name = "bytes"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
+
+[[package]]
+name = "cc"
+version = "1.0.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c"
+
+[[package]]
+name = "futures-io"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e"
+
+[[package]]
+name = "futures-task"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65"
+
+[[package]]
+name = "futures-util"
+version = "0.3.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
+
+[[package]]
+name = "gloo"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28999cda5ef6916ffd33fb4a7b87e1de633c47c0dc6d97905fee1cdaa142b94d"
+dependencies = [
+ "gloo-console",
+ "gloo-dialogs",
+ "gloo-events",
+ "gloo-file",
+ "gloo-history",
+ "gloo-net",
+ "gloo-render",
+ "gloo-storage",
+ "gloo-timers",
+ "gloo-utils",
+ "gloo-worker",
+]
+
+[[package]]
+name = "gloo-console"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-dialogs"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-events"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-file"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
+dependencies = [
+ "gloo-events",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-history"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85725d90bf0ed47063b3930ef28e863658a7905989e9929a8708aab74a1d5e7f"
+dependencies = [
+ "gloo-events",
+ "gloo-utils",
+ "serde",
+ "serde-wasm-bindgen",
+ "serde_urlencoded",
+ "thiserror",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-net"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a66b4e3c7d9ed8d315fd6b97c8b1f74a7c6ecbbc2320e65ae7ed38b7068cc620"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "futures-sink",
+ "gloo-utils",
+ "http",
+ "js-sys",
+ "pin-project",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-render"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-storage"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-worker"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13471584da78061a28306d1359dd0178d8d6fc1c7c80e5e35d27260346e0516a"
+dependencies = [
+ "anymap2",
+ "bincode",
+ "gloo-console",
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+
+[[package]]
+name = "http"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "implicit-clone"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c6ecbd987bb94f1f3c76c6787879756cf4b6f73bfff48d79308e8c56b46f65f"
+dependencies = [
+ "indexmap",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
+
+[[package]]
+name = "js-sys"
+version = "0.3.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.147"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
+
+[[package]]
+name = "log"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+
+[[package]]
+name = "memchr"
+version = "2.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "object"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
+
+[[package]]
+name = "pin-project"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pinned"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a829027bd95e54cfe13e3e258a1ae7b645960553fb82b75ff852c29688ee595b"
+dependencies = [
+ "futures",
+ "rustversion",
+ "thiserror",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
+dependencies = [
+ "proc-macro2",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prokio"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03b55e106e5791fa5a13abd13c85d6127312e8e09098059ca2bc9b03ca4cf488"
+dependencies = [
+ "futures",
+ "gloo",
+ "num_cpus",
+ "once_cell",
+ "pin-project",
+ "pinned",
+ "tokio",
+ "tokio-stream",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
+[[package]]
+name = "ryu"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+
+[[package]]
+name = "serde"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde-wasm-bindgen"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e"
+dependencies = [
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.188"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.105"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "tokio"
+version = "1.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9"
+dependencies = [
+ "backtrace",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tracing"
+version = "0.1.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8"
+dependencies = [
+ "cfg-if",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.29",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.87"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
+
+[[package]]
+name = "wasm-fs-demo"
+version = "0.1.0"
+dependencies = [
+ "yew",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "yew"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5dbecfe44343b70cc2932c3eb445425969ae21754a8ab3a0966981c1cf7af1cc"
+dependencies = [
+ "console_error_panic_hook",
+ "futures",
+ "gloo",
+ "implicit-clone",
+ "indexmap",
+ "js-sys",
+ "prokio",
+ "rustversion",
+ "serde",
+ "slab",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "yew-macro",
+]
+
+[[package]]
+name = "yew-macro"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b64c253c1d401f1ea868ca9988db63958cfa15a69f739101f338d6f05eea8301"
+dependencies = [
+ "boolinator",
+ "once_cell",
+ "prettyplease",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
diff --git a/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/Cargo.toml b/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/Cargo.toml
new file mode 100644
index 0000000000..4a445065e4
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "wasm-fs-demo"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+yew = { version = "0.20.0", features = [ "csr" ]}
diff --git a/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/index.html b/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/index.html
new file mode 100644
index 0000000000..e024c466cd
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/index.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8" />
+        <title>wasm-fs-demo</title>
+    </head>
+</html>
diff --git a/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/src/main.rs b/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/src/main.rs
new file mode 100644
index 0000000000..4ad177ad7a
--- /dev/null
+++ b/users/tazjin/presentations/tvix-eval-2023/wasm-fs-demo/src/main.rs
@@ -0,0 +1,41 @@
+use std::time::{SystemTime, UNIX_EPOCH};
+use yew::prelude::*;
+
+fn time_example() -> Html {
+    let epoch = match SystemTime::now().duration_since(UNIX_EPOCH) {
+        Ok(duration) => duration.as_secs(),
+        Err(err) => {
+            return html! {
+                format!("failed to calculate duration: {}", err)
+            }
+        }
+    };
+
+    html! {
+        <p>
+          {"Seconds since epoch: "}{epoch}
+        </p>
+    }
+}
+
+struct App;
+impl Component for App {
+    type Message = ();
+    type Properties = ();
+
+    fn create(_: &Context<Self>) -> Self {
+        Self
+    }
+
+    fn update(&mut self, _: &Context<Self>, _: Self::Message) -> bool {
+        false
+    }
+
+    fn view(&self, _: &Context<Self>) -> Html {
+        time_example()
+    }
+}
+
+fn main() {
+    yew::Renderer::<App>::new().render();
+}
diff --git a/users/tazjin/renderMarkdown.nix b/users/tazjin/renderMarkdown.nix
deleted file mode 100644
index 58f29c30e0..0000000000
--- a/users/tazjin/renderMarkdown.nix
+++ /dev/null
@@ -1,9 +0,0 @@
-# Render a Markdown file to HTML.
-{ depot, ... }:
-
-with depot.nix.yants;
-
-defun [ path drv ] (file: depot.third_party.runCommandNoCC "${file}.rendered.html" {} ''
-  cat ${file} | ${depot.tools.cheddar}/bin/cheddar --about-filter ${file} > $out
-'')
-
diff --git a/users/tazjin/rlox/.gitignore b/users/tazjin/rlox/.gitignore
new file mode 100644
index 0000000000..29e65519ba
--- /dev/null
+++ b/users/tazjin/rlox/.gitignore
@@ -0,0 +1,3 @@
+result
+/target
+**/*.rs.bk
diff --git a/users/tazjin/rlox/Cargo.lock b/users/tazjin/rlox/Cargo.lock
new file mode 100644
index 0000000000..d8107726e0
--- /dev/null
+++ b/users/tazjin/rlox/Cargo.lock
@@ -0,0 +1,6 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+[[package]]
+name = "rlox"
+version = "0.1.0"
+
diff --git a/users/tazjin/rlox/Cargo.toml b/users/tazjin/rlox/Cargo.toml
new file mode 100644
index 0000000000..b66af6ba85
--- /dev/null
+++ b/users/tazjin/rlox/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "rlox"
+version = "0.1.0"
+authors = ["Vincent Ambo <mail@tazj.in>"]
+edition = "2018"
+
+[features]
+# Enables debugging/disassembling in the bytecode interpreter. Off by
+# default as it is quite spammy.
+disassemble = []
diff --git a/users/tazjin/rlox/README.md b/users/tazjin/rlox/README.md
new file mode 100644
index 0000000000..1d2692d09c
--- /dev/null
+++ b/users/tazjin/rlox/README.md
@@ -0,0 +1,7 @@
+This is an interpreter for the Lox language, based on the book "[Crafting
+Interpreters](https://craftinginterpreters.com/)".
+
+The book's original code uses Java, but I don't want to use Java, so I've
+decided to take on the extra complexity of porting it to Rust.
+
+Note: This implements the first of two Lox interpreters.
diff --git a/users/tazjin/rlox/default.nix b/users/tazjin/rlox/default.nix
new file mode 100644
index 0000000000..e50ac32be4
--- /dev/null
+++ b/users/tazjin/rlox/default.nix
@@ -0,0 +1,5 @@
+{ depot, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+}
diff --git a/users/tazjin/rlox/examples/builtins.lox b/users/tazjin/rlox/examples/builtins.lox
new file mode 100644
index 0000000000..39af1d73c4
--- /dev/null
+++ b/users/tazjin/rlox/examples/builtins.lox
@@ -0,0 +1 @@
+print clock();
diff --git a/users/tazjin/rlox/examples/fib.lox b/users/tazjin/rlox/examples/fib.lox
new file mode 100644
index 0000000000..1b91e9db94
--- /dev/null
+++ b/users/tazjin/rlox/examples/fib.lox
@@ -0,0 +1,6 @@
+fun fib(n) {
+  if (n <= 1) return n;
+  return fib(n - 2) + fib(n - 1);
+}
+
+print fib(30);
diff --git a/users/tazjin/rlox/examples/func.lox b/users/tazjin/rlox/examples/func.lox
new file mode 100644
index 0000000000..d197ad1138
--- /dev/null
+++ b/users/tazjin/rlox/examples/func.lox
@@ -0,0 +1,5 @@
+fun foo(name) {
+  print("hello " + name);
+}
+
+foo("bar");
diff --git a/users/tazjin/rlox/examples/hello.lox b/users/tazjin/rlox/examples/hello.lox
new file mode 100644
index 0000000000..31752d9e2f
--- /dev/null
+++ b/users/tazjin/rlox/examples/hello.lox
@@ -0,0 +1,34 @@
+var a = 12;
+var b = a * 2;
+
+{
+  var b = a * 3;
+  a = 42;
+  print b;
+}
+
+print a;
+print b;
+
+if (5 > 4)
+  print "it's true";
+else
+  print "it's false";
+
+if (false)
+  print "it's not true";
+
+if (true and false)
+  print "won't happen";
+
+if (true or false)
+  print "will happen";
+
+var n = 5;
+while (n > 0) {
+  print "counting down";
+  n = n - 1;
+}
+
+for(var i = 0; i < 10; i = i + 1)
+  print "bla";
diff --git a/users/tazjin/rlox/examples/if.lox b/users/tazjin/rlox/examples/if.lox
new file mode 100644
index 0000000000..5f335c0e8b
--- /dev/null
+++ b/users/tazjin/rlox/examples/if.lox
@@ -0,0 +1,7 @@
+if (false) {
+  print "yes";
+} else {
+  print "no";
+}
+
+print "afterwards";
diff --git a/users/tazjin/rlox/examples/scope.lox b/users/tazjin/rlox/examples/scope.lox
new file mode 100644
index 0000000000..d563807943
--- /dev/null
+++ b/users/tazjin/rlox/examples/scope.lox
@@ -0,0 +1,19 @@
+var a = "global a";
+var b = "global b";
+var c = "global c";
+{
+  var a = "outer a";
+  var b = "outer b";
+  {
+    var a = "inner a";
+    print a;
+    print b;
+    print c;
+  }
+  print a;
+  print b;
+  print c;
+}
+print a;
+print b;
+print c;
diff --git a/users/tazjin/rlox/examples/scope2.lox b/users/tazjin/rlox/examples/scope2.lox
new file mode 100644
index 0000000000..f826c86588
--- /dev/null
+++ b/users/tazjin/rlox/examples/scope2.lox
@@ -0,0 +1,10 @@
+var a = "global";
+{
+  fun showA() {
+    print a;
+  }
+
+  showA();
+  var a = "block";
+  showA();
+}
diff --git a/users/tazjin/rlox/examples/slow.lox b/users/tazjin/rlox/examples/slow.lox
new file mode 100644
index 0000000000..dd6fb5e4bf
--- /dev/null
+++ b/users/tazjin/rlox/examples/slow.lox
@@ -0,0 +1,9 @@
+fun fib(n) {
+  if (n < 2) return n;
+  return fib(n - 1) + fib(n - 2);
+}
+
+var before = clock();
+print fib(40);
+var after = clock();
+print after - before;
diff --git a/users/tazjin/rlox/examples/var.lox b/users/tazjin/rlox/examples/var.lox
new file mode 100644
index 0000000000..7af90b3f0b
--- /dev/null
+++ b/users/tazjin/rlox/examples/var.lox
@@ -0,0 +1,8 @@
+var a = 10;
+var b = 5;
+
+{
+  var b = 10;
+  var c = 2;
+  a * b * c;
+}
diff --git a/users/tazjin/rlox/rustfmt.toml b/users/tazjin/rlox/rustfmt.toml
new file mode 100644
index 0000000000..df99c69198
--- /dev/null
+++ b/users/tazjin/rlox/rustfmt.toml
@@ -0,0 +1 @@
+max_width = 80
diff --git a/users/tazjin/rlox/src/bytecode/chunk.rs b/users/tazjin/rlox/src/bytecode/chunk.rs
new file mode 100644
index 0000000000..fc5cd34fdf
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/chunk.rs
@@ -0,0 +1,93 @@
+use std::ops::Index;
+
+use super::opcode::{CodeIdx, ConstantIdx, OpCode};
+use super::value;
+
+// In the book, this type is a hand-rolled dynamic array
+// implementation in C. The main benefit of following that approach
+// would be avoiding issues with OpCode variants not having equal
+// sizes, but for the purpose of this I'm going to ignore that
+// problem.
+#[derive(Debug, Default)]
+pub struct Chunk {
+    pub code: Vec<OpCode>,
+    lines: Vec<Span>,
+    constants: Vec<value::Value>,
+}
+
+#[derive(Debug)]
+struct Span {
+    /// Source code line
+    line: usize,
+
+    /// Number of instructions derived from this line
+    count: usize,
+}
+
+impl Chunk {
+    pub fn add_op(&mut self, data: OpCode, line: usize) -> CodeIdx {
+        let idx = self.code.len();
+        self.code.push(data);
+        self.add_line(line);
+        CodeIdx(idx)
+    }
+
+    pub fn add_constant(&mut self, data: value::Value) -> usize {
+        let idx = self.constants.len();
+        self.constants.push(data);
+        idx
+    }
+
+    pub fn constant(&self, idx: ConstantIdx) -> &value::Value {
+        self.constants.index(idx.0)
+    }
+
+    fn add_line(&mut self, line: usize) {
+        match self.lines.last_mut() {
+            Some(span) if span.line == line => span.count += 1,
+            _ => self.lines.push(Span { line, count: 1 }),
+        }
+    }
+
+    pub fn get_line(&self, offset: usize) -> usize {
+        let mut pos = 0;
+        for span in &self.lines {
+            pos += span.count;
+            if pos > offset {
+                return span.line;
+            }
+        }
+
+        panic!("invalid chunk state: line missing for offset {}", offset);
+    }
+}
+
+// Disassembler
+
+/// Print a single disassembled instruction at the specified offset.
+/// Some instructions are printed "raw", others have special handling.
+#[cfg(feature = "disassemble")]
+pub fn disassemble_instruction(chunk: &Chunk, offset: usize) {
+    print!("{:04} ", offset);
+
+    let line = chunk.get_line(offset);
+    if offset > 0 && line == chunk.get_line(offset - 1) {
+        print!("   | ");
+    } else {
+        print!("{:4} ", line);
+    }
+
+    match chunk.code.index(offset) {
+        OpCode::OpConstant(idx) => {
+            println!("OpConstant({:?}) '{:?}'", idx, chunk.constant(*idx))
+        }
+        op => println!("{:?}", op),
+    }
+}
+
+#[cfg(feature = "disassemble")]
+pub fn disassemble_chunk(chunk: &Chunk) {
+    for (idx, _) in chunk.code.iter().enumerate() {
+        disassemble_instruction(chunk, idx);
+    }
+}
diff --git a/users/tazjin/rlox/src/bytecode/compiler.rs b/users/tazjin/rlox/src/bytecode/compiler.rs
new file mode 100644
index 0000000000..89584f19d7
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/compiler.rs
@@ -0,0 +1,702 @@
+use super::chunk::Chunk;
+use super::errors::{Error, ErrorKind, LoxResult};
+use super::interner::{InternedStr, Interner};
+use super::opcode::{CodeIdx, CodeOffset, ConstantIdx, OpCode, StackIdx};
+use super::value::Value;
+use crate::scanner::{self, Token, TokenKind};
+
+#[cfg(feature = "disassemble")]
+use super::chunk;
+
+#[derive(Debug)]
+enum Depth {
+    Unitialised,
+    At(usize),
+}
+
+impl Depth {
+    fn above(&self, theirs: usize) -> bool {
+        match self {
+            Depth::Unitialised => false,
+            Depth::At(ours) => *ours > theirs,
+        }
+    }
+
+    fn below(&self, theirs: usize) -> bool {
+        match self {
+            Depth::Unitialised => false,
+            Depth::At(ours) => *ours < theirs,
+        }
+    }
+}
+
+#[derive(Debug)]
+struct Local {
+    name: Token,
+    depth: Depth,
+}
+
+#[derive(Debug, Default)]
+struct Locals {
+    locals: Vec<Local>,
+    scope_depth: usize,
+}
+
+struct Compiler<T: Iterator<Item = Token>> {
+    tokens: T,
+    chunk: Chunk,
+    panic: bool,
+    errors: Vec<Error>,
+    strings: Interner,
+    locals: Locals,
+
+    current: Option<Token>,
+    previous: Option<Token>,
+}
+
+#[derive(Debug, PartialEq, PartialOrd)]
+enum Precedence {
+    None,
+    Assignment, // =
+    Or,         // or
+    And,        // and
+    Equality,   // == !=
+    Comparison, // < > <= >=
+    Term,       // + -
+    Factor,     //
+    // 
+    // * /
+    Unary, // ! -
+    Call,  // . ()
+    Primary,
+}
+
+type ParseFn<T> = fn(&mut Compiler<T>) -> LoxResult<()>;
+
+struct ParseRule<T: Iterator<Item = Token>> {
+    prefix: Option<ParseFn<T>>,
+    infix: Option<ParseFn<T>>,
+    precedence: Precedence,
+}
+
+impl<T: Iterator<Item = Token>> ParseRule<T> {
+    fn new(prefix: Option<ParseFn<T>>, infix: Option<ParseFn<T>>, precedence: Precedence) -> Self {
+        ParseRule {
+            prefix,
+            infix,
+            precedence,
+        }
+    }
+}
+
+impl Precedence {
+    // Return the next highest precedence, if there is one.
+    fn next(&self) -> Self {
+        match self {
+            Precedence::None => Precedence::Assignment,
+            Precedence::Assignment => Precedence::Or,
+            Precedence::Or => Precedence::And,
+            Precedence::And => Precedence::Equality,
+            Precedence::Equality => Precedence::Comparison,
+            Precedence::Comparison => Precedence::Term,
+            Precedence::Term => Precedence::Factor,
+            Precedence::Factor => Precedence::Unary,
+            Precedence::Unary => Precedence::Call,
+            Precedence::Call => Precedence::Primary,
+            Precedence::Primary => {
+                panic!("invalid parser state: no higher precedence than Primary")
+            }
+        }
+    }
+}
+
+fn rule_for<T: Iterator<Item = Token>>(token: &TokenKind) -> ParseRule<T> {
+    match token {
+        TokenKind::LeftParen => ParseRule::new(Some(Compiler::grouping), None, Precedence::None),
+
+        TokenKind::Minus => ParseRule::new(
+            Some(Compiler::unary),
+            Some(Compiler::binary),
+            Precedence::Term,
+        ),
+
+        TokenKind::Plus => ParseRule::new(None, Some(Compiler::binary), Precedence::Term),
+
+        TokenKind::Slash => ParseRule::new(None, Some(Compiler::binary), Precedence::Factor),
+
+        TokenKind::Star => ParseRule::new(None, Some(Compiler::binary), Precedence::Factor),
+
+        TokenKind::Number(_) => ParseRule::new(Some(Compiler::number), None, Precedence::None),
+
+        TokenKind::True => ParseRule::new(Some(Compiler::literal), None, Precedence::None),
+
+        TokenKind::False => ParseRule::new(Some(Compiler::literal), None, Precedence::None),
+
+        TokenKind::Nil => ParseRule::new(Some(Compiler::literal), None, Precedence::None),
+
+        TokenKind::Bang => ParseRule::new(Some(Compiler::unary), None, Precedence::None),
+
+        TokenKind::BangEqual => ParseRule::new(None, Some(Compiler::binary), Precedence::Equality),
+
+        TokenKind::EqualEqual => ParseRule::new(None, Some(Compiler::binary), Precedence::Equality),
+
+        TokenKind::Greater => ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison),
+
+        TokenKind::GreaterEqual => {
+            ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison)
+        }
+
+        TokenKind::Less => ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison),
+
+        TokenKind::LessEqual => {
+            ParseRule::new(None, Some(Compiler::binary), Precedence::Comparison)
+        }
+
+        TokenKind::Identifier(_) => {
+            ParseRule::new(Some(Compiler::variable), None, Precedence::None)
+        }
+
+        TokenKind::String(_) => ParseRule::new(Some(Compiler::string), None, Precedence::None),
+
+        _ => ParseRule::new(None, None, Precedence::None),
+    }
+}
+
+macro_rules! consume {
+    ( $self:ident, $expected:pat, $err:expr ) => {
+        match $self.current().kind {
+            $expected => $self.advance(),
+            _ => $self.error_at($self.current().line, $err),
+        }
+    };
+}
+
+impl<T: Iterator<Item = Token>> Compiler<T> {
+    fn compile(&mut self) -> LoxResult<()> {
+        self.advance();
+
+        while !self.match_token(&TokenKind::Eof) {
+            self.declaration()?;
+        }
+
+        self.end_compiler()
+    }
+
+    fn advance(&mut self) {
+        self.previous = self.current.take();
+        self.current = self.tokens.next();
+    }
+
+    fn expression(&mut self) -> LoxResult<()> {
+        self.parse_precedence(Precedence::Assignment)
+    }
+
+    fn var_declaration(&mut self) -> LoxResult<()> {
+        let idx = self.parse_variable()?;
+
+        if self.match_token(&TokenKind::Equal) {
+            self.expression()?;
+        } else {
+            self.emit_op(OpCode::OpNil);
+        }
+
+        self.expect_semicolon("expect ';' after variable declaration")?;
+        self.define_variable(idx)
+    }
+
+    fn define_variable(&mut self, var: Option<ConstantIdx>) -> LoxResult<()> {
+        if self.locals.scope_depth == 0 {
+            self.emit_op(OpCode::OpDefineGlobal(var.expect("should be global")));
+        } else {
+            self.locals
+                .locals
+                .last_mut()
+                .expect("fatal: variable not yet added at definition")
+                .depth = Depth::At(self.locals.scope_depth);
+        }
+
+        Ok(())
+    }
+
+    fn declaration(&mut self) -> LoxResult<()> {
+        if self.match_token(&TokenKind::Var) {
+            self.var_declaration()?;
+        } else {
+            self.statement()?;
+        }
+
+        if self.panic {
+            self.synchronise();
+        }
+
+        Ok(())
+    }
+
+    fn statement(&mut self) -> LoxResult<()> {
+        if self.match_token(&TokenKind::Print) {
+            self.print_statement()
+        } else if self.match_token(&TokenKind::If) {
+            self.if_statement()
+        } else if self.match_token(&TokenKind::LeftBrace) {
+            self.begin_scope();
+            self.block()?;
+            self.end_scope();
+            Ok(())
+        } else {
+            self.expression_statement()
+        }
+    }
+
+    fn print_statement(&mut self) -> LoxResult<()> {
+        self.expression()?;
+        self.expect_semicolon("expect ';' after print statement")?;
+        self.emit_op(OpCode::OpPrint);
+        Ok(())
+    }
+
+    fn begin_scope(&mut self) {
+        self.locals.scope_depth += 1;
+    }
+
+    fn end_scope(&mut self) {
+        debug_assert!(self.locals.scope_depth > 0, "tried to end global scope");
+        self.locals.scope_depth -= 1;
+
+        while self.locals.locals.len() > 0
+            && self.locals.locals[self.locals.locals.len() - 1]
+                .depth
+                .above(self.locals.scope_depth)
+        {
+            self.emit_op(OpCode::OpPop);
+            self.locals.locals.remove(self.locals.locals.len() - 1);
+        }
+    }
+
+    fn block(&mut self) -> LoxResult<()> {
+        while !self.check(&TokenKind::RightBrace) && !self.check(&TokenKind::Eof) {
+            self.declaration()?;
+        }
+
+        consume!(
+            self,
+            TokenKind::RightBrace,
+            ErrorKind::ExpectedToken("Expected '}' after block.")
+        );
+        Ok(())
+    }
+
+    fn expression_statement(&mut self) -> LoxResult<()> {
+        self.expression()?;
+        self.expect_semicolon("expect ';' after expression")?;
+        // TODO(tazjin): Why did I add this originally?
+        // self.emit_op(OpCode::OpPop);
+        Ok(())
+    }
+
+    fn if_statement(&mut self) -> LoxResult<()> {
+        consume!(
+            self,
+            TokenKind::LeftParen,
+            ErrorKind::ExpectedToken("Expected '(' after 'if'")
+        );
+
+        self.expression()?;
+
+        consume!(
+            self,
+            TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expected ')' after condition")
+        );
+
+        let then_jump = self.emit_op(OpCode::OpJumpPlaceholder(false));
+        self.emit_op(OpCode::OpPop);
+        self.statement()?;
+        let else_jump = self.emit_op(OpCode::OpJumpPlaceholder(true));
+        self.patch_jump(then_jump);
+        self.emit_op(OpCode::OpPop);
+
+        if self.match_token(&TokenKind::Else) {
+            self.statement()?;
+        }
+
+        self.patch_jump(else_jump);
+
+        Ok(())
+    }
+
+    fn number(&mut self) -> LoxResult<()> {
+        if let TokenKind::Number(num) = self.previous().kind {
+            self.emit_constant(Value::Number(num), true);
+            return Ok(());
+        }
+
+        unreachable!("internal parser error: entered number() incorrectly")
+    }
+
+    fn grouping(&mut self) -> LoxResult<()> {
+        self.expression()?;
+        consume!(
+            self,
+            TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expected ')' after expression")
+        );
+        Ok(())
+    }
+
+    fn unary(&mut self) -> LoxResult<()> {
+        // TODO(tazjin): Avoid clone
+        let kind = self.previous().kind.clone();
+
+        // Compile the operand
+        self.parse_precedence(Precedence::Unary)?;
+
+        // Emit operator instruction
+        match kind {
+            TokenKind::Bang => self.emit_op(OpCode::OpNot),
+            TokenKind::Minus => self.emit_op(OpCode::OpNegate),
+            _ => unreachable!("only called for unary operator tokens"),
+        };
+
+        Ok(())
+    }
+
+    fn binary(&mut self) -> LoxResult<()> {
+        // Remember the operator
+        let operator = self.previous().kind.clone();
+
+        // Compile the right operand
+        let rule: ParseRule<T> = rule_for(&operator);
+        self.parse_precedence(rule.precedence.next())?;
+
+        // Emit operator instruction
+        match operator {
+            TokenKind::Minus => self.emit_op(OpCode::OpSubtract),
+            TokenKind::Plus => self.emit_op(OpCode::OpAdd),
+            TokenKind::Star => self.emit_op(OpCode::OpMultiply),
+            TokenKind::Slash => self.emit_op(OpCode::OpDivide),
+
+            TokenKind::BangEqual => {
+                self.emit_op(OpCode::OpEqual);
+                self.emit_op(OpCode::OpNot)
+            }
+
+            TokenKind::EqualEqual => self.emit_op(OpCode::OpEqual),
+            TokenKind::Greater => self.emit_op(OpCode::OpGreater),
+
+            TokenKind::GreaterEqual => {
+                self.emit_op(OpCode::OpLess);
+                self.emit_op(OpCode::OpNot)
+            }
+
+            TokenKind::Less => self.emit_op(OpCode::OpLess),
+            TokenKind::LessEqual => {
+                self.emit_op(OpCode::OpGreater);
+                self.emit_op(OpCode::OpNot)
+            }
+
+            _ => unreachable!("only called for binary operator tokens"),
+        };
+
+        Ok(())
+    }
+
+    fn literal(&mut self) -> LoxResult<()> {
+        match self.previous().kind {
+            TokenKind::Nil => self.emit_op(OpCode::OpNil),
+            TokenKind::True => self.emit_op(OpCode::OpTrue),
+            TokenKind::False => self.emit_op(OpCode::OpFalse),
+            _ => unreachable!("only called for literal value tokens"),
+        };
+
+        Ok(())
+    }
+
+    fn string(&mut self) -> LoxResult<()> {
+        let val = match &self.previous().kind {
+            TokenKind::String(s) => s.clone(),
+            _ => unreachable!("only called for strings"),
+        };
+
+        let id = self.strings.intern(val);
+        self.emit_constant(Value::String(id.into()), true);
+
+        Ok(())
+    }
+
+    fn named_variable(&mut self, name: Token) -> LoxResult<()> {
+        let local_idx = self.resolve_local(&name);
+
+        let ident = if local_idx.is_some() {
+            None
+        } else {
+            Some(self.identifier_constant(&name)?)
+        };
+
+        if self.match_token(&TokenKind::Equal) {
+            self.expression()?;
+            match local_idx {
+                Some(idx) => self.emit_op(OpCode::OpSetLocal(idx)),
+                None => self.emit_op(OpCode::OpSetGlobal(ident.unwrap())),
+            };
+        } else {
+            match local_idx {
+                Some(idx) => self.emit_op(OpCode::OpGetLocal(idx)),
+                None => self.emit_op(OpCode::OpGetGlobal(ident.unwrap())),
+            };
+        }
+
+        Ok(())
+    }
+
+    fn variable(&mut self) -> LoxResult<()> {
+        let name = self.previous().clone();
+        self.named_variable(name)
+    }
+
+    fn parse_precedence(&mut self, precedence: Precedence) -> LoxResult<()> {
+        self.advance();
+        let rule: ParseRule<T> = rule_for(&self.previous().kind);
+        let prefix_fn = match rule.prefix {
+            None => unimplemented!("expected expression or something, unclear"),
+            Some(func) => func,
+        };
+
+        prefix_fn(self)?;
+
+        while precedence <= rule_for::<T>(&self.current().kind).precedence {
+            self.advance();
+            match rule_for::<T>(&self.previous().kind).infix {
+                Some(func) => {
+                    func(self)?;
+                }
+                None => {
+                    unreachable!("invalid compiler state: error in parse rules")
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn identifier_str(&mut self, token: &Token) -> LoxResult<InternedStr> {
+        let ident = match &token.kind {
+            TokenKind::Identifier(ident) => ident.to_string(),
+            _ => {
+                return Err(Error {
+                    line: self.current().line,
+                    kind: ErrorKind::ExpectedToken("Expected identifier"),
+                })
+            }
+        };
+
+        Ok(self.strings.intern(ident))
+    }
+
+    fn identifier_constant(&mut self, name: &Token) -> LoxResult<ConstantIdx> {
+        let ident = self.identifier_str(name)?;
+        Ok(self.emit_constant(Value::String(ident.into()), false))
+    }
+
+    fn resolve_local(&self, name: &Token) -> Option<StackIdx> {
+        for (idx, local) in self.locals.locals.iter().enumerate().rev() {
+            if name.lexeme == local.name.lexeme {
+                if let Depth::Unitialised = local.depth {
+                    // TODO(tazjin): *return* err
+                    panic!("can't read variable in its own initialiser");
+                }
+                return Some(StackIdx(idx));
+            }
+        }
+
+        None
+    }
+
+    fn add_local(&mut self, name: Token) {
+        let local = Local {
+            name,
+            depth: Depth::Unitialised,
+        };
+
+        self.locals.locals.push(local);
+    }
+
+    fn declare_variable(&mut self) -> LoxResult<()> {
+        if self.locals.scope_depth == 0 {
+            return Ok(());
+        }
+
+        let name = self.previous().clone();
+
+        for local in self.locals.locals.iter().rev() {
+            if local.depth.below(self.locals.scope_depth) {
+                break;
+            }
+
+            if name.lexeme == local.name.lexeme {
+                return Err(Error {
+                    kind: ErrorKind::VariableShadowed(name.lexeme.into()),
+                    line: name.line,
+                });
+            }
+        }
+
+        self.add_local(name);
+        Ok(())
+    }
+
+    fn parse_variable(&mut self) -> LoxResult<Option<ConstantIdx>> {
+        consume!(
+            self,
+            TokenKind::Identifier(_),
+            ErrorKind::ExpectedToken("expected identifier")
+        );
+
+        self.declare_variable()?;
+        if self.locals.scope_depth > 0 {
+            return Ok(None);
+        }
+
+        let name = self.previous().clone();
+        let id = self.identifier_str(&name)?;
+        Ok(Some(self.emit_constant(Value::String(id.into()), false)))
+    }
+
+    fn current_chunk(&mut self) -> &mut Chunk {
+        &mut self.chunk
+    }
+
+    fn end_compiler(&mut self) -> LoxResult<()> {
+        self.emit_op(OpCode::OpReturn);
+
+        #[cfg(feature = "disassemble")]
+        {
+            chunk::disassemble_chunk(&self.chunk);
+            println!("== compilation finished ==");
+        }
+
+        Ok(())
+    }
+
+    fn emit_op(&mut self, op: OpCode) -> CodeIdx {
+        let line = self.previous().line;
+        self.current_chunk().add_op(op, line)
+    }
+
+    fn emit_constant(&mut self, val: Value, with_op: bool) -> ConstantIdx {
+        let idx = ConstantIdx(self.chunk.add_constant(val));
+
+        if with_op {
+            self.emit_op(OpCode::OpConstant(idx));
+        }
+
+        idx
+    }
+
+    fn patch_jump(&mut self, idx: CodeIdx) {
+        let offset = CodeOffset(self.chunk.code.len() - idx.0 - 1);
+
+        if let OpCode::OpJumpPlaceholder(true) = self.chunk.code[idx.0] {
+            self.chunk.code[idx.0] = OpCode::OpJump(offset);
+            return;
+        }
+
+        if let OpCode::OpJumpPlaceholder(false) = self.chunk.code[idx.0] {
+            self.chunk.code[idx.0] = OpCode::OpJumpIfFalse(offset);
+            return;
+        }
+
+        panic!(
+            "attempted to patch unsupported op: {:?}",
+            self.chunk.code[idx.0]
+        );
+    }
+
+    fn previous(&self) -> &Token {
+        self.previous
+            .as_ref()
+            .expect("invalid internal compiler state: missing previous token")
+    }
+
+    fn current(&self) -> &Token {
+        self.current
+            .as_ref()
+            .expect("invalid internal compiler state: missing current token")
+    }
+
+    fn error_at(&mut self, line: usize, kind: ErrorKind) {
+        if self.panic {
+            return;
+        }
+
+        self.panic = true;
+        self.errors.push(Error { kind, line })
+    }
+
+    fn match_token(&mut self, token: &TokenKind) -> bool {
+        if !self.check(token) {
+            return false;
+        }
+
+        self.advance();
+        true
+    }
+
+    fn check(&self, token: &TokenKind) -> bool {
+        return self.current().kind == *token;
+    }
+
+    fn synchronise(&mut self) {
+        self.panic = false;
+
+        while self.current().kind != TokenKind::Eof {
+            if self.previous().kind == TokenKind::Semicolon {
+                return;
+            }
+
+            match self.current().kind {
+                TokenKind::Class
+                | TokenKind::Fun
+                | TokenKind::Var
+                | TokenKind::For
+                | TokenKind::If
+                | TokenKind::While
+                | TokenKind::Print
+                | TokenKind::Return => return,
+
+                _ => {
+                    self.advance();
+                }
+            }
+        }
+    }
+
+    fn expect_semicolon(&mut self, msg: &'static str) -> LoxResult<()> {
+        consume!(self, TokenKind::Semicolon, ErrorKind::ExpectedToken(msg));
+        Ok(())
+    }
+}
+
+pub fn compile(code: &str) -> Result<(Interner, Chunk), Vec<Error>> {
+    let chars = code.chars().collect::<Vec<char>>();
+    let tokens = scanner::scan(&chars)
+        .map_err(|errors| errors.into_iter().map(Into::into).collect::<Vec<Error>>())?;
+
+    let mut compiler = Compiler {
+        tokens: tokens.into_iter().peekable(),
+        chunk: Default::default(),
+        panic: false,
+        errors: vec![],
+        strings: Interner::with_capacity(1024),
+        locals: Default::default(),
+        current: None,
+        previous: None,
+    };
+
+    compiler.compile()?;
+
+    if compiler.errors.is_empty() {
+        Ok((compiler.strings, compiler.chunk))
+    } else {
+        Err(compiler.errors)
+    }
+}
diff --git a/users/tazjin/rlox/src/bytecode/errors.rs b/users/tazjin/rlox/src/bytecode/errors.rs
new file mode 100644
index 0000000000..988031f763
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/errors.rs
@@ -0,0 +1,51 @@
+use crate::scanner::ScannerError;
+
+use std::fmt;
+
+#[derive(Debug)]
+pub enum ErrorKind {
+    UnexpectedChar(char),
+    UnterminatedString,
+    ExpectedToken(&'static str),
+    InternalError(&'static str),
+    TypeError(String),
+    VariableShadowed(String),
+}
+
+#[derive(Debug)]
+pub struct Error {
+    pub kind: ErrorKind,
+    pub line: usize,
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "[line NYI] Error: {:?}", self.kind)
+    }
+}
+
+impl From<ScannerError> for Error {
+    fn from(err: ScannerError) -> Self {
+        match err {
+            ScannerError::UnexpectedChar { line, unexpected } => Error {
+                line,
+                kind: ErrorKind::UnexpectedChar(unexpected),
+            },
+
+            ScannerError::UnterminatedString { line } => Error {
+                line,
+                kind: ErrorKind::UnterminatedString,
+            },
+        }
+    }
+}
+
+// Convenience implementation as we're often dealing with vectors of
+// errors (to report as many issues as possible before terminating)
+impl From<Error> for Vec<Error> {
+    fn from(err: Error) -> Self {
+        vec![err]
+    }
+}
+
+pub type LoxResult<T> = Result<T, Error>;
diff --git a/users/tazjin/rlox/src/bytecode/interner/mod.rs b/users/tazjin/rlox/src/bytecode/interner/mod.rs
new file mode 100644
index 0000000000..1da1a24b2c
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/interner/mod.rs
@@ -0,0 +1,87 @@
+//! String-interning implementation for values that are likely to
+//! benefit from fast comparisons and deduplication (e.g. instances of
+//! variable names).
+//!
+//! This uses a trick from the typed-arena crate for guaranteeing
+//! stable addresses by never resizing the existing String buffer, and
+//! collecting full buffers in a vector.
+
+use std::collections::HashMap;
+
+#[cfg(test)]
+mod tests;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
+pub struct InternedStr {
+    id: usize,
+}
+
+#[derive(Default)]
+pub struct Interner {
+    map: HashMap<&'static str, InternedStr>,
+    vec: Vec<&'static str>,
+    buf: String,
+    full: Vec<String>,
+}
+
+impl Interner {
+    pub fn with_capacity(cap: usize) -> Self {
+        Interner {
+            buf: String::with_capacity(cap),
+            ..Default::default()
+        }
+    }
+
+    pub fn intern<S: AsRef<str>>(&mut self, name: S) -> InternedStr {
+        let name = name.as_ref();
+        if let Some(&id) = self.map.get(name) {
+            return id;
+        }
+
+        let name = self.alloc(name);
+        let id = InternedStr {
+            id: self.vec.len() as usize,
+        };
+
+        self.map.insert(name, id);
+        self.vec.push(name);
+
+        debug_assert!(self.lookup(id) == name);
+        debug_assert!(self.intern(name) == id);
+
+        id
+    }
+
+    pub fn lookup<'a>(&'a self, id: InternedStr) -> &'a str {
+        self.vec[id.id]
+    }
+
+    fn alloc<'a>(&'a mut self, name: &str) -> &'static str {
+        let cap = self.buf.capacity();
+        if cap < self.buf.len() + name.len() {
+            let new_cap = (cap.max(name.len()) + 1).next_power_of_two();
+            let new_buf = String::with_capacity(new_cap);
+            let old_buf = std::mem::replace(&mut self.buf, new_buf);
+            self.full.push(old_buf);
+        }
+
+        let interned: &'a str = {
+            let start = self.buf.len();
+            self.buf.push_str(name);
+            &self.buf[start..]
+        };
+
+        unsafe {
+            // This is sound for two reasons:
+            //
+            // 1. This function (Interner::alloc) is private, which
+            //    prevents users from allocating a supposedly static
+            //    reference.
+            //
+            // 2. Interner::lookup explicitly shortens the lifetime of
+            //    references that are handed out to that of the
+            //    reference to self.
+            return &*(interned as *const str);
+        }
+    }
+}
diff --git a/users/tazjin/rlox/src/bytecode/interner/tests.rs b/users/tazjin/rlox/src/bytecode/interner/tests.rs
new file mode 100644
index 0000000000..b34bf68353
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/interner/tests.rs
@@ -0,0 +1,24 @@
+use super::*;
+
+#[test]
+fn interns_strings() {
+    let mut interner = Interner::with_capacity(128);
+    let id = interner.intern("hello world");
+    assert_eq!("hello world", interner.lookup(id));
+}
+
+#[test]
+fn deduplicates_strings() {
+    let mut interner = Interner::with_capacity(128);
+    let id_1 = interner.intern("hello world");
+    let id_2 = interner.intern("hello world");
+    assert_eq!(id_1, id_2);
+}
+
+#[test]
+fn ids_survive_growing() {
+    let mut interner = Interner::with_capacity(16);
+    let id = interner.intern("hello");
+    interner.intern("excessively large string that will cause eallocation");
+    assert_eq!("hello", interner.lookup(id));
+}
diff --git a/users/tazjin/rlox/src/bytecode/mod.rs b/users/tazjin/rlox/src/bytecode/mod.rs
new file mode 100644
index 0000000000..117f17824a
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/mod.rs
@@ -0,0 +1,30 @@
+//! Bytecode interpreter for Lox.
+//!
+//! https://craftinginterpreters.com/chunks-of-bytecode.html
+
+mod chunk;
+mod compiler;
+mod errors;
+mod interner;
+mod opcode;
+mod value;
+mod vm;
+
+#[cfg(test)]
+mod tests;
+
+pub struct Interpreter {}
+
+impl crate::Lox for Interpreter {
+    type Error = errors::Error;
+    type Value = value::Value;
+
+    fn create() -> Self {
+        Interpreter {}
+    }
+
+    fn interpret(&mut self, code: String) -> Result<Self::Value, Vec<Self::Error>> {
+        let (strings, chunk) = compiler::compile(&code)?;
+        vm::interpret(strings, chunk).map_err(|e| vec![e])
+    }
+}
diff --git a/users/tazjin/rlox/src/bytecode/opcode.rs b/users/tazjin/rlox/src/bytecode/opcode.rs
new file mode 100644
index 0000000000..8a106f9691
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/opcode.rs
@@ -0,0 +1,56 @@
+#[derive(Clone, Copy, Debug)]
+pub struct ConstantIdx(pub usize);
+
+#[derive(Clone, Copy, Debug)]
+pub struct StackIdx(pub usize);
+
+#[derive(Clone, Copy, Debug)]
+pub struct CodeIdx(pub usize);
+
+#[derive(Clone, Copy, Debug)]
+pub struct CodeOffset(pub usize);
+
+#[derive(Debug)]
+pub enum OpCode {
+    /// Push a constant onto the stack.
+    OpConstant(ConstantIdx),
+
+    // Literal pushes
+    OpNil,
+    OpTrue,
+    OpFalse,
+
+    /// Return from the current function.
+    OpReturn,
+
+    // Boolean & comparison operators
+    OpNot,
+    OpEqual,
+    OpGreater,
+    OpLess,
+
+    /// Unary negation
+    OpNegate,
+
+    // Arithmetic operators
+    OpAdd,
+    OpSubtract,
+    OpMultiply,
+    OpDivide,
+
+    // Built in operations
+    OpPrint,
+    OpPop,
+
+    // Variable management
+    OpDefineGlobal(ConstantIdx),
+    OpGetGlobal(ConstantIdx),
+    OpSetGlobal(ConstantIdx),
+    OpGetLocal(StackIdx),
+    OpSetLocal(StackIdx),
+
+    // Control flow
+    OpJumpPlaceholder(bool),
+    OpJump(CodeOffset),
+    OpJumpIfFalse(CodeOffset),
+}
diff --git a/users/tazjin/rlox/src/bytecode/tests.rs b/users/tazjin/rlox/src/bytecode/tests.rs
new file mode 100644
index 0000000000..bc7d6cb878
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/tests.rs
@@ -0,0 +1,152 @@
+use super::value::Value;
+use super::*;
+
+use crate::Lox;
+
+fn expect(code: &str, value: Value) {
+    let result = Interpreter::create()
+        .interpret(code.into())
+        .expect("evaluation failed");
+    assert_eq!(result, value);
+}
+
+fn expect_num(code: &str, value: f64) {
+    expect(code, Value::Number(value))
+}
+
+fn expect_bool(code: &str, value: bool) {
+    expect(code, Value::Bool(value))
+}
+
+fn expect_str(code: &str, value: &str) {
+    expect(code, Value::String(value.to_string().into()))
+}
+
+#[test]
+fn numbers() {
+    expect_num("1;", 1.0);
+    expect_num("13.37;", 13.37);
+}
+
+#[test]
+fn negative_numbers() {
+    // Note: This technically tests unary operators.
+    expect_num("-1;", -1.0);
+    expect_num("-13.37;", -13.37);
+}
+
+#[test]
+fn terms() {
+    expect_num("1 + 2;", 3.0);
+    expect_num("3 - 1;", 2.0);
+    expect_num("0.7 + 0.3;", 1.0);
+    expect_num("1 + -3;", -2.0);
+    expect_num("-1 - -1;", 0.0);
+    expect_num("10 - -10 + 10;", 30.0);
+}
+
+#[test]
+fn factors() {
+    expect_num("1 * 2;", 2.0);
+    expect_num("10 / 5;", 2.0);
+    expect_num("0.7 * 4 / 1.4;", 2.0);
+    expect_num("10 * -10 / 10;", -10.0);
+}
+
+#[test]
+fn arithmetic() {
+    expect_num("10 - 3 * 2;", 4.0);
+    expect_num("-4 * -4 + (14 - 5);", 25.0);
+    expect_num("(702 + 408) - ((239 - 734) / -5) + -4;", 1007.0);
+}
+
+#[test]
+fn trivial_literals() {
+    expect("true;", Value::Bool(true));
+    expect("false;", Value::Bool(false));
+    expect("nil;", Value::Nil);
+}
+
+#[test]
+fn negation() {
+    expect_bool("!true;", false);
+    expect_bool("!false;", true);
+    expect_bool("!nil;", true);
+    expect_bool("!13.5;", false);
+    expect_bool("!-42;", false);
+}
+
+#[test]
+fn equality() {
+    expect_bool("42 == 42;", true);
+    expect_bool("42 != 42;", false);
+    expect_bool("42 == 42.0;", true);
+
+    expect_bool("true == true;", true);
+    expect_bool("true == false;", false);
+    expect_bool("true == !false;", true);
+    expect_bool("true != true;", false);
+    expect_bool("true != false;", true);
+
+    expect_bool("42 == false;", false);
+    expect_bool("42 == true;", false);
+    expect_bool("!42 == !true;", true);
+}
+
+#[test]
+fn comparisons() {
+    expect_bool("42 > 23;", true);
+    expect_bool("42 < 23;", false);
+    expect_bool("42 <= 42;", true);
+    expect_bool("42 <= 23;", false);
+    expect_bool("42 >= 42;", true);
+    expect_bool("42 >= 23;", true);
+}
+
+#[test]
+fn strings() {
+    expect_str("\"hello\";", "hello");
+    expect_str("\"hello\" + \" world\";", "hello world");
+}
+
+#[test]
+fn global_variables() {
+    expect_num("var a = 5; a;", 5.0);
+    expect_num("var a = 5; var b = 2; a * b;", 10.0);
+    expect_str(
+        "var greeting = \"hello\"; var name = \"Zubnog\"; greeting + \" \" + name;",
+        "hello Zubnog",
+    );
+}
+
+#[test]
+fn global_assignment() {
+    expect_str(
+        r#"
+          var breakfast = "beignets";
+          var beverage = "cafe au lait";
+          breakfast = "beignets with " + beverage;
+          breakfast;
+        "#,
+        "beignets with cafe au lait",
+    );
+}
+
+#[test]
+fn local_variables() {
+    expect_num(
+        r#"
+          var a = 10;
+          var b = 5;
+          var result = 0;
+          {
+            var b = 10;
+            var c = 2;
+            result = a * b * c;
+          }
+
+          result;
+        "#,
+        200.0,
+    );
+}
diff --git a/users/tazjin/rlox/src/bytecode/value.rs b/users/tazjin/rlox/src/bytecode/value.rs
new file mode 100644
index 0000000000..4170efadf8
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/value.rs
@@ -0,0 +1,37 @@
+use super::interner::InternedStr;
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum Value {
+    Nil,
+    Bool(bool),
+    Number(f64),
+    String(LoxString),
+}
+
+#[derive(Clone, Debug, PartialEq, Eq, Hash)]
+pub enum LoxString {
+    Heap(String),
+    Interned(InternedStr),
+}
+
+impl From<String> for LoxString {
+    fn from(s: String) -> Self {
+        LoxString::Heap(s)
+    }
+}
+
+impl From<InternedStr> for LoxString {
+    fn from(s: InternedStr) -> Self {
+        LoxString::Interned(s)
+    }
+}
+
+impl Value {
+    pub fn is_falsey(&self) -> bool {
+        match self {
+            Value::Nil => true,
+            Value::Bool(false) => true,
+            _ => false,
+        }
+    }
+}
diff --git a/users/tazjin/rlox/src/bytecode/vm.rs b/users/tazjin/rlox/src/bytecode/vm.rs
new file mode 100644
index 0000000000..30ffebc79c
--- /dev/null
+++ b/users/tazjin/rlox/src/bytecode/vm.rs
@@ -0,0 +1,272 @@
+use std::collections::HashMap;
+
+use super::chunk;
+use super::errors::*;
+use super::interner::Interner;
+use super::opcode::OpCode;
+use super::value::{LoxString, Value};
+
+pub struct VM {
+    chunk: chunk::Chunk,
+
+    // TODO(tazjin): Accessing array elements constantly is not ideal,
+    // lets see if something clever can be done with iterators.
+    ip: usize,
+
+    stack: Vec<Value>,
+    strings: Interner,
+
+    globals: HashMap<LoxString, Value>,
+
+    // Operations that consume values from the stack without pushing
+    // anything leave their last value in this slot, which makes it
+    // possible to return values from interpreters that ran code which
+    // ended with a statement.
+    last_drop: Option<Value>,
+}
+
+impl VM {
+    fn push(&mut self, value: Value) {
+        self.stack.push(value)
+    }
+
+    fn pop(&mut self) -> Value {
+        self.stack.pop().expect("fatal error: stack empty!")
+    }
+}
+
+macro_rules! with_type {
+    ( $self:ident, $val:ident, $type:pat, $body:expr ) => {
+        match $val {
+            $type => $body,
+            _ => {
+                return Err(Error {
+                    line: $self.chunk.get_line($self.ip - 1),
+                    kind: ErrorKind::TypeError(format!(
+                        "Expected type {}, but found value: {:?}",
+                        stringify!($type),
+                        $val,
+                    )),
+                })
+            }
+        }
+    };
+}
+
+macro_rules! binary_op {
+    ( $vm:ident, $type:tt, $op:tt ) => {
+        binary_op!($vm, $type, $type, $op)
+    };
+
+    ( $vm:ident, $in_type:tt, $out_type:tt, $op:tt ) => {{
+        let b = $vm.pop();
+        let a = $vm.pop();
+
+        with_type!($vm, b, Value::$in_type(val_b), {
+            with_type!($vm, a, Value::$in_type(val_a), {
+                $vm.push(Value::$out_type(val_a $op val_b))
+            })
+        })
+    }};
+}
+
+impl VM {
+    fn run(&mut self) -> LoxResult<Value> {
+        loop {
+            let op = &self.chunk.code[self.ip];
+
+            #[cfg(feature = "disassemble")]
+            chunk::disassemble_instruction(&self.chunk, self.ip);
+
+            self.ip += 1;
+
+            match op {
+                OpCode::OpReturn => {
+                    if !self.stack.is_empty() {
+                        let val = self.pop();
+                        return Ok(self.return_value(val));
+                    } else if self.last_drop.is_some() {
+                        let val = self.last_drop.take().unwrap();
+                        return Ok(self.return_value(val));
+                    } else {
+                        return Ok(Value::Nil);
+                    }
+                }
+
+                OpCode::OpConstant(idx) => {
+                    let c = self.chunk.constant(*idx).clone();
+                    self.push(c);
+                }
+
+                OpCode::OpNil => self.push(Value::Nil),
+                OpCode::OpTrue => self.push(Value::Bool(true)),
+                OpCode::OpFalse => self.push(Value::Bool(false)),
+
+                OpCode::OpNot => {
+                    let v = self.pop();
+                    self.push(Value::Bool(v.is_falsey()));
+                }
+
+                OpCode::OpEqual => {
+                    let b = self.pop();
+                    let a = self.pop();
+                    self.push(Value::Bool(a == b));
+                }
+
+                OpCode::OpLess => binary_op!(self, Number, Bool, <),
+                OpCode::OpGreater => binary_op!(self, Number, Bool, >),
+
+                OpCode::OpNegate => {
+                    let v = self.pop();
+                    with_type!(self, v, Value::Number(num), self.push(Value::Number(-num)));
+                }
+
+                OpCode::OpSubtract => binary_op!(self, Number, -),
+                OpCode::OpMultiply => binary_op!(self, Number, *),
+                OpCode::OpDivide => binary_op!(self, Number, /),
+
+                OpCode::OpAdd => {
+                    let b = self.pop();
+                    let a = self.pop();
+
+                    match (a, b) {
+                        (Value::String(s_a), Value::String(s_b)) => {
+                            let mut new_s = self.resolve_str(&s_a).to_string();
+                            new_s.push_str(self.resolve_str(&s_b));
+                            self.push(Value::String(new_s.into()));
+                        }
+
+                        (Value::Number(n_a), Value::Number(n_b)) => {
+                            self.push(Value::Number(n_a + n_b))
+                        }
+
+                        _ => {
+                            return Err(Error {
+                                line: self.chunk.get_line(self.ip - 1),
+                                kind: ErrorKind::TypeError(
+                                    "'+' operator only works on strings and numbers".into(),
+                                ),
+                            })
+                        }
+                    }
+                }
+
+                OpCode::OpPrint => {
+                    let val = self.pop();
+                    println!("{}", self.print_value(val));
+                }
+
+                OpCode::OpPop => {
+                    self.last_drop = Some(self.pop());
+                }
+
+                OpCode::OpDefineGlobal(name_idx) => {
+                    let name = self.chunk.constant(*name_idx);
+                    with_type!(self, name, Value::String(name), {
+                        let name = name.clone();
+                        let val = self.pop();
+                        self.globals.insert(name, val);
+                    });
+                }
+
+                OpCode::OpGetGlobal(name_idx) => {
+                    let name = self.chunk.constant(*name_idx);
+                    with_type!(self, name, Value::String(name), {
+                        let val = match self.globals.get(name) {
+                            None => unimplemented!("variable not found error"),
+                            Some(val) => val.clone(),
+                        };
+                        self.push(val)
+                    });
+                }
+
+                OpCode::OpSetGlobal(name_idx) => {
+                    let name = self.chunk.constant(*name_idx).clone();
+                    let new_val = self.pop();
+                    with_type!(self, name, Value::String(name), {
+                        match self.globals.get_mut(&name) {
+                            None => unimplemented!("variable not found error"),
+                            Some(val) => {
+                                *val = new_val;
+                            }
+                        }
+                    });
+                }
+
+                OpCode::OpGetLocal(local_idx) => {
+                    let value = self.stack[local_idx.0].clone();
+                    self.push(value);
+                }
+
+                OpCode::OpSetLocal(local_idx) => {
+                    debug_assert!(
+                        self.stack.len() > local_idx.0,
+                        "stack is not currently large enough for local"
+                    );
+                    self.stack[local_idx.0] = self.stack.last().unwrap().clone();
+                }
+
+                OpCode::OpJumpPlaceholder(_) => {
+                    panic!("unpatched jump detected - this is a fatal compiler error!");
+                }
+
+                OpCode::OpJump(offset) => {
+                    self.ip += offset.0;
+                }
+
+                OpCode::OpJumpIfFalse(offset) => {
+                    if self
+                        .stack
+                        .last()
+                        .expect("condition should leave a value on the stack")
+                        .is_falsey()
+                    {
+                        self.ip += offset.0;
+                    }
+                }
+            }
+
+            #[cfg(feature = "disassemble")]
+            println!("=> {:?}", self.stack);
+        }
+    }
+
+    // For some types of values (e.g. interned strings), returns
+    // should no longer include any references into the interpreter.
+    fn return_value(&self, val: Value) -> Value {
+        match val {
+            Value::String(string @ LoxString::Interned(_)) => {
+                Value::String(self.resolve_str(&string).to_string().into())
+            }
+            _ => val,
+        }
+    }
+
+    fn resolve_str<'a>(&'a self, string: &'a LoxString) -> &'a str {
+        match string {
+            LoxString::Heap(s) => s.as_str(),
+            LoxString::Interned(id) => self.strings.lookup(*id),
+        }
+    }
+
+    fn print_value(&self, val: Value) -> String {
+        match val {
+            Value::String(LoxString::Heap(s)) => s,
+            Value::String(LoxString::Interned(id)) => self.strings.lookup(id).into(),
+            _ => format!("{:?}", val),
+        }
+    }
+}
+
+pub fn interpret(strings: Interner, chunk: chunk::Chunk) -> LoxResult<Value> {
+    let mut vm = VM {
+        chunk,
+        strings,
+        globals: HashMap::new(),
+        ip: 0,
+        stack: vec![],
+        last_drop: None,
+    };
+
+    vm.run()
+}
diff --git a/users/tazjin/rlox/src/main.rs b/users/tazjin/rlox/src/main.rs
new file mode 100644
index 0000000000..ee61ae01a1
--- /dev/null
+++ b/users/tazjin/rlox/src/main.rs
@@ -0,0 +1,71 @@
+use std::io::Write;
+use std::{env, fs, io, process};
+
+mod bytecode;
+mod scanner;
+mod treewalk;
+
+/// Trait for making the different interpreters callable in the same
+/// way.
+pub trait Lox {
+    type Value: std::fmt::Debug;
+    type Error: std::fmt::Display;
+
+    fn create() -> Self;
+    fn interpret(&mut self, source: String) -> Result<Self::Value, Vec<Self::Error>>;
+}
+
+fn main() {
+    let mut args = env::args();
+    if args.len() > 2 {
+        println!("Usage: rlox [script]");
+        process::exit(1);
+    }
+
+    match env::var("LOX_INTERPRETER").as_ref().map(String::as_str) {
+        Ok("treewalk") => pick::<treewalk::interpreter::Interpreter>(args.nth(1)),
+        _ => pick::<bytecode::Interpreter>(args.nth(1)),
+    }
+}
+
+fn pick<I: Lox>(file_arg: Option<String>) {
+    if let Some(file) = file_arg {
+        run_file::<I>(&file);
+    } else {
+        run_prompt::<I>();
+    }
+}
+
+// Run Lox code from a file and print results to stdout
+fn run_file<I: Lox>(file: &str) {
+    let contents = fs::read_to_string(file).expect("failed to read the input file");
+    let mut lox = I::create();
+    run(&mut lox, contents);
+}
+
+// Evaluate Lox code interactively in a shitty REPL.
+fn run_prompt<I: Lox>() {
+    let mut line = String::new();
+    let mut lox = I::create();
+
+    loop {
+        print!("> ");
+        io::stdout().flush().unwrap();
+        io::stdin()
+            .read_line(&mut line)
+            .expect("failed to read user input");
+        run(&mut lox, std::mem::take(&mut line));
+        line.clear();
+    }
+}
+
+fn run<I: Lox>(lox: &mut I, code: String) {
+    match lox.interpret(code) {
+        Ok(result) => println!("=> {:?}", result),
+        Err(errors) => {
+            for error in errors {
+                eprintln!("{}", error);
+            }
+        }
+    }
+}
diff --git a/users/tazjin/rlox/src/scanner.rs b/users/tazjin/rlox/src/scanner.rs
new file mode 100644
index 0000000000..314b56d6d3
--- /dev/null
+++ b/users/tazjin/rlox/src/scanner.rs
@@ -0,0 +1,284 @@
+#[derive(Clone, Debug, PartialEq)]
+pub enum TokenKind {
+    // Single-character tokens.
+    LeftParen,
+    RightParen,
+    LeftBrace,
+    RightBrace,
+    Comma,
+    Dot,
+    Minus,
+    Plus,
+    Semicolon,
+    Slash,
+    Star,
+
+    // One or two character tokens.
+    Bang,
+    BangEqual,
+    Equal,
+    EqualEqual,
+    Greater,
+    GreaterEqual,
+    Less,
+    LessEqual,
+
+    // Literals.
+    Identifier(String),
+    String(String),
+    Number(f64),
+    True,
+    False,
+    Nil,
+
+    // Keywords.
+    And,
+    Class,
+    Else,
+    Fun,
+    For,
+    If,
+    Or,
+    Print,
+    Return,
+    Super,
+    This,
+    Var,
+    While,
+
+    // Special things
+    Eof,
+}
+
+#[derive(Clone, Debug)]
+pub struct Token {
+    pub kind: TokenKind,
+    pub lexeme: String,
+    pub line: usize,
+}
+
+pub enum ScannerError {
+    UnexpectedChar { line: usize, unexpected: char },
+    UnterminatedString { line: usize },
+}
+
+struct Scanner<'a> {
+    source: &'a [char],
+    tokens: Vec<Token>,
+    errors: Vec<ScannerError>,
+    start: usize,   // offset of first character in current lexeme
+    current: usize, // current offset into source
+    line: usize,    // current line in source
+}
+
+impl<'a> Scanner<'a> {
+    fn is_at_end(&self) -> bool {
+        return self.current >= self.source.len();
+    }
+
+    fn advance(&mut self) -> char {
+        self.current += 1;
+        self.source[self.current - 1]
+    }
+
+    fn add_token(&mut self, kind: TokenKind) {
+        let lexeme = &self.source[self.start..self.current];
+        self.tokens.push(Token {
+            kind,
+            lexeme: lexeme.into_iter().collect(),
+            line: self.line,
+        })
+    }
+
+    fn scan_token(&mut self) {
+        match self.advance() {
+            // simple single-character tokens
+            '(' => self.add_token(TokenKind::LeftParen),
+            ')' => self.add_token(TokenKind::RightParen),
+            '{' => self.add_token(TokenKind::LeftBrace),
+            '}' => self.add_token(TokenKind::RightBrace),
+            ',' => self.add_token(TokenKind::Comma),
+            '.' => self.add_token(TokenKind::Dot),
+            '-' => self.add_token(TokenKind::Minus),
+            '+' => self.add_token(TokenKind::Plus),
+            ';' => self.add_token(TokenKind::Semicolon),
+            '*' => self.add_token(TokenKind::Star),
+
+            // possible multi-character tokens
+            '!' => self.add_if_next('=', TokenKind::BangEqual, TokenKind::Bang),
+            '=' => self.add_if_next('=', TokenKind::EqualEqual, TokenKind::Equal),
+            '<' => self.add_if_next('=', TokenKind::LessEqual, TokenKind::Less),
+            '>' => self.add_if_next('=', TokenKind::GreaterEqual, TokenKind::Greater),
+
+            '/' => {
+                // support comments until EOL by discarding characters
+                if self.match_next('/') {
+                    while self.peek() != '\n' && !self.is_at_end() {
+                        self.advance();
+                    }
+                } else {
+                    self.add_token(TokenKind::Slash);
+                }
+            }
+
+            // ignore whitespace
+            ws if ws.is_whitespace() => {
+                if ws == '\n' {
+                    self.line += 1
+                }
+            }
+
+            '"' => self.scan_string(),
+
+            digit if digit.is_digit(10) => self.scan_number(),
+
+            chr if chr.is_alphabetic() || chr == '_' => self.scan_identifier(),
+
+            unexpected => self.errors.push(ScannerError::UnexpectedChar {
+                line: self.line,
+                unexpected,
+            }),
+        };
+    }
+
+    fn match_next(&mut self, expected: char) -> bool {
+        if self.is_at_end() || self.source[self.current] != expected {
+            false
+        } else {
+            self.current += 1;
+            true
+        }
+    }
+
+    fn add_if_next(&mut self, expected: char, then: TokenKind, or: TokenKind) {
+        if self.match_next(expected) {
+            self.add_token(then);
+        } else {
+            self.add_token(or);
+        }
+    }
+
+    fn peek(&self) -> char {
+        if self.is_at_end() {
+            return '\0';
+        } else {
+            return self.source[self.current];
+        }
+    }
+
+    fn peek_next(&self) -> char {
+        if self.current + 1 >= self.source.len() {
+            return '\0';
+        } else {
+            return self.source[self.current + 1];
+        }
+    }
+
+    fn scan_string(&mut self) {
+        while self.peek() != '"' && !self.is_at_end() {
+            if self.peek() == '\n' {
+                self.line += 1;
+            }
+
+            self.advance();
+        }
+
+        if self.is_at_end() {
+            self.errors
+                .push(ScannerError::UnterminatedString { line: self.line });
+            return;
+        }
+
+        // closing '"'
+        self.advance();
+
+        // add token without surrounding quotes
+        let string: String = self.source[(self.start + 1)..(self.current - 1)]
+            .iter()
+            .collect();
+        self.add_token(TokenKind::String(string));
+    }
+
+    fn scan_number(&mut self) {
+        while self.peek().is_digit(10) {
+            self.advance();
+        }
+
+        // Look for a fractional part
+        if self.peek() == '.' && self.peek_next().is_digit(10) {
+            // consume '.'
+            self.advance();
+
+            while self.peek().is_digit(10) {
+                self.advance();
+            }
+        }
+
+        let num: f64 = self.source[self.start..self.current]
+            .iter()
+            .collect::<String>()
+            .parse()
+            .expect("float parsing should always work");
+
+        self.add_token(TokenKind::Number(num));
+    }
+
+    fn scan_identifier(&mut self) {
+        while self.peek().is_alphanumeric() || self.peek() == '_' {
+            self.advance();
+        }
+
+        let ident: String = self.source[self.start..self.current].iter().collect();
+
+        // Determine whether this is an identifier, or a keyword:
+        let token_kind = match ident.as_str() {
+            "and" => TokenKind::And,
+            "class" => TokenKind::Class,
+            "else" => TokenKind::Else,
+            "false" => TokenKind::False,
+            "for" => TokenKind::For,
+            "fun" => TokenKind::Fun,
+            "if" => TokenKind::If,
+            "nil" => TokenKind::Nil,
+            "or" => TokenKind::Or,
+            "print" => TokenKind::Print,
+            "return" => TokenKind::Return,
+            "super" => TokenKind::Super,
+            "this" => TokenKind::This,
+            "true" => TokenKind::True,
+            "var" => TokenKind::Var,
+            "while" => TokenKind::While,
+            _ => TokenKind::Identifier(ident),
+        };
+
+        self.add_token(token_kind);
+    }
+
+    fn scan_tokens(&mut self) {
+        while !self.is_at_end() {
+            self.start = self.current;
+            self.scan_token();
+        }
+
+        self.add_token(TokenKind::Eof);
+    }
+}
+
+pub fn scan<'a>(input: &'a [char]) -> Result<Vec<Token>, Vec<ScannerError>> {
+    let mut scanner = Scanner {
+        source: &input,
+        tokens: vec![],
+        errors: vec![],
+        start: 0,
+        current: 0,
+        line: 0,
+    };
+
+    scanner.scan_tokens();
+
+    if !scanner.errors.is_empty() {
+        return Err(scanner.errors);
+    }
+
+    return Ok(scanner.tokens);
+}
diff --git a/users/tazjin/rlox/src/treewalk/errors.rs b/users/tazjin/rlox/src/treewalk/errors.rs
new file mode 100644
index 0000000000..391663d51b
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/errors.rs
@@ -0,0 +1,59 @@
+use crate::scanner::ScannerError;
+use crate::treewalk::interpreter::Value;
+
+use std::fmt;
+
+#[derive(Debug)]
+pub enum ErrorKind {
+    UnexpectedChar(char),
+    UnterminatedString,
+    UnmatchedParens,
+    ExpectedExpression(String),
+    ExpectedSemicolon,
+    ExpectedClosingBrace,
+    ExpectedToken(&'static str),
+    TypeError(String),
+    UndefinedVariable(String),
+    InternalError(String),
+    InvalidAssignmentTarget(String),
+    RuntimeError(String),
+    StaticError(String),
+
+    // This variant is not an error, rather it is used for
+    // short-circuiting out of a function body that hits a `return`
+    // statement.
+    //
+    // It's implemented this way because in the original book the
+    // author uses exceptions for control flow, and this is the
+    // closest equivalent that I had available without diverging too
+    // much.
+    FunctionReturn(Value),
+}
+
+#[derive(Debug)]
+pub struct Error {
+    pub line: usize,
+    pub kind: ErrorKind,
+}
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "[line {}] Error: {:?}", self.line, self.kind)
+    }
+}
+
+impl From<ScannerError> for Error {
+    fn from(err: ScannerError) -> Self {
+        match err {
+            ScannerError::UnexpectedChar { line, unexpected } => Error {
+                line,
+                kind: ErrorKind::UnexpectedChar(unexpected),
+            },
+
+            ScannerError::UnterminatedString { line } => Error {
+                line,
+                kind: ErrorKind::UnterminatedString,
+            },
+        }
+    }
+}
diff --git a/users/tazjin/rlox/src/treewalk/interpreter.rs b/users/tazjin/rlox/src/treewalk/interpreter.rs
new file mode 100644
index 0000000000..3285775bbe
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/interpreter.rs
@@ -0,0 +1,498 @@
+use crate::treewalk::errors::{Error, ErrorKind};
+use crate::treewalk::parser::{self, Block, Expr, Literal, Statement};
+use crate::treewalk::resolver;
+use crate::treewalk::scanner::{self, TokenKind};
+use crate::Lox;
+use std::collections::HashMap;
+use std::rc::Rc;
+use std::sync::RwLock;
+
+// Implementation of built-in functions.
+mod builtins;
+
+#[cfg(test)]
+mod tests;
+
+// Tree-walk interpreter
+
+// Representation of all callables, including builtins & user-defined
+// functions.
+#[derive(Clone, Debug)]
+pub enum Callable {
+    Builtin(&'static dyn builtins::Builtin),
+    Function {
+        func: Rc<parser::Function>,
+        closure: Rc<RwLock<Environment>>,
+    },
+}
+
+impl Callable {
+    fn arity(&self) -> usize {
+        match self {
+            Callable::Builtin(builtin) => builtin.arity(),
+            Callable::Function { func, .. } => func.params.len(),
+        }
+    }
+
+    fn call(&self, lox: &mut Interpreter, args: Vec<Value>) -> Result<Value, Error> {
+        match self {
+            Callable::Builtin(builtin) => builtin.call(args),
+
+            Callable::Function { func, closure } => {
+                let mut fn_env: Environment = Default::default();
+                fn_env.enclosing = Some(closure.clone());
+
+                for (param, value) in func.params.iter().zip(args.into_iter()) {
+                    fn_env.define(param, value)?;
+                }
+
+                let result =
+                    lox.interpret_block_with_env(Some(Rc::new(RwLock::new(fn_env))), &func.body);
+
+                match result {
+                    // extract returned values if applicable
+                    Err(Error {
+                        kind: ErrorKind::FunctionReturn(value),
+                        ..
+                    }) => Ok(value),
+
+                    // otherwise just return the result itself
+                    _ => result,
+                }
+            }
+        }
+    }
+}
+
+// Representation of an in-language value.
+#[derive(Clone, Debug)]
+pub enum Value {
+    Literal(Literal),
+    Callable(Callable),
+}
+
+impl PartialEq for Value {
+    fn eq(&self, other: &Self) -> bool {
+        match (self, other) {
+            (Value::Literal(lhs), Value::Literal(rhs)) => lhs == rhs,
+            // functions do not have equality
+            _ => false,
+        }
+    }
+}
+
+impl From<Literal> for Value {
+    fn from(lit: Literal) -> Value {
+        Value::Literal(lit)
+    }
+}
+
+impl Value {
+    fn expect_literal(self) -> Result<Literal, Error> {
+        match self {
+            Value::Literal(lit) => Ok(lit),
+            _ => unimplemented!(), // which error? which line?
+        }
+    }
+}
+
+#[derive(Debug, Default)]
+pub struct Environment {
+    enclosing: Option<Rc<RwLock<Environment>>>,
+    values: HashMap<String, Value>,
+}
+
+impl Environment {
+    fn define(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> {
+        let ident = identifier_str(name)?;
+        self.values.insert(ident.into(), value);
+        Ok(())
+    }
+
+    fn get(&self, ident: &str, line: usize, depth: usize) -> Result<Value, Error> {
+        if depth > 0 {
+            match &self.enclosing {
+                None => {
+                    return Err(Error {
+                        line,
+                        kind: ErrorKind::InternalError(format!(
+                            "invalid depth {} for {}",
+                            depth, ident
+                        )),
+                    })
+                }
+                Some(parent) => {
+                    let env = parent.read().expect("fatal: environment lock poisoned");
+                    return env.get(ident, line, depth - 1);
+                }
+            }
+        }
+
+        self.values
+            .get(ident)
+            .map(Clone::clone)
+            .ok_or_else(|| Error {
+                line,
+                kind: ErrorKind::UndefinedVariable(ident.into()),
+            })
+    }
+
+    fn assign(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> {
+        let ident = identifier_str(name)?;
+
+        match self.values.get_mut(ident) {
+            Some(target) => {
+                *target = value;
+                Ok(())
+            }
+            None => {
+                if let Some(parent) = &self.enclosing {
+                    return parent.write().unwrap().assign(name, value);
+                }
+
+                Err(Error {
+                    line: name.line,
+                    kind: ErrorKind::UndefinedVariable(ident.into()),
+                })
+            }
+        }
+    }
+}
+
+fn identifier_str(name: &scanner::Token) -> Result<&str, Error> {
+    if let TokenKind::Identifier(ident) = &name.kind {
+        Ok(ident)
+    } else {
+        Err(Error {
+            line: name.line,
+            kind: ErrorKind::InternalError("unexpected identifier kind".into()),
+        })
+    }
+}
+
+#[derive(Debug)]
+pub struct Interpreter {
+    env: Rc<RwLock<Environment>>,
+}
+
+impl Lox for Interpreter {
+    type Value = Value;
+    type Error = Error;
+
+    /// Create a new interpreter and configure the initial global
+    /// variable set.
+    fn create() -> Self {
+        let mut globals = HashMap::new();
+
+        globals.insert(
+            "clock".into(),
+            Value::Callable(Callable::Builtin(&builtins::Clock {})),
+        );
+
+        Interpreter {
+            env: Rc::new(RwLock::new(Environment {
+                enclosing: None,
+                values: globals,
+            })),
+        }
+    }
+
+    fn interpret(&mut self, code: String) -> Result<Value, Vec<Error>> {
+        let chars: Vec<char> = code.chars().collect();
+
+        let mut program = scanner::scan(&chars)
+            .map_err(|errors| errors.into_iter().map(Into::into).collect())
+            .and_then(|tokens| parser::parse(tokens))?;
+
+        let globals = self
+            .env
+            .read()
+            .expect("static globals lock poisoned")
+            .values
+            .keys()
+            .map(Clone::clone)
+            .collect::<Vec<String>>();
+
+        resolver::resolve(&globals, &mut program).map_err(|e| vec![e])?;
+        self.interpret_block_with_env(None, &program)
+            .map_err(|e| vec![e])
+    }
+}
+
+impl Interpreter {
+    // Environment modification helpers
+    fn define_var(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> {
+        self.env
+            .write()
+            .expect("environment lock is poisoned")
+            .define(name, value)
+    }
+
+    fn assign_var(&mut self, name: &scanner::Token, value: Value) -> Result<(), Error> {
+        self.env
+            .write()
+            .expect("environment lock is poisoned")
+            .assign(name, value)
+    }
+
+    fn get_var(&mut self, var: &parser::Variable) -> Result<Value, Error> {
+        let ident = identifier_str(&var.name)?;
+        let depth = var.depth.ok_or_else(|| Error {
+            line: var.name.line,
+            kind: ErrorKind::UndefinedVariable(ident.into()),
+        })?;
+
+        self.env
+            .read()
+            .expect("environment lock is poisoned")
+            .get(ident, var.name.line, depth)
+    }
+
+    /// Interpret the block in the supplied environment. If no
+    /// environment is supplied, a new one is created using the
+    /// current one as its parent.
+    fn interpret_block_with_env(
+        &mut self,
+        env: Option<Rc<RwLock<Environment>>>,
+        block: &parser::Block,
+    ) -> Result<Value, Error> {
+        let env = match env {
+            Some(env) => env,
+            None => {
+                let env: Rc<RwLock<Environment>> = Default::default();
+                set_enclosing_env(&env, self.env.clone());
+                env
+            }
+        };
+
+        let previous = std::mem::replace(&mut self.env, env);
+        let result = self.interpret_block(block);
+
+        // Swap it back, discarding the child env.
+        self.env = previous;
+
+        return result;
+    }
+
+    fn interpret_block(&mut self, program: &Block) -> Result<Value, Error> {
+        let mut value = Value::Literal(Literal::Nil);
+
+        for stmt in program {
+            value = self.interpret_stmt(stmt)?;
+        }
+
+        Ok(value)
+    }
+
+    fn interpret_stmt(&mut self, stmt: &Statement) -> Result<Value, Error> {
+        let value = match stmt {
+            Statement::Expr(expr) => self.eval(expr)?,
+            Statement::Print(expr) => {
+                let result = self.eval(expr)?;
+                let output = format!("{:?}", result);
+                println!("{}", output);
+                Value::Literal(Literal::String(output))
+            }
+            Statement::Var(var) => return self.interpret_var(var),
+            Statement::Block(block) => return self.interpret_block_with_env(None, block),
+            Statement::If(if_stmt) => return self.interpret_if(if_stmt),
+            Statement::While(while_stmt) => return self.interpret_while(while_stmt),
+            Statement::Function(func) => return self.interpret_function(func.clone()),
+            Statement::Return(ret) => {
+                return Err(Error {
+                    line: 0,
+                    kind: ErrorKind::FunctionReturn(self.eval(&ret.value)?),
+                })
+            }
+        };
+
+        Ok(value)
+    }
+
+    fn interpret_var(&mut self, var: &parser::Var) -> Result<Value, Error> {
+        let init = var.initialiser.as_ref().ok_or_else(|| Error {
+            line: var.name.line,
+            kind: ErrorKind::InternalError("missing variable initialiser".into()),
+        })?;
+        let value = self.eval(init)?;
+        self.define_var(&var.name, value.clone())?;
+        Ok(value)
+    }
+
+    fn interpret_if(&mut self, if_stmt: &parser::If) -> Result<Value, Error> {
+        let condition = self.eval(&if_stmt.condition)?;
+
+        if eval_truthy(&condition) {
+            self.interpret_stmt(&if_stmt.then_branch)
+        } else if let Some(else_branch) = &if_stmt.else_branch {
+            self.interpret_stmt(else_branch)
+        } else {
+            Ok(Value::Literal(Literal::Nil))
+        }
+    }
+
+    fn interpret_while(&mut self, stmt: &parser::While) -> Result<Value, Error> {
+        let mut value = Value::Literal(Literal::Nil);
+        while eval_truthy(&self.eval(&stmt.condition)?) {
+            value = self.interpret_stmt(&stmt.body)?;
+        }
+
+        Ok(value)
+    }
+
+    fn interpret_function(&mut self, func: Rc<parser::Function>) -> Result<Value, Error> {
+        let name = func.name.clone();
+        let value = Value::Callable(Callable::Function {
+            func,
+            closure: self.env.clone(),
+        });
+        self.define_var(&name, value.clone())?;
+        Ok(value)
+    }
+
+    fn eval(&mut self, expr: &Expr) -> Result<Value, Error> {
+        match expr {
+            Expr::Assign(assign) => self.eval_assign(assign),
+            Expr::Literal(lit) => Ok(lit.clone().into()),
+            Expr::Grouping(grouping) => self.eval(&*grouping.0),
+            Expr::Unary(unary) => self.eval_unary(unary),
+            Expr::Binary(binary) => self.eval_binary(binary),
+            Expr::Variable(var) => self.get_var(var),
+            Expr::Logical(log) => self.eval_logical(log),
+            Expr::Call(call) => self.eval_call(call),
+        }
+    }
+
+    fn eval_unary(&mut self, expr: &parser::Unary) -> Result<Value, Error> {
+        let right = self.eval(&*expr.right)?;
+
+        match (&expr.operator.kind, right) {
+            (TokenKind::Minus, Value::Literal(Literal::Number(num))) => {
+                Ok(Literal::Number(-num).into())
+            }
+            (TokenKind::Bang, right) => Ok(Literal::Boolean(!eval_truthy(&right)).into()),
+
+            (op, right) => Err(Error {
+                line: expr.operator.line,
+                kind: ErrorKind::TypeError(format!(
+                    "Operator '{:?}' can not be called with argument '{:?}'",
+                    op, right
+                )),
+            }),
+        }
+    }
+
+    fn eval_binary(&mut self, expr: &parser::Binary) -> Result<Value, Error> {
+        let left = self.eval(&*expr.left)?.expect_literal()?;
+        let right = self.eval(&*expr.right)?.expect_literal()?;
+
+        let result = match (&expr.operator.kind, left, right) {
+            // Numeric
+            (TokenKind::Minus, Literal::Number(l), Literal::Number(r)) => Literal::Number(l - r),
+            (TokenKind::Slash, Literal::Number(l), Literal::Number(r)) => Literal::Number(l / r),
+            (TokenKind::Star, Literal::Number(l), Literal::Number(r)) => Literal::Number(l * r),
+            (TokenKind::Plus, Literal::Number(l), Literal::Number(r)) => Literal::Number(l + r),
+
+            // Strings
+            (TokenKind::Plus, Literal::String(l), Literal::String(r)) => {
+                Literal::String(format!("{}{}", l, r))
+            }
+
+            // Comparators (on numbers only?)
+            (TokenKind::Greater, Literal::Number(l), Literal::Number(r)) => Literal::Boolean(l > r),
+            (TokenKind::GreaterEqual, Literal::Number(l), Literal::Number(r)) => {
+                Literal::Boolean(l >= r)
+            }
+            (TokenKind::Less, Literal::Number(l), Literal::Number(r)) => Literal::Boolean(l < r),
+            (TokenKind::LessEqual, Literal::Number(l), Literal::Number(r)) => {
+                Literal::Boolean(l <= r)
+            }
+
+            // Equality
+            (TokenKind::Equal, l, r) => Literal::Boolean(l == r),
+            (TokenKind::BangEqual, l, r) => Literal::Boolean(l != r),
+
+            (op, left, right) => {
+                return Err(Error {
+                    line: expr.operator.line,
+                    kind: ErrorKind::TypeError(format!(
+                        "Operator '{:?}' can not be called with arguments '({:?}, {:?})'",
+                        op, left, right
+                    )),
+                })
+            }
+        };
+
+        Ok(result.into())
+    }
+
+    fn eval_assign(&mut self, assign: &parser::Assign) -> Result<Value, Error> {
+        let value = self.eval(&assign.value)?;
+        self.assign_var(&assign.name, value.clone())?;
+        Ok(value)
+    }
+
+    fn eval_logical(&mut self, logical: &parser::Logical) -> Result<Value, Error> {
+        let left = eval_truthy(&self.eval(&logical.left)?);
+        let right = eval_truthy(&self.eval(&logical.right)?);
+
+        match &logical.operator.kind {
+            TokenKind::And => Ok(Literal::Boolean(left && right).into()),
+            TokenKind::Or => Ok(Literal::Boolean(left || right).into()),
+            kind => Err(Error {
+                line: logical.operator.line,
+                kind: ErrorKind::InternalError(format!("Invalid logical operator: {:?}", kind)),
+            }),
+        }
+    }
+
+    fn eval_call(&mut self, call: &parser::Call) -> Result<Value, Error> {
+        let callable = match self.eval(&call.callee)? {
+            Value::Callable(c) => c,
+            Value::Literal(v) => {
+                return Err(Error {
+                    line: call.paren.line,
+                    kind: ErrorKind::RuntimeError(format!("not callable: {:?}", v)),
+                })
+            }
+        };
+
+        let mut args = vec![];
+        for arg in &call.args {
+            args.push(self.eval(arg)?);
+        }
+
+        if callable.arity() != args.len() {
+            return Err(Error {
+                line: call.paren.line,
+                kind: ErrorKind::RuntimeError(format!(
+                    "Expected {} arguments, but got {}",
+                    callable.arity(),
+                    args.len(),
+                )),
+            });
+        }
+
+        callable.call(self, args)
+    }
+}
+
+// Interpreter functions not dependent on interpreter-state.
+
+fn eval_truthy(lit: &Value) -> bool {
+    if let Value::Literal(lit) = lit {
+        match lit {
+            Literal::Nil => false,
+            Literal::Boolean(b) => *b,
+            _ => true,
+        }
+    } else {
+        false
+    }
+}
+
+fn set_enclosing_env(this: &RwLock<Environment>, parent: Rc<RwLock<Environment>>) {
+    this.write()
+        .expect("environment lock is poisoned")
+        .enclosing = Some(parent);
+}
diff --git a/users/tazjin/rlox/src/treewalk/interpreter/builtins.rs b/users/tazjin/rlox/src/treewalk/interpreter/builtins.rs
new file mode 100644
index 0000000000..c502d2a171
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/interpreter/builtins.rs
@@ -0,0 +1,25 @@
+use std::fmt;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use crate::treewalk::errors::Error;
+use crate::treewalk::interpreter::Value;
+use crate::treewalk::parser::Literal;
+
+pub trait Builtin: fmt::Debug {
+    fn arity(&self) -> usize;
+    fn call(&self, args: Vec<Value>) -> Result<Value, Error>;
+}
+
+// Builtin to return the current timestamp.
+#[derive(Debug)]
+pub struct Clock {}
+impl Builtin for Clock {
+    fn arity(&self) -> usize {
+        0
+    }
+
+    fn call(&self, _args: Vec<Value>) -> Result<Value, Error> {
+        let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
+        Ok(Value::Literal(Literal::Number(now.as_secs() as f64)))
+    }
+}
diff --git a/users/tazjin/rlox/src/treewalk/interpreter/tests.rs b/users/tazjin/rlox/src/treewalk/interpreter/tests.rs
new file mode 100644
index 0000000000..2fc6f4fee9
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/interpreter/tests.rs
@@ -0,0 +1,97 @@
+use super::*;
+
+/// Evaluate a code snippet, returning a value.
+fn parse_eval(code: &str) -> Value {
+    Interpreter::create()
+        .interpret(code.into())
+        .expect("could not interpret code")
+}
+
+#[test]
+fn test_if() {
+    let result = parse_eval(
+        r#"
+if (42 > 23)
+  "pass";
+else
+  "fail";
+"#,
+    );
+
+    assert_eq!(Value::Literal(Literal::String("pass".into())), result,);
+}
+
+#[test]
+fn test_scope() {
+    let result = parse_eval(
+        r#"
+var result = "";
+
+var a = "global a, ";
+var b = "global b, ";
+var c = "global c";
+
+{
+  var a = "outer a, ";
+  var b = "outer b, ";
+
+  {
+    var a = "inner a, ";
+    result = a + b + c;
+  }
+}
+"#,
+    );
+
+    assert_eq!(
+        Value::Literal(Literal::String("inner a, outer b, global c".into())),
+        result,
+    );
+}
+
+#[test]
+fn test_binary_operators() {
+    assert_eq!(Value::Literal(Literal::Number(42.0)), parse_eval("40 + 2;"));
+
+    assert_eq!(
+        Value::Literal(Literal::String("foobar".into())),
+        parse_eval("\"foo\" + \"bar\";")
+    );
+}
+
+#[test]
+fn test_functions() {
+    let result = parse_eval(
+        r#"
+fun add(a, b, c) {
+  a + b + c;
+}
+
+add(1, 2, 3);
+"#,
+    );
+
+    assert_eq!(Value::Literal(Literal::Number(6.0)), result);
+}
+
+#[test]
+fn test_closure() {
+    let result = parse_eval(
+        r#"
+fun makeCounter() {
+  var i = 0;
+  fun count() {
+    i = i + 1;
+  }
+
+  return count;
+}
+
+var counter = makeCounter();
+counter(); // "1".
+counter(); // "2".
+"#,
+    );
+
+    assert_eq!(Value::Literal(Literal::Number(2.0)), result);
+}
diff --git a/users/tazjin/rlox/src/treewalk/mod.rs b/users/tazjin/rlox/src/treewalk/mod.rs
new file mode 100644
index 0000000000..2d82b3320a
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/mod.rs
@@ -0,0 +1,6 @@
+use crate::scanner;
+
+mod errors;
+pub mod interpreter;
+mod parser;
+mod resolver;
diff --git a/users/tazjin/rlox/src/treewalk/parser.rs b/users/tazjin/rlox/src/treewalk/parser.rs
new file mode 100644
index 0000000000..5794b42d15
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/parser.rs
@@ -0,0 +1,700 @@
+// This implements the grammar of Lox as described starting in the
+// Crafting Interpreters chapter "Representing Code". Note that the
+// upstream Java implementation works around Java being bad at value
+// classes by writing a code generator for Java.
+//
+// My Rust implementation skips this step because it's unnecessary, we
+// have real types.
+use crate::treewalk::errors::{Error, ErrorKind};
+use crate::treewalk::scanner::{Token, TokenKind};
+use std::rc::Rc;
+
+// AST
+
+#[derive(Debug)]
+pub struct Assign {
+    pub name: Token,
+    pub value: Box<Expr>,
+    pub depth: Option<usize>,
+}
+
+#[derive(Debug)]
+pub struct Binary {
+    pub left: Box<Expr>,
+    pub operator: Token,
+    pub right: Box<Expr>,
+}
+
+#[derive(Debug)]
+pub struct Logical {
+    pub left: Box<Expr>,
+    pub operator: Token,
+    pub right: Box<Expr>,
+}
+
+#[derive(Debug)]
+pub struct Grouping(pub Box<Expr>);
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum Literal {
+    Boolean(bool),
+    Number(f64),
+    String(String),
+    Nil,
+}
+
+#[derive(Debug)]
+pub struct Unary {
+    pub operator: Token,
+    pub right: Box<Expr>,
+}
+
+#[derive(Debug)]
+pub struct Call {
+    pub callee: Box<Expr>,
+    pub paren: Token,
+    pub args: Vec<Expr>,
+}
+
+// Not to be confused with `Var`, which is for assignment.
+#[derive(Debug)]
+pub struct Variable {
+    pub name: Token,
+    pub depth: Option<usize>,
+}
+
+#[derive(Debug)]
+pub enum Expr {
+    Assign(Assign),
+    Binary(Binary),
+    Grouping(Grouping),
+    Literal(Literal),
+    Unary(Unary),
+    Call(Call),
+    Variable(Variable),
+    Logical(Logical),
+}
+
+// Variable assignment. Not to be confused with `Variable`, which is
+// for access.
+#[derive(Debug)]
+pub struct Var {
+    pub name: Token,
+    pub initialiser: Option<Expr>,
+}
+
+#[derive(Debug)]
+pub struct Return {
+    pub value: Expr,
+}
+
+#[derive(Debug)]
+pub struct If {
+    pub condition: Expr,
+    pub then_branch: Box<Statement>,
+    pub else_branch: Option<Box<Statement>>,
+}
+
+#[derive(Debug)]
+pub struct While {
+    pub condition: Expr,
+    pub body: Box<Statement>,
+}
+
+pub type Block = Vec<Statement>;
+
+#[derive(Debug)]
+pub struct Function {
+    pub name: Token,
+    pub params: Vec<Token>,
+    pub body: Block,
+}
+
+#[derive(Debug)]
+pub enum Statement {
+    Expr(Expr),
+    Print(Expr),
+    Var(Var),
+    Block(Block),
+    If(If),
+    While(While),
+    Function(Rc<Function>),
+    Return(Return),
+}
+
+// Parser
+
+// program        โ†’ declaration* EOF ;
+//
+// declaration    โ†’ funDecl
+// | varDecl
+// | statement ;
+//
+// funDecl        โ†’ "fun" function ;
+// function       โ†’ IDENTIFIER "(" parameters? ")" block ;
+// parameters     โ†’ IDENTIFIER ( "," IDENTIFIER )* ;
+//
+//
+// statement      โ†’ exprStmt
+// | forStmt
+// | ifStmt
+// | printStmt
+// | returnStmt
+// | whileStmt
+// | block ;
+//
+// forStmt        โ†’ "for" "(" ( varDecl | exprStmt | ";" )
+// expression? ";"
+// expression? ")" statement ;
+//
+// returnStmt     โ†’ "return" expression? ";" ;
+//
+// whileStmt      โ†’ "while" "(" expression ")" statement ;
+//
+// exprStmt       โ†’ expression ";" ;
+//
+// ifStmt         โ†’ "if" "(" expression ")" statement
+// ( "else" statement )? ;
+//
+// printStmt      โ†’ "print" expression ";" ;
+//
+// expression     โ†’ assignment ;
+// assignment     โ†’ IDENTIFIER "=" assignment
+// | logic_or ;
+// logic_or       โ†’ logic_and ( "or" logic_and )* ;
+// logic_and      โ†’ equality ( "and" equality )* ;
+// equality       โ†’ comparison ( ( "!=" | "==" ) comparison )* ;
+// comparison     โ†’ term ( ( ">" | ">=" | "<" | "<=" ) term )* ;
+// term           โ†’ factor ( ( "-" | "+" ) factor )* ;
+// factor         โ†’ unary ( ( "/" | "*" ) unary )* ;
+// unary          โ†’ ( "!" | "-" ) unary | call ;
+// call           โ†’ primary ( "(" arguments? ")" )* ;
+// arguments      โ†’ expression ( "," expression )* ;
+// primary        โ†’ NUMBER | STRING | "true" | "false" | "nil"
+// | "(" expression ")" ;
+
+struct Parser {
+    tokens: Vec<Token>,
+    current: usize,
+}
+
+type ExprResult = Result<Expr, Error>;
+type StmtResult = Result<Statement, Error>;
+
+impl Parser {
+    // recursive-descent parser functions
+
+    fn declaration(&mut self) -> StmtResult {
+        if self.match_token(&TokenKind::Fun) {
+            return self.function();
+        }
+
+        if self.match_token(&TokenKind::Var) {
+            return self.var_declaration();
+        }
+
+        self.statement()
+    }
+
+    fn function(&mut self) -> StmtResult {
+        let name = self.identifier("Expected function name.")?;
+
+        self.consume(
+            &TokenKind::LeftParen,
+            ErrorKind::ExpectedToken("Expect '(' after function name."),
+        )?;
+
+        let mut params = vec![];
+
+        if !self.check_token(&TokenKind::RightParen) {
+            loop {
+                if params.len() >= 255 {
+                    return Err(Error {
+                        line: self.peek().line,
+                        kind: ErrorKind::InternalError("255 parameter limit exceeded.".into()),
+                    });
+                }
+
+                params.push(self.identifier("Expected parameter name.")?);
+
+                if !self.match_token(&TokenKind::Comma) {
+                    break;
+                }
+            }
+        }
+
+        self.consume(
+            &TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expect ')' after parameters."),
+        )?;
+
+        self.consume(
+            &TokenKind::LeftBrace,
+            ErrorKind::ExpectedToken("Expect '{' before function body."),
+        )?;
+
+        Ok(Statement::Function(Rc::new(Function {
+            name,
+            params,
+            body: self.block_statement()?,
+        })))
+    }
+
+    fn var_declaration(&mut self) -> StmtResult {
+        // Since `TokenKind::Identifier` carries data, we can't use
+        // `consume`.
+        let mut var = Var {
+            name: self.identifier("Expected variable name.")?,
+            initialiser: None,
+        };
+
+        if self.match_token(&TokenKind::Equal) {
+            var.initialiser = Some(self.expression()?);
+        }
+
+        self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?;
+        Ok(Statement::Var(var))
+    }
+
+    fn statement(&mut self) -> StmtResult {
+        if self.match_token(&TokenKind::Print) {
+            self.print_statement()
+        } else if self.match_token(&TokenKind::LeftBrace) {
+            Ok(Statement::Block(self.block_statement()?))
+        } else if self.match_token(&TokenKind::If) {
+            self.if_statement()
+        } else if self.match_token(&TokenKind::While) {
+            self.while_statement()
+        } else if self.match_token(&TokenKind::For) {
+            self.for_statement()
+        } else if self.match_token(&TokenKind::Return) {
+            self.return_statement()
+        } else {
+            self.expr_statement()
+        }
+    }
+
+    fn print_statement(&mut self) -> StmtResult {
+        let expr = self.expression()?;
+        self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?;
+        Ok(Statement::Print(expr))
+    }
+
+    fn block_statement(&mut self) -> Result<Block, Error> {
+        let mut block: Block = vec![];
+
+        while !self.check_token(&TokenKind::RightBrace) && !self.is_at_end() {
+            block.push(self.declaration()?);
+        }
+
+        self.consume(&TokenKind::RightBrace, ErrorKind::ExpectedClosingBrace)?;
+
+        Ok(block)
+    }
+
+    fn if_statement(&mut self) -> StmtResult {
+        self.consume(
+            &TokenKind::LeftParen,
+            ErrorKind::ExpectedToken("Expected '(' after 'if'"),
+        )?;
+        let condition = self.expression()?;
+        self.consume(
+            &TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expected ')' after condition"),
+        )?;
+
+        let then_branch = Box::new(self.statement()?);
+
+        let mut stmt = If {
+            condition,
+            then_branch,
+            else_branch: Option::None,
+        };
+
+        if self.match_token(&TokenKind::Else) {
+            stmt.else_branch = Some(Box::new(self.statement()?));
+        }
+
+        Ok(Statement::If(stmt))
+    }
+
+    fn while_statement(&mut self) -> StmtResult {
+        self.consume(
+            &TokenKind::LeftParen,
+            ErrorKind::ExpectedToken("Expected '(' after 'while'"),
+        )?;
+
+        let condition = self.expression()?;
+
+        self.consume(
+            &TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expected ')' after 'while'"),
+        )?;
+
+        Ok(Statement::While(While {
+            condition,
+            body: Box::new(self.statement()?),
+        }))
+    }
+
+    fn for_statement(&mut self) -> StmtResult {
+        // Parsing of clauses ...
+        self.consume(
+            &TokenKind::LeftParen,
+            ErrorKind::ExpectedToken("Expected '(' after 'for'"),
+        )?;
+
+        let initialiser = if self.match_token(&TokenKind::Semicolon) {
+            None
+        } else if self.match_token(&TokenKind::Var) {
+            Some(self.var_declaration()?)
+        } else {
+            Some(self.expr_statement()?)
+        };
+
+        let condition = if self.check_token(&TokenKind::Semicolon) {
+            // unspecified condition => infinite loop
+            Expr::Literal(Literal::Boolean(true))
+        } else {
+            self.expression()?
+        };
+
+        self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?;
+
+        let increment = if self.check_token(&TokenKind::RightParen) {
+            None
+        } else {
+            Some(self.expression()?)
+        };
+
+        self.consume(
+            &TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expected ')' after for clauses"),
+        )?;
+
+        let mut body = self.statement()?;
+
+        // ... desugaring to while
+
+        if let Some(inc) = increment {
+            body = Statement::Block(vec![body, Statement::Expr(inc)]);
+        }
+
+        body = Statement::While(While {
+            condition,
+            body: Box::new(body),
+        });
+
+        if let Some(init) = initialiser {
+            body = Statement::Block(vec![init, body]);
+        }
+
+        Ok(body)
+    }
+
+    fn return_statement(&mut self) -> StmtResult {
+        let value = self.expression()?;
+        self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?;
+        Ok(Statement::Return(Return { value }))
+    }
+
+    fn expr_statement(&mut self) -> StmtResult {
+        let expr = self.expression()?;
+        self.consume(&TokenKind::Semicolon, ErrorKind::ExpectedSemicolon)?;
+        Ok(Statement::Expr(expr))
+    }
+
+    fn expression(&mut self) -> ExprResult {
+        self.assignment()
+    }
+
+    fn assignment(&mut self) -> ExprResult {
+        let expr = self.logic_or()?;
+
+        if self.match_token(&TokenKind::Equal) {
+            let equals = self.previous().clone();
+            let value = self.assignment()?;
+
+            if let Expr::Variable(Variable { name, .. }) = expr {
+                return Ok(Expr::Assign(Assign {
+                    name,
+                    value: Box::new(value),
+                    depth: None,
+                }));
+            }
+
+            return Err(Error {
+                line: equals.line,
+                kind: ErrorKind::InvalidAssignmentTarget(format!("{:?}", equals)),
+            });
+        }
+
+        Ok(expr)
+    }
+
+    fn logic_or(&mut self) -> ExprResult {
+        let mut expr = self.logic_and()?;
+
+        while self.match_token(&TokenKind::Or) {
+            expr = Expr::Logical(Logical {
+                left: Box::new(expr),
+                operator: self.previous().clone(),
+                right: Box::new(self.logic_and()?),
+            })
+        }
+
+        Ok(expr)
+    }
+
+    fn logic_and(&mut self) -> ExprResult {
+        let mut expr = self.equality()?;
+
+        while self.match_token(&TokenKind::And) {
+            expr = Expr::Logical(Logical {
+                left: Box::new(expr),
+                operator: self.previous().clone(),
+                right: Box::new(self.equality()?),
+            })
+        }
+
+        Ok(expr)
+    }
+
+    fn equality(&mut self) -> ExprResult {
+        self.binary_operator(
+            &[TokenKind::BangEqual, TokenKind::EqualEqual],
+            Self::comparison,
+        )
+    }
+
+    fn comparison(&mut self) -> ExprResult {
+        self.binary_operator(
+            &[
+                TokenKind::Greater,
+                TokenKind::GreaterEqual,
+                TokenKind::Less,
+                TokenKind::LessEqual,
+            ],
+            Self::term,
+        )
+    }
+
+    fn term(&mut self) -> ExprResult {
+        self.binary_operator(&[TokenKind::Minus, TokenKind::Plus], Self::factor)
+    }
+
+    fn factor(&mut self) -> ExprResult {
+        self.binary_operator(&[TokenKind::Slash, TokenKind::Star], Self::unary)
+    }
+
+    fn unary(&mut self) -> ExprResult {
+        if self.match_token(&TokenKind::Bang) || self.match_token(&TokenKind::Minus) {
+            return Ok(Expr::Unary(Unary {
+                operator: self.previous().clone(),
+                right: Box::new(self.unary()?),
+            }));
+        }
+
+        return self.call();
+    }
+
+    fn call(&mut self) -> ExprResult {
+        let mut expr = self.primary()?;
+
+        loop {
+            if self.match_token(&TokenKind::LeftParen) {
+                expr = self.finish_call(expr)?;
+            } else {
+                break;
+            }
+        }
+
+        Ok(expr)
+    }
+
+    fn finish_call(&mut self, callee: Expr) -> ExprResult {
+        let mut args = vec![];
+
+        if !self.check_token(&TokenKind::RightParen) {
+            loop {
+                // TODO(tazjin): Check for max args count
+                args.push(self.expression()?);
+                if !self.match_token(&TokenKind::Comma) {
+                    break;
+                }
+            }
+        }
+
+        let paren = self.consume(
+            &TokenKind::RightParen,
+            ErrorKind::ExpectedToken("Expect ')' after arguments."),
+        )?;
+
+        Ok(Expr::Call(Call {
+            args,
+            callee: Box::new(callee),
+            paren,
+        }))
+    }
+
+    fn primary(&mut self) -> ExprResult {
+        let next = self.advance();
+        let literal = match next.kind {
+            TokenKind::True => Literal::Boolean(true),
+            TokenKind::False => Literal::Boolean(false),
+            TokenKind::Nil => Literal::Nil,
+            TokenKind::Number(num) => Literal::Number(num),
+            TokenKind::String(string) => Literal::String(string),
+
+            TokenKind::LeftParen => {
+                let expr = self.expression()?;
+                self.consume(&TokenKind::RightParen, ErrorKind::UnmatchedParens)?;
+                return Ok(Expr::Grouping(Grouping(Box::new(expr))));
+            }
+
+            TokenKind::Identifier(_) => {
+                return Ok(Expr::Variable(Variable {
+                    name: next,
+                    depth: None,
+                }))
+            }
+
+            unexpected => {
+                eprintln!("encountered {:?}", unexpected);
+                return Err(Error {
+                    line: next.line,
+                    kind: ErrorKind::ExpectedExpression(next.lexeme),
+                });
+            }
+        };
+
+        Ok(Expr::Literal(literal))
+    }
+
+    // internal helpers
+
+    fn identifier(&mut self, err: &'static str) -> Result<Token, Error> {
+        if let TokenKind::Identifier(_) = self.peek().kind {
+            Ok(self.advance())
+        } else {
+            Err(Error {
+                line: self.peek().line,
+                kind: ErrorKind::ExpectedToken(err),
+            })
+        }
+    }
+
+    /// Check if the next token is in `oneof`, and advance if it is.
+    fn match_token(&mut self, token: &TokenKind) -> bool {
+        if self.check_token(token) {
+            self.advance();
+            return true;
+        }
+
+        false
+    }
+
+    /// Return the next token and advance parser state.
+    fn advance(&mut self) -> Token {
+        if !self.is_at_end() {
+            self.current += 1;
+        }
+
+        return self.previous().clone();
+    }
+
+    fn is_at_end(&self) -> bool {
+        self.check_token(&TokenKind::Eof)
+    }
+
+    /// Is the next token `token`?
+    fn check_token(&self, token: &TokenKind) -> bool {
+        self.peek().kind == *token
+    }
+
+    fn peek(&self) -> &Token {
+        &self.tokens[self.current]
+    }
+
+    fn previous(&self) -> &Token {
+        &self.tokens[self.current - 1]
+    }
+
+    fn consume(&mut self, kind: &TokenKind, err: ErrorKind) -> Result<Token, Error> {
+        if self.check_token(kind) {
+            return Ok(self.advance());
+        }
+
+        Err(Error {
+            line: self.peek().line,
+            kind: err,
+        })
+    }
+
+    fn synchronise(&mut self) {
+        self.advance();
+
+        while !self.is_at_end() {
+            if self.previous().kind == TokenKind::Semicolon {
+                return;
+            }
+
+            match self.peek().kind {
+                TokenKind::Class
+                | TokenKind::Fun
+                | TokenKind::Var
+                | TokenKind::For
+                | TokenKind::If
+                | TokenKind::While
+                | TokenKind::Print
+                | TokenKind::Return => return,
+
+                _ => {
+                    self.advance();
+                }
+            }
+        }
+    }
+
+    fn binary_operator(
+        &mut self,
+        oneof: &[TokenKind],
+        each: fn(&mut Parser) -> ExprResult,
+    ) -> ExprResult {
+        let mut expr = each(self)?;
+
+        while oneof.iter().any(|t| self.match_token(t)) {
+            expr = Expr::Binary(Binary {
+                left: Box::new(expr),
+                operator: self.previous().clone(),
+                right: Box::new(each(self)?),
+            })
+        }
+
+        return Ok(expr);
+    }
+}
+
+pub fn parse(tokens: Vec<Token>) -> Result<Block, Vec<Error>> {
+    let mut parser = Parser { tokens, current: 0 };
+    let mut program: Block = vec![];
+    let mut errors: Vec<Error> = vec![];
+
+    while !parser.is_at_end() {
+        match parser.declaration() {
+            Err(err) => {
+                errors.push(err);
+                parser.synchronise();
+            }
+            Ok(decl) => {
+                program.push(decl);
+            }
+        }
+    }
+
+    if errors.is_empty() {
+        Ok(program)
+    } else {
+        Err(errors)
+    }
+}
diff --git a/users/tazjin/rlox/src/treewalk/resolver.rs b/users/tazjin/rlox/src/treewalk/resolver.rs
new file mode 100644
index 0000000000..3d12973aa0
--- /dev/null
+++ b/users/tazjin/rlox/src/treewalk/resolver.rs
@@ -0,0 +1,199 @@
+// Resolves variable access to their specific instances in the
+// environment chain.
+//
+// https://craftinginterpreters.com/resolving-and-binding.html
+
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use crate::treewalk::errors::{Error, ErrorKind};
+use crate::treewalk::parser::{self, Expr, Statement};
+use crate::treewalk::scanner::Token;
+
+#[derive(Default)]
+struct Resolver<'a> {
+    scopes: Vec<HashMap<&'a str, bool>>,
+}
+
+impl<'a> Resolver<'a> {
+    // AST traversal
+    fn resolve(&mut self, program: &'a mut parser::Block) -> Result<(), Error> {
+        self.begin_scope();
+        for stmt in program {
+            self.resolve_stmt(stmt)?;
+        }
+        self.end_scope();
+
+        Ok(())
+    }
+
+    fn resolve_stmt(&mut self, stmt: &'a mut Statement) -> Result<(), Error> {
+        match stmt {
+            Statement::Expr(expr) => self.resolve_expr(expr),
+            Statement::Print(expr) => self.resolve_expr(expr),
+            Statement::Var(var) => self.resolve_var(var),
+            Statement::Return(ret) => self.resolve_expr(&mut ret.value),
+            Statement::Block(block) => self.resolve(block),
+
+            Statement::If(if_stmt) => {
+                self.resolve_expr(&mut if_stmt.condition)?;
+                self.resolve_stmt(&mut if_stmt.then_branch)?;
+
+                if let Some(branch) = if_stmt.else_branch.as_mut() {
+                    self.resolve_stmt(branch)?;
+                }
+
+                Ok(())
+            }
+
+            Statement::While(while_stmt) => {
+                self.resolve_expr(&mut while_stmt.condition)?;
+                self.resolve_stmt(&mut while_stmt.body)
+            }
+
+            Statement::Function(func) => match Rc::get_mut(func) {
+                Some(func) => self.resolve_function(func),
+                // The resolver does not clone references, so unless
+                // the interpreter is called before the resolver this
+                // case should never happen.
+                None => {
+                    return Err(Error {
+                        line: 0,
+                        kind: ErrorKind::InternalError(
+                            "multiple function references before interpretation".into(),
+                        ),
+                    })
+                }
+            },
+        }
+    }
+
+    fn resolve_var(&mut self, var: &'a mut parser::Var) -> Result<(), Error> {
+        self.declare(&var.name.lexeme);
+
+        if let Some(init) = &mut var.initialiser {
+            self.resolve_expr(init)?;
+        }
+
+        self.define(&var.name.lexeme);
+
+        Ok(())
+    }
+
+    fn resolve_function(&mut self, func: &'a mut parser::Function) -> Result<(), Error> {
+        self.declare(&func.name.lexeme);
+        self.define(&func.name.lexeme);
+
+        self.begin_scope();
+
+        for param in &func.params {
+            self.declare(&param.lexeme);
+            self.define(&param.lexeme);
+        }
+
+        for stmt in &mut func.body {
+            self.resolve_stmt(stmt)?;
+        }
+
+        self.end_scope();
+
+        Ok(())
+    }
+
+    fn resolve_expr(&mut self, expr: &'a mut Expr) -> Result<(), Error> {
+        match expr {
+            Expr::Variable(var) => self.resolve_variable(var),
+            Expr::Assign(assign) => self.resolve_assign(assign),
+            Expr::Grouping(grouping) => self.resolve_expr(&mut grouping.0),
+            Expr::Call(call) => self.resolve_call(call),
+            Expr::Literal(_) => Ok(()),
+            Expr::Unary(unary) => self.resolve_expr(&mut unary.right),
+
+            Expr::Logical(log) => {
+                self.resolve_expr(&mut log.left)?;
+                self.resolve_expr(&mut log.right)
+            }
+
+            Expr::Binary(binary) => {
+                self.resolve_expr(&mut binary.left)?;
+                self.resolve_expr(&mut binary.right)
+            }
+        }
+    }
+
+    fn resolve_variable(&mut self, var: &'a mut parser::Variable) -> Result<(), Error> {
+        if let Some(scope) = self.scopes.last_mut() {
+            if let Some(false) = scope.get(var.name.lexeme.as_str()) {
+                return Err(Error {
+                    line: var.name.line,
+                    kind: ErrorKind::StaticError(
+                        "can't read local variable in its own initialiser".into(),
+                    ),
+                });
+            }
+        }
+
+        var.depth = self.resolve_local(&var.name);
+        Ok(())
+    }
+
+    fn resolve_assign(&mut self, assign: &'a mut parser::Assign) -> Result<(), Error> {
+        self.resolve_expr(&mut assign.value)?;
+        assign.depth = self.resolve_local(&assign.name);
+        Ok(())
+    }
+
+    fn resolve_local(&mut self, name: &'a Token) -> Option<usize> {
+        for (c, scope) in self.scopes.iter().rev().enumerate() {
+            if scope.contains_key(name.lexeme.as_str()) {
+                return Some(c);
+            }
+        }
+
+        None
+    }
+
+    fn resolve_call(&mut self, call: &'a mut parser::Call) -> Result<(), Error> {
+        self.resolve_expr(&mut call.callee)?;
+
+        for arg in call.args.iter_mut() {
+            self.resolve_expr(arg)?;
+        }
+
+        Ok(())
+    }
+
+    // Internal helpers
+
+    fn declare(&mut self, name: &'a str) {
+        if let Some(scope) = self.scopes.last_mut() {
+            scope.insert(&name, false);
+        }
+    }
+
+    fn define(&mut self, name: &'a str) {
+        if let Some(scope) = self.scopes.last_mut() {
+            scope.insert(&name, true);
+        }
+    }
+
+    fn begin_scope(&mut self) {
+        self.scopes.push(Default::default());
+    }
+
+    fn end_scope(&mut self) {
+        self.scopes.pop();
+    }
+}
+
+pub fn resolve(globals: &[String], block: &mut parser::Block) -> Result<(), Error> {
+    let mut resolver: Resolver = Default::default();
+
+    // Scope for static globals only starts, never ends.
+    resolver.begin_scope();
+    for global in globals {
+        resolver.define(global);
+    }
+
+    resolver.resolve(block)
+}
diff --git a/users/tazjin/russian/helpers.el b/users/tazjin/russian/helpers.el
new file mode 100644
index 0000000000..41d4aa34f4
--- /dev/null
+++ b/users/tazjin/russian/helpers.el
@@ -0,0 +1,7 @@
+;; Helper functions for creating the other files.
+
+(defun wiktionary-lookup-at-point (ask-lang)
+  (interactive "P")
+  (let ((language (if ask-lang (read-string "Language code? ") "ru")))
+    (eww (concat "https://ru.wiktionary.org/wiki/"
+                 (thing-at-point 'word)))))
diff --git a/users/tazjin/russian/roots.el b/users/tazjin/russian/roots.el
new file mode 100644
index 0000000000..77d09b4726
--- /dev/null
+++ b/users/tazjin/russian/roots.el
@@ -0,0 +1,28 @@
+;; '(root explanation)
+;;
+;; All roots without explanations are TODOs.
+;;
+;; In some cases, roots are not direct morphological roots of their
+;; descendent words (e.g. -ะณะพะปะพะฒ- => ะณะปะฐะฒะฝั‹ะน)
+
+'(("-ะฒะตััŒ-" "everything, all, every, etc.")
+  ("-ะฒะธะด-" "seeing, viewing etc.")
+  ("-ะฒั€ะตะผ-" "time")
+  ("-ะณะพะฒะพั€-" "related to talking")
+  ("-ะณะพะปะพะฒ-" "head, main, etc.")
+  ("-ะดั€ัƒะณ-" nil)
+  ("-ะดัƒะผ-" "thinking, thoughts")
+  ("-ะถะธ-" "life")
+  ("-ะทะฝะฐ-" "knowing, knowledge")
+  ("-ะธะผั-" "name")
+  ("-ะน-" "walking, moving to")
+  ("-ะผะพั‡ัŒ-" "ability, permission")
+  ("-ะฝะพะฒ-" "new")
+  ("-ะพะฑั‰-" "common?")
+  ("-ะฟั€ะฐะฒะด-" "truth")
+  ("-ะฟั€ะพั-" "question")
+  ("-ัะบะฐะท-" nil)
+  ("-ัะผะพั‚ั€-" "watching, viewing")
+  ("-ัั‚ั€ะฐะฝ-" "country?")
+  ("-ั…ะพะด-" "movement")
+  ("-ั…ะพั€ะพัˆ-" "goodness, niceness"))
diff --git a/users/tazjin/russian/russian.el b/users/tazjin/russian/russian.el
new file mode 100644
index 0000000000..28f1addeaa
--- /dev/null
+++ b/users/tazjin/russian/russian.el
@@ -0,0 +1,97 @@
+(require 'cl-macs)
+(require 'ht)
+(require 'seq)
+(require 's)
+
+;; Type definitions for Russian structures
+
+(cl-defstruct russian-word
+  "Definition and metadata of a single Russian word."
+  (word nil :type string)
+  (translations :type list
+                :documentation "List of lists of strings, each a set of translations.")
+
+  (notes nil :type list ;; of string
+         :documentation "free-form notes about this word")
+
+  (roots nil :type list ;; of string
+         :documentation "list of strings that correspond with roots (exact string match)"))
+
+(defun russian--merge-words (previous new)
+  "Merge two Russian word definitions together. If no previous
+  definition exists, only the new one will be returned."
+  (if (not previous) new
+    (cl-assert (equal (russian-word-word previous)
+                      (russian-word-word new))
+               "different words passed into merge function")
+    (make-russian-word :word (russian-word-word previous)
+                       :translations (-concat (russian-word-translations previous)
+                                              (russian-word-translations new))
+                       :notes (-concat (russian-word-notes previous)
+                                       (russian-word-notes new))
+                       :roots (-concat (russian-word-roots previous)
+                                       (russian-word-roots new)))))
+
+;; Definitions for creating a data structure of all Russian words.
+
+(defvar russian-words (make-hash-table)
+  "Table of all Russian words in the corpus.")
+
+(defun russian--define-word (word)
+  "Define a single word in the corpus, optionally merging it with
+  another entry."
+  (let ((key (russian-word-word word)))
+    (ht-set russian-words key (russian--merge-words
+                               (ht-get russian-words key)
+                               word))))
+
+(defmacro define-russian-words (&rest words)
+  "Define the list of all available words. There may be more than
+  one entry for a word in some cases."
+  (declare (indent defun))
+
+  ;; Clear the table before proceeding with insertion
+  (setq russian-words (make-hash-table))
+
+  (seq-map
+   (lambda (word)
+     (russian--define-word (make-russian-word :word (car word)
+                                              :translations (cadr word)
+                                              :notes (caddr word)
+                                              :roots (cadddr word))))
+   words)
+
+  '(message "Defined %s unique words." (ht-size russian-words)))
+
+;; Helpers to train Russian words through passively.
+
+(defun russian--format-word (word)
+  "Format a Russian word suitable for echo display."
+  (apply #'s-concat
+         (-flatten
+          (list (russian-word-word word)
+                " - "
+                (s-join ", " (russian-word-translations word))
+                (when-let ((roots (russian-word-roots word)))
+                  (list " [" (s-join ", " roots) "]"))
+                (when-let ((notes (russian-word-notes word)))
+                  (list " (" (s-join "; " notes) ")"))))))
+
+(defun display-russian-words ()
+  "Convert Russian words to passively terms and start passively."
+  (interactive)
+  (setq passively-learn-terms (make-hash-table))
+  (ht-map
+   (lambda (k v)
+     (ht-set passively-learn-terms k (russian--format-word v)))
+   russian-words)
+  (passively-enable))
+
+(defun lookup-last-russian-word (in-eww)
+  "Look up the last Russian word in Wiktionary"
+  (interactive "P")
+  (let ((url (concat "https://ru.wiktionary.org/wiki/" passively-last-displayed)))
+    (if in-eww (eww url)
+      (browse-url url))))
+
+(provide 'russian)
diff --git a/users/tazjin/russian/words.el b/users/tazjin/russian/words.el
new file mode 100644
index 0000000000..784e5bddde
--- /dev/null
+++ b/users/tazjin/russian/words.el
@@ -0,0 +1,723 @@
+;; entries :: '(entry ...)'
+;; entry :: '(word translations note roots)
+;; note :: (or nil string)
+;; translations :: '(translation ...)
+;; roots :: '(root ...)
+
+(require 'russian)
+
+(define-russian-words
+  ;; 1-50
+  ("ะธ" ("and" "though"))
+  ("ะฒ" ("in" "at"))
+  ("ะฝะต" ("not"))
+  ("ะพะฝ" ("he"))
+  ("ะฝะฐ" ("on" "it" "at" "to"))
+  ("ั" ("I"))
+  ("ั‡ั‚ะพ" ("what" "that" "why"))
+  ("ั‚ะพั‚" ("that"))
+  ("ะฑั‹ั‚ัŒ" ("to be"))
+  ("ั" ("with" "and" "from" "of"))
+  ("ะฐ" ("while" "and" "but"))
+  ("ะฒะตััŒ" ("all" "everything") nil ("-ะฒะตััŒ-"))
+  ("ัั‚ะพ" ("that" "this" "it"))
+  ("ะบะฐะบ" ("how" "what" "as" "like"))
+  ("ะพะฝะฐ" ("she"))
+  ("ะฟะพ" ("on" "along" "by"))
+  ("ะฝะพ" ("but"))
+  ("ะพะฝะธ" ("they"))
+  ("ะบ" ("to" "for" "by"))
+  ("ัƒ" ("by" "with" "of"))
+  ("ั‚ั‹" ("you"))
+  ("ะธะท" ("from" "of" "in"))
+  ("ะผั‹" ("we"))
+  ("ะทะฐ" ("behind" "over" "at" "after"))
+  ("ะฒั‹" ("you"))
+  ("ั‚ะฐะบ" ("so" "thus" "then"))
+  ("ะถะต" ("and" "as for" "but" "same"))
+  ("ะพั‚" ("from" "of" "for"))
+  ("ัะบะฐะทะฐั‚ัŒ" ("to say" "to speak") nil ("-ัะบะฐะท-"))
+  ("ัั‚ะพั‚" ("this"))
+  ("ะบะพั‚ะพั€ั‹ะน" ("which" "who" "that"))
+  ("ะผะพั‡ัŒ" ("be able" "can") nil ("-ะผะพั‡ัŒ-"))
+  ("ั‡ะตะปะพะฒะตะบ" ("man" "person"))
+  ("ะพ" ("of" "about" "against"))
+  ("ะพะดะธะฝ" ("one" "some" "alone"))
+  ("ะตั‰ั‘" ("still" "yet"))
+  ("ะฑั‹" ("would"))
+  ("ั‚ะฐะบะพะน" ("such" "so" "some"))
+  ("ั‚ะพะปัŒะบะพ" ("only" "merely" "but"))
+  ("ัะตะฑั" ("myself" "himself" "herself"))
+  ("ัะฒะพั‘" ("one's own" "my" "our"))
+  ("ะบะฐะบะพะน" ("what" "which" "how"))
+  ("ะบะพะณะดะฐ" ("when" "while" "as"))
+  ("ัƒะถะต" ("already" "by now"))
+  ("ะดะปั" ("for" "to"))
+  ("ะฒะพั‚" ("here" "there" "this is" "that's")
+   ("calling attention to something"))
+  ("ะบั‚ะพ" ("who" "that" "some"))
+  ("ะดะฐ" ("yes" "but") ("affirmation (..., right?)"))
+  ("ะณะพะฒะพั€ะธั‚ัŒ" ("to say" "to tell" "to speak") nil ("-ะณะพะฒะพั€-"))
+  ("ะณะพะด" ("year"))
+
+  ;; 51 - 100
+  ("ะทะฝะฐั‚ัŒ" ("to know" "be aware") nil ("-ะทะฝะฐ-"))
+  ("ะผะพะน" ("my" "mine"))
+  ("ะดะพ" ("to" "up to" "about" "before"))
+  ("ะธะปะธ" ("or"))
+  ("ะตัะปะธ" ("if"))
+  ("ะฒั€ะตะผั" ("time" "season") nil ("-ะฒั€ะตะผ-"))
+  ("ั€ัƒะบะฐ" ("hand" "arm"))
+  ("ะฝะตั‚" ("no" "not" "but"))
+  ("ัะฐะผั‹ะน" ("most" "the very" "the same"))
+  ("ะฝะธ" ("not a" "not" "neither ... nor"))
+  ("ัั‚ะฐั‚ัŒ" ("to become" "begin" "come"))
+  ("ะฑะพะปัŒัˆะพะน" ("big" "large" "important"))
+  ("ะดะฐะถะต" ("even"))
+  ("ะดั€ัƒะณะพะน" ("other" "another" "different") nil ("-ะดั€ัƒะณ-"))
+  ("ะฝะฐัˆ" ("our" "ours"))
+  ("ัะฒะพะน" ("one's own"))
+  ("ะฝัƒ" ("now" "right" "well" "come on"))
+  ("ะฟะพะด" ("under" "for" "towards" "to"))
+  ("ะณะดะต" ("where"))
+  ("ะดะตะปะพ" ("business" "affair" "matter"))
+  ("ะตัั‚ัŒ" ("to eat" "to be"))
+  ("ัะฐะผ" ("oneself"))
+  ("ั€ะฐะท" ("time" "once" "since"))
+  ("ั‡ั‚ะพะฑั‹" ("that" "in order that"))
+  ("ะดะฒะฐ" ("two"))
+  ("ั‚ะฐะผ" ("there" "then"))
+  ("ั‡ะตะผ" ("than" "instead of")
+   ("ั‡ะตะผ ..., ั‚ะตะผ ..."))
+  ("ะณะปะฐะท" ("eye" "sight"))
+  ("ะถะธะทะฝัŒ" ("life") nil ("-ะถะธ-"))
+  ("ะฟะตั€ะฒั‹ะน" ("first" "front" "former"))
+  ("ะดะตะฝัŒ" ("day"))
+  ("ั‚ัƒั‚" ("here" "now" "then"))
+  ("ะฒะพ" ("in" "at")
+   ("as particle also: wow, exactly, ..."))
+  ("ะฝะธั‡ั‚ะพ" ("nothing"))
+  ("ะฟะพั‚ะพะผ" ("afterwards" "then"))
+  ("ะพั‡ะตะฝัŒ" ("very"))
+  ("ัะพ" ("with"))
+  ("ั…ะพั‚ะตั‚ัŒ" ("to want"))
+  ("ะปะธ" ("whether" "if"))
+  ("ะฟั€ะธ" ("attached to" "in the presence of" "by" "about"))
+  ("ะณะพะปะพะฒะฐ" ("head" "mind" "brains") nil ("-ะณะพะปะพะฒ-"))
+  ("ะฝะฐะดะพ" ("over" "above" "ought to"))
+  ("ะฑะตะท" ("without"))
+  ("ะฒะธะดะตั‚ัŒ" ("to see") nil ("-ะฒะธะด-"))
+  ("ะธะดั‚ะธ" ("to go" "to come"))
+  ("ั‚ะตะฟะตั€ัŒ" ("now" "nowadays"))
+  ("ั‚ะพะถะต" ("also" "as well" "too"))
+  ("ัั‚ะพัั‚ัŒ" ("to stand" "be" "stand up"))
+  ("ะดั€ัƒะณ" ("friend"))
+  ("ะดะพะผ" ("house" "home"))
+
+  ;; 101-150
+  ("ัะตะนั‡ะฐั" ("now" "presently" "soon"))
+  ("ะผะพะถะฝะพ" ("possible" "permitted") nil ("-ะผะพั‡ัŒ-"))
+  ("ะฟะพัะปะต" ("after" "afterwards"))
+  ("ัะปะพะฒะพ" ("word"))
+  ("ะทะดะตััŒ" ("here"))
+  ("ะดัƒะผะฐั‚ัŒ" ("to think" "to believe") nil ("-ะดัƒะผ-"))
+  ("ะผะตัั‚ะพ" ("place" "seat"))
+  ("ัะฟั€ะพัะธั‚ัŒ" ("to ask") nil ("-ะฟั€ะพั-"))
+  ("ั‡ะตั€ะตะท" ("through" "across"))
+  ("ะปะธั†ะพ" ("face" "person"))
+  ("ั‡ั‚ะพ" ("what" "which" "that"))
+  ("ั‚ะพะณะดะฐ" ("then"))
+  ("ั…ะพั€ะพัˆะธะน" ("good" "nice") nil ("-ั…ะพั€ะพัˆ-"))
+  ("ะบะฐะถะดั‹ะน" ("every" "each"))
+  ("ะฝะพะฒั‹ะน" ("new" "modern") nil ("-ะฝะพะฒ-"))
+  ("ะถะธั‚ัŒ" ("to live") nil ("-ะถะธ-"))
+  ("ะดะพะปะถะฝั‹ะน" ("due" "proper" "should"))
+  ("ัะผะพั‚ั€ะตั‚ัŒ" ("to look" "watch"))
+  ("ะฟะพั‡ะตะผัƒ" ("why"))
+  ("ะฟะพั‚ะพะผัƒ" ("that's why"))
+  ("ัั‚ะพั€ะพะฝะฐ" ("side" "party"))
+  ("ะฟั€ะพัั‚ะพ" ("simply"))
+  ("ะฝะพะณะฐ" ("foot" "leg"))
+  ("ัะธะดะตั‚ัŒ" ("to sit"))
+  ("ะฟะพะฝัั‚ัŒ" ("to understand" "to realise"))
+  ("ะธะผะตั‚ัŒ" ("to own" "to have"))
+  ("ะบะพะฝะตั‡ะฝั‹ะน" ("final" "last"))
+  ("ะดะตะปะฐั‚ัŒ" ("to do" "make"))
+  ("ะฒะดั€ัƒะณ" ("suddenly"))
+  ("ะฝะฐะด" ("above" "over"))
+  ("ะฒะทัั‚ัŒ" ("to take"))
+  ("ะฝะธะบั‚ะพ" ("nobody"))
+  ("ะฟะพะฝะธะผะฐั‚ัŒ" ("to understand"))
+  ("ะบะฐะทะฐั‚ัŒัั" ("to seem" "to appear"))
+  ("ั€ะฐะฑะพั‚ะฐ" ("work" "job"))
+  ("ั‚ั€ะธ" ("three"))
+  ("ะฒะฐัˆ" ("yours"))
+  ("ัƒะถ" ("really" "already"))
+  ("ะทะตะผะปั" ("earth" "land" "soil"))
+  ("ะบะพะฝะตั†" ("end" "distance"))
+  ("ะฝะตัะบะพะปัŒะบะพ" ("several" "some"))
+  ("ั‡ะฐั" ("hour" "time"))
+  ("ะณะพะปะพั" ("voice"))
+  ("ะณะพั€ะพะด" ("town" "city"))
+  ("ะฟะพัะปะตะดะฝะธะน" ("last" "the latest" "new"))
+
+  ;; 151-200
+  ("ะฟะพะบะฐ" ("for the present")) ;; TODO(tazjin): review
+  ("ั…ะพั€ะพัˆะพ" ("well") nil ("-ั…ะพั€ะพัˆ-"))
+  ("ะดะฐะฒะฐั‚ัŒ" ("to give" "to grant"))
+  ("ะฒะพะดะฐ" ("water"))
+  ("ะฑะพะปะตะต" ("more"))
+  ("ั…ะพั‚ั" ("although"))
+  ("ะฒัะตะณะดะฐ" ("always"))
+  ("ะฒั‚ะพั€ะพะน" ("second"))
+  ("ะบัƒะดะฐ" ("where" "what for" "much"))
+  ("ะฟะพะนั‚ะธ" ("to go") nil ("-ะน-"))
+  ("ัั‚ะพะป" ("table" "desk" "board"))
+  ("ั€ะตะฑั‘ะฝะพะบ" ("child" "kid" "infant"))
+  ("ัƒะฒะธะดะตั‚ัŒ" ("to see"))
+  ("ัะธะปะฐ" ("strength" "force"))
+  ("ะพั‚ะตั†" ("father"))
+  ("ะถะตะฝั‰ะธะฝะฐ" ("woman"))
+  ("ะผะฐัˆะธะฝะฐ" ("car" "machine" "engine"))
+  ("ัะปัƒั‡ะฐะน" ("case" "occasion" "incident"))
+  ("ะฝะพั‡ัŒ" ("night"))
+  ("ัั€ะฐะทัƒ" ("at once" "right away" "just"))
+  ("ะผะธั€" ("world" "peace"))
+  ("ัะพะฒัะตะผ" ("quite" "entirely" "totally"))
+  ("ะพัั‚ะฐั‚ัŒัั" ("to remain" "to stay"))
+  ("ะพะฑ" ("about" "of"))
+  ("ะฒะธะด" ("appearance" "look" "view"))
+  ("ะฒั‹ะนั‚ะธ" ("to go out" "to exit" "to come out" "to appear") nil ("-ะน-"))
+  ("ะดะฐั‚ัŒ" ("to give"))
+  ("ั€ะฐะฑะพั‚ะฐั‚ัŒ" ("to work"))
+  ("ะปัŽะฑะธั‚ัŒ" ("to work"))
+  ("ัั‚ะฐั€ั‹ะน" ("old"))
+  ("ะฟะพั‡ั‚ะธ" ("almost"))
+  ("ั€ัะด" ("row" "line"))
+  ("ะพะบะฐะทะฐั‚ัŒัั" ("find oneself" "turn out"))
+  ("ะฝะฐั‡ะฐะปะพ" ("beginning" "origin" "source"))
+  ("ั‚ะฒะพะน" ("your" "yours"))
+  ("ะฒะพะฟั€ะพั" ("question" "matter" "problem") nil ("-ะฟั€ะพั-"))
+  ("ะผะฝะพะณะพ" ("many" "much"))
+  ("ะฒะพะนะฝะฐ" ("war"))
+  ("ัะฝะพะฒะฐ" ("again"))
+  ("ะพั‚ะฒะตั‚ะธั‚ัŒ" ("to answer" "to reply"))
+  ("ะผะตะถะดัƒ" ("between" "among"))
+  ("ะฟะพะดัƒะผะฐั‚ัŒ" ("to think"))
+  ("ะพะฟัั‚ัŒ" ("again"))
+  ("ะฑะตะปั‹ะน" ("white"))
+  ("ะดะตะฝัŒะณะธ" ("money"))
+  ("ะทะฝะฐั‡ะธั‚ัŒ" ("to mean" "to signify") nil ("-ะทะฝะฐ-"))
+  ("ะฟั€ะพ" ("about" "for"))
+  ("ะปะธัˆัŒ" ("only" "as soon as"))
+  ("ะผะธะฝัƒั‚ะฐ" ("minute" "moment"))
+  ("ะถะตะฝะฐ" ("wife"))
+
+  ;; 201-300
+  ("ะฟะพัะผะพั‚ั€ะตั‚ัŒ" ("to watch" "to look" "to inspect") nil ("-ัะผะพั‚ั€-"))
+  ("ะฟั€ะฐะฒะดะฐ" ("truth") nil ("-ะฟั€ะฐะฒะด-"))
+  ("ะณะปะฐะฒะฝั‹ะน" ("main" "chief") nil ("-ะณะพะปะพะฒ-"))
+  ("ัั‚ั€ะฐะฝะฐ" ("country") nil ("-ัั‚ั€ะฐะฝ-"))
+  ("ัะฒะตั‚" ("light" "world"))
+  ("ะถะดะฐั‚ัŒ" ("to wait"))
+  ("ะผะฐั‚ัŒ" ("mother"))
+  ("ะฑัƒะดั‚ะพ" ("as if" "as though"))
+  ("ะฝะธะบะพะณะดะฐ" ("never"))
+  ("ั‚ะพะฒะฐั€ะธัˆ" ("comrade" "friend"))
+  ("ะดะพั€ะพะณะฐ" ("road" "way" "journey"))
+  ("ะพะดะฝะฐะบะพ" ("however" "although"))
+  ("ะปะตะถะฐั‚ัŒ" ("to lie" "to be situated"))
+  ("ะธะผะตะฝะฝะพ" ("namely" "just" "exactly") nil ("-ะธะผั-"))
+  ("ะพะบะฝะพ" ("window"))
+  ("ะฝะธะบะฐะบะพะน" ("no" "none"))
+  ("ะฝะฐะนั‚ะธ" ("to find" "to discover") nil ("-ะน-"))
+  ("ะฟะธัะฐั‚ัŒ" ("to write"))
+  ("ะบะพะผะฝะฐั‚ะฐ" ("room"))
+  ("ะœะพัะบะฒะฐ" ("Moscow"))
+  ("ั‡ะฐัั‚ัŒ" ("part" "share" "department"))
+  ("ะฒะพะพะฑั‰ะต" ("in general" "altogether" "on the whole") nil ("-ะพะฑั‰-"))
+  ("ะบะฝะธะณะฐ" ("book"))
+  ("ะผะฐะปะตะฝัŒะบะธะน" ("small" "little"))
+  ("ัƒะปะธั†ะฐ" ("street"))
+  ("ั€ะตะถะธั‚ัŒ" ("to decide" "to solve"))
+  ("ะดะฐะปะตะบะธะน" ("distant" "remote"))
+  ("ะดัƒัˆะฐ" ("soul" "spirit"))
+  ("ั‡ัƒั‚ัŒ" ("hardly" "slightly"))
+  ("ะฒะตั€ะฝัƒั‚ัŒัั" ("to return"))
+  ("ัƒั‚ั€ะพ" ("morning"))
+  ("ะฝะตะบะพั‚ะพั€ั‹ะน" ("some"))
+  ("ัั‡ะธั‚ะฐั‚ัŒ" ("to count" "to consider"))
+  ("ัะบะพะปัŒะบะพ" ("how much" "how many"))
+  ("ะฟะพะผะฝะธั‚ัŒ" ("to remember"))
+  ("ะฒะตั‡ะตั€" ("evening"))
+  ("ะฟะพะป" ("floor" "gender"))
+  ("ั‚ะฐะบะธ" ("after all"))
+  ("ะฟะพะปัƒั‡ะธั‚ัŒ" ("to receive" "to get" "to obtain"))
+  ("ะฝะฐั€ะพะด" ("people" "nation"))
+  ("ะฟะปะตั‡ะพ" ("shoulder" "upper arm"))
+  ("ั…ะพั‚ัŒ" ("even" "if you want" "though"))
+  ("ัะตะณะพะดะฝั" ("today"))
+  ("ะฑะพะณ" ("god"))
+  ("ะฒะผะตัั‚ะต" ("together"))
+  ("ะฒะทะณะปัะด" ("look" "glance" "view"))
+  ("ั…ะพะดะธั‚ัŒ" ("to go" "to walk") nil ("-ั…ะพะด-"))
+  ("ะทะฐั‡ะตะผ" ("what for" "why"))
+  ("ัะพะฒะตั‚ัะบะธะน" ("Soviet"))
+  ("ั€ัƒััะบะธะน" ("Russian"))
+  ("ะฑั‹ะฒะฐั‚ัŒ" ("to be" "to visit" "to happen"))
+  ("ะฟะพะปะฝั‹ะน" ("full" "complete" "whole"))
+  ("ะฟั€ะธะนั‚ะธ" ("to arrive" "to come") nil ("-ะน-"))
+  ("ะฟะฐะปะตั†" ("finger" "toe"))
+  ("ะ ะพััะธั" ("Russia"))
+  ("ะปัŽะฑะพะน" ("any" "every"))
+  ("ะธัั‚ะพั€ะธั" ("history" "story" "event"))
+  ("ะฝะฐะบะพะฝะตั†" ("finally" "at least"))
+  ("ะผั‹ัะปัŒ" ("thought" "idea"))
+  ("ัƒะทะฝะฐั‚ัŒ" ("to know" "to learn" "to recognise") nil ("-ะทะฝะฐ-"))
+  ("ะฝะฐะทะฐะด" ("back" "backwards" "ago"))
+  ("ะพะฑั‰ะธะน" ("general" "common") nil ("-ะพะฑั‰-"))
+  ("ะทะฐะผะตั‚ะธั‚ัŒ" ("to notice" "to observe"))
+  ("ัะปะพะฒะฝะพ" ("as if" "like"))
+  ("ะฟั€ะพัˆะปั‹ะน" ("past" "vergangen") nil ("-ะน-"))
+  ("ัƒะนั‚ะธ" ("to leave" "to go away") nil ("-ะน-"))
+  ("ะธะทะฒะตัั‚ะฝั‹ะน" ("well-known" "famous"))
+  ("ะดะฐะฒะฝะพ" ("long ago"))
+  ("ัะปั‹ัˆะฐั‚ัŒ" ("to hear"))
+  ("ัะปัƒัˆะฐั‚ัŒ" ("to listen" "to hear"))
+  ("ะฑะพัั‚ัŒัั" ("to be afraid" "fear"))
+  ("ัั‹ะฝ" ("son"))
+  ("ะฝะตะปัŒะทั" ("it is impossible" "can't"))
+  ("ะฟั€ัะผะพ" ("straight" "frankly"))
+  ("ะดะพะปะณะพ" ("for a long time"))
+  ("ะฑั‹ัั‚ั€ะพ" ("fast" "quickly"))
+  ("ะปะตั" ("forest"))
+  ("ะฟะพั…ะพะถะธะน" ("similar" "alike") nil ("-ั…ะพะด-"))
+  ("ะฟะพั€ะฐ" ("time" "pore"))
+  ("ะฟัั‚ัŒ" ("five"))
+  ("ะณะปัะดะตั‚ัŒ" ("to look" "to gaze"))
+  ("ะพะฝะพ" ("it"))
+  ("ัะตัั‚ัŒ" ("to sit"))
+  ("ะธะผั" ("name") nil ("-ะธะผั-"))
+  ("ะถ" ("and" "as for" "but"))
+  ("ั€ะฐะทะณะพะฒะพั€" ("conversation" "talk") nil ("-ะณะพะฒะพั€-"))
+  ("ั‚ะตะปะพ" ("body"))
+  ("ะผะพะปะพะดะพะน" ("young"))
+  ("ัั‚ะตะฝะฐ" ("wall"))
+  ("ะบั€ะฐัะฝั‹ะน" ("red"))
+  ("ั‡ะธั‚ะฐั‚ัŒ" ("to read"))
+  ("ะฟั€ะฐะฒะพ" ("right"))
+  ("ัั‚ะฐั€ะธะบ" ("old man"))
+  ("ั€ะฐะฝะฝะธะน" ("early"))
+  ("ั…ะพั‚ะตั‚ัŒัั" ("to want" "to like"))
+  ("ะผะฐะผะฐ" ("mummy" "mum"))
+  ("ะพัั‚ะฐะฒะฐั‚ัŒัั" ("to remain" "to stay"))
+  ("ะฒั‹ัะพะบะธะน" ("tall" "high"))
+  ("ะฟัƒั‚ัŒ" ("way" "track" "path"))
+  ("ะฟะพัั‚ะพะผัƒ" ("therefore"))
+
+  ;; 301-400
+  ("ัะพะฒะตั€ัˆะตะฝะฝะพ" ("absolutely" "quite"))
+  ("ะบั€ะพะผะต" ("except" "besides"))
+  ("ั‚ั‹ััั‡ะฐ" ("a thousand"))
+  ("ะผะตััั†" ("month"))
+  ("ะฑั€ะฐั‚ัŒ" ("to take" "to hire"))
+  ("ะฝะฐะฟะธัะฐั‚ัŒ" ("to write"))
+  ("ั†ะตะปั‹ะน" ("intact" "whole" "entire"))
+  ("ะพะณั€ะพะผะฝั‹ะน" ("huge" "enormous"))
+  ("ะฝะฐั‡ะธะฝะฐั‚ัŒ" ("to begin"))
+  ("ัะฟะธะฝะฐ" ("back"))
+  ("ะฝะฐัั‚ะพัั‰ะธะน" ("present" "real" "true"))
+  ("ะฟัƒัั‚ัŒ" ("let's" "though"))
+  ("ัะทั‹ะบ" ("tongue" "language"))
+  ("ั‚ะพั‡ะฝะพ" ("exactly"))
+  ("ัั€ะตะดะธ" ("among"))
+  ("ั‡ัƒัั‚ะฒะพะฒะฐั‚ัŒ" ("to feel"))
+  ("ัะตั€ะดั†ะต" ("heart"))
+  ("ะฒะตัั‚ะธ" ("to lead"))
+  ("ะธะฝะพะณะดะฐ" ("sometimes"))
+  ("ะผะฐะปัŒั‡ะธะบ" ("boy"))
+  ("ัƒัะฟะตั‚ัŒ" ("to be in time" "to be successful"))
+  ("ะฝะตะฑะพ" ("sky"))
+  ("ะถะธะฒะพะน" ("living" "lively" "alive"))
+  ("ัะผะตั€ั‚ัŒ" ("death"))
+  ("ะฟั€ะพะดะพะปะถะฐั‚ัŒ" ("to continue"))
+  ("ะดะตะฒัƒัˆะบะฐ" ("girl"))
+  ("ะพะฑั€ะฐะท" ("shape" "form" "image"))
+  ("ะบะพ" ("to" "towards" "by"))
+  ("ะทะฐะฑั‹ั‚ัŒ" ("to forget"))
+  ("ะฒะพะบั€ัƒะณ" ("around"))
+  ("ะฟะธััŒะผะพ" ("letter"))
+  ("ะฒะปะฐัั‚ัŒ" ("power"))
+  ("ั‡ั‘ั€ะฝั‹ะน" ("black"))
+  ("ะฟั€ะพะนั‚ะธ" ("to pass" "go by" "be over") nil ("-ะน-"))
+  ("ะฟะพัะฒะธั‚ัŒัั" ("to appear" "to show up"))
+  ("ะฒะพะทะดัƒั…" ("air"))
+  ("ั€ะฐะทะฝั‹ะน" ("different"))
+  ("ะฒั‹ั…ะพะดะธั‚ัŒ" ("to go out" "to exit") ("MR says 'to nurse'??") ("-ั…ะพะด-"))
+  ("ะฟั€ะพัะธั‚ัŒ" ("to ask"))
+  ("ะฑั€ะฐั‚" ("brat"))
+  ("ัะพะฑัั‚ะฒะตะฝะฝั‹ะน" ("one's own"))
+  ("ะพั‚ะฝะพัˆะตะฝะธะต" ("relationship" "attitude"))
+  ("ะทะฐั‚ะตะผ" ("then" "after that"))
+  ("ะฟั‹ั‚ะฐั‚ัŒัั" ("to try"))
+  ("ะฟะพะบะฐะทะฐั‚ัŒ" ("to show" "to display"))
+  ("ะฒัะฟะพะผะฝะธั‚ัŒ" ("to remember" "to recall"))
+  ("ัะธัั‚ะตะผะฐ" ("system"))
+  ("ั‡ะตั‚ั‹ั€ะต" ("four"))
+  ("ะบะฒะฐั€ั‚ะธั€ะฐ" ("flat" "apartment"))
+  ("ะดะตั€ะถะฐั‚ัŒ" ("to hold" "to keep"))
+  ("ั‚ะฐะบะถะต" ("also" "as well" "too"))
+  ("ะปัŽะฑะพะฒัŒ" ("love"))
+  ("ัะพะปะดะฐั‚" ("soldier"))
+  ("ะพั‚ะบัƒะดะฐ" ("from where"))
+  ("ั‡ั‚ะพะฑ" ("that" "in order that"))
+  ("ะฝะฐะทั‹ะฒะฐั‚ัŒ" ("to call" "to name"))
+  ("ั‚ั€ะตั‚ะธะน" ("third"))
+  ("ั…ะพะทัะธะฝ" ("master" "boss" "host"))
+  ("ะฒั€ะพะดะต" ("like" "not unlike"))
+  ("ัƒั…ะพะดะธั‚ัŒ" ("to leave" "to go away") nil ("-ั…ะพะด-"))
+  ("ะฟะพะดะพะนั‚ะธ" ("to approach" "to come up") nil ("-ะน-"))
+  ("ะฟะพะดะฝัั‚ัŒ" ("to lift" "to raise"))
+  ("ัะฟั€ะฐัˆะธะฒะฐั‚ัŒ" ("to ask" "to inquire"))
+  ("ะฝะฐั‡ะฐะปัŒะฝะธะบ" ("chief" "head" "superior"))
+  ("ะพะฑะฐ" ("both"))
+  ("ะฑั€ะพัะธั‚ัŒ" ("to throw"))
+  ("ัˆะบะพะปะฐ" ("school"))
+  ("ะฟะฐั€ะตะฝัŒ" ("boy" "fellow" "guy"))
+  ("ะบั€ะพะฒัŒ" ("blood"))
+  ("ะดะฒะฐะดั†ะฐั‚ัŒ" ("twenty"))
+  ("ัะพะปะฝั†ะต" ("sun"))
+  ("ะฝะตะดะตะปั" ("week"))
+  ("ะฟะพัะปะฐั‚ัŒ" ("to send" "to dispatch"))
+  ("ะฝะฐั…ะพะดะธั‚ัŒัั" ("to be found" "to turn up") nil ("-ั…ะพะด-"))
+  ("ั€ะตะฑัั‚ะฐ" ("guys" "children"))
+  ("ะฟะพัั‚ะฐะฒะธั‚ัŒ" ("to put" "to place" "to set"))
+  ("ะฒัั‚ะฐั‚ัŒ" ("to get up" "to rise" "to stand up"))
+  ("ะฝะฐะฟั€ะธะผะตั€" ("for example" "for instance"))
+  ("ัˆะฐะณ" ("step"))
+  ("ะผัƒะถั‡ะธะฝะฐ" ("man" "male"))
+  ("ั€ะฐะฒะฝะพ" ("alike" "in like manner"))
+  ("ะฝะพั" ("nose"))
+  ("ะผะฐะปะพ" ("little" "few"))
+  ("ะฒะฝะธะผะฐะฝะธะต" ("attention"))
+  ("ะบะฐะฟะธั‚ะฐะฝ" ("captain" "master"))
+  ("ัƒั…ะพ" ("ear"))
+  ("ั‚ัƒะดะฐ" ("to there"))
+  ("ััŽะดะฐ" ("to here"))
+  ("ะธะณั€ะฐั‚ัŒ" ("to play"))
+  ("ัะปะตะดะพะฒะฐั‚ัŒ" ("to follow" "to come next"))
+  ("ั€ะฐััะบะฐะทะฐั‚ัŒ" ("to tell" "to narrate"))
+  ("ะฒะตะปะธะบะธะน" ("great"))
+  ("ะดะตะนัั‚ะฒะธั‚ะตะปัŒะฝะพ" ("indeed" "really"))
+  ("ัะปะธัˆะบะพะผ" ("too much"))
+  ("ั‚ัะถั‘ะปั‹ะน" ("heavy"))
+  ("ัะฟะฐั‚ัŒ" ("to sleep"))
+  ("ะพัั‚ะฐะฒะธั‚ัŒ" ("to leave" "to abandon"))
+  ("ะฒะพะนั‚ะธ" ("to enter" "to come in") nil ("-ะน-"))
+  ("ะดะปะธะฝะฝั‹ะน" ("long"))
+
+  ;; 401 - 500
+  ("ั‡ัƒะฒัั‚ะฒะพ" ("feeling"))
+  ("ะธะพะปั‡ะฐั‚ัŒ" ("to keep silence" "make no complaint" "say nothing"))
+  ("ั€ะฐััะบะฐะทั‹ะฒะฐั‚ัŒ" ("to tell" "narrate"))
+  ("ะพั‚ะฒะตั‡ะฐั‚ัŒ" ("to answer" "to reply"))
+  ("ัั‚ะฐะฝะพะฒะธั‚ัŒัั" ("to stand" "to become"))
+  ("ะพัั‚ะฐะฝะพะฒะธั‚ัŒัั" ("to stop"))
+  ("ะฑะตั€ะตะณ" ("bank" "shore" "coast"))
+  ("ัะตะผัŒั" ("family"))
+  ("ะธัะบะฐั‚ัŒ" ("to search"))
+  ("ะณะตะฝะตั€ะฐะป" ("general"))
+  ("ะผะพะผะตะฝั‚" ("moment" "instant"))
+  ("ะดะตััั‚ัŒ" ("ten"))
+  ("ะฝะฐั‡ะฐั‚ัŒ" ("to begin"))
+  ("ัะปะตะดัƒัŽัˆะธะน" ("next" "following"))
+  ("ะปะธั‡ะฝั‹ะน" ("personal"))
+  ("ั‚ั€ัƒะด" ("labour" "work"))
+  ("ะฒะตั€ะธั‚ัŒ" ("to believe"))
+  ("ะณั€ัƒะฟะฟะฐ" ("group"))
+  ("ะฝะตะผะฝะพะณะพ" ("a little"))
+  ("ะฒะฟั€ะพั‡ะตะผ" ("however" "though"))
+  ("ะฒะธะดะฝะพ" ("evidently" "obviously"))
+  ("ัะฒะปัั‚ัŒัั" ("to appear"))
+  ("ะผัƒะถ" ("husband"))
+  ("ั€ะฐะทะฒะต" ("really?" "perhaps") ("when pondering something"))
+  ("ะดะฒะธะถะตะฝะธะต" ("movement" "motion"))
+  ("ะฟะพั€ัะดะพะบ" ("order"))
+  ("ะพั‚ะฒะตั‚" ("answer" "reply"))
+  ("ั‚ะธั…ะพ" ("quietly" "silently") ("also as exclamation"))
+  ("ะทะฝะฐะบะพะผั‹ะน" ("familiar" "acquainted"))
+  ("ะณะฐะทะตั‚ะฐ" ("newspaper"))
+  ("ะฟะพะผะพั‰ัŒ" ("help"))
+  ("ัะธะปัŒะฝั‹ะน" ("strong" "powerful"))
+  ("ัะบะพั€ั‹ะน" ("quick" "fast"))
+  ("ัะพะฑะฐะบะฐ" ("dog"))
+  ("ะดะตั€ะตะฒะพ" ("tree"))
+  ("ัะฝะตะณ" ("snow"))
+  ("ัะพะฝ" ("dream"))
+  ("ัะผั‹ัะป" ("sense" "meaning" "purpose") ("making sense" "in the sense"))
+  ("ัะผะพั‡ัŒ" ("to be able") ("ัะฒ"))
+  ("ะฟั€ะพั‚ะธะฒ" ("against" "opposite" "contrary to"))
+  ("ะฑะตะถะฐั‚ัŒ" ("to run" "to hurry"))
+  ("ะดะฒะพั€" ("yard" "court"))
+  ("ั„ะพั€ะผะฐ" ("form" "shape" "uniform"))
+  ("ะฟั€ะพัั‚ะพะน" ("simple" "easy" "plain"))
+  ("ะฟั€ะธะตั…ะฐั‚ัŒ" ("to arrive" "to come"))
+  ("ะธะฝะพะน" ("different" "other"))
+  ("ะบั€ะธั‡ะฐั‚ัŒ" ("to cry" "to shout"))
+  ("ะฒะพะทะผะพะถะฝะพัั‚ัŒ" ("possibility" "opportunity" "chance"))
+  ("ะพะฑั‰ะตัั‚ะฒะพ" ("society"))
+  ("ะทะตะปั‘ะฝั‹ะน" ("green"))
+  ("ะณั€ัƒะดัŒ" ("breast" "chest"))
+  ("ัƒะณะพะป" ("corner" "angle"))
+  ("ะพั‚ะบั€ั‹ั‚ัŒ" ("to open"))
+  ("ะฟั€ะพะธัั…ะพะดะธั‚ัŒ" ("to happen" "to occur" "to take place"))
+  ("ะปะฐะดะฝะพ" ("well" "all right" "okay"))
+  ("ั‡ั‘ั€ะฝั‹ะน" ("black") ("noun (m.): as in 'she wears black'"))
+  ("ะฒะตะบ" ("century" "age"))
+  ("ะบะฐั€ะผะฐะฝ" ("pocket"))
+  ("ะตั…ะฐั‚ัŒ" ("to go" "ride" "drive" "travel"))
+  ("ะฝะตะผะตั†" ("German"))
+  ("ะฝะฐะฒะตั€ะฝะพะต" ("probably" "most likely"))
+  ("ะณัƒะฑะฐ" ("lip"))
+  ("ะดัะดั" ("uncle"))
+  ("ะฟั€ะธั…ะพะดะธั‚ัŒ" ("to come" "to arrive"))
+  ("ั‡ะฐัั‚ะพ" ("often"))
+  ("ะดะพะผะพะน" ("home") ("as in direction"))
+  ("ะพะณะพะฝัŒ" ("fire"))
+  ("ะฟะธัะฐั‚ะตะปัŒ" ("writer"))
+  ("ะฐั€ะผะธั" ("army"))
+  ("ัะพัั‚ะพัะฝะธะต" ("state" "condition" "fortune"))
+  ("ะทัƒะฑ" ("tooth"))
+  ("ะพั‡ะตั€ะตะดัŒ" ("queue" "line" "turn"))
+  ("ะบะพะน" ("which") ("old-fashioned, literary (in set expressions)"))
+  ("ะฟะพะดะฝัั‚ัŒัั" ("to rise" "to climb"))
+  ("ะบะฐะผะตะฝัŒ" ("stone"))
+  ("ะณะพัั‚ัŒ" ("guest"))
+  ("ะฟะพะบะฐะทะฐั‚ัŒัั" ("to appear" "to come in sight"))
+  ("ะฒะตั‚ะตั€" ("window"))
+  ("ัะพะฑะธั€ะฐั‚ัŒัั" ("to gather" "to assemble" "to intend") ("TODO: intend??"))
+  ("ะฟะพะฟะฐัั‚ัŒ" ("to hit" "to find oneself") ("to get (in phrases)"))
+  ("ะฟั€ะธะฝัั‚ัŒ" ("to take" "to admit" "to accept"))
+  ("ัะฝะฐั‡ะฐะปะฐ" ("at first" "from the beginning"))
+  ("ะปะธะฑะพ" ("or"))
+  ("ะฟะพะตั…ะฐั‚ัŒ" ("to depart" "to set off"))
+  ("ัƒัะปั‹ัˆะฐั‚ัŒ" ("to hear"))
+  ("ัƒะผะตั‚ัŒ" ("to be able" "know" "can"))
+  ("ัะปัƒั‡ะธั‚ัŒัั" ("to happen"))
+  ("ัั‚ั€ะฐะฝะฝั‹ะน" ("strange"))
+  ("ะตะดะธะฝัั‚ะฒะตะฝะฝั‹ะน" ("only" "sole"))
+  ("ั€ะพั‚ะฐ" ("company") ("(military)"))
+  ("ะทะฐะบะพะฝ" ("law" "act" "statute"))
+  ("ะบะพั€ะพั‚ะบะธะน" ("short"))
+  ("ะผะพั€ะต" ("sea"))
+  ("ะดะพะฑั€ั‹ะน" ("kind"))
+  ("ั‚ั‘ะผะฝั‹ะน" ("dark"))
+  ("ะณะพั€ะฐ" ("mountain" "hill"))
+  ("ะฒั€ะฐั‡" ("doctor"))
+  ("ะบั€ะฐะน" ("border, edge" "land, country"))
+  ("ัั‚ะฐั€ะฐั‚ัŒัั" ("to try" "to endeavour"))
+  ("ะปัƒั‡ัˆะธะน" ("better" "best"))
+
+  ;; 501 - 600
+  ("ั€ะตะบะฐ" ("river"))
+  ("ะฒะพะตะฝะฝั‹ะน" ("military"))
+  ("ะผะตั€ะฐ" ("measure" "step"))
+  ("ัั‚ั€ะฐัˆะฝั‹ะน" ("terrible" "frightful"))
+  ("ะฒะฟะพะปะฝะต" ("quite" "fully"))
+  ("ะทะฒะฐั‚ัŒ" ("to call"))
+  ("ะฟั€ะพะธะทะพะนั‚ะธ" ("to happen" "to occur" "take place"))
+  ("ะฒะฟะตั€ะตะด" ("forward"))
+  ("ะผะตะดะปะตะฝะฝะพ" ("slowly"))
+  ("ะฒะพะทะปะต" ("by" "near" "close by"))
+  ("ะฝะธะบะฐะบ" ("in no way" "by no means"))
+  ("ะทะฐะฝะธะผะฐั‚ัŒัั" ("to be occupied" "to engage"))
+  ("ะดะตะนัั‚ะฒะธะต" ("action" "effort"))
+  ("ะดะพะฒะพะปัŒะฝะพ" ("enough" "rather"))
+  ("ะฒะตั‰ัŒ" ("thing"))
+  ("ะฝะตะพะฑั…ะพะดะธะผั‹ะน" ("necessary") ("not possible to go around"))
+  ("ั…ะพะด" ("move"))
+  ("ะฑะพะปัŒ" ("pain"))
+  ("ััƒะดัŒะฑะฐ" ("fate" "fortune" "destiny"))
+  ("ะฟั€ะธั‡ะธะฝะฐ" ("cause" "reason" "motive"))
+  ("ะฟะพะปะพะถะธั‚ัŒ" ("to lay down" "put down" "place"))
+  ("ะตะดะฒะฐ" ("hardly" "just" "barely"))
+  ("ั‡ะตั€ั‚ะฐ" ("line" "boundary" "trait"))
+  ("ะดะตะฒะพั‡ะบะฐ" ("girl" "little girl"))
+  ("ะปั‘ะณะบะธะน" ("light" "easy"))
+  ("ะฒะพะปะพั" ("hair"))
+  ("ะบัƒะฟะธั‚ัŒ" ("to buy" "purchase"))
+  ("ะฝะพะผะตั€" ("number" "size" "room" "issue"))
+  ("ะพัะฝะพะฒะฝะพะน" ("main"))
+  ("ัˆะธั€ะพะบะธะน" ("wide"))
+  ("ัƒะผะตั€ะตั‚ัŒ" ("to die"))
+  ("ะดะฐะปะตะบะพ" ("far" "far off"))
+  ("ะฟะปะพั…ะพ" ("badly"))
+  ("ะณะปะฐะฒะฐ" ("head" "chief"))
+  ("ะบั€ะฐัะธะฒั‹ะน" ("beautiful"))
+  ("ัะตั€ั‹ะน" ("grey" "dull"))
+  ("ะฟะธั‚ัŒ" ("to drink"))
+  ("ะบะพะผะฐะฝะดะธั€" ("commander" "officer"))
+  ("ะพะฑั‹ั‡ะฝะพ" ("usually"))
+  ("ะฟะฐั€ั‚ะธั" ("party"))
+  ("ะฟั€ะพะฑะปะตะผะฐ" ("problem" "issue"))
+  ("ัั‚ั€ะฐั…" ("fear"))
+  ("ะฟั€ะพั…ะพะดะธั‚ัŒ" ("to pass" "go" "study"))
+  ("ััะฝะพ" ("clear" "clearly"))
+  ("ัะฝัั‚ัŒ" ("to take away" "take off"))
+  ("ะฑัƒะผะฐะณะฐ" ("paper"))
+  ("ะณะตั€ะพะน" ("hero"))
+  ("ะฟะฐั€ะฐ" ("pair" "couple"))
+  ("ะณะพััƒะดะฐั€ัั‚ะฒะพ" ("State"))
+  ("ะดะตั€ะตะฒะฝั" ("village"))
+  ("ั€ะตั‡ัŒ" ("speech"))
+  ("ะฝะฐั‡ะฐั‚ัŒัั" ("to begin"))
+  ("ัั€ะตะดัั‚ะฒะพ" ("means" "remedy"))
+  ("ะฟะพะปะพะถะตะฝะธะต" ("position" "posture" "condition" "state"))
+  ("ัะฒัะทัŒ" ("tie, bond" "connection, relation"))
+  ("ะฝะตะฑะพะปัŒัˆะพะน" ("small" "not great"))
+  ("ะฟั€ะตะดัั‚ะฐะฒะปัั‚ัŒ" ("to present" "introduce" "imagine"))
+  ("ะทะฐะฒั‚ั€ะฐ" ("tomorrow"))
+  ("ะพะฑัŠััะฝะธั‚ัŒ" ("to explain"))
+  ("ะฟัƒัั‚ะพะน" ("empty" "hollow" "idle"))
+  ("ะฟั€ะพะธะทะฝะตัั‚ะธ" ("to pronounce" "say" "utter"))
+  ("ั‡ะตะปะพะฒะตั‡ะตัะบะธะน" ("human"))
+  ("ะฝั€ะฐะฒะธั‚ัŒัั" ("to please" "be likeable to"))
+  ("ะพะดะฝะฐะถะดั‹" ("once" "one day"))
+  ("ะผะธะผะพ" ("past" "by"))
+  ("ะธะฝะฐั‡ะต" ("otherwise" "differently|"))
+  ("ััƒั‰ะตัั‚ะฒั€ะพะฒะฐั‚ัŒ" ("to exist" "to be"))
+  ("ะบะปะฐัั" ("class"))
+  ("ัƒะดะฐั‚ัŒัั" ("turn out well" "succeed" "manage"))
+  ("ั‚ะพะปัั‚ั‹ะน" ("thick" "heavy" "fat"))
+  ("ั†ะตะปัŒ" ("goal" "object" "target"))
+  ("ัะบะฒะพะทัŒ" ("through"))
+  ("ะฟั€ะธะนั‚ะธััŒ" ("to fit" "fall" "have to") ("ั‚ะตะฑะต ะฟั€ะธะดั‘ั‚ัั - you have to"))
+  ("ั‡ะธัั‚ั‹ะน" ("clean" "pure"))
+  ("ะทะฝะฐั‚ัŒ" ("to know"))
+  ("ะฟั€ะตะถะฝะธะน" ("former"))
+  ("ะฟั€ะพั„ะตััะพั€" ("professor"))
+  ("ะณะพัะฟะพะดะธะฝ" ("gentleman" "Mr."))
+  ("ัั‡ะฐัั‚ัŒะต" ("happiness" "luck"))
+  ("ั…ัƒะดะพะน" ("thin" "skinny"))
+  ("ะดัƒั…" ("spirit"))
+  ("ะฟะปะฐะฝ" ("plan"))
+  ("ั‡ัƒะถะพะน" ("somebody else's" "strange" "foreign"))
+  ("ะทะฐะป" ("hall"))
+  ("ะฟั€ะตะดัั‚ะฐะฒะธั‚ัŒ" ("to present" "produce" "introduce"))
+  ("ะพัะพะฑั‹ะน" ("special"))
+  ("ะดะธั€ะตะบั‚ะพั€" ("director" "manager"))
+  ("ะฑั‹ะฒัˆะธะน" ("former" "ex-"))
+  ("ะฟะฐะผัั‚ัŒ" ("memory"))
+  ("ะฑะปะธะทะบะธะน" ("near" "similar" "intimate"))
+  ("ัะตะน" ("this"))
+  ("ั€ะตะทัƒะปัŒั‚ะฐั‚" ("result" "outcome"))
+  ("ะฑะพะปัŒะฝะพะน" ("sick"))
+  ("ะดะฐะฝะฝั‹ะน" ("given" "present"))
+  ("ะบัั‚ะฐั‚ะธ" ("to the point" "at the same time"))
+  ("ะฝะฐะทะฒะฐั‚ัŒ" ("to call" "name"))
+  ("ัะปะตะด" ("track" "footprint"))
+  ("ัƒะปั‹ะฑะฐั‚ัŒัั" ("to smile") ("ะฝัะฒ"))
+  ("ะฑัƒั‚ั‹ะปะบะฐ" ("bottle"))
+
+  ;; 601 - 700
+  ("ั‚ั€ัƒะดะฝะพ" ("with difficulty"))
+  ("ัƒัะปะพะฒะธะต" ("condition" "term"))
+  ("ะฟั€ะตะถะดะต" ("before"))
+  ("ัƒะผ" ("mind" "brains" "intellect"))
+  ("ัƒะปั‹ะฑะฝัƒั‚ัŒัั" ("to smile"))
+  ("ะฟั€ะพั†ะตัั" ("process"))
+  ("ะบะฐั€ั‚ะธะฝะฐ" ("picture" "painting"))
+  ("ะฒะผะตัั‚ะพ" ("instead"))
+  ("ัั‚ะฐั€ัˆะธะน" ("elder" "senior"))
+  ("ะปะตะณะบะพ" ("easily" "lightly"))
+  ("ั†ะตะฝั‚ั€" ("center"))
+  ("ะฟะพะดะพะฑะฝั‹ะน" ("similar" "like"))
+  ("ะฒะพะทะผะพะถะฝะพ" ("possible") ("as ... as possible"))
+  ("ะพะบะพะปะพ" ("by" "near"))
+  ("ัะผะตัั‚ัŒัั" ("to laugh"))
+  ("ัั‚ะพ" ("hundred"))
+  ("ะฑัƒะดัƒั‰ะตะต" ("future"))
+  ("ั…ะฒะฐั‚ะฐั‚ัŒ" ("to snatch" "to seize" "to suffice") ("ะฝัะฒ"))
+  ("ั‡ะธัะปะพ" ("number"))
+  ("ะฒััะบะพะต" ("any" "every"))
+  ("ั€ัƒะฑะปัŒ" ("ruble"))
+  ("ะฟะพั‡ัƒะฒัั‚ะฒะพะฒะฐั‚ัŒ" ("to feel") ("ัะฒ"))
+  ("ะฟั€ะธะฝะตัั‚ะธ" ("to bring"))
+  ("ะฒะตั€ะฐ" ("faith" "belief"))
+  ("ะฒะพะฒัะต" ("quiet" "not ... at all"))
+  ("ัƒะดะฐั€" ("blow" "stroke"))
+  ("ั‚ะตะปะตั„ะพะฝ" ("telephone"))
+  ("ะบะพะปะตะฝะพ" ("knee"))
+  ("ัะพะณะปะฐัะธั‚ัŒัั" ("to agree" "to consent"))
+  ("ะผะฐะปะพ" ("little" "few" "not enough"))
+  ("ะบะพั€ะธะดะพั€" ("corridor" "passage"))
+  ("ะผัƒะถะธะบ" ("man"))
+  ("ะฟั€ะฐะฒั‹ะน" ("right"))
+  ("ะฐะฒั‚ะพั€" ("author"))
+  ("ั…ะพะปะพะดะฝั‹ะน" ("cold" "cool"))
+  ("ั…ะฒะฐั‚ะธั‚" ("to snatch" "to seize" "to suffice") ("ัะฒ"))
+  ("ะผะฝะพะณะธะต" ("many"))
+  ("ะฒัั‚ั€ะตั‡ะฐ" ("meeting" "reception"))
+  ("ะบะฐะฑะธะฝะตั‚" ("study" "room" "office suite"))
+  ("ะดะพะบัƒะผะตะฝั‚" ("document"))
+  ("ัะฐะผะพะปั‘ั‚" ("airplane"))
+  ("ะฒะฝะธะท" ("down" "downwards"))
+  ("ะฟั€ะธะฝะธะผะฐั‚ัŒ" ("to take" "to admit" "to accept"))
+  ("ะธะณั€ะฐ" ("game" "play"))
+  ("ั€ะฐััะบะฐะท" ("story"))
+  ("ั…ะปะตะฑ" ("bread"))
+  ("ั€ะฐะทะฒะธั‚ะธะต" ("development"))
+  ("ัƒะฑะธั‚ัŒ" ("to kill"))
+  ("ั€ะพะดะฝะพะน" ("own" "native" "dear"))
+  ("ะพั‚ะบั€ั‹ั‚ั‹ะน" ("open"))
+  ("ะผะตะฝะตะต" ("less"))
+  ("ะฟั€ะตะดะปะพะถะธั‚ัŒ" ("to offer" "to propose" "to suggest"))
+  ("ะถั‘ะปั‚ั‹ะน" ("yellow"))
+  ("ะฟั€ะธั…ะพะดะธั‚ัŒัั" ("to fit" "to fall" "to have to"))
+  ("ะฒั‹ะฟะธั‚ัŒ" ("to drink"))
+  ("ะบั€ะธะบะฝัƒั‚ัŒ" ("to cry" "to shout"))
+  ("ั‚ั€ัƒะฑะบะฐ" ("tube" "roll" "pipe"))
+  ("ะฒั€ะฐะณ" ("enemy"))
+  ("ะฟะพะบะฐะทั‹ะฒะฐั‚ัŒ" ("to show" "to display"))
+  ("ะดะฒะพะต" ("two") ("cardinal number"))
+  ("ะดะพะบั‚ะพั€" ("doctor"))
+  ("ะปะฐะดะพะฝัŒ" ("palm"))
+  ("ะฒั‹ะทะฒะฐั‚ัŒ" ("to call" "to send"))
+  ("ัะฟะพะบะพะนะฝะพ" ("quietly"))
+  ("ะฟะพะฟั€ะพัะธั‚ัŒ" ("to ask"))
+  ("ะฝะฐัƒะบะฐ" ("science"))
+  ("ะปะตะนั‚ะตะฝะฐะฝั‚" ("lieutenant"))
+  ("ัะปัƒะถะฑะฐ" ("service" "work"))
+  ("ะพะบะฐะทั‹ะฒะฐั‚ัŒัั" ("to turn out" "to find oneself"))
+  ("ะฟั€ะธะฒะตัั‚ะธ" ("to bring"))
+  ("ัะพั€ะพะบ" ("forty"))
+  ("ัั‡ั‘ั‚" ("bill" "account"))
+  ("ะฒะพะทะฒั€ะฐั‰ะฐั‚ัŒัั" ("to return"))
+  ("ะทะพะปะพั‚ะพะน" ("golden"))
+  ("ะผะตัั‚ะฝั‹ะน" ("local"))
+  ("ะบัƒั…ะฝั" ("kitchen"))
+  ("ะบั€ัƒะฟะฝั‹ะน" ("large" "big" "prominent"))
+  ("ั€ะตัˆะตะฝะธะต" ("decision" "conclusion"))
+  ("ะผะพะปะพะดะฐั" ("bride" "young"))
+  ("ั‚ั€ะธะดั†ะฐั‚ัŒ" ("thirty"))
+  ("ั€ะพะผะฐะฝ" ("novel" "romance"))
+  ("ะบะพะผะฟะฐะฝะธั" ("company"))
+  ("ั‡ะฐัั‚ั‹ะน" ("frequent"))
+  ("ั€ะพััะธะนัะบะธะน" ("Russian"))
+  ("ั€ะฐะฑะพั‡ะธะน" ("worky"))
+  ("ะฟะพั‚ะตั€ัั‚ัŒ" ("to lose"))
+  ("ั‚ะตั‡ะตะฝะธะต" ("current"))
+  ("ัะธะฝะธะน" ("dark blue"))
+  ("ัั‚ะพะปัŒะบะพ" ("so much" "so many"))
+  ("ั‚ั‘ะฟะปั‹ะน" ("warm"))
+  ("ะผะตั‚ั€" ("metre"))
+  ("ะดะพัั‚ะฐั‚ัŒ" ("to reach" "get" "obtain"))
+  ("ะถะตะปะตะทะฝั‹ะน" ("ferreous" "iron"))
+  ("ะธะฝัั‚ะธั‚ัƒั‚" ("institute"))
+  ("ัะพะพะฑั‰ะธั‚ัŒ" ("to report" "to let know"))
+  ("ะธะฝั‚ะตั€ะตั" ("interest"))
+  ("ะพะฑั‹ั‡ะฝั‹ะน" ("usual" "ordinary"))
+  ("ะฟะพัะฒะปัั‚ัŒัั" ("to appear" "to show up"))
+  ("ัƒะฟะฐัั‚ัŒ" ("to fall")))
+
+(provide 'russian-words)
diff --git a/users/tazjin/rustfmt.toml b/users/tazjin/rustfmt.toml
new file mode 100644
index 0000000000..0c719dcfec
--- /dev/null
+++ b/users/tazjin/rustfmt.toml
@@ -0,0 +1,22 @@
+edition = "2021"
+newline_style = "Unix"
+
+# Default code with is 100 characters, comments should follow
+# suit.
+wrap_comments = true
+
+# The default of this option creates hard-to-read nesting of
+# conditionals, turn it off.
+combine_control_expr = false
+
+# Group imports by module, but no higher. This avoids hard-to-read
+# nested use statements.
+imports_granularity = "Module"
+
+# Avoid vertical visual clutter by unnecessarily exploding
+# block-like arguments.
+overflow_delimited_expr = true
+
+# Miscellaneous
+format_code_in_doc_comments = true
+normalize_comments = true
diff --git a/users/tazjin/secrets/default.nix b/users/tazjin/secrets/default.nix
new file mode 100644
index 0000000000..5550103c5a
--- /dev/null
+++ b/users/tazjin/secrets/default.nix
@@ -0,0 +1,3 @@
+{ depot, ... }:
+
+depot.ops.secrets.mkSecrets ./. (import ./secrets.nix)
diff --git a/users/tazjin/secrets/geesefs-tazjins-files.age b/users/tazjin/secrets/geesefs-tazjins-files.age
new file mode 100644
index 0000000000..9132c7d108
--- /dev/null
+++ b/users/tazjin/secrets/geesefs-tazjins-files.age
@@ -0,0 +1,18 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw SrmIul/C/aRTYy5+vVBB0H2bS65XayYf2TXrOSTEbGg
+Js016EtAxiFmyJ4gTmXEjsKT9JmIntcMNgAds+qT7Js
+-> ssh-ed25519 zcCuhA NfUQBKL1KgvUosB2y3oI5HwPjA+4kf8kbBbpNf43JAk
+oE4R2rz1sdBitKzQlzMzneyu8Rvc5utHYRyCeGCQR8g
+-> ssh-rsa zXi7VA
+aDDiygAF5benqqJ1387F9qVDyvb48BkBLAwRi7eUYWkG41s9XUdmK5ppjFdxy1c8
+fx5YcPjO3m84pIv0RxiK7rZhkVi1/eiHhT5lId83wIzQdybjKFSc85YjFO3mGv9A
+EeFMEmlfsRkBHYq/j6Npbg4M5kMxSuSwGSyt6qnoHSWT2phS+41WLA/XT9ln4pRR
+dBDO0ZyK/CpgfDuGo/JARLiGeEMwt7SvkyXidcbD8glg9buu1VGxb/8m/ob1yrbn
+y3mjfOFzO2zF8ZHuScWQlZgvaVk412Xne+n+wva12tS52dEX4FRSMtmUOB8Ai4oq
+wvWB6Ikru5jXRxe8NgDoZw
+-> ssh-ed25519 At5Mag yBwWJVhArq9iwngwaIph56iGfje8T55Ig0nW9268Kic
+T9IWxRJF1U0STinVlBJoaGoegERnWRjnGZeW0HHGQ9Y
+-> C"!?nfs%-grease >>.%|I mA Fd7?aw2m 37I
+vRH3yR7+Ow
+--- AKc40DwXghKw6GHzJUYNJYE0JqMr4M+hR41VRA1IvS4
+๒ลืtฺœ๑ใ)u;T™•	Œ'เ๖8๖=๘S7Rอ<Cx†t๙z_mdา0ZH2สแ^N—jท†๏*Cษ—	๋Šฃด1อนyฦผa—eน8c~วๆi	5/ฒ‹xลŸ7nลS^ฑaวsyg’ะฏ(CŠพ™คฅอ ั%N’ธกะ่๘|Y!ฎvะA๐๐I
\ No newline at end of file
diff --git a/users/tazjin/secrets/miniflux.age b/users/tazjin/secrets/miniflux.age
new file mode 100644
index 0000000000..753dc6f034
--- /dev/null
+++ b/users/tazjin/secrets/miniflux.age
@@ -0,0 +1,14 @@
+age-encryption.org/v1
+-> ssh-ed25519 dcsaLw SJBK+ym6o6dcB/+HFWzArbXS9RmyDjnglVxcXduJA1g
+pPWIi2A4G4X7I14HoZUWsNd/MOfhW1ZanwB/5OROSrw
+-> ssh-ed25519 zcCuhA oo/8OTqpV85g/9pha0qkmxwlYAlsc7v+nXbbtj67Jmc
+AexsAIgW6e5fYoPNJJZYdP61OvON2bKiL9ZJgLdG/zU
+-> ssh-ed25519 ph9lig 4evTl0M3SfdlmTixm3WnVqfHMPf/TYIyBKPdlfPisC0
+AK4GyhgqXN2wxbcFRGwbNNQJ4/2iFPt3CKGHosNJbmY
+-> ssh-ed25519 At5Mag JJ8r/qD5i+LLAY7jnnHXAgykAuHtzxtGGzdqw7BAogY
+wotjW3yaTq1IdqVUwoCVwzglXsmnzniQIt7SDBrF4jY
+-> sPHo{W-grease , h6 =mEp^w `ccnF
+QQEb+Vh1+Fv++oPQwdTfOB2Cg5JaP4GCOq0o3J+xSqCY1gE0cguwLGXwa6+Tylu2
+Kuh4pMovAxnlHUt44u6f
+--- yWQyncCrxJzVHffFaFT704BEp8hjUn09a+23r4S39N4
+}ปาฃฯ
๏l
˜ุI&m_{้ฝ์จXฬฎฟด๋?f1Mฝจษง|„ฦJ’‰ฝืVฦLไ<H๕5 ŒN›Q๖ใผๆQV์#>็งs2Mฐ0 ขI
\ No newline at end of file
diff --git a/users/tazjin/secrets/secrets.nix b/users/tazjin/secrets/secrets.nix
new file mode 100644
index 0000000000..12f12f721c
--- /dev/null
+++ b/users/tazjin/secrets/secrets.nix
@@ -0,0 +1,16 @@
+let
+  myKeys = import ../keys { };
+  allKeys = [
+    # local keys
+    myKeys.tverskoy_ed25519
+    myKeys.zamalek_ed25519
+    myKeys.khamovnik_agenix
+    # koptevo
+    "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMw2ZfdNZCXCOtbQNT6hztXCIkTcO9MBrOuDqMlmGOYK root@koptevo"
+  ];
+in
+{
+  "geesefs-tazjins-files.age".publicKeys = allKeys;
+  "miniflux.age".publicKeys = allKeys;
+  "tgsa-yandex.age".publicKeys = allKeys;
+}
diff --git a/users/tazjin/secrets/tgsa-yandex.age b/users/tazjin/secrets/tgsa-yandex.age
new file mode 100644
index 0000000000..b1400d0673
--- /dev/null
+++ b/users/tazjin/secrets/tgsa-yandex.age
Binary files differdiff --git a/users/tazjin/tgsa/.gitignore b/users/tazjin/tgsa/.gitignore
new file mode 100644
index 0000000000..29e65519ba
--- /dev/null
+++ b/users/tazjin/tgsa/.gitignore
@@ -0,0 +1,3 @@
+result
+/target
+**/*.rs.bk
diff --git a/users/tazjin/tgsa/Cargo.lock b/users/tazjin/tgsa/Cargo.lock
new file mode 100644
index 0000000000..6be9c490d4
--- /dev/null
+++ b/users/tazjin/tgsa/Cargo.lock
@@ -0,0 +1,1567 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.81"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
+
+[[package]]
+name = "ascii"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
+
+[[package]]
+name = "autocfg"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
+
+[[package]]
+name = "base64"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+
+[[package]]
+name = "base64"
+version = "0.21.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+
+[[package]]
+name = "buf_redux"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
+dependencies = [
+ "memchr",
+ "safemem",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.15.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "cc"
+version = "1.0.90"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "num-traits",
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "chunked_transfer"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901"
+
+[[package]]
+name = "convert_case"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
+
+[[package]]
+name = "crimp"
+version = "4087.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ead2c83f7d1f9b8e5a6f7a25985d0d1759ccd2cd72abb1eee2db65d05e12b39"
+dependencies = [
+ "curl",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "cssparser"
+version = "0.27.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a"
+dependencies = [
+ "cssparser-macros",
+ "dtoa-short",
+ "itoa 0.4.8",
+ "matches",
+ "phf 0.8.0",
+ "proc-macro2",
+ "quote",
+ "smallvec",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "cssparser-macros"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+dependencies = [
+ "quote",
+ "syn 2.0.57",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "windows-sys",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.72+curl-8.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "windows-sys",
+]
+
+[[package]]
+name = "deranged"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "derive_more"
+version = "0.99.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
+dependencies = [
+ "convert_case",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "dtoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653"
+
+[[package]]
+name = "dtoa-short"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74"
+dependencies = [
+ "dtoa",
+]
+
+[[package]]
+name = "ego-tree"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591"
+
+[[package]]
+name = "errno"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
+
+[[package]]
+name = "filetime"
+version = "0.2.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "windows-sys",
+]
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
+dependencies = [
+ "mac",
+ "new_debug_unreachable",
+]
+
+[[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "getopts"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.9.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "html5ever"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
+dependencies = [
+ "log",
+ "mac",
+ "markup5ever",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "idna"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+]
+
+[[package]]
+name = "itoa"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.153"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+
+[[package]]
+name = "libz-sys"
+version = "1.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
+
+[[package]]
+name = "lock_api"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "mac"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
+
+[[package]]
+name = "markup5ever"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016"
+dependencies = [
+ "log",
+ "phf 0.10.1",
+ "phf_codegen 0.10.0",
+ "string_cache",
+ "string_cache_codegen",
+ "tendril",
+]
+
+[[package]]
+name = "matches"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
+
+[[package]]
+name = "memchr"
+version = "2.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "multipart"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182"
+dependencies = [
+ "buf_redux",
+ "httparse",
+ "log",
+ "mime",
+ "mime_guess",
+ "quick-error",
+ "rand 0.8.5",
+ "safemem",
+ "tempfile",
+ "twoway",
+]
+
+[[package]]
+name = "new_debug_unreachable"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+
+[[package]]
+name = "nodrop"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
+
+[[package]]
+name = "num-conv"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
+
+[[package]]
+name = "num-traits"
+version = "0.2.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "num_cpus"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
+dependencies = [
+ "hermit-abi",
+ "libc",
+]
+
+[[package]]
+name = "num_threads"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "openssl"
+version = "0.10.64"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f"
+dependencies = [
+ "bitflags 2.5.0",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.57",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "phf"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
+dependencies = [
+ "phf_macros",
+ "phf_shared 0.8.0",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
+dependencies = [
+ "phf_shared 0.10.0",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
+dependencies = [
+ "phf_shared 0.8.0",
+ "rand 0.7.3",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared 0.10.0",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c"
+dependencies = [
+ "phf_generator 0.8.0",
+ "phf_shared 0.8.0",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "precomputed-hash"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
+
+[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.79"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quick-error"
+version = "1.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
+
+[[package]]
+name = "quote"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
+dependencies = [
+ "getrandom 0.1.16",
+ "libc",
+ "rand_chacha 0.2.2",
+ "rand_core 0.5.1",
+ "rand_hc",
+ "rand_pcg",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha 0.3.1",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core 0.6.4",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
+dependencies = [
+ "getrandom 0.1.16",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom 0.2.12",
+]
+
+[[package]]
+name = "rand_hc"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "rand_pcg"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
+dependencies = [
+ "rand_core 0.5.1",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "ring"
+version = "0.16.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
+dependencies = [
+ "cc",
+ "libc",
+ "once_cell",
+ "spin",
+ "untrusted",
+ "web-sys",
+ "winapi",
+]
+
+[[package]]
+name = "rouille"
+version = "3.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3716fbf57fc1084d7a706adf4e445298d123e4a44294c4e8213caf1b85fcc921"
+dependencies = [
+ "base64 0.13.1",
+ "chrono",
+ "filetime",
+ "multipart",
+ "percent-encoding",
+ "rand 0.8.5",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "sha1_smol",
+ "threadpool",
+ "time",
+ "tiny_http",
+ "url",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
+dependencies = [
+ "bitflags 2.5.0",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
+
+[[package]]
+name = "schannel"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "scraper"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5684396b456f3eb69ceeb34d1b5cb1a2f6acf7ca4452131efa3ba0ee2c2d0a70"
+dependencies = [
+ "cssparser",
+ "ego-tree",
+ "getopts",
+ "html5ever",
+ "matches",
+ "selectors",
+ "smallvec",
+ "tendril",
+]
+
+[[package]]
+name = "selectors"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe"
+dependencies = [
+ "bitflags 1.3.2",
+ "cssparser",
+ "derive_more",
+ "fxhash",
+ "log",
+ "matches",
+ "phf 0.8.0",
+ "phf_codegen 0.8.0",
+ "precomputed-hash",
+ "servo_arc",
+ "smallvec",
+ "thin-slice",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca"
+
+[[package]]
+name = "serde"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.197"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.57",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.115"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
+dependencies = [
+ "itoa 1.0.11",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "servo_arc"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432"
+dependencies = [
+ "nodrop",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "sha1_smol"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "socket2"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "spin"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "string_cache"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
+dependencies = [
+ "new_debug_unreachable",
+ "once_cell",
+ "parking_lot",
+ "phf_shared 0.10.0",
+ "precomputed-hash",
+ "serde",
+]
+
+[[package]]
+name = "string_cache_codegen"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988"
+dependencies = [
+ "phf_generator 0.10.0",
+ "phf_shared 0.10.0",
+ "proc-macro2",
+ "quote",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11a6ae1e52eb25aab8f3fb9fca13be982a373b8f1157ca14b897a825ba4a2d35"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "tendril"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0"
+dependencies = [
+ "futf",
+ "mac",
+ "utf-8",
+]
+
+[[package]]
+name = "tgsa"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "base64 0.21.7",
+ "crimp",
+ "ego-tree",
+ "lazy_static",
+ "openssl",
+ "ring",
+ "rouille",
+ "scraper",
+ "serde",
+ "serde_json",
+ "url",
+]
+
+[[package]]
+name = "thin-slice"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
+
+[[package]]
+name = "threadpool"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
+dependencies = [
+ "num_cpus",
+]
+
+[[package]]
+name = "time"
+version = "0.3.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
+dependencies = [
+ "deranged",
+ "libc",
+ "num-conv",
+ "num_threads",
+ "powerfmt",
+ "serde",
+ "time-core",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
+
+[[package]]
+name = "tiny_http"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82"
+dependencies = [
+ "ascii",
+ "chunked_transfer",
+ "httpdate",
+ "log",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "twoway"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "unicase"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
+dependencies = [
+ "version_check",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-width"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
+
+[[package]]
+name = "untrusted"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
+
+[[package]]
+name = "url"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf-8"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasi"
+version = "0.9.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.57",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.57",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
+
+[[package]]
+name = "web-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.4",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.4",
+ "windows_aarch64_msvc 0.52.4",
+ "windows_i686_gnu 0.52.4",
+ "windows_i686_msvc 0.52.4",
+ "windows_x86_64_gnu 0.52.4",
+ "windows_x86_64_gnullvm 0.52.4",
+ "windows_x86_64_msvc 0.52.4",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
diff --git a/users/tazjin/tgsa/Cargo.toml b/users/tazjin/tgsa/Cargo.toml
new file mode 100644
index 0000000000..8764ef6524
--- /dev/null
+++ b/users/tazjin/tgsa/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "tgsa"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0"
+crimp = "4087.0"
+rouille = { version = "3.5", default-features = false }
+url = "2.3"
+scraper = "0.13"
+ego-tree = "0.6" # in tandem with 'scraper'
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+ring = "0.16.20"
+openssl = "0.10.54"
+base64 = "0.21.2"
+lazy_static = "1.4.0"
diff --git a/users/tazjin/tgsa/default.nix b/users/tazjin/tgsa/default.nix
new file mode 100644
index 0000000000..063781047a
--- /dev/null
+++ b/users/tazjin/tgsa/default.nix
@@ -0,0 +1,17 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = depot.nix.sparseTree {
+    root = ./.;
+    paths = [
+      ./Cargo.lock
+      ./Cargo.toml
+      ./src
+    ];
+  };
+
+  buildInputs = with pkgs; [
+    pkg-config
+    openssl
+  ];
+}
diff --git a/users/tazjin/tgsa/src/main.rs b/users/tazjin/tgsa/src/main.rs
new file mode 100644
index 0000000000..d9a5d4abc2
--- /dev/null
+++ b/users/tazjin/tgsa/src/main.rs
@@ -0,0 +1,403 @@
+use anyhow::{anyhow, Context, Result};
+use scraper::{Html, Selector};
+use std::collections::HashMap;
+use std::sync::RwLock;
+use std::time::{Duration, Instant};
+
+mod translate;
+
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
+struct TgLink {
+    username: String,
+    message_id: usize,
+    translated: bool,
+}
+
+impl TgLink {
+    fn human_friendly_url(&self) -> String {
+        format!("t.me/{}/{}", self.username, self.message_id)
+    }
+
+    fn to_url(&self, embed: bool) -> String {
+        format!(
+            "https://t.me/{}/{}{}",
+            self.username,
+            self.message_id,
+            if embed { "?embed=1" } else { "" }
+        )
+    }
+
+    fn parse(url: &str, translated: bool) -> Option<Self> {
+        let url = url.strip_prefix('/')?;
+        let parsed = url::Url::parse(url).ok()?;
+
+        if parsed.host()? != url::Host::Domain("t.me") {
+            // only t.me links are supported
+            return None;
+        }
+
+        let parts = parsed.path_segments()?.collect::<Vec<&str>>();
+        if parts.len() != 2 {
+            // only message links are supported
+            return None;
+        }
+
+        Some(TgLink {
+            username: parts[0].into(),
+            message_id: parts[1].parse().ok()?,
+            translated,
+        })
+    }
+}
+
+fn fetch_post(link: &TgLink, embed: bool) -> Result<String> {
+    println!("fetching {}#{}", link.username, link.message_id);
+    let response = crimp::Request::get(&link.to_url(embed))
+        .send()
+        .context("failed to fetch embed data")?
+        .as_string()
+        .context("failed to decode embed data")?
+        .error_for_status(|resp| {
+            anyhow!("telegram request failed: {} ({})", resp.body, resp.status)
+        })?;
+
+    Ok(response.body)
+}
+
+// in some cases, posts can not be embedded, but telegram still
+// includes their content in metadata tags for content previews.
+//
+// we skip images in this case, as they are scaled down to thumbnail
+// size and not useful.
+fn fetch_fallback(link: &TgLink) -> Result<Option<String>> {
+    let post = fetch_post(link, false)?;
+    let doc = Html::parse_document(&post);
+    let desc_sel = Selector::parse("meta[property=\"og:description\"]").unwrap();
+    let desc_elem = match doc.select(&desc_sel).next() {
+        None => return Ok(None),
+        Some(elem) => elem,
+    };
+
+    let content = match desc_elem.value().attr("content") {
+        None => return Ok(None),
+        Some(content) => content.to_string(),
+    };
+
+    Ok(Some(content))
+}
+
+#[derive(Debug)]
+struct TgMessage {
+    author: String,
+    message: Option<String>,
+    photos: Vec<String>,
+    videos: Vec<String>,
+    has_audio: bool,
+}
+
+fn extract_photo_url(style: &str) -> Option<&str> {
+    let url_start = style.find("url('")? + 5;
+    let url_end = style.find("')")?;
+
+    Some(&style[url_start..url_end])
+}
+
+fn parse_tgmessage(embed: &str) -> Result<TgMessage> {
+    let doc = Html::parse_document(embed);
+
+    let author_sel = Selector::parse("a.tgme_widget_message_owner_name").unwrap();
+    let author = doc
+        .select(&author_sel)
+        .next()
+        .ok_or_else(|| anyhow!("failed to find message author"))?
+        .text()
+        .collect::<Vec<&str>>()
+        .concat();
+
+    let msg_sel = Selector::parse("div.tgme_widget_message_text.js-message_text").unwrap();
+
+    // The ElementRef::text() iterator does not yield newlines present
+    // in the message, so it is partially reimplemented here.
+    let message = if let Some(msg_elem) = doc.select(&msg_sel).next() {
+        use ego_tree::iter::Edge;
+        use scraper::node::Node;
+
+        let mut out = String::new();
+
+        for edge in &mut msg_elem.traverse() {
+            if let Edge::Open(node) = edge {
+                match node.value() {
+                    Node::Text(ref text) => out.push_str(text),
+                    Node::Element(elem) if elem.name() == "br" => out.push('\n'),
+                    _ => {}
+                }
+            }
+        }
+
+        Some(out)
+    } else {
+        // Not all Telegram messages have a textual message.
+        None
+    };
+
+    let photo_sel = Selector::parse("a.tgme_widget_message_photo_wrap").unwrap();
+    let mut photos = vec![];
+
+    for photo in doc.select(&photo_sel) {
+        if let Some(style) = photo.value().attr("style") {
+            if let Some(url) = extract_photo_url(style) {
+                photos.push(url.to_string())
+            }
+        }
+    }
+
+    let video_sel = Selector::parse("i.tgme_widget_message_video_thumb").unwrap();
+    let mut videos = vec![];
+
+    for video in doc.select(&video_sel) {
+        if let Some(style) = video.value().attr("style") {
+            if let Some(url) = extract_photo_url(style) {
+                videos.push(url.to_string())
+            }
+        }
+    }
+
+    let audio_sel = Selector::parse("audio.tgme_widget_message_voice.js-message_voice").unwrap();
+    let mut has_audio = false;
+    if doc.select(&audio_sel).next().is_some() {
+        has_audio = true;
+    }
+
+    Ok(TgMessage {
+        author,
+        message,
+        photos,
+        videos,
+        has_audio,
+    })
+}
+
+// create a permanent media url that tgsa can redirect if telegram
+// changes its upstream links.
+//
+// assumes that tgsa lives at tgsa.tazj.in (which it does)
+fn media_url(link: &TgLink, idx: usize) -> String {
+    format!(
+        "https://tgsa.tazj.in/img/{}/{}/{}",
+        link.username, link.message_id, idx
+    )
+}
+
+fn to_bbcode(link: &TgLink, msg: &TgMessage) -> String {
+    let mut out = String::new();
+
+    out.push_str(&format!("[quote=\"{}\"]\n", msg.author));
+
+    for video in 0..msg.videos.len() {
+        out.push_str(&format!("[url=\"{}\"]", link.to_url(true)));
+
+        // video thumbnail links are appended to the photos, hence the
+        // addition here
+        out.push_str(&format!(
+            "[img]{}[/img]",
+            media_url(link, video + msg.photos.len())
+        ));
+
+        out.push_str("[/url]\n");
+        out.push_str("[sub](Click thumbnail to open video)[/sub]\n")
+    }
+
+    for photo in 0..msg.photos.len() {
+        out.push_str(&format!("[timg]{}[/timg]\n", media_url(link, photo)));
+    }
+
+    if msg.has_audio {
+        out.push_str(&format!(
+            "[i]This message has audio attached. Go [url=\"{}\"]to Telegram[/url] to listen.[/i]",
+            link.to_url(true),
+        ));
+    }
+
+    if let Some(message) = &msg.message {
+        out.push_str(message);
+    }
+
+    out.push_str("\n[/quote]\n");
+
+    out.push_str(&format!(
+        "[sub](from [url=\"{}\"]{}[/url], via [url=\"https://tgsa.tazj.in\"]tgsa[/url])[/sub]\n",
+        link.to_url(true),
+        link.human_friendly_url(),
+    ));
+
+    out
+}
+
+// cache everything for one hour
+const CACHE_EXPIRY: Duration = Duration::from_secs(60 * 60);
+
+#[derive(Clone)]
+struct TgPost {
+    bbcode: String,
+    at: Instant,
+    media: Vec<String>,
+}
+
+type Cache = RwLock<HashMap<TgLink, TgPost>>;
+
+fn fetch_with_cache(cache: &Cache, link: &TgLink) -> Result<TgPost> {
+    if let Some(entry) = cache.read().unwrap().get(link) {
+        if Instant::now() - entry.at < CACHE_EXPIRY {
+            println!("serving {}#{} from cache", link.username, link.message_id);
+            return Ok(entry.clone());
+        }
+    }
+
+    // limit concurrent fetching
+    // TODO(tazjin): per link?
+    let mut writer = cache.write().unwrap();
+
+    let post = fetch_post(link, true)?;
+    let mut msg = parse_tgmessage(&post)?;
+
+    if msg.message.is_none() {
+        msg.message = fetch_fallback(link)?;
+    }
+
+    if let Some(message) = &msg.message {
+        if link.translated {
+            println!("translating {}#{}", link.username, link.message_id);
+            msg.message = Some(translate::fetch_translation(message)?);
+        }
+    }
+
+    let bbcode = to_bbcode(link, &msg);
+
+    let mut media = vec![];
+    media.append(&mut msg.photos);
+    media.append(&mut msg.videos);
+
+    let post = TgPost {
+        bbcode,
+        media,
+        at: Instant::now(),
+    };
+
+    writer.insert(link.clone(), post.clone());
+
+    Ok(post)
+}
+
+fn handle_img_redirect(cache: &Cache, img_path: &str) -> Result<rouille::Response> {
+    // img_path:
+    //
+    // RWApodcast/113/1
+    // ^          ^   ^
+    // |          |   |
+    // |          |   image (0-indexed)
+    // |          post ID
+    // username
+
+    let img_parts: Vec<&str> = img_path.split('/').collect();
+
+    if img_parts.len() != 3 {
+        println!("invalid image link: {}", img_path);
+        return Err(anyhow!("not a valid image link: {}", img_path));
+    }
+
+    let link = TgLink {
+        username: img_parts[0].into(),
+        message_id: img_parts[1].parse().context("failed to parse message_id")?,
+        translated: false,
+    };
+
+    let img_idx: usize = img_parts[2].parse().context("failed to parse img_idx")?;
+    let post = fetch_with_cache(cache, &link)?;
+
+    if img_idx >= post.media.len() {
+        return Err(anyhow!(
+            "there is no {}. image in {}/{}",
+            img_idx,
+            link.username,
+            link.message_id
+        ));
+    }
+
+    Ok(rouille::Response::redirect_303(post.media[img_idx].clone()))
+}
+
+fn handle_tg_link(cache: &Cache, link: &TgLink) -> Result<rouille::Response> {
+    let post = fetch_with_cache(cache, link)?;
+    Ok(rouille::Response::text(post.bbcode))
+}
+
+fn main() {
+    crimp::init();
+
+    let cache: Cache = RwLock::new(HashMap::new());
+
+    rouille::start_server("0.0.0.0:8472", move |request| {
+        let mut raw_url = request.raw_url();
+        let mut translate = false;
+
+        let response = loop {
+            if raw_url.starts_with("/img/") {
+                break handle_img_redirect(&cache, &raw_url[5..]);
+            }
+
+            if raw_url.starts_with("/translate/") {
+                translate = true;
+                raw_url = &raw_url[10..];
+            }
+
+            break match TgLink::parse(raw_url, translate) {
+                None => Ok(rouille::Response::text(
+                    r#"tgsa
+----
+
+this is a stupid program that lets you turn telegram message links
+into BBcode suitable for pasting on somethingawful dot com
+
+you can use it by putting a valid telegram message link in the url and
+waiting for some bbcode to show up.
+
+for example:
+
+  https://tgsa.tazj.in/https://t.me/RWApodcast/113
+
+yes, that looks stupid, but it works
+
+if you see this message and think you did the above correctly, you
+didn't. try again. idiot.
+
+it can also translate posts from russian, ukrainian or whatever other
+dumb language you speak into english by adding `/translate/`, for
+example:
+
+  https://tgsa.tazj.in/translate/https://t.me/strelkovii/4329
+
+expect this to be slow though. that's the price to pay for translating
+shitty slang.
+
+pm me on the forums if any of this makes you mad or something.
+"#,
+                )),
+                Some(link) => handle_tg_link(&cache, &link),
+            };
+        };
+
+        match response {
+            Ok(resp) => resp,
+            Err(err) => {
+                println!("something failed: {}", err);
+                rouille::Response::text(format!(
+                    r#"ugh, something broke: {}
+
+nobody has been alerted about this and it has probably not been
+logged. pm me on the forums if you think it's important."#,
+                    err
+                ))
+            }
+        }
+    });
+}
diff --git a/users/tazjin/tgsa/src/translate.rs b/users/tazjin/tgsa/src/translate.rs
new file mode 100644
index 0000000000..35d7b35ca8
--- /dev/null
+++ b/users/tazjin/tgsa/src/translate.rs
@@ -0,0 +1,191 @@
+//! integration with yandex cloud translate api, for automatically
+//! translating telegram posts.
+//!
+//! most of this module is concerned with handling the authentication
+//! tokens for yandex cloud, as jwt signing needs to be handled
+//! manually (none of the rust jwt libraries that i tried actually
+//! work).
+
+use anyhow::{anyhow, Context, Result};
+use base64::prelude::BASE64_URL_SAFE_NO_PAD as B64;
+use base64::Engine;
+use lazy_static::lazy_static;
+use ring::signature as sig;
+use serde::Deserialize;
+use serde_json::{json, Value};
+use std::sync::Mutex;
+use std::time::{Duration, SystemTime};
+
+/// token exchange url (exchanging a signed jwt for an iam token
+/// understood by the translation service)
+const TOKEN_URL: &str = "https://iam.api.cloud.yandex.net/iam/v1/tokens";
+
+/// translation endpoint
+const TRANSLATE_URL: &str = "https://translate.api.cloud.yandex.net/translate/v2/translate";
+
+/// describes the private key as downloaded from yandex, pem-encoded.
+#[derive(Deserialize)]
+struct AuthorizedKey {
+    id: String,
+    service_account_id: String,
+    private_key: String,
+}
+
+/// cached iam token for yandex cloud
+struct Token {
+    token: String,
+    expiry: SystemTime,
+}
+
+impl Token {
+    fn is_expired(&self) -> bool {
+        self.expiry < SystemTime::now()
+    }
+}
+
+lazy_static! {
+    static ref KEY_FILE: String =
+        std::env::var("YANDEX_KEY_FILE").expect("`YANDEX_KEY_FILE` variable should be set");
+    static ref CACHED_TOKEN: Mutex<Token> = {
+        let token = refresh_token().expect("fetching initial translation token must not fail");
+        Mutex::new(token)
+    };
+}
+
+/// wrap all the authentication logic below into a single function.
+fn refresh_token() -> Result<Token> {
+    let file = std::fs::File::open(KEY_FILE.as_str())?;
+    let key: AuthorizedKey = serde_json::from_reader(file)?;
+    let jwt = sign_yandex_jwt(&key)?;
+    let token = fetch_iam_token(&jwt)?;
+
+    Ok(Token {
+        token,
+        expiry: SystemTime::now() + Duration::from_secs(3600),
+    })
+}
+
+/// wrapper around the cached token that refreshes if required.
+fn current_token() -> Result<String> {
+    let mut token = CACHED_TOKEN
+        .lock()
+        .expect("thread operating on token should never fail");
+
+    if token.is_expired() {
+        println!("refreshing translation token");
+        *token = refresh_token().context("refreshing translation token")?;
+    }
+
+    Ok(token.token.clone())
+}
+
+/// use openssl to read the pem-encoded key, as ring itself is not
+/// capable of this.
+fn read_pem_key(key: &AuthorizedKey) -> Result<sig::RsaKeyPair> {
+    let rsa = openssl::rsa::Rsa::private_key_from_pem(key.private_key.as_bytes())
+        .context("parsing RSA key")?;
+
+    let der = rsa
+        .private_key_to_der()
+        .context("encoding key as DER for ring")?;
+
+    sig::RsaKeyPair::from_der(&der).map_err(|err| anyhow!("decoding DER key in ring: {}", err))
+}
+
+/// manually construct and sign the jwt required to perform the
+/// iam-token key exchange with yandex.
+fn sign_yandex_jwt(key: &AuthorizedKey) -> Result<String> {
+    let iat = SystemTime::now()
+        .duration_since(SystemTime::UNIX_EPOCH)?
+        .as_secs();
+
+    let header = json!({
+        "typ": "JWT",
+        "alg": "PS256",
+        "kid": key.id,
+    })
+    .to_string();
+
+    let payload = json!({
+        "iss": key.service_account_id,
+        "aud": TOKEN_URL,
+        "iat": iat,
+        "exp": iat + 60,
+    })
+    .to_string();
+
+    let unsigned = format!("{}.{}", B64.encode(header), B64.encode(payload));
+    let key_pair = read_pem_key(key)?;
+
+    let rng = ring::rand::SystemRandom::new();
+    let mut signature = vec![0; key_pair.public_modulus_len()];
+    key_pair
+        .sign(
+            &sig::RSA_PSS_SHA256,
+            &rng,
+            unsigned.as_bytes(),
+            &mut signature,
+        )
+        .map_err(|err| anyhow!("while signing JWT: {}", err))?;
+
+    Ok(format!("{}.{}", unsigned, B64.encode(&signature)))
+}
+
+/// exchange the jwt for an iam token
+fn fetch_iam_token(token: &str) -> Result<String> {
+    #[derive(Deserialize)]
+    #[serde(rename_all = "camelCase")]
+    struct TokenResponse {
+        iam_token: String,
+    }
+
+    let response = crimp::Request::post(TOKEN_URL)
+        .json(&json!({
+            "jwt": token,
+        }))?
+        .send()?
+        .error_for_status(|resp| {
+            anyhow::anyhow!("{} ({})", String::from_utf8_lossy(&resp.body), resp.status)
+        })?
+        .as_json::<TokenResponse>()
+        .context("deserialising IAM token")?;
+
+    Ok(response.body.iam_token)
+}
+
+pub fn fetch_translation(message: &str) -> Result<String> {
+    let request_body = json!({
+        "folderId": "b1g5k8f0tgimg06i6p5h",
+        "texts": [ message ],
+        "targetLanguageCode": "en",
+    });
+
+    let response = crimp::Request::post(TRANSLATE_URL)
+        .bearer_auth(&current_token()?)
+        .context("adding 'Bearer' token")?
+        .json(&request_body)
+        .context("preparing JSON body")?
+        .send()
+        .context("failed to fetch translation from yandex")?
+        .error_for_status(|resp| {
+            anyhow!(
+                "translation request failed: {} ({})",
+                String::from_utf8_lossy(&resp.body),
+                resp.status
+            )
+        })?
+        .as_json::<Value>()?
+        .body;
+
+    let translation = response
+        .get("translations")
+        .ok_or_else(|| anyhow!("missing 'translations' key"))?
+        .get(0)
+        .ok_or_else(|| anyhow!("translations list is empty"))?
+        .get("text")
+        .ok_or_else(|| anyhow!("translation missing 'text' key"))?
+        .as_str()
+        .ok_or_else(|| anyhow!("'text' was not a string"))?;
+
+    Ok(translation.to_string())
+}
diff --git a/users/tazjin/tvix-eval-value.d2 b/users/tazjin/tvix-eval-value.d2
new file mode 100644
index 0000000000..dad2dbcef2
--- /dev/null
+++ b/users/tazjin/tvix-eval-value.d2
@@ -0,0 +1,98 @@
+# D2 diagram of tvix-eval's `Value` type.
+#
+# can be rendered at https://play.d2lang.com/
+#
+# colours have meanings:
+#
+# yellow: recurses
+# orange: heap allocation
+# red: refcount
+#
+# this intentionally does *not* include some internal variants
+
+Value -> Null
+Value -> Bool
+Value -> Integer
+Value -> Float
+
+Box*.style.fill: "lightsalmon"
+Rc*.style.fill: "salmon"
+Vec\<*.style.fill: "salmon"
+
+Value -> String -> NixString -> "Box<str>"
+
+Value -> Path -> "Box<PathBuf>" -> PathBuf
+PathBuf.style.fill: "lightsalmon"
+
+# attribute sets are kinda complicated
+Value -> Attrs -> "Box<NixAttrs>" -> NixAttrs
+NixAttrs -> Empty
+NixAttrs -> KV
+KV.style.fill: "LemonChiffon"
+KV -> Value
+KV -> Value
+NixAttrs -> Map
+Map -> "OrdMap<NixString, Value>" -> "MapEntry<NixString, Value>"
+"OrdMap<NixString, Value>".style.fill: "lightsalmon"
+"MapEntry<NixString, Value>".style.fill: "salmon"
+"MapEntry<NixString, Value>".style.multiple: true
+"MapEntry<NixString, Value>" -> NixString
+"MapEntry<NixString, Value>" -> Value
+"MapEntry<NixString, Value>".style.stroke-width: 15
+"MapEntry<NixString, Value>".style.stroke: "lemonchiffon"
+
+Value -> List -> NixList -> "Rc<imbl::Vector<Value>>"
+"Rc<imbl::Vector<Value>>" -> "VecEntry<Value>" -> Value
+"VecEntry<Value>".style.multiple: true
+"VecEntry<Value>".style.fill: "salmon"
+"VecEntry<Value>".style.stroke-width: 15
+"VecEntry<Value>".style.stroke: "lemonchiffon"
+
+# closures
+
+Value -> Closure -> "Rc<Closure>" -> Closure
+Closure -> "Rc<Lambda>" -> Lambda
+
+Lambda -> Chunk
+Lambda -> SmolStr: sometimes allocates
+SmolStr.style.fill: "lightsalmon"
+Lambda -> usize
+Lambda -> "Option<Formals>" -> Formals
+
+Formals -> "HashMap<NixString, bool>" -> "MapEntry<NixString, bool>"
+"HashMap<NixString, bool>".style.fill: "lightsalmon"
+"MapEntry<NixString, bool>".style.fill: "salmon"
+"MapEntry<NixString, bool>".style.multiple: true
+"MapEntry<NixString, bool>" -> NixString
+
+Closure -> "Rc<Upvalues>" -> Upvalues
+
+Upvalues -> "Vec<Value>"
+"Vec<Value>" -> Value
+"Vec<Value>".style.stroke-width: 15
+"Vec<Value>".style.stroke: "lemonchiffon"
+Upvalues -> "Option<Vec<Value>>"
+"Option<Vec<Value>>" -> Value
+"Option<Vec<Value>>".style.fill: "lightsalmon"
+"Option<Vec<Value>>".style.stroke-width: 15
+"Option<Vec<Value>>".style.stroke: "lemonchiffon"
+
+Value -> Blueprint -> "Rc<Lambda>"
+
+# builtins
+
+Value -> Builtin -> "Box<BuiltinRepr>" -> BuiltinRepr
+BuiltinRepr -> "Rc<dyn BuiltinGen>"
+BuiltinRepr -> "Vec<Value>"
+
+# thunks
+
+Value -> Thunk -> "Rc<RefCell<ThunkRepr>>" -> ThunkRepr
+ThunkRepr -> Suspended
+Suspended -> "Rc<Lambda>"
+Suspended -> "Rc<Upvalues>"
+
+ThunkRepr -> Native -> "Box<dyn Fn() -> Result<Value, ErrorKind>>"
+ThunkRepr -> Blackhole
+ThunkRepr -> Evaluated -> Value
+Evaluated.style.fill: "lemonchiffon"
diff --git a/users/tazjin/yddns/.gitignore b/users/tazjin/yddns/.gitignore
new file mode 100644
index 0000000000..2f7896d1d1
--- /dev/null
+++ b/users/tazjin/yddns/.gitignore
@@ -0,0 +1 @@
+target/
diff --git a/users/tazjin/yddns/Cargo.lock b/users/tazjin/yddns/Cargo.lock
new file mode 100644
index 0000000000..58b37d553b
--- /dev/null
+++ b/users/tazjin/yddns/Cargo.lock
@@ -0,0 +1,1425 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "addr2line"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
+
+[[package]]
+name = "async-stream"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51"
+dependencies = [
+ "async-stream-impl",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-stream-impl"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.39",
+]
+
+[[package]]
+name = "async-trait"
+version = "0.1.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.39",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "axum"
+version = "0.6.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bitflags 1.3.2",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "sync_wrapper",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "mime",
+ "rustversion",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
+dependencies = [
+ "addr2line",
+ "cc",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+]
+
+[[package]]
+name = "base64"
+version = "0.21.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
+
+[[package]]
+name = "bytes"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
+
+[[package]]
+name = "cc"
+version = "1.0.84"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
+[[package]]
+name = "crc32fast"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crimp"
+version = "4087.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ead2c83f7d1f9b8e5a6f7a25985d0d1759ccd2cd72abb1eee2db65d05e12b39"
+dependencies = [
+ "curl",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "curl"
+version = "0.4.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2 0.4.10",
+ "winapi",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.68+curl-8.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4a0d18d88360e374b16b2273c832b5e57258ffc1d4aa4f96b108e0738d5752f"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "windows-sys",
+]
+
+[[package]]
+name = "either"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+
+[[package]]
+name = "fixedbitset"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
+
+[[package]]
+name = "flate2"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "futures-channel"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
+
+[[package]]
+name = "futures-sink"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
+
+[[package]]
+name = "futures-task"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
+
+[[package]]
+name = "futures-util"
+version = "0.3.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "pin-utils",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
+
+[[package]]
+name = "h2"
+version = "0.3.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833"
+dependencies = [
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "http",
+ "indexmap 1.9.3",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "hashbrown"
+version = "0.14.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156"
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "home"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "http"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f95b9abcae896730d42b78e09c155ed4ddf82c07b4de772c64aee5b2d8b7c150"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
+dependencies = [
+ "bytes",
+ "http",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "0.14.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "socket2 0.4.10",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "want",
+]
+
+[[package]]
+name = "hyper-timeout"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1"
+dependencies = [
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tokio-io-timeout",
+]
+
+[[package]]
+name = "indexmap"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
+dependencies = [
+ "autocfg",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.14.2",
+]
+
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "libc"
+version = "0.2.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
+
+[[package]]
+name = "libz-sys"
+version = "1.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d97137b25e321a73eef1418d1d5d2eda4d77e12813f8e6dead84bc52c5870a7b"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829"
+
+[[package]]
+name = "log"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "memchr"
+version = "2.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+dependencies = [
+ "adler",
+]
+
+[[package]]
+name = "mio"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys",
+]
+
+[[package]]
+name = "multimap"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
+
+[[package]]
+name = "object"
+version = "0.32.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.95"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40a4130519a360279579c2053038317e40eff64d13fd3f004f9e1b72b8a6aaf9"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
+
+[[package]]
+name = "petgraph"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
+dependencies = [
+ "fixedbitset",
+ "indexmap 2.1.0",
+]
+
+[[package]]
+name = "pin-project"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.39",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
+
+[[package]]
+name = "prettyplease"
+version = "0.1.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8646e95016a7a6c4adea95bafa8a16baab64b583356217f2c85db4a39d9a86"
+dependencies = [
+ "proc-macro2",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "prost"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd"
+dependencies = [
+ "bytes",
+ "prost-derive",
+]
+
+[[package]]
+name = "prost-build"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "119533552c9a7ffacc21e099c24a0ac8bb19c2a2a3f363de84cd9b844feab270"
+dependencies = [
+ "bytes",
+ "heck",
+ "itertools",
+ "lazy_static",
+ "log",
+ "multimap",
+ "petgraph",
+ "prettyplease",
+ "prost",
+ "prost-types",
+ "regex",
+ "syn 1.0.109",
+ "tempfile",
+ "which",
+]
+
+[[package]]
+name = "prost-derive"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
+dependencies = [
+ "anyhow",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213622a1460818959ac1181aaeb2dc9c7f63df720db7d788b3e24eacd1983e13"
+dependencies = [
+ "prost",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "regex"
+version = "1.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+
+[[package]]
+name = "ring"
+version = "0.17.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b"
+dependencies = [
+ "cc",
+ "getrandom",
+ "libc",
+ "spin",
+ "untrusted",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
+
+[[package]]
+name = "rustix"
+version = "0.38.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b426b0506e5d50a7d8dafcf2e81471400deb602392c7dd110815afb4eaf02a3"
+dependencies = [
+ "bitflags 2.4.1",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys",
+]
+
+[[package]]
+name = "rustls"
+version = "0.21.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c"
+dependencies = [
+ "log",
+ "ring",
+ "rustls-webpki",
+ "sct",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00"
+dependencies = [
+ "openssl-probe",
+ "rustls-pemfile",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pemfile"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c"
+dependencies = [
+ "base64",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.101.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+
+[[package]]
+name = "ryu"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "sct"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
+dependencies = [
+ "ring",
+ "untrusted",
+]
+
+[[package]]
+name = "security-framework"
+version = "2.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
+dependencies = [
+ "bitflags 1.3.2",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.192"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.192"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.39",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.108"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9"
+dependencies = [
+ "libc",
+ "windows-sys",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
+
+[[package]]
+name = "tempfile"
+version = "3.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5"
+dependencies = [
+ "cfg-if",
+ "fastrand",
+ "redox_syscall",
+ "rustix",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio"
+version = "1.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2 0.5.5",
+ "tokio-macros",
+ "windows-sys",
+]
+
+[[package]]
+name = "tokio-io-timeout"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf"
+dependencies = [
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.39",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.24.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+ "tracing",
+]
+
+[[package]]
+name = "tonic"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a"
+dependencies = [
+ "async-stream",
+ "async-trait",
+ "axum",
+ "base64",
+ "bytes",
+ "flate2",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "hyper",
+ "hyper-timeout",
+ "percent-encoding",
+ "pin-project",
+ "prost",
+ "rustls-native-certs",
+ "rustls-pemfile",
+ "tokio",
+ "tokio-rustls",
+ "tokio-stream",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tonic-build"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6fdaae4c2c638bb70fe42803a26fbd6fc6ac8c72f5c59f67ecc2a2dcabf4b07"
+dependencies = [
+ "prettyplease",
+ "proc-macro2",
+ "prost-build",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "indexmap 1.9.3",
+ "pin-project",
+ "pin-project-lite",
+ "rand",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0"
+
+[[package]]
+name = "tower-service"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
+
+[[package]]
+name = "tracing"
+version = "0.1.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.39",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "walkdir"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "yandex-cloud"
+version = "2023.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98f67140264e8e090e26b70925096adf295569c057a8b2ad2cd7e0f10c01cfae"
+dependencies = [
+ "prost",
+ "prost-types",
+ "tonic",
+ "tonic-build",
+ "walkdir",
+]
+
+[[package]]
+name = "yddns"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "crimp",
+ "tokio",
+ "yandex-cloud",
+]
diff --git a/users/tazjin/yddns/Cargo.toml b/users/tazjin/yddns/Cargo.toml
new file mode 100644
index 0000000000..78691f303d
--- /dev/null
+++ b/users/tazjin/yddns/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "yddns"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+anyhow = "1.0"
+crimp = "4087.0"
+tokio = "*" # pulled in by yandex-cloud
+yandex-cloud = "2023.6.13"
diff --git a/users/tazjin/yddns/default.nix b/users/tazjin/yddns/default.nix
new file mode 100644
index 0000000000..40b0d1c24e
--- /dev/null
+++ b/users/tazjin/yddns/default.nix
@@ -0,0 +1,9 @@
+{ depot, pkgs, ... }:
+
+depot.third_party.naersk.buildPackage {
+  src = ./.;
+  buildInputs = with pkgs; [
+    pkg-config
+    openssl
+  ];
+}
diff --git a/users/tazjin/yddns/src/main.rs b/users/tazjin/yddns/src/main.rs
new file mode 100644
index 0000000000..2e2f9fe02f
--- /dev/null
+++ b/users/tazjin/yddns/src/main.rs
@@ -0,0 +1,142 @@
+use anyhow::{anyhow, bail, Context, Result};
+use crimp::Request;
+use std::env;
+use std::net::Ipv4Addr;
+use tokio::runtime;
+use yandex_cloud::tonic_exports::{Channel, Endpoint, InterceptedService};
+use yandex_cloud::yandex::cloud::dns::v1 as dns;
+use yandex_cloud::yandex::cloud::dns::v1::dns_zone_service_client::DnsZoneServiceClient;
+use yandex_cloud::{AuthInterceptor, TokenProvider};
+
+type DnsClient<T> = DnsZoneServiceClient<InterceptedService<Channel, AuthInterceptor<T>>>;
+
+/// Fetch the current IP from the given URL. It should be the URL of a
+/// site that responds only with the IP in plain text, and nothing else.
+fn get_current_ip(source: &str) -> Result<Ipv4Addr> {
+    let response = Request::get(source)
+        .send()
+        .context("failed to fetch current IP")?
+        .error_for_status(|resp| anyhow!("error response ({})", resp.status))
+        .context("received error response for IP")?
+        .as_string()?
+        .body;
+
+    Ok(response.trim().parse().with_context(|| {
+        format!(
+            "failed to parse IP address from response body: {}",
+            response
+        )
+    })?)
+}
+
+/// Fetch the current address of the target record.
+async fn fetch_current_record_addr<T: TokenProvider>(
+    client: &mut DnsClient<T>,
+    zone_id: &str,
+    record_name: &str,
+) -> Result<Ipv4Addr> {
+    let req = dns::GetDnsZoneRecordSetRequest {
+        dns_zone_id: zone_id.into(),
+        name: record_name.into(),
+        r#type: "A".into(),
+    };
+
+    let response = client
+        .get_record_set(req)
+        .await
+        .context("failed to fetch current record set")?
+        .into_inner();
+
+    if response.data.len() != 1 {
+        bail!(
+            "expected exactly one record for 'A {}', but found {}",
+            record_name,
+            response.data.len()
+        );
+    }
+
+    Ok(response.data[0]
+        .parse()
+        .context("failed to parse returned record")?)
+}
+
+/// Update the record with the new address, if required.
+async fn update_record<T: TokenProvider>(
+    client: &mut DnsClient<T>,
+    zone_id: &str,
+    record_name: &str,
+    new_address: Ipv4Addr,
+) -> Result<()> {
+    let request = dns::UpsertRecordSetsRequest {
+        dns_zone_id: zone_id.into(),
+        replacements: vec![dns::RecordSet {
+            name: record_name.into(),
+            r#type: "A".into(),
+            ttl: 3600, // 1 hour
+            data: vec![new_address.to_string()],
+        }],
+        ..Default::default()
+    };
+
+    client
+        .upsert_record_sets(request)
+        .await
+        .context("failed to update record")?;
+
+    Ok(())
+}
+
+/// Compare the record with the expected value, and issue an update if
+/// necessary.
+async fn compare_update_record<T: TokenProvider>(
+    client: &mut DnsClient<T>,
+    zone_id: &str,
+    record_name: &str,
+    new_ip: Ipv4Addr,
+) -> Result<()> {
+    let old_ip = fetch_current_record_addr(client, zone_id, record_name).await?;
+
+    if old_ip == new_ip {
+        println!("IP address unchanged ({})", old_ip);
+        return Ok(());
+    }
+
+    println!(
+        "IP address changed: current record points to {}, but address is {}",
+        old_ip, new_ip
+    );
+
+    update_record(client, zone_id, record_name, new_ip).await?;
+    println!("successfully updated '{}' to 'A {}'", record_name, new_ip);
+
+    Ok(())
+}
+
+fn main() -> Result<()> {
+    let token =
+        env::var("YANDEX_CLOUD_TOKEN").context("Yandex Cloud authentication token unset")?;
+    let target_zone_id =
+        env::var("TARGET_ZONE").unwrap_or_else(|_| "dnsd0tif5mokfu0mg8i5".to_string());
+    let target_record = env::var("TARGET_RECORD").unwrap_or_else(|_| "khtrsk".to_string());
+
+    let current_ip = get_current_ip("http://ifconfig.me")?;
+    println!("current IP address is '{}'", current_ip);
+
+    let rt = runtime::Builder::new_current_thread()
+        .enable_time()
+        .enable_io()
+        .build()?;
+
+    rt.block_on(async move {
+        let channel = Endpoint::from_static("https://dns.api.cloud.yandex.net")
+            .connect()
+            .await?;
+
+        let mut client =
+            DnsZoneServiceClient::with_interceptor(channel, AuthInterceptor::new(token));
+
+        compare_update_record(&mut client, &target_zone_id, &target_record, current_ip).await
+    })?;
+
+    Ok(())
+}