<?xml version="1.0" encoding="utf-8" ?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>1klb</title>
  <icon>www.1klb.com/icon.png</icon>
  <logo>www.1klb.com/icon.png</logo>

  <link href="www.1klb.com" />
  <link href="www.1klb.com/blog.xml" rel="self" />

  <updated>2026-02-16T08:15:56.480Z</updated>
  <author>
    <name>Jan-Yves Ruzicka</name>
    <email>jan@1klb.com</email>
  </author>

  <id>www.1klb.com/blog.xml</id>
  <generator>11ty atom parser</generator>

  
  <entry>
    <title><![CDATA[🌲 Subdomains with Avahi]]></title>
    <link href="www.1klb.com/g/subdomains_with_avahi/" />
    <updated></updated>
    <id>www.1klb.com/g/subdomains_with_avahi/</id>
    <content type="html"><![CDATA[<p><a href="www.1klb.com/g/subdomains_with_avahi/https://en.wikipedia.org/wiki/Multicast_DNS">Multicast DNS</a> (or mDNS) is a protocol that allows devices on a local network to publish (and be referenced by) their own addresses (eg <code>computername.local</code>) rather than forcing everyone to resort to IP addresses (eg <code>192.168.1.52</code>).</p>
<p>It's pretty cool because:</p>
<ul>
<li>It's easier to remember your computer is at <code>computername.local</code> than it is to remember it's at <code>192.168.1.52</code>.</li>
<li>While it's possible to set up your router so that your computer is always at the same IP, it's still a little work.</li>
</ul>
<p>mDNS is implemented in OS X through Apple's Bonjour implementation, and (generally) in Linux through <a href="www.1klb.com/g/subdomains_with_avahi/https://avahi.org/">Avahi</a>. When you do this, your device's URL will usually be <code>&lt;devicename&gt;.local</code>.</p>
<p>If you're running a homeserver, you might have multiple services, each running on its own port. And if you've hung out on the web for a bit, you'll be wondering if there's any way to set up subdomains of your lovely mDNS-implemented local URL. If you give it a web search, you'll find some people doing the same, some people telling you to use python libraries for Avahi (which I'm not sure are still maintained), and even some AI slop telling you to change config settings in Avahi which don't exist.</p>
<p>And here's how you <em>actually</em> do it.</p>
<div class="callout-box"><div class="title">Current settings</div>
Because things get out of date on the web, here's some points of reference for you to gauge whether or not this advice is still current and correct:
<ul>
<li>Date of writing: January 2026</li>
<li>Operating system: Fedora release 43</li>
<li>Avahi version: 0.9~rc2</li>
</ul>
</div>
<h1 id="step-0%3A-prerequisites" tabindex="-1">Step 0: Prerequisites</h1>
<p>This assumes you're running on a Linux distro which uses<code>systemd</code> for service management. You should install avahi using your preferred package manager.</p>
<h1 id="step-1%3A-outline-the-subdomains-you-want" tabindex="-1">Step 1: Outline the subdomains you want</h1>
<p>We're going to be cute for this step: we're going to set up a little file in Avahi's config space for our subdomains. For me, this is <code>/etc/avahi</code>. Your mileage may vary.</p>
<p>Let's set up three subdomains: <code>tv</code>, <code>music</code>, and <code>wiki</code> (again, assuming we're running some services on our home server) - these will go to eg <code>tv.computername.local</code>, <code>music.computername.local</code> and <code>wiki.computername.local</code>. Which will be pretty cool.</p>
<p>In your terminal:</p>
<pre><code>cd /etc/avahi
echo tv\nmusic\nwiki &gt; subdomains
</code></pre>
<p>This will create a file <code>subdomains</code> with three lines, each line showing a subdomain. Easy!</p>
<h1 id="step-2%3A-build-the-publication-process" tabindex="-1">Step 2: Build the publication process</h1>
<p>To let other devices on our network know about these subdomains, we need Avahi to <em>publish</em> them. The following shell script does this, leaning heavily on <a href="www.1klb.com/g/subdomains_with_avahi/https://github.com/u1aryz/how-to-avahi-subdomain">u1aryz' how-to-avahi-subdomain script</a>.</p>
<pre class="language-bash"><code class="language-bash"><span class="token shebang important">#!/usr/bin/env bash</span>
<span class="token builtin class-name">set</span> <span class="token parameter variable">-euo</span> pipefail

<span class="token comment"># Default interface</span>
<span class="token builtin class-name">readonly</span> <span class="token assign-left variable">DEFAULT_INTERFACE</span><span class="token operator">=</span><span class="token string">"wlp0s20u2"</span>

<span class="token comment"># Where we store aliases</span>
<span class="token builtin class-name">readonly</span> <span class="token assign-left variable">SUBDOMAIN_FILE</span><span class="token operator">=</span><span class="token string">"/etc/avahi/subdomains"</span>


<span class="token comment"># Logging functions</span>
<span class="token function-name function">log_info</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token builtin class-name">echo</span> <span class="token parameter variable">-e</span> <span class="token string">"[INFO] <span class="token variable">$*</span>"</span> <span class="token operator">></span><span class="token file-descriptor important">&amp;2</span>
<span class="token punctuation">}</span>

<span class="token function-name function">log_error</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token builtin class-name">echo</span> <span class="token parameter variable">-e</span> <span class="token string">"[ERROR] <span class="token variable">$*</span>"</span> <span class="token operator">></span><span class="token file-descriptor important">&amp;2</span>
<span class="token punctuation">}</span>

<span class="token function-name function">show_usage</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token function">cat</span> <span class="token operator">></span><span class="token file-descriptor important">&amp;2</span> <span class="token operator">&lt;&lt;-</span><span class="token string">EOF
    Usage: <span class="token variable">$0</span> [-i &lt;interface>]

    Options:
      -i &lt;interface>  Network interface to use (default: <span class="token variable">$DEFAULT_INTERFACE</span>)
      -h              Show this help message
EOF</span>
<span class="token punctuation">}</span>

<span class="token function-name function">check_requirements</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token builtin class-name">local</span> <span class="token assign-left variable">missing_deps</span><span class="token operator">=</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
  <span class="token builtin class-name">local</span> <span class="token assign-left variable">required_commands</span><span class="token operator">=</span><span class="token punctuation">(</span><span class="token string">"avahi-publish"</span> <span class="token string">"ip"</span><span class="token punctuation">)</span>

  <span class="token keyword">for</span> <span class="token for-or-select variable">cmd</span> <span class="token keyword">in</span> <span class="token string">"<span class="token variable">${required_commands<span class="token punctuation">[</span>@<span class="token punctuation">]</span>}</span>"</span><span class="token punctuation">;</span> <span class="token keyword">do</span>
    <span class="token keyword">if</span> <span class="token operator">!</span> <span class="token builtin class-name">command</span> <span class="token parameter variable">-v</span> <span class="token string">"<span class="token variable">$cmd</span>"</span> <span class="token operator">&amp;></span>/dev/null<span class="token punctuation">;</span> <span class="token keyword">then</span>
      <span class="token assign-left variable">missing_deps</span><span class="token operator">+=</span><span class="token punctuation">(</span><span class="token string">"<span class="token variable">$cmd</span>"</span><span class="token punctuation">)</span>
    <span class="token keyword">fi</span>
  <span class="token keyword">done</span>

  <span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">${ <span class="token operator">#</span>missing_deps<span class="token punctuation">[</span>@<span class="token punctuation">]</span>}</span> <span class="token parameter variable">-gt</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    log_error <span class="token string">"Missing required dependencies: <span class="token variable">${missing_deps<span class="token punctuation">[</span>*<span class="token punctuation">]</span>}</span>"</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
  <span class="token keyword">fi</span>
<span class="token punctuation">}</span>

<span class="token function-name function">validate_interface</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token builtin class-name">local</span> <span class="token assign-left variable">interface</span><span class="token operator">=</span><span class="token string">"<span class="token variable">$1</span>"</span>

  <span class="token keyword">if</span> <span class="token operator">!</span> <span class="token function">ip</span> <span class="token function">link</span> show <span class="token string">"<span class="token variable">$interface</span>"</span> <span class="token operator">&amp;></span>/dev/null<span class="token punctuation">;</span> <span class="token keyword">then</span>
    log_error <span class="token string">"Network interface '<span class="token variable">$interface</span>' not found."</span>
    log_error <span class="token string">"Available interfaces:"</span>
    <span class="token function">ip</span> <span class="token function">link</span> show <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-E</span> <span class="token string">'^[0-9]+:'</span> <span class="token operator">|</span> <span class="token function">awk</span> -F<span class="token string">': '</span> <span class="token string">'{print "  " $2}'</span> <span class="token operator">|</span> <span class="token function">sed</span> <span class="token string">'s/@.*//'</span> <span class="token operator">></span><span class="token file-descriptor important">&amp;2</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
  <span class="token keyword">fi</span>
<span class="token punctuation">}</span>

<span class="token function-name function">get_interface_addresses</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  <span class="token builtin class-name">local</span> <span class="token assign-left variable">interface</span><span class="token operator">=</span><span class="token string">"<span class="token variable">$1</span>"</span>

  <span class="token builtin class-name">local</span> <span class="token assign-left variable">ipv4_addresses</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">ip</span> <span class="token parameter variable">-4</span> <span class="token parameter variable">-o</span> addr show dev <span class="token string">"<span class="token variable">$interface</span>"</span> scope global <span class="token operator">|</span> <span class="token function">awk</span> <span class="token string">'{print $4}'</span> <span class="token operator">|</span> <span class="token function">cut</span> -d/ <span class="token parameter variable">-f1</span><span class="token variable">)</span></span>

  <span class="token comment"># Check if we have IPv4 addresses</span>
  <span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token parameter variable">-z</span> <span class="token string">"<span class="token variable">$ipv4_addresses</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    log_error <span class="token string">"No IPv4 addresses found on interface '<span class="token variable">$interface</span>'."</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
  <span class="token keyword">fi</span>

  <span class="token builtin class-name">local</span> <span class="token assign-left variable">ipv6_addresses</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">ip</span> <span class="token parameter variable">-6</span> <span class="token parameter variable">-o</span> addr show dev <span class="token string">"<span class="token variable">$interface</span>"</span> scope global <span class="token operator">|</span> <span class="token function">awk</span> <span class="token string">'{print $4}'</span> <span class="token operator">|</span> <span class="token function">cut</span> -d/ <span class="token parameter variable">-f1</span><span class="token variable">)</span></span>

  <span class="token comment"># Combine IPv4 and IPv6 addresses</span>
  <span class="token builtin class-name">local</span> <span class="token assign-left variable">all_addresses</span><span class="token operator">=</span><span class="token string">"<span class="token variable">$ipv4_addresses</span>"</span><span class="token string">$'<span class="token entity" title="\n">\n</span>'</span>"<span class="token variable">$ipv6_addresses</span><span class="token string">"

  echo "</span><span class="token variable">$all_addresses</span><span class="token string">"
}

publish_addresses() {
  local fqdn="</span><span class="token variable">$1</span><span class="token string">"
  local addresses="</span><span class="token variable">$2</span><span class="token string">"

  log_info "</span>Publishing <span class="token string">'$fqdn'</span> with addresses:<span class="token string">"

  for addr in <span class="token variable">$addresses</span>; do
    log_info "</span>  <span class="token variable">$addr</span><span class="token string">"
    avahi-publish -f -a -R "</span><span class="token variable">$fqdn</span><span class="token string">" "</span><span class="token variable">$addr</span><span class="token string">" &amp;
  done
}

main() {
  local interface="</span><span class="token variable">$DEFAULT_INTERFACE</span><span class="token string">"

  # Parse command line options
  while getopts "</span>:i:h<span class="token string">" opt; do
    case "</span><span class="token variable">$opt</span><span class="token string">" in
    i) interface="</span><span class="token variable">$OPTARG</span><span class="token string">" ;;
    h)
      show_usage
      exit 0
      ;;
     \?)
       log_error "</span>Invalid option -<span class="token variable">$OPTARG</span><span class="token string">"
       show_usage
       exit 1
       ;;
     :)
       log_error "</span>Option -<span class="token variable">$OPTARG</span> requires an argument<span class="token string">"
       show_usage
       exit 1
       ;;
     esac
   done
   shift <span class="token variable"><span class="token variable">$((</span>OPTIND <span class="token operator">-</span> <span class="token number">1</span><span class="token variable">))</span></span>

   # Validate arguments
   if [[ <span class="token variable">$#</span> -ne 0 ]]; then
     log_error "</span>No arguments are required.<span class="token string">"
     show_usage
     exit 1
   fi

   # Check alias file exists
   if [ ! -f <span class="token variable">$SUBDOMAIN_FILE</span> ]; then
     log_error "</span>File <span class="token variable">$SUBDOMAIN_FILE</span> does not exist.<span class="token string">"
     exit 1
   fi

   # Check dependencies
   check_requirements

   # Validate interface
   validate_interface "</span><span class="token variable">$interface</span><span class="token string">"

   # Get addresses
   local addresses=<span class="token variable"><span class="token variable">$(</span>get_interface_addresses <span class="token string">"<span class="token variable">$interface</span>"</span><span class="token variable">)</span></span>

   # Populate
   for subdomain in <span class="token variable"><span class="token variable">$(</span><span class="token function">cat</span> $ALIAS_FILE<span class="token variable">)</span></span>
   do
     local fqdn="</span><span class="token variable">${subdomain}</span><span class="token builtin class-name">.</span><span class="token variable"><span class="token variable">$(</span><span class="token function">hostname</span><span class="token variable">)</span></span>.local<span class="token string">"

     # Publish addresses
     publish_addresses "</span><span class="token variable">$fqdn</span><span class="token string">" "</span><span class="token variable">$addresses</span>"
   <span class="token keyword">done</span>

   <span class="token comment"># Wait for all background processes</span>
   <span class="token function">wait</span>
 <span class="token punctuation">}</span>

main
</code></pre>
<p>If you've had to change where you keep your subdomain file, change the constant <code>SUBDOMAIN_FILE</code>. Depending on your computer, your wireless interface may also be different from mine - check out how you're connected to the network through <code>ip link show</code> and select the appropriate interface.</p>
<p>As a guided tour of the script:</p>
<ul>
<li><strong>Lines 1-85</strong> set up various constants and auxiliary functions.</li>
<li><strong>Lines 88-118</strong> parse arguments to ensure we're advertising through the right interface.</li>
<li><strong>Lines 119-123</strong> are simply us checking a subdomain file exists.</li>
<li><strong>Lines 125-132</strong> involve us ensuring the system has the right things installed, that the interface exists, and collecting our IP address(es) on that interface.</li>
<li><strong>Lines 134-141</strong> are the loop where we do the work - we use <code>avahi-publish</code> to publish each subdomain, pointing to our IP addresses. These processes are run in the background as child processes to this one.</li>
<li>On <strong>Line 144</strong> we call <code>wait</code>, allowing all background processes to halt before we quit out.</li>
</ul>
<p>Store this where you will - I've saved mine as <code>/usr/local/bin/avahi-publish-subdomain</code> (and run <code>chmod +x</code> on it to ensure it can be run).</p>
<p>Of course, it sucks having to open up your terminal and run this script just to get the functionality going. Thankfully, we can run it through <code>systemd</code>.</p>
<h1 id="step-3%3A-run-through-systemd" tabindex="-1">Step 3: Run through systemd</h1>
<p>To do this, we'll make a nice simple <code>systemd</code> entry. <code>cd</code> to <code>/etc/systemd/system</code>, and create a file <code>avahi-subdomain.service</code> with the content:</p>
<pre><code>[Unit]
Description=Publish mDNS subdomains under %H.local
Requires=avahi-daemon.service
After=avahi-daemon.service
StartLimitBurst=5
StartLimitIntervalSec=10s

[Service]
Type=simple
ExecStart=/usr/local/bin/avahi-publish-subdomain
Restart=on-failure
RestartSec=2

[Install]
WantedBy=multi-user.target
</code></pre>
<p>You'll notice that this file references our script <code>avahi-publish-subdomain</code> above - point it to wherever your file is.</p>
<p>Now all we need to do it queue it up in systemd:</p>
<pre><code>sudo systemctl enable avahi-publish-subdomain
</code></pre>
<p>And you should be good to go!</p>
]]></content>
  </entry>
  
  <entry>
    <title><![CDATA[📯 Five books I enjoyed, Spring/Summer 2025]]></title>
    <link href="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/" />
    <updated>2026-01-02T00:00:00.000Z</updated>
    <id>www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/</id>
    <content type="html"><![CDATA[<p>Gosh, it's been a whole six months and <em>nothing</em> on this blog except for &quot;five books I enjoyed&quot; posts. The winter was long and dark, I feel, and for a bit there it felt like doing <em>anything</em> outside of just existing and hitting the regular life commitments was a slog. We're well out of that, and I feel myself swinging back up - while I may not find myself <em>inundated</em> with energy, I do find myself with time to actually do things. I'm hoping the next few months, with a combination of both continuing sunshine and some changes to my life schedule, will give me a chance to put something else of substance up here.</p>
<p>The last part of winter, and the first part of spring, were entirely taken up by my sudden desire to read <em>The Lord of the Rings</em>, inspired in no small part by <a href="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/https://www.youtube.com/watch?v=I3g6mFI4Fug">this dumb song</a>. Finishing that up, and feeling like a treat, I ended up running through a bunch of recent <a href="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/https://en.wikipedia.org/wiki/Hugo_Award_for_Best_Novel">Hugo Award</a> nominees to finish off the year (with the occasional non-fiction book thrown in for good measure). Embarassingly, back from our summer vacation I've found that I left my Kobo e-reader on a bedside shelf in a bach. So it's paper books for me for the next week until I get it back.</p>
<div class="divider"></div>
<img src="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/lotr.jpg" class="leftfloat" />
<p><strong>The Lord of the Rings - J. R. R. Tolkein.</strong> You don't need a synopsis of this one. I decided to re-read it after two decades (and with my memory warped by the movies), and found myself enjoying a real tome of a novel, familiar enough, but with enough of a twist from the films to make the whole thing refreshing and interesting.</p>
<p>Obviously there are elements that exist in the books and not in the movies - Tom Bombadil, the Scouring of the Shire, etc. - but even the stuff which exists in both is subtley different<sup class="footnote-ref"><a href="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/#fn1" id="fnref1">[1]</a></sup>. The emphasis on battles, for example, in the movies (which makes complete sense I think - the visual spectacle of some of those battles is one of the defining elements of the last two movies), or the differing characterisation of some of the human characters.</p>
<p>Outside of comparisons with the movies, there's a real <em>voice</em> to Tolkein's work, that I feel echoes his familiarity with the tone and pacing of epic storytelling. It's not always there - a lot of the book feels written in a relatively everyday tone (at least for the time in which it was written), but then something happens - a scene changes, an event occurs, and a literary switch flips. It's not jarring in the least: it feels more like a director employing a particular film technique to signal to the viewer that <em>this</em> is a momentous occasion.</p>
<img src="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/alien.jpg" class="rightfloat" />
<p><strong>Alien Clay - Adrian Tchaikovsky.</strong> I read a couple of Adrian Tchaikovsky books as a result of my summer Hugo-chasing read, but this is the one that stood our the more. A political prisoner is sent to labour on an alien planet, an effective death sentence, but instead uses the opportunity to join up with his former dissident colleagues. As they plan rebellion, the alien planet itself - and its ecology - offer a few surprises of their own.</p>
<p>One definition of science fiction that I've often seen mentioned is that it uses scientific or technological progress as a lens through which we can examine human society. If we go by this metric, I think <em>Alien Clay</em> succeeds - arguably by putting forward such a strong metaphor for human cooperation that even I pick it up on a first read-through. But alongside this, Tchaikovsky does a great job of maintaining both narrative tension and internal consistency to make a satisfying read.</p>
<img src="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/notebook.jpg" class="leftfloat" />
<p><strong>The Notebook - Roland Allen.</strong> A rambling journey through the development of the notebook in western civilisation. Early on, the book takes a relatively linear run through the development of paper, books, and the notebook in particular, but once we get into the Renaissance, the story starts diverging. Some interesting tidbits include the development of most western notebooks from Florentine accounting practices, as well as the impact of work books on the development of western art during the Renaissance.</p>
<p>I feel this is a niche read. As someone with a nice stack of (empty and filled) notebooks on my shelf, it appeals to me. If you have a bunch of them, it'll appeal to you. And if not, it probably won't? Idunno, that feels a little like damning with faint praise.</p>
<img src="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/mintime.jpg" class="rightfloat" />
<p><strong>The Ministry of Time - Kaliane Bradley.</strong> Part science fiction thriller, part romance, this book follows a young British public servant as she becaomes a companion/monitor for a refugee from the past. The thriller part of this book is completely adequate, but it mainly serves as a vehicle for the collection of misfits the eponymous Ministry has to babysit, as they adapt to the twentieth century and, well, do what people do when left in each other's company for any amount of time.</p>
<p>Wikipedia informs me that Bradley started this novel during COVID-19 lockdown, writing vignettes of the main characters brushing up against the twentieth century. True to form, it's these sections that have the most depth of thought, the most character and charm.</p>
<p>I ended up buying this book as a present for two folks this Christmas. I would have only bought one, except for logistical reasons. The book I would have bought <em>instead</em> of this one would have been...</p>
<img src="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/cup.jpg" class="leftfloat" />
<p><strong>The Tainted Cup - Robert Jackson Bennett.</strong> A dark fantasy Sherlock Holmes, in which a young apprentice is teamed up with an eccentric investigator to uncover a string of strange murders, in a world where monstrous beasts threaten civilisation.</p>
<p>I feel the perfect murder mystery exists as a series of staged mini-mysteries: at each one, you have all the clues you need to solve it, if only you can put them together right, and the revelation leaves you going, &quot;Of course! How could I have missed that?&quot; I feel Bennett manages this extremely well in <em>The Tainted Cup</em> - not only does he manage it, he does so by incorporating the elements of worldbuilding which he's been layering throughout the book since the beginning.</p>
<p>When I put this on the end of my to-read list, it had not yet won the 2025 Hugo Award. Upon finishing it, checking the internet, and finding it had won, I cannot say I'm surprised.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p><a href="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/https://www.youtube.com/watch?v=AIdEVFHJ2Fw">This series of recaps</a> gives a great breakdown of this, by the way. <a href="www.1klb.com/posts/2026/01/02/five_books_i_enjoyed_spring_summer_2025/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content>
  </entry>
  
  <entry>
    <title><![CDATA[📯 Five books I enjoyed, Autumn/Winter 2025 edition]]></title>
    <link href="www.1klb.com/posts/2025/06/30/five_books_i_enjoyed_autumn_winter_2025_edition/" />
    <updated>2025-06-30T00:00:00.000Z</updated>
    <id>www.1klb.com/posts/2025/06/30/five_books_i_enjoyed_autumn_winter_2025_edition/</id>
    <content type="html"><![CDATA[<p>It's the time once again to write about books I've read. The new book-tracking format <a href="www.1klb.com/posts/2024/12/29/five_books_i_enjoyed__winter_spring_2024_edition/">I mentioned last time</a> is doing me well - I have half a blog post in draft form about the process of building small CLI tools for just this kind of job - and making it really easy for me to review which books I've read, and which I want to talk about.</p>
<p>The past six months have been a real dive into genre fiction, to the extent I haven't managed for a few years now, brought on by a few recommendations I've received recently. It's been nice.</p>
<div class="divider"></div>
<img src="www.1klb.com/posts/2025/06/30/five_books_i_enjoyed_autumn_winter_2025_edition/starless-sea.png" class="leftfloat" />
<p><strong>The Starless Sea - Erin Morgenstern.</strong> Sometimes an author just writes a book that's very clearly a love letter to books, and this is one of them. Morgenstern builds a fantasy world hidden just below ours, like Borges but more romantic, perhaps, full of factions and secret societies and masked balls.</p>
<p>The world-building is wonderous, only enhanced by the fact that the main narrative arc takes place in a ruin of the place, long after all its inhabitants have left. In fact, I can't help but wonder if half the magic of this book is the fact that Morgenstern gives with one hand while taking with the other - showing us what wonders this place <em>used</em> to be, but never actually pointing the camera at the Starless Sea in its heyday.</p>
<p>A book this elaborate needs to be a masterpiece to stick the landing, and this is something I don't think Morgenstern quite handles. The climax and denoument just don't seem to hit quite right.</p>
<img src="www.1klb.com/posts/2025/06/30/five_books_i_enjoyed_autumn_winter_2025_edition/dawnhounds.jpg" class="rightfloat" />
<p><strong>The Dawnhounds - Sascha Stronach.</strong> Obligatory New Zealand author inclusion. Possibly the most New Zealand things about <em>The Dawnhounds</em> was the way I found out about the series - a colleague mentioning that a friend of theirs had published a book.</p>
<p><em>The Dawnhounds</em> starts off as a murder mystery police procedural set in a weird fantasy mid-future Pacific city, and how you react to that is, I feel, a good determinant of whether or not you'll like the book. The thing quickly switches pace into queer near-future fantasy, and I was saddened by this only because of the potential in the murder mystery space.</p>
<p>The book is weird and queer and revels in both of those areas. There is a sequel and I've yet to see if it continues that through to the second book.</p>
<img src="www.1klb.com/posts/2025/06/30/five_books_i_enjoyed_autumn_winter_2025_edition/moominpappa.jpg" class="leftfloat" />
<p><strong>Moominpappa at Sea - Tove Jansson.</strong> I read several of Jansson's books as a teenager, but I could swear she'd written more than the nine novels she actually wrote. My mind has whole cloth invented several scenes I could sear happened in Moomin books, which (upon re-reading) I've found never existed - six months ago I'd have bet real money that there was a novel where the Moomins go on a proper holiday to something akin to the Riviera, for example. My own personal false memory.</p>
<p>Regardless, <em>Moominpappa at Sea</em> was actually one of the Moomin books that I skipped over as a child. It's a real pity<sup class="footnote-ref"><a href="www.1klb.com/posts/2025/06/30/five_books_i_enjoyed_autumn_winter_2025_edition/#fn1" id="fnref1">[1]</a></sup> because this feels like a real <em>tour de force</em>. The earlier books, while they show some of their essential melancholy, are generally upbeat and cozy. In comparison, this book feels like...not quite a horror novel, but something akin. Each character appears to be battling their own inner demons, somehow isolated from one another just as the characters themselves are isolated from the rest of their world through the sea. Even at the end it feels less like they conquer their issues, and more that they come to terms.</p>
<img src="www.1klb.com/posts/2025/06/30/five_books_i_enjoyed_autumn_winter_2025_edition/deadly.jpg" class="rightfloat" />
<p><strong>A Deadly Education - Naomi Novik</strong>. I got a few recommendations for books recently from a friend, and this was one of them. <em>A Deadly Education</em> grabbed my pretty rapidly thanks to its weird Battle Royale-esque setup coupled with a nice riff on what I've come to refer tongue-in-cheek as the <em>Zauberkindbildungsroman</em> (lit.: &quot;Magical child coming of age story&quot;).</p>
<p>I cannot help but shelf this series alongside Lev Grossman's <em>Magicians</em> series in my head. Tone-wise, the two books are quite different, but they both have a number of similarities beyond just their focus on magical high schools, the kind of people these places would produce, and what happens after these folks graduate.</p>
<img src="www.1klb.com/posts/2025/06/30/five_books_i_enjoyed_autumn_winter_2025_edition/asr.jpg" class="leftfloat" />
<p><strong>All Systems Red - Martha Wells</strong>. Another recommendation to me. I'd previously read (and not particularly enjoyed, I don't think?) some of Wells' fantasy work, so it took a couple of recommendations for me to pick this series up.</p>
<p><em>All Systems Red</em> is a great conceit well-executed: a security construct, basically a human being modified <em>Robocop</em> style into a mobile sentient weapons platform and leased out as a service, goes rogue, but rather than kill everyone, it just wants to watch TV all day.</p>
<p>As the series continues, Wells executes a competent pivot from gag novel to introspective series about what it means to be human. But I'm not sure that it really quite recaptures the frictionless joy-in-the-conceit of the first book or so.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Or perhaps not: teenage me was also <em>sure</em> that Ursula Le Guin's <em>A Wizard of Earthsea</em> was a boring and dour book about nothing much. My taste has improved, I swear. <a href="www.1klb.com/posts/2025/06/30/five_books_i_enjoyed_autumn_winter_2025_edition/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content>
  </entry>
  
  <entry>
    <title><![CDATA[📯 Omnifocus → Taskwarrior]]></title>
    <link href="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/" />
    <updated>2025-04-27T00:00:00.000Z</updated>
    <id>www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/</id>
    <content type="html"><![CDATA[<p>I started using <a href="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/https://www.omnigroup.com/omnifocus">OmniFocus</a> at the start of my postgraduate, many many many years ago. Now I'm looking at perhaps   switching operating systems in the future, and I want my project list to come with me. Which means finally switching task managers.</p>
<p><a href="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/https://taskwarrior.org/">TaskWarrior</a> is what I'm currently looking at. It's a CLi-based, open source task management tool which portable to basically any operating system, and has a bunch of extensions.</p>
<p>But exporting tasks and projects from one project, and importing them to another, is a big deal. Let's see how hard it can be.</p>
<h1 id="the-big-picture" tabindex="-1">The big picture</h1>
<p>The good news is that OmniFocus has a good export system, and TaskWarrior has a good import system.</p>
<h2 id="omnifocus-exports-to-xml" tabindex="-1">OmniFocus exports to xml</h2>
<p>If you open OmniFocus, you can export your current database to file by selecting <strong>File</strong> &gt; <strong>Export</strong>:</p>
<figure class="">
  <div class="images"><img class="lightbox" src="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/of-export.png" alt="The location of the Export menu item in OmniFocus"  class=""/></div>
  <figcaption><p>The location of the Export menu item in OmniFocus</p>
</figcaption>
</figure>
<p>This will prompt you to save an <code>.ofocus</code> file somewhere on your computer, but if you happen to inspect it in terminal or even just gently rename it to remove the <code>.ofocus</code> extension, OS X will reveal to you what it really is: a folder.</p>
<figure class="">
  <div class="images"><img class="lightbox" src="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/ofocus-folder.png" alt="The contents of a typical `.ofocus` "file"."  class=""/></div>
  <figcaption><p>The contents of a typical <code>.ofocus</code> &quot;file&quot;.</p>
</figcaption>
</figure>
<p>There's a bunch of stuff in here, but there's always one zip file, named something crazy and random-looking, which is actually a zipped <code>.xml</code> file. And if you unzip and open <em>that</em>...</p>
<figure class="">
  <div class="images"><img class="lightbox" src="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/xml-contents.png" alt="The contents of that zipped xml file."  class=""/></div>
  <figcaption><p>The contents of that zipped xml file.</p>
</figcaption>
</figure>
<p>Not quite complete nonsense - this is actually a file containing every folder, project, context, perspective, and task that currently exists in your OmniFocus database. And <em>this</em> is gold.</p>
<h2 id="taskwarrior-imports-from-json" tabindex="-1">TaskWarrior imports from json</h2>
<p>You can find the specifics <a href="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/https://github.com/GothenburgBitFactory/taskwarrior/blob/develop/doc/devel/rfcs/task.md">here</a>, but the good news is that even though you <em>could</em> make a program to systematically call TaskWarrior and add task after task, you can also run a bulk import on a JSON file. There are some things we'll need to work around - for example, TaskWarrior doesn't have a good handle on nested tasks, and projects and folders are a much more nebulous concept - but we can indeed work around them.</p>
<p>As we go through the next few steps, I'll be referring back to their JSON spec above to work out how to handle our various task and project properties.</p>
<h1 id="ok%2C-so-how-do-we-export-this-stuff%3F" tabindex="-1">OK, so how do we export this stuff?</h1>
<p>The first thing will be to read the XML file. I'm doing this in ruby, my glue language of choice, and in ruby the standard XML parsing gem is <a href="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/https://nokogiri.org/">nokogiri</a>.</p>
<p>Here's a script to load in <code>contents.xml</code>, check through the top-level elements, and see what they are.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'nokogiri'</span></span>

doc <span class="token operator">=</span> <span class="token builtin">File</span><span class="token punctuation">.</span>open<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"contents.xml"</span></span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token operator">|</span>f<span class="token operator">|</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">XML</span><span class="token punctuation">(</span>f<span class="token punctuation">)</span> <span class="token punctuation">}</span>

doc<span class="token punctuation">.</span>root<span class="token punctuation">.</span>children<span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>elem<span class="token operator">|</span>
  puts elem<span class="token punctuation">.</span>name
<span class="token keyword">end</span>
</code></pre>
<p>This will provide you with the following element types:</p>
<ul>
<li><strong>attachment</strong>: A file attachment</li>
<li><strong>context</strong>: A task context</li>
<li><strong>folder</strong>: A project folder</li>
<li><strong>perspective</strong>: A saved perspective</li>
<li><strong>setting</strong>: A document setting</li>
<li><strong>task</strong>: A project or task (OmniFocus treats projects as subsets of tasks)</li>
<li><strong>task-to-tag</strong>: Basically a many-to-many join table between tasks and contexts</li>
</ul>
<p>We're going to be focusing on the <strong>folder</strong> and <strong>task</strong> elements here.</p>
<p>Here's a simple ruby script which iterates through all your tasks, showing each task's name:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'nokogiri'</span></span>

doc <span class="token operator">=</span> <span class="token builtin">File</span><span class="token punctuation">.</span>open<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"contents.xml"</span></span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token operator">|</span>f<span class="token operator">|</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">XML</span><span class="token punctuation">(</span>f<span class="token punctuation">)</span> <span class="token punctuation">}</span>

doc<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">":root > task"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span>t<span class="token operator">|</span>
  puts t<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"name"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text
<span class="token keyword">end</span>
</code></pre>
<p>We're using CSS-style selection in <code>doc.search</code> - Nokogiri also supports XPath, but I'm most familiar with CSS so I'm going to be using that for this example. If you've run this, you should see a big ol' list of task and project names in your console. As I mentioned, OmniFocus makes little structural distinction between projects and tasks, as they both share many properties (name, contexts, due date, repetition rule, etc.). The project-specific properties for a project are stored in the <code>project</code> element underneath the <code>task</code> object. In fact, we can split our tasks into actual tasks and projects pretty handily:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'nokogiri'</span></span>

doc <span class="token operator">=</span> <span class="token builtin">File</span><span class="token punctuation">.</span>open<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"contents.xml"</span></span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token operator">|</span>f<span class="token operator">|</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">XML</span><span class="token punctuation">(</span>f<span class="token punctuation">)</span> <span class="token punctuation">}</span>

<span class="token constant">ALL_TASKS_AND_PROJECTS</span> <span class="token operator">=</span> doc<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">":root > task"</span></span><span class="token punctuation">)</span>
puts <span class="token string-literal"><span class="token string">"I have </span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content"><span class="token constant">ALL_TASKS_AND_PROJECTS</span><span class="token punctuation">.</span>length</span><span class="token delimiter punctuation">}</span></span><span class="token string"> tasks/projects!"</span></span>


<span class="token constant">ALL_TASKS</span> <span class="token operator">=</span> <span class="token constant">ALL_TASKS_AND_PROJECTS</span><span class="token punctuation">.</span>select<span class="token punctuation">{</span> <span class="token operator">|</span>elem<span class="token operator">|</span> elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"project"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>children<span class="token punctuation">.</span>length <span class="token operator">==</span> <span class="token number">0</span> <span class="token punctuation">}</span>
puts <span class="token string-literal"><span class="token string">"  </span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content"><span class="token constant">ALL_TASKS</span><span class="token punctuation">.</span>length</span><span class="token delimiter punctuation">}</span></span><span class="token string"> of these are tasks"</span></span>

<span class="token constant">ALL_PROJECTS</span> <span class="token operator">=</span> <span class="token constant">ALL_TASKS_AND_PROJECTS</span><span class="token punctuation">.</span>select<span class="token punctuation">{</span> <span class="token operator">|</span>elem<span class="token operator">|</span> elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"project"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>children<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span> <span class="token punctuation">}</span>
puts <span class="token string-literal"><span class="token string">"  </span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content"><span class="token constant">ALL_PROJECTS</span><span class="token punctuation">.</span>length</span><span class="token delimiter punctuation">}</span></span><span class="token string"> of these are projects"</span></span>
</code></pre>
<p>As mentioned, TaskWarrior doesn't really treat projects as first-class citizens, so we want to focus on just exporting tasks. We'll deal with projects as we go. Let's have a first stab at making something resembling TaskWarrior's import JSON format:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'nokogiri'</span></span>
<span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'json'</span></span>

doc <span class="token operator">=</span> <span class="token builtin">File</span><span class="token punctuation">.</span>open<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"contents.xml"</span></span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token operator">|</span>f<span class="token operator">|</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">XML</span><span class="token punctuation">(</span>f<span class="token punctuation">)</span> <span class="token punctuation">}</span>

<span class="token constant">ALL_TASKS_AND_PROJECTS</span> <span class="token operator">=</span> doc<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">":root > task"</span></span><span class="token punctuation">)</span>
<span class="token constant">ALL_TASKS</span> <span class="token operator">=</span> <span class="token constant">ALL_TASKS_AND_PROJECTS</span><span class="token punctuation">.</span>select<span class="token punctuation">{</span> <span class="token operator">|</span>elem<span class="token operator">|</span> elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"project"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>children<span class="token punctuation">.</span>length <span class="token operator">==</span> <span class="token number">0</span> <span class="token punctuation">}</span>

taskwarrior_import_array <span class="token operator">=</span> <span class="token constant">ALL_TASKS</span><span class="token punctuation">.</span>map<span class="token punctuation">{</span> <span class="token operator">|</span>elem<span class="token operator">|</span>
  <span class="token punctuation">{</span> <span class="token string-literal"><span class="token string">'description'</span></span> <span class="token operator">=></span> elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"name"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token builtin">File</span><span class="token punctuation">.</span>open<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"import.json"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"w"</span></span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token operator">|</span>io<span class="token operator">|</span> io<span class="token punctuation">.</span>puts <span class="token constant">JSON</span><span class="token punctuation">.</span>pretty_generate<span class="token punctuation">(</span>taskwarrior_import_array<span class="token punctuation">)</span> <span class="token punctuation">}</span>
</code></pre>
<p>This will generate a JSON array of tasks, all given the correct name. It <em>almost</em> looks like this could be a valid import to TakWarrior - except it's not, of course. Let's check TaskWarrior's documentation on the subject:</p>
<blockquote>
<p>At a minimum, a valid task contains:</p>
<ul>
<li>uuid</li>
<li>status</li>
<li>entry</li>
<li>description</li>
</ul>
</blockquote>
<p>OK, so as well as a task description (which we've now added), we need a <code>uuid</code> (a unique identifier in a standard form), an <code>entry</code> (the date/time at which the task was created), and a <code>status</code> (active/complete/etc). Let's see what we can do.</p>
<h2 id="generating-a-uuid" tabindex="-1">Generating a UUID</h2>
<p>TaskWarrior's documentation states that:</p>
<blockquote>
<p>A UUID is a 32-hex-character lower case string, formatted in this way:</p>
<pre><code>xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
</code></pre>
<p>An example:</p>
<pre><code>296d835e-8f85-4224-8f36-c612cad1b9f8
</code></pre>
</blockquote>
<p>While OmniFocus does use unique IDs for its tasks, it doesn't use this format. The easiest thing for us to do is to create them whole cloth, one per new task.</p>
<p>It turns out you can really easily create those in ruby using the <code>securerandom</code> package:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'securerandom'</span></span>

SecureRandom<span class="token punctuation">.</span>uuid <span class="token comment"># => A nice UUID</span>
</code></pre>
<h2 id="adding-an-entry-field" tabindex="-1">Adding an entry field</h2>
<p>TaskWarrior is pretty strict about date/time formats:</p>
<blockquote>
<p>Dates are rendered in ISO 8601 combined date and time in UTC format using the template:</p>
<pre><code>YYYYMMDDTHHMMSSZ
</code></pre>
<p>An example:</p>
<pre><code>20120110T231200Z
</code></pre>
<p>No other formats are supported.</p>
</blockquote>
<p>Annoyingly, OmniFocus stores its dates and times in a <em>slightly</em> different format. Here's an example of OmniFocus' <code>added</code> field, which represents when the task was added to the program, and is analagous to TaskWarrior's <code>entry</code> field:</p>
<pre><code>&lt;added&gt;2017-05-15T09:19:04.940Z&lt;/added&gt;
</code></pre>
<p>You could parse this into a <code>Time</code> object, then output using <code>strftime</code>. Or you could just regex the task into submission:</p>
<pre><code>tw_time = of_time.gsub(/[-:]/, &quot;&quot;).gsub(/\..*Z$/, &quot;Z&quot;)
</code></pre>
<h2 id="adding-status" tabindex="-1">Adding status</h2>
<p>Ignoring recurring tasks, TaskWarrior recognises four statuses: pending, deleted, completed, and waiting. All our tasks will be considered to be pending for now, just to make things easy.</p>
<p>Handily, pending tasks don't need any more information.</p>
<h2 id="a-bare-bones-legal-export" tabindex="-1">A bare bones legal export</h2>
<p>So let's look at the code which will give us the <em>bare minimum</em> setup in TaskWarrior from our OmniFocus data:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'nokogiri'</span></span>
<span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'json'</span></span>
<span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'securerandom'</span></span>

doc <span class="token operator">=</span> <span class="token builtin">File</span><span class="token punctuation">.</span>open<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"contents.xml"</span></span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token operator">|</span>f<span class="token operator">|</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">XML</span><span class="token punctuation">(</span>f<span class="token punctuation">)</span> <span class="token punctuation">}</span>

<span class="token constant">ALL_TASKS_AND_PROJECTS</span> <span class="token operator">=</span> doc<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">":root > task"</span></span><span class="token punctuation">)</span>
<span class="token constant">ALL_TASKS</span> <span class="token operator">=</span> <span class="token constant">ALL_TASKS_AND_PROJECTS</span><span class="token punctuation">.</span>select<span class="token punctuation">{</span> <span class="token operator">|</span>elem<span class="token operator">|</span> elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"project"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>children<span class="token punctuation">.</span>length <span class="token operator">==</span> <span class="token number">0</span> <span class="token punctuation">}</span>

taskwarrior_import_array <span class="token operator">=</span> <span class="token constant">ALL_TASKS</span><span class="token punctuation">.</span>map<span class="token punctuation">{</span> <span class="token operator">|</span>elem<span class="token operator">|</span>
  <span class="token punctuation">{</span>
    <span class="token string-literal"><span class="token string">'description'</span></span> <span class="token operator">=></span>  elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"name"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text<span class="token punctuation">,</span>
    <span class="token string-literal"><span class="token string">'entry'</span></span> <span class="token operator">=></span> elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"added"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text<span class="token punctuation">.</span>gsub<span class="token punctuation">(</span><span class="token regex-literal"><span class="token regex">/[-:]/</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">""</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>gsub<span class="token punctuation">(</span><span class="token regex-literal"><span class="token regex">/\..*Z$/</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"Z"</span></span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token string-literal"><span class="token string">'uuid'</span></span> <span class="token operator">=></span> SecureRandom<span class="token punctuation">.</span>uuid<span class="token punctuation">,</span>
    <span class="token string-literal"><span class="token string">'status'</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">'pending'</span></span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token builtin">File</span><span class="token punctuation">.</span>open<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"import.json"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"w"</span></span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token operator">|</span>io<span class="token operator">|</span> io<span class="token punctuation">.</span>puts <span class="token constant">JSON</span><span class="token punctuation">.</span>pretty_generate<span class="token punctuation">(</span>taskwarrior_import_array<span class="token punctuation">)</span> <span class="token punctuation">}</span>
</code></pre>
<p>Not too shabby, all told - but what about our other fields?</p>
<h1 id="advanced-migration" tabindex="-1">Advanced migration</h1>
<p>Nothing under this section is <em>vital</em>, but chances are you'll want to bring one or more of the following data types along with you.</p>
<h2 id="projects" tabindex="-1">Projects</h2>
<p>As mentioned, TaskWarrior treats projects as more of an auxiliary item than a first-class citizen. On the one hand, this means we won't be keeping any cool project-level info like project-wide tags, start dates, or due dates, but it also means that we don't have to try creating a relational database just to port things over.</p>
<p>TaskWarrior allows each task to have a project, which can in turn be nested within one or more project groups. TaskWarrior's projects take the form: <code>Proj1.Proj2.Proj3</code>, where <code>Proj3</code> is nested inside a parent project <code>Proj2</code>, which is in turn nested inside a parent project <code>Proj1</code>.</p>
<p>In contrast, OmniFocus has a more complex folder-project-task heirarchy. Projects cannot be children of other Projects, but can be children of Folders, which can in turn be children of other Folders. Tasks can be children of Projects, or they can be children of other tasks.</p>
<p>Whew.</p>
<p>If you're using all of the levels of OmniFocus, well, you're gonna have to collapse some of that in TaskWarrior<sup class="footnote-ref"><a href="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/#fn1" id="fnref1">[1]</a></sup>. The main thing that'll give us headaches is nested tasks. I went through my projects before exporting to TaskWarrior and just ensured that every project was a simple list of tasks with no nesting - if you want to keep your nesting in your exported file, you may need to do some more exploration of the file structure.</p>
<p>OmniFocus generates its task and project heirarchy as follows:</p>
<ul>
<li>Every task has a <code>&lt;task&gt;</code> child element with an <code>idref</code> attribute. This attribute corresponds to the id of the task or project that this task is a child of. If this is absent, this task has no parent (ie it's an inbox task).</li>
<li>Every project has a <code>&lt;project&gt;</code> child element, which in turn has a <code>&lt;folder&gt;</code> child element with an <code>idref</code> attribute. Again, this corresponds to the parent folder (if any).</li>
<li>Every folder <em>may</em> have a <code>&lt;folder&gt;</code> child element, which (if it does) has an <code>idref</code> attribute. And this corresponds to the parent folder.</li>
</ul>
<p>Given all that, we can create a kind of &quot;path&quot; for each task by combining these in sequence. We'll be using recursion to ensure this can always trace the task's heirarchy back to the root of our OmniFocus document. We have to tread carefully as it's difficult to tell whether a task is a child of another task, or whether it's a child of a project, without inspecting the parent element.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'nokogiri'</span></span>
<span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'json'</span></span>
<span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'securerandom'</span></span>

doc <span class="token operator">=</span> <span class="token builtin">File</span><span class="token punctuation">.</span>open<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"contents.xml"</span></span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token operator">|</span>f<span class="token operator">|</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">XML</span><span class="token punctuation">(</span>f<span class="token punctuation">)</span> <span class="token punctuation">}</span>

<span class="token constant">ALL_TASKS_AND_PROJECTS</span> <span class="token operator">=</span> doc<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">":root > task"</span></span><span class="token punctuation">)</span>
<span class="token constant">ALL_TASKS</span> <span class="token operator">=</span> <span class="token constant">ALL_TASKS_AND_PROJECTS</span><span class="token punctuation">.</span>select<span class="token punctuation">{</span> <span class="token operator">|</span>elem<span class="token operator">|</span> elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"project"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>children<span class="token punctuation">.</span>length <span class="token operator">==</span> <span class="token number">0</span> <span class="token punctuation">}</span>
<span class="token constant">ALL_FOLDERS</span> <span class="token operator">=</span> doc<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">":root > folder"</span></span><span class="token punctuation">)</span>

<span class="token keyword">def</span> <span class="token method-definition"><span class="token function">path_to_folder</span></span><span class="token punctuation">(</span>f<span class="token punctuation">)</span>
  <span class="token keyword">if</span> f<span class="token punctuation">.</span><span class="token keyword">nil</span><span class="token operator">?</span>
    <span class="token keyword">return</span> <span class="token string-literal"><span class="token string">"&lt;ERROR>"</span></span>
  <span class="token keyword">end</span>
  folder_name <span class="token operator">=</span> f<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"name"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text
  parent_element <span class="token operator">=</span> f<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"folder"</span></span><span class="token punctuation">)</span>

  <span class="token keyword">if</span> parent_element<span class="token punctuation">.</span><span class="token keyword">nil</span><span class="token operator">?</span> <span class="token operator">||</span> parent_element<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"idref"</span></span><span class="token punctuation">]</span><span class="token punctuation">.</span><span class="token keyword">nil</span><span class="token operator">?</span>
    <span class="token comment"># No parent - path is just the folder name</span>
    folder_name
  <span class="token keyword">else</span>
    <span class="token comment"># Parent - path = parent path + this name</span>
    parent_folder <span class="token operator">=</span> <span class="token constant">ALL_FOLDERS</span><span class="token punctuation">.</span>find<span class="token punctuation">{</span> <span class="token operator">|</span>f<span class="token operator">|</span> f<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"id"</span></span><span class="token punctuation">]</span> <span class="token operator">==</span> parent_element<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"idref"</span></span><span class="token punctuation">]</span> <span class="token punctuation">}</span>
    path_to_folder<span class="token punctuation">(</span>parent_folder<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string-literal"><span class="token string">"."</span></span> <span class="token operator">+</span> folder_name
  <span class="token keyword">end</span>
<span class="token keyword">end</span>

<span class="token keyword">def</span> <span class="token method-definition"><span class="token function">path_to_taskproject</span></span><span class="token punctuation">(</span>tp<span class="token punctuation">,</span> top <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
  elem_is_project <span class="token operator">=</span> tp<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"project"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>children<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span>

  elem_name <span class="token operator">=</span>  tp<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"name"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text

  <span class="token keyword">if</span> elem_is_project
    parent_element <span class="token operator">=</span> tp<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"project folder"</span></span><span class="token punctuation">)</span>

    <span class="token keyword">if</span> parent_element<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"idref"</span></span><span class="token punctuation">]</span>
      <span class="token comment"># Parent folder - path = parent path + this name</span>
      parent_folder <span class="token operator">=</span> <span class="token constant">ALL_FOLDERS</span><span class="token punctuation">.</span>find<span class="token punctuation">{</span> <span class="token operator">|</span>f<span class="token operator">|</span> f<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"id"</span></span><span class="token punctuation">]</span> <span class="token operator">==</span> parent_element<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"idref"</span></span><span class="token punctuation">]</span> <span class="token punctuation">}</span>
      path_to_folder<span class="token punctuation">(</span>parent_folder<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string-literal"><span class="token string">"."</span></span> <span class="token operator">+</span> elem_name
    <span class="token keyword">else</span>
      <span class="token comment"># No parent - path is just the project name</span>
      elem_name
    <span class="token keyword">end</span>
  <span class="token keyword">else</span>
    <span class="token comment"># Task - go through &lt;task> element</span>
    parent_element <span class="token operator">=</span> tp<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"task"</span></span><span class="token punctuation">)</span>
    
    <span class="token keyword">if</span> parent_element<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"idref"</span></span><span class="token punctuation">]</span>
      <span class="token comment"># Has a parent: path = parent path + this name</span>
      parent_taskproject <span class="token operator">=</span> <span class="token constant">ALL_TASKS_AND_PROJECTS</span><span class="token punctuation">.</span>find<span class="token punctuation">{</span> <span class="token operator">|</span>tp<span class="token operator">|</span> tp<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"id"</span></span><span class="token punctuation">]</span> <span class="token operator">==</span> parent_element<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"idref"</span></span><span class="token punctuation">]</span> <span class="token punctuation">}</span>
      <span class="token keyword">if</span> top
        path_to_taskproject<span class="token punctuation">(</span>parent_taskproject<span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span>
      <span class="token keyword">else</span>  
        path_to_taskproject<span class="token punctuation">(</span>parent_taskproject<span class="token punctuation">,</span> <span class="token boolean">false</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string-literal"><span class="token string">"."</span></span> <span class="token operator">+</span> elem_name
      <span class="token keyword">end</span>
    <span class="token keyword">else</span>
      <span class="token comment"># No parent - inbox task!</span>
      <span class="token keyword">nil</span>
    <span class="token keyword">end</span>
  <span class="token keyword">end</span>
<span class="token keyword">end</span>
</code></pre>
<p>And we can test this as follows:</p>
<pre class="language-ruby"><code class="language-ruby">sample_task <span class="token operator">=</span> <span class="token constant">ALL_TASKS</span><span class="token punctuation">.</span>sort_by<span class="token punctuation">{</span> rand <span class="token punctuation">}</span><span class="token punctuation">.</span>first
puts <span class="token string-literal"><span class="token string">"Path for '</span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content">sample_task<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"name"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text</span><span class="token delimiter punctuation">}</span></span><span class="token string">':"</span></span>
puts path_to_taskproject<span class="token punctuation">(</span>sample_task<span class="token punctuation">)</span>
</code></pre>
<p>At this point, we can add this to our output! The <code>project</code> field is just a string value, after all:</p>
<pre class="language-ruby"><code class="language-ruby">taskwarrior_import_array <span class="token operator">=</span> <span class="token constant">ALL_TASKS</span><span class="token punctuation">.</span>map<span class="token punctuation">{</span> <span class="token operator">|</span>elem<span class="token operator">|</span>
 <span class="token punctuation">{</span>
   <span class="token string-literal"><span class="token string">'description'</span></span> <span class="token operator">=></span>  elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"name"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text<span class="token punctuation">,</span>
   <span class="token string-literal"><span class="token string">'entry'</span></span> <span class="token operator">=></span> elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"added"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text<span class="token punctuation">.</span>gsub<span class="token punctuation">(</span><span class="token regex-literal"><span class="token regex">/[-:]/</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">""</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>gsub<span class="token punctuation">(</span><span class="token regex-literal"><span class="token regex">/\..*Z$/</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"Z"</span></span><span class="token punctuation">)</span><span class="token punctuation">,</span>
   <span class="token string-literal"><span class="token string">'uuid'</span></span> <span class="token operator">=></span> SecureRandom<span class="token punctuation">.</span>uuid<span class="token punctuation">,</span>
   <span class="token string-literal"><span class="token string">'project'</span></span> <span class="token operator">=></span> path_to_taskproject<span class="token punctuation">(</span>elem<span class="token punctuation">)</span><span class="token punctuation">,</span>
   <span class="token string-literal"><span class="token string">'status'</span></span> <span class="token operator">=></span> <span class="token string-literal"><span class="token string">'pending'</span></span>
 <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre>
<h2 id="contexts" tabindex="-1">Contexts</h2>
<p>OmniFocus' Contexts map nicely to TaskWarrior's <strong>tag</strong> attribute. Unlike contexts, tags don't naturally support a heirarchy, and to be honest I suspect trying to implement heirarchical tags would make your filters and reports somewhat unwieldy in TaskWarrior. Heirachical context may be an indicator of some other sorting criterion, which could be a candidate for setting up through <a href="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/https://taskwarrior.org/docs/udas/">User-defined attributes</a> (something I haven't really gotten into yet, but I'm keen to try out?).</p>
<p>I didn't end up bringing my contexts through to TaskWarrior, partly because most of the tasks I'm tracking have one real context (viz: <code>Computer</code>) and partly because I wanted to reset my current context system and remove some needless complexity from my projects.</p>
<p><em>Given all that</em>, let's look at how we could map contexts to tags, ignoring your context heirarchy for now.</p>
<p>Our first job will be to extract a task's contexts from the OmniFocus <code>xml</code> file. Each task may have a <code>context</code> child element, which links (via <code>idref</code>) to exactly one context - but tasks can have multiple contexts, so this isn't a guarantee we'll catch everything of use. Thus, we must use the one join table OmniFocus encodes into its <code>xml</code> file - the <code>task-to-tag</code> element grouping.</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'nokogiri'</span></span>
<span class="token keyword">require</span> <span class="token string-literal"><span class="token string">'json'</span></span>

doc <span class="token operator">=</span> <span class="token builtin">File</span><span class="token punctuation">.</span>open<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"contents.xml"</span></span><span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token operator">|</span>f<span class="token operator">|</span> Nokogiri<span class="token double-colon punctuation">::</span><span class="token constant">XML</span><span class="token punctuation">(</span>f<span class="token punctuation">)</span> <span class="token punctuation">}</span>

<span class="token constant">ALL_TASKS_AND_PROJECTS</span> <span class="token operator">=</span> doc<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">":root > task"</span></span><span class="token punctuation">)</span>
<span class="token constant">ALL_TASKS</span> <span class="token operator">=</span> <span class="token constant">ALL_TASKS_AND_PROJECTS</span><span class="token punctuation">.</span>select<span class="token punctuation">{</span> <span class="token operator">|</span>elem<span class="token operator">|</span> elem<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"project"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>children<span class="token punctuation">.</span>length <span class="token operator">==</span> <span class="token number">0</span> <span class="token punctuation">}</span>

<span class="token comment"># Contexts</span>
<span class="token constant">ALL_CONTEXTS</span> <span class="token operator">=</span> doc<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">":root > context"</span></span><span class="token punctuation">)</span>
<span class="token constant">CONTEXT_TASK_JOIN</span> <span class="token operator">=</span> doc<span class="token punctuation">.</span>search<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">":root > task-to-tag"</span></span><span class="token punctuation">)</span>

<span class="token keyword">def</span> <span class="token method-definition"><span class="token function">context_ids</span></span><span class="token punctuation">(</span>task_id<span class="token punctuation">)</span>
  <span class="token constant">CONTEXT_TASK_JOIN</span>
    <span class="token punctuation">.</span>select<span class="token punctuation">{</span> <span class="token operator">|</span>ctj<span class="token operator">|</span> ctj<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"task"</span></span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"idref"</span></span><span class="token punctuation">]</span> <span class="token operator">==</span> task_id <span class="token punctuation">}</span>
    <span class="token punctuation">.</span>map<span class="token punctuation">{</span> <span class="token operator">|</span>ctj<span class="token operator">|</span> ctj<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"context"</span></span><span class="token punctuation">)</span><span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"idref"</span></span><span class="token punctuation">]</span> <span class="token punctuation">}</span>
<span class="token keyword">end</span>

<span class="token keyword">def</span> <span class="token method-definition"><span class="token function">context</span></span><span class="token punctuation">(</span>id<span class="token punctuation">)</span>
  <span class="token constant">ALL_CONTEXTS</span><span class="token punctuation">.</span>find<span class="token punctuation">{</span> <span class="token operator">|</span>c<span class="token operator">|</span> c<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"id"</span></span><span class="token punctuation">]</span> <span class="token operator">==</span> id <span class="token punctuation">}</span>
<span class="token keyword">end</span>
  
<span class="token keyword">def</span> <span class="token method-definition"><span class="token function">context_name</span></span><span class="token punctuation">(</span>id<span class="token punctuation">)</span>
  c <span class="token operator">=</span> context<span class="token punctuation">(</span>id<span class="token punctuation">)</span>
  <span class="token keyword">return</span> c <span class="token keyword">if</span> c<span class="token punctuation">.</span><span class="token keyword">nil</span><span class="token operator">?</span>
  <span class="token keyword">return</span> c<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"name"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text
<span class="token keyword">end</span>

<span class="token keyword">def</span> <span class="token method-definition"><span class="token function">task_contexts</span></span><span class="token punctuation">(</span>task_id<span class="token punctuation">)</span>
  context_ids<span class="token punctuation">(</span>task_id<span class="token punctuation">)</span><span class="token punctuation">.</span>map<span class="token punctuation">{</span> <span class="token operator">|</span>id<span class="token operator">|</span> context_name<span class="token punctuation">(</span>id<span class="token punctuation">)</span> <span class="token punctuation">}</span>
<span class="token keyword">end</span>
  
sample_task <span class="token operator">=</span> <span class="token constant">ALL_TASKS</span><span class="token punctuation">.</span>first
st_name <span class="token operator">=</span> sample_task<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"name"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text
puts <span class="token string-literal"><span class="token string">"</span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content">st_name</span><span class="token delimiter punctuation">}</span></span><span class="token string">: </span><span class="token interpolation"><span class="token delimiter punctuation">#{</span><span class="token content">task_contexts<span class="token punctuation">(</span>sample_task<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"id"</span></span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">.</span>join<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">', '</span></span><span class="token punctuation">)</span></span><span class="token delimiter punctuation">}</span></span><span class="token string">"</span></span>
</code></pre>
<p>The above snippet pulls out that join table and uses it to identify every link between a task and a context. Then, it looks up that context to get the context's name. We can then add that to our JSON - TaskWarrior expects a task's <code>tag</code> attribute to be an array of string values, so it's not too much of an issue to just add those context labels in there.</p>
<p>What's that? You'd like to keep the context heirarchy? Well, you do you - the following code will do that:</p>
<pre class="language-ruby"><code class="language-ruby"><span class="token keyword">def</span> <span class="token method-definition"><span class="token function">full_context_name</span></span><span class="token punctuation">(</span>id<span class="token punctuation">)</span>
  c <span class="token operator">=</span> context<span class="token punctuation">(</span>id<span class="token punctuation">)</span>
  <span class="token keyword">return</span> c <span class="token keyword">if</span> c<span class="token punctuation">.</span><span class="token keyword">nil</span><span class="token operator">?</span>
   
  c_name <span class="token operator">=</span> c<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"name"</span></span><span class="token punctuation">)</span><span class="token punctuation">.</span>text

  c_context <span class="token operator">=</span> c<span class="token punctuation">.</span>at<span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"context"</span></span><span class="token punctuation">)</span>
  <span class="token keyword">if</span> c_context <span class="token operator">&amp;&amp;</span> c_context<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"idref"</span></span><span class="token punctuation">]</span>
    <span class="token keyword">return</span> full_context_name<span class="token punctuation">(</span>c_context<span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"idref"</span></span><span class="token punctuation">]</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string-literal"><span class="token string">"."</span></span> <span class="token operator">+</span> c_name
  <span class="token keyword">else</span>
    <span class="token keyword">return</span> c_name
  <span class="token keyword">end</span>
<span class="token keyword">end</span>
</code></pre>
<h2 id="recurrence" tabindex="-1">Recurrence</h2>
<p>Recurrence is, as far as I can tell, a bugbear for every single task management application. You want this task to repeat every week on Saturday, huh? What if you haven't completed it by the time next Saturday rolls around? What if you complete it on Friday? Do you want the task to restart the following day? Do you want the task to be due again in a week, or just to reappear on your task list in a week?</p>
<p>OmniFocus does a <em>lot</em> to make task recurrence work exactly how you want. It's a bit complex when you first encounter it, but it'll do whatever you need it to.</p>
<p>TaskWarrior's <a href="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/https://taskwarrior.org/docs/recurrence/">recurrence</a> rules are a little more basic. This means any fancy recurrences you have set up in OmniFocus, well, won't translate over, and even some basic functionality (like <a href="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/https://taskwarrior.org/docs/using_dates/#the-scheduled-date">scheduled dates</a>) doesn't work well with recurrence. In fact, at this stage I'd be hesitant to even try providing a migration strategy for recurrence. I'm planning on reviewing my current recurring tasks and either reimplementing them in TaskWarrior, or setting up some sort of calendar reminder system. It's a work in progress.</p>
<div class="divider"></div>
<p>That became quite a bit longer than expected, mainly due to code snippets and the like. I hope if you've reached this far in the article, it's been of use as you shift into TaskWarrior. Who knows - within a couple of months, I may be looking for alternative task management apps. But in the meantime, I'm hoping to keep this all documented in the <a href="www.1klb.com/g/taskwarrior/" class="internal-link">garden</a>.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>It's possible to replicate complex task groupings with task dependencies, which isn't something I've delved into yet. Perhaps at a later date I'll explore that. <a href="www.1klb.com/posts/2025/04/27/omnifocus_taskwarrior/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content>
  </entry>
  
  <entry>
    <title><![CDATA[📯 The garden: open for business]]></title>
    <link href="www.1klb.com/posts/2025/03/08/the_garden_open_for_business/" />
    <updated>2025-03-08T00:00:00.000Z</updated>
    <id>www.1klb.com/posts/2025/03/08/the_garden_open_for_business/</id>
    <content type="html"><![CDATA[<p><a href="www.1klb.com/g/digital_gardens/" class="internal-link">Digital gardens</a> have been on my mind for some time, but it's only recently that my own has really started to coalesce. So I thought I'd write about it a little.</p>
<div class="divider"></div>
<p>A digital garden, in essence, is a collection of linked pieces of writing. <a href="www.1klb.com/posts/2025/03/08/the_garden_open_for_business/https://maggieappleton.com/garden-history">Maggie Appleton's useful summary</a> of the term elaborates:</p>
<blockquote>
<p>A garden is a collection of evolving ideas that aren’t strictly organised by their publication date. They’re inherently exploratory – notes are linked through contextual associations. They aren’t refined or complete - notes are published as half-finished thoughts that will grow and evolve over time. They’re less rigid, less performative, and less perfect than the personal websites we’re used to seeing.</p>
<p>It harkens back to the early days of the web when people had fewer notions of how websites “should be.” It’s an ethos that is both classically old and newly imagined.</p>
</blockquote>
<p>There's lots of different <em>types</em> of digital garden: one of the things I quite like about the form is that you can impose as much (or as little) structure as you like. Each item in your garden can be carefully categorised by topic, situated amongst others, linked to within an inch of its life to provide a tight webbing of ideas and correlations - or your notes can exist free-form, with nothing but a title and some text to their name, occasionally linked to others in a sparse net.</p>
<p>In fact, this is one of the things that delayed my really putting the garden front-and-centre on here. I wasn't quite sure how I wanted my garden organised, and whenever I <em>did</em> get ideas about organisation, I found that organising the stuff I did have was more effort than it felt it merited.</p>
<p>So now I have what I have: a series of nodes, loosely interlinked, with little metadata or additional structure. It's small now, hopefully growing over time, slowly, but gradually.</p>
<p><a href="www.1klb.com/posts/2025/03/08/the_garden_open_for_business/https://1klb.com/g/">It's over here</a>.</p>
]]></content>
  </entry>
  
  <entry>
    <title><![CDATA[📯 Five books I enjoyed, Winter/Spring 2024 edition]]></title>
    <link href="www.1klb.com/posts/2024/12/29/five_books_i_enjoyed__winter_spring_2024_edition/" />
    <updated>2024-12-29T00:00:00.000Z</updated>
    <id>www.1klb.com/posts/2024/12/29/five_books_i_enjoyed__winter_spring_2024_edition/</id>
    <content type="html"><![CDATA[<p>Like a flash, it's the end of the year, and it's time for another of these run-downs. The last six months were charaterised by a number of quite chunky volumes, which means the list is a little less exclusive this time around. It also marks the first time I've got to use my new book logging framework, which I might have to write about some time.</p>
<div class="divider"></div>
<img src="www.1klb.com/posts/2024/12/29/five_books_i_enjoyed__winter_spring_2024_edition/monte_christo.avif" class="leftfloat" />
<p><strong>The Counte of Monte Christo - Alexandre Dumas</strong>. I have a dim childhood memory of, it seems, watching a TV film of this with my parents - mainly I remember Dantès being imprisoned, and taking years to escape, picking up skills along the way. Weirdly I don't remember much else, given how this takes up perhaps a quarter of the novel. Perhaps as a small child I wandered off after this, I have no idea.</p>
<p>As I'm sure I've observed for other pre-20c books, this story really takes its time building up to the finale. I've heard the theory that these books didn't have to compete with TV or radio, so they could spread out at their lesisure, and that definitely feels the case with this. That means that it feels like this book spans the kind of narrative breadth that a modern-day author would split into a trilogy; it also means that the climax of the book has to tread the narrow line between underselling it on the one hand, and melodrama on the other. (<em>Monte Christo</em> decides to pick melodrama, for what it's worth.)</p>
<p>The book is a product of its time – both in how it assumes knowledge of, and interweaves the politics of, post-Napoleonic France, and in its treatment of women and people of colour – but with that in mind it's still an involving and labrynthine tale of slow revenge which casts a long shadow on the genre.</p>
<img src="www.1klb.com/posts/2024/12/29/five_books_i_enjoyed__winter_spring_2024_edition/fermentation.jpg" class="rightfloat" />
<p><strong>The Art of Fermentation - Sandor Felix Katz</strong>. This is, put simply, a book about the many ways humankind has fermented foods. It covers everything from basic vegetable fermentation through country wines, ginger beers, mold-based fermentation (sake, tofu, etc), as well as fermentation of meat and fish. It's one part practical guide, one part attempt to cover the main ways humankind has fermented foods, and one part a collection of the author's own experiences.</p>
<p>The book isn't comprehensive (although this is usually evidenced by the author freely admitting such in the various sections, and providing secondary references for the curious reader), and some of the later book (discussing non-food uses of fermentation) feels quite tacked-on, but it more than makes up for this in its character and enthusiasm. Katz' interest in, and keenness to explore, the world of fermentation is palpable, and it's hard to read without feeling the urge to get up and just start following along in your own kitchen. Even if it's merely the springboard for further reading, I feel it's of immense value.</p>
<img src="www.1klb.com/posts/2024/12/29/five_books_i_enjoyed__winter_spring_2024_edition/goriot.avif" class="leftfloat" />
<p><strong>Father Goriot - Honoré de Balzac</strong>. I started reading this after I finished <em>Monte Christo</em> and realised Dumas was basically the only French author I could name. Balzac often seems to appear in the same vein, so I prevailed upon <a href="www.1klb.com/posts/2024/12/29/five_books_i_enjoyed__winter_spring_2024_edition/https://standardebooks.org/">Standard Ebooks</a> to provide me with a copy of his novel <em>Father Goriot</em>.</p>
<p>If you read Wikipedia, it'll tell you that <em>Goriot</em> is part of Balzac's <em>Comédie humaine</em>, or &quot;Human Comedy&quot;. Don't be fooled: <em>Goriot</em> is at heart a tragedy, a story about a bunch of young folks who are doing whatever they can to get ahead in a world wracked by social upheaval. It has all the tools available to be an uplifting romance, but these things aren't meant to be. In contrast to Dumas' <em>Monte Christo</em>, this is a more realist tale of the Bourbon Restoration. Edmund Dantès and his contemporaries often feel like archetypes or like ideas made flesh, while Balzac's characters feel more flesh-and-blood, more affected by the forces surrounding them.</p>
<p>I'm intrigued by descriptions of Balzac's <em>Comiédie humaine</em>: an interweaving set of myriad books whose characters may pop up in multiple novels, exploring the facets of Paris and France during the time. It sounds like an immense undertaking and something I'd love to make my way through - but each individual book is <em>so long</em> that I'm really not sure if it's worth doing. Perhaps one of these will just be Balzac.</p>
<img src="www.1klb.com/posts/2024/12/29/five_books_i_enjoyed__winter_spring_2024_edition/cloud_cuckoo_land.jpg" class="rightfloat" />
<p><strong>Cloud Cuckoo Land – Anthony Doerr</strong>. A story of five youths, growing up in troubled times, finding solace in an the fragments of ancient Greek writer's comedy, connected by this thin thread and by the occasional chance meeting.</p>
<p>Doerr's structure and writing - the interspersed stories told across time, the use of written materials as a linking structure, the delving into the near future of humanity - remind me strongly of David Mitchell, although I think Doerr's take is slightly more optimistic and humanist than Mitchell's.</p>
<p>One highlight of the book for me was the haunting description of Constantinople in the <a href="www.1klb.com/posts/2024/12/29/five_books_i_enjoyed__winter_spring_2024_edition/https://en.wikipedia.org/wiki/Fall_of_constantinople">final days of the Byzantine Empire</a>.</p>
<img src="www.1klb.com/posts/2024/12/29/five_books_i_enjoyed__winter_spring_2024_edition/grass.jpg" class="leftfloat" />
<p><strong>Grass – Sheri S. Tepper</strong>. This novel got mentioned somewhere on the internet, I wish I could remember where. Someone was mentioning it in passing, from memory, and I thought it sounded interesting.</p>
<p><em>Grass</em> is a science fiction story that follows a family sent undercover as diplomats on a fact-finding mission for a global church to an argarian plant in the back of beyond, as an incurable plague starts to sweep through humanity. It reminds me of plenty of other science fiction books I've enjoyed: the sweeping vistas of the titular planet Grass parallel in my mind to the sands of Dune, and the inter-family squabbles that occupy the first half or so of the book also remind me of the same squabbles you see in the first third of <em>Dune</em>. Some other elements (and some of the alien weirdness the family experience on the planet Grass) remind me of Dan Simons' 1989 novel <em>Hyperion</em>, while the relationship between the protagonist and the aliens she encounters feels somewhat akin to some of Le Guin's <em>Ansible</em> series.</p>
<p>Perhaps the highest praise I can give is that it started me reading through the rest of Tepper's bibliography, which is where I find myself now as I round out the year. So far nothing has been quite as good as <em>Grass</em>, but it's early days yet.</p>
<div class="divider"></div>
<p>This year – the last couple of years, honestly – have been a bit of a posting desert. I'm hoping that over 2025 I can get back to posting more regularly on other subjects, as well as improving some parts of the site which I know are lacking, and perhaps finishing off some incomplete series. Only time will tell.</p>
]]></content>
  </entry>
  
  <entry>
    <title><![CDATA[📯 Five books I enjoyed, Summer/Autumn 2024 Edition]]></title>
    <link href="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/" />
    <updated>2024-09-05T00:00:00.000Z</updated>
    <id>www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/</id>
    <content type="html"><![CDATA[<p>This should have come out more than a month ago, but I sunk a lot of time in to redeveloping the site, and because my ability to use version control drops off a cliff past around 7pm (it seems), I never bothered to keep the activities of <em>mucking about with the website proper</em> and <em>writing articles</em> split.</p>
<p>This marks the first of these posts since I decided to track my reading digitally<sup class="footnote-ref"><a href="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/#fn1" id="fnref1">[1]</a></sup> (as opposed to an analogue system). Previously I'd keep track of this through my personal notebook, making entries per book and then writing an index in the back of the notebook. This was fine, except that each time I finish a book (which is getting rarer now we've got a kid around to take up all my spare time) I'd be starting a new index. I really need to externalise the index into its own book, which seems like a lot of work.</p>
<p>While a digital book catalogue doesn't feel nearly as romantic or tactile, I hope that it's actually more useful in some imagined future where I need to consult my notes on a given book. That doesn't normally happen - I'm not a practicing academic or anything, and a lot of the time if I'm that interested in what a book had to say I'd much prefer to get it off the shelf and leaf through it, but you never know.</p>
<p>This list is curiously divided in two: I tended to read large, weighty, &quot;high-fibre&quot; books - the kind of thing you feel you should read, it keeps appearing on all these lists - interspersed with re-reads of some of my favourite, comforting books. It makes sense: after digesting something challenging, you want to return to something easy, something you know you'll enjoy. A section of those books appear below.</p>
<div class="divider"></div>
<img class="leftfloat" src="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/power_broker.jpg" />
<p><strong>The Power Broker - Robert Caro</strong>. This absolute doorstop of a biography follows the life of New York urban planner and public official <a href="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/https://en.wikipedia.org/wiki/Robert_Moses">Robert Moses</a>, who &quot;built more structures and moved more earth than anyone in human history&quot;, as per <a href="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/https://99percentinvisible.org/episode/breaking-down-the-power-broker/">99% Invisible's introduction</a> to the book. I read it as part of the 99% Invisible read-along, and once again I must recommend the e-reader as a device which masks the immensity of large books. A 1,200 page biography, in physical form, is daunting (as well as a good work-out for your arms and a handy weapon in case of home invasion). On your e-reader, that just means the progress bar at the bottom of the screen fills at a slow rate.</p>
<p>It's worth situating this book, outside of its context. Again quoting from 99pi:</p>
<blockquote>
<p>Outside of New York City, Robert Moses wasn’t exceptionally well known. Inside of New York, he was mostly accepted by the media as simply the man who built all those nice parks. But <em>The Power Broker</em>, which is subtitled <em>Robert Moses and The Fall of New York</em>, changed all that.</p>
</blockquote>
<p>Importantly, this book highlighted not just <em>what</em> Moses managed throughout his career, but <em>how</em> he managed it: by conning planning committees into providing funding, running roughshod over communities, bullying his peers and his superiors into his way of working, and focussing myopically on the car as the sole means of transport. It's by no means a one-sided affair - Moses managed to build a lot of impressive infrastructure in his time - but it highlights just how much Moses' methods and overall philosophy overshadow his achievements.</p>
<p>Ironically, despite all the bridges and parks and roads Moses built, this book feels like it cements him most strongly into history. The final chapters of the book show Moses in retirement, already forgotten and increasingly irrelevant to the world he's left behind. Perhaps the most poignant lesson from the book is that even if the entirity of a city's infrastructural system revolves around you, work <em>still</em> won't love you back.</p>
<p>Anyway, yeah, this book won a bunch of prizes and is well worth reading to understand how cities, local government, and infrastructure work.</p>
<img class="rightfloat" src="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/fever_crumb.jpg" />
<p><strong>Fever Crumb - Philip Reeve</strong>. I <a href="www.1klb.com/2020/12/31/ten-books-i-enjoyed-this-year/">first read the <em>Mortal Engines</em> series in 2020</a>, and I returned to them following Caro because I needed something simple to decompress, and because I'd been reminded of the series through a discussion with a friend. The original tetralogy holds up passing well, but I'll be honest - it didn't blow me away as much as it did the first time. I actually prefer <em>Fever Crumb</em>, Reeve's second trilogy set in the same world, a prequel of sorts that showed the world <em>Mortal Engines</em> grew from. Perhaps it's because the series relies less on the gimmicks of the first, and instead just lets its characters explore the world. While the later plots get to the world-hangs-on-your-decisions scale we saw in the first series, at least for the first two books the scale stays nicely personal.</p>
<img class="leftfloat" src="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/magic_for_beginners.jpg" />
<p><strong>Magic for Beginners - Kelly Link</strong>. Link has written a number of magical realism short story collections, but this is one of my favourites. I feel the stories walk that fine line of surrealism without complete absurdity, of speaking in metaphor without falling all the way into literary abstraction. And also it contains the novella also entitled <em>Magic for Beginners</em> which I'm particularly fond of, and which lives in my mind rent-free.</p>
<img class="rightfloat" src="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/thin_red_line.jpg" />
<p><strong>The Thin Red Line - James Jones</strong>. Sometimes you remember a film or a book exists, and that it takes up an oversized amount of space in the general consciousness even though most people couldn't tell you what goes on in the book, and you decide &quot;I guess I should read it to work out what it's all about&quot;. That's why I read this book.</p>
<p>The novel - semi-autobiographical, or at least &quot;based on a true story&quot; - follows an inexperienced United States Army company as they land on and battle for the Pacific island of Guadalcanal in the Second World War. We see them lose their innocence and become inured to the tragedy of war and the arbitrary way in which injuries and death stalk them on the battlefield.</p>
<p>I don't tend to read war fiction, so I couldn't tell you how much (if any) of the storytelling in this book breaks new ground in the genre. Even absent any of that context, though, it felt incredibly empathic to the poor grunts involved in the dying portion of the war. We spent some time on the high-level action, sure, but only enough to situate the personal.</p>
<img class="leftfloat" src="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/empress.jpg" />
<p><strong>The Will of the Empress - Tamora Pierce</strong>. It's a wonder that Tamora Pierce has never appeared on one of these lists in the past. Her novels occupy a warm spot in my heart as the childhood series I never read during childhood. When I get sick, I tend to load up one of the may series on my e-reader to tide me over<sup class="footnote-ref"><a href="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/#fn2" id="fnref2">[2]</a></sup>. And sometimes I'll just go, &quot;It's been about six months since I read one, which shall I read this time?&quot;</p>
<p>Among them all, Pierce's <em>Circle of Magic</em> series (and its sequels) are perhaps my favourite. I have a soft spot for magical <em>Bildungsromans</em> so it's no surprise. The first tetralogy follows four misfit mages as they are rescued from their current lives and brought to the Winding Circle temple in Pierce's medieval/renaissance fantasy world. The four kids, aged around ten, build a strong rapport and face various challenges across the books, each story allowing one of the children to hold the spotlight while the other three support them. The second series, <em>The Circle Opens</em>, takes place around four years later, as the children separately travel the world with their mentors. I think it's not as strong as the first series, but still a great series. And following this are three books, of which I believe <em>The Will of the Empress</em> is the standout. It follows the children, now aged around eighteen, as they deal with how they've all changed while also trying to ensure they solve the problem of the day.</p>
<p>There's a narrative Stockholm Syndrome which happens in long series I think - the more time you spend with characters, the more joy you get from their interactions. This definitely applies here.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p>Inspired by <a href="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/https://www.honest-broker.com/p/how-i-take-notes">this blog post</a>. <a href="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>Ironically, I started re-reading this series about two weeks before our household contracted COVID-19. I ended up finishing the second series while I recovered. Correlation? Or some kind of weird atemporal causation? Who knows? <a href="www.1klb.com/posts/2024/09/5/five_books_i_enjoyed__summer_autumn_2024_edition/#fnref2" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]></content>
  </entry>
  
  <entry>
    <title><![CDATA[📯 Moving house]]></title>
    <link href="www.1klb.com/posts/2024/07/26/moving_house/" />
    <updated>2024-07-26T00:00:00.000Z</updated>
    <id>www.1klb.com/posts/2024/07/26/moving_house/</id>
    <content type="html"><![CDATA[<p>A quick housekeeping note, more than anything: this blog is now shifted over to <a href="www.1klb.com/posts/2024/07/26/moving_house/https://www.11ty.dev/">11ty</a>!</p>
<p>I used to use <a href="www.1klb.com/posts/2024/07/26/moving_house/https://nanoc.app/">nanoc</a>, a ruby-based SSG for the site, and I've been doing that since, well, it feels like forever. However recently it's been feeling a little long in the tooth, escpecially as I start thinking about building out the garden and running some pre- and post-processing tasks.</p>
<p>Shifting everything to 11ty has been pretty time-consuming - it's been an evening and weekend activity for most of the last month or two - but if you're reading this it means I've now deployed it in place of my old install. Things are still broken - search, writing, probably other things I've just glossed over right now. But I want to ship this now, and then to try to rebuild bits as needed. As with everything, I'll be spending 80% of my effort on the last 20% of the site, working out how I can take the stupid hacks I made to nanoc and ruby and apply them to 11ty and javascript.</p>
<p>I've definitely noticed a significant speed boost when generating the site - I'm not sure what's causing it, as I strongly suspect it's not the switch in languages. Whatever the reason, though, that should make development and testing a lot easier as I slowly implement the old features I want to keep, and add stuff over time.</p>
<p>I have plenty of thoughts about how to improve things - how better to organise the garden, how to bring things into more order - how to make the whole thing more navigable even as I add more. And also, to actually write something again, rather than just fiddle with it. But this first.</p>
]]></content>
  </entry>
  
  <entry>
    <title><![CDATA[📯 Street numbering in Istallia]]></title>
    <link href="www.1klb.com/posts/2024/05/05/street-numbering-in-istallia/" />
    <updated>2024-05-05T00:00:00.000Z</updated>
    <id>www.1klb.com/posts/2024/05/05/street-numbering-in-istallia/</id>
    <content type="html"><![CDATA[<p>Dear R––,</p>
<p>I know that at the end of our last <em>Fall of Magic</em> campaign, you told me (paraphrased):</p>
<blockquote>
<p>I would like to play this game again, but I don't know I'll be able to
because I don't think it'll ever be as good as that game.</p>
</blockquote>
<p>I'm writing this because I want to tell you that - as long as you find a group of
folk who're on the same wavelength as you - you'll play games as good as that.</p>
<p>There's a few games that I come back to time after time. Some systems, like <em>Apocalypse World</em> and <em>Dungeon World</em>, came along right as I was entering that halcyon time back in University where you could be in three campaigns at once and not even sweat. I played them so much that their systems and mechanics are like second nature to me. I return to them because I want to tell a story, I want to author something for my friends, and my hands have formed calluses around these systems. I know how to turn the rules to do the thing I want, and when and how to ignore them and let them fade to the background.</p>
<p>And then there's games whose core premise is so gripping, so interesting, so worth revisiting, that I'll run them again and again, interacting with them in different ways, letting things rhyme, stealing bits from previous games and incorporating them into the next, and just steeping in them. Games like <a href="www.1klb.com/tags/society_of_dreamers">Society of Dreamers</a>, games like <a href="www.1klb.com/tags/fall_of_magic">Fall of Magic</a>.</p>
<p>I could tell you about all the things our group has explored - about the golems and their weird trance-link mindlinks; about the culture of the Foxes of Mistwood, how they've been run out of their home and had to set up subcommunities elsewhere, and their shared religions; about the Barley Lord and his annual immolation; about our awkward teenage love triangles aboard the <em>Sea Wing</em> - but these kind of things, momentous or detailed world-building, tend to lose something on retelling.</p>
<p>Instead, I remember some of the diversions we had in our campaign - the fifteen-minute discussion about the statue in the Market Square - and thought you might like to know a similar thing that came up in our game, about how street numbering works in Istallia.</p>
<figure class="">
  <div class="images"><img class="lightbox" src="www.1klb.com/posts/2024/05/05/street-numbering-in-istallia/istallia.jpg" alt=""  class=""/></div>
</figure>
<p>The great city of Istallia can be divided into three major districts, arranged like a set of concentric circles. The largest is the Outer City, sandwiched between the Old and New Walls, the most populous and diverse of the districts. Inside of this is the Inner City, the old city, reformed and re-reformed through the pet projects of a number of successive rulers, and home of the rich and powerful, as well as the esteemed Starfall Academy. And inside that, the Gilded District, the home of the Gilded One themselves.</p>
<p>Istallia has always grown in layers. New growth occurs on the edges, and as the edges creep out, the old boundaries become roads and alleyways. Over time, ambitious rulers will tear up the houses along these desire paths to widen them, properly pave them, and turn them into roads worthy of the name.</p>
<p>The issue with the ring roads (and they are important enough and numerous enough to earn their own moniker) is that it's not just their structure that's ad-hoc. Street numbering along the roads is a topic of discussion, ridicule, and sometimes even pride from residents. As the streets are formed organically and according to demand, the numbering system is usually agreed upon by residents as required - which means the &quot;start&quot; of the street could actually be anywhere along the circumference of the city. In some cases, in the process of paving and widening the ring roads, the authorities will merge several smaller roads, all of which started at different points along the city's boundary, and renumbering from whichever road is the largest and busiest.</p>
<p>Even when the city is given the chance to renumber its roads, though, the numbering system varies from road to road. Street number 1 on one ring road may be halfway across the city from street number 1 on its outer neighbour, and may even flow the opposite way to the one directly inside of it (that is, one ring may go clockwise, and the other anticlockwise). It's a common trope for a tourist to spend an afternoon wandering a ring road with a map, trying to find the house of a friend.</p>
<p>For the residents of Istallia, the inconsistent numbering scheme is just something to be lived with. People learn how each ring road works and unconsciously remember where each road &quot;starts&quot;. But as Istallia grew and become more of a destination for tourists over the past century, successive rulers have identified the confusing ring road system as something worthy of reform.</p>
<p>Theron of Istallia was Gilded One - that is, ruler of Istallia, elected by its Council - many decades ago. Of note to us, he completed his full education at Starfall Academy, and this kind of academic rigour, slightly divorced from reality, tinged his decisions as a leader. It was his idea to try to bring some sort of order to the ring road's haphazard numbering system.</p>
<p>How? Theron issued a city-wide decree that outlined strict rules that the roads would follow. Those roads which did not obey would gradually be brought into line, houses renumbered, some torn down - the argument being that a little disruption <em>now</em> would pay dividends <em>forever more</em>.</p>
<p>The first two sections of the decree were somewhat arbitrary but uncontroversial -  first, street number 1 for every ring road would be the easternmost property on the road, and second, every ring road's street numbers would increase as the road turned anticlockwise from this origin point. This was the kind of analytical thinking Theron inherited from his lecturers at the Academy.</p>
<p>But the third section was unintuitive, divisive, and eventually caused the scheme to be canned. It was brought in to settle another, related, issue that always came up with the ring roads: as you travelled further out from the centre of the city, the ring roads had to cover more and more distance. As this happened, the number of houses on each road increased, and the highest number on the road increased as well. The inner-most ring roads may only have a few hundred houses on them, but by the time you reached the outskirts of the Outer City, roads would have a thousand or more houses on them.</p>
<p>In an attempt to make navigation easier for tourists, Theron decreed that the highest number for any property on a ring road would be 360, this being the house just south of the easternmost house (which is, of course, numbered 1). The numbers would mimic the degrees in a circle, with house number 90 being the northernmost house on the road, 180 being the westernmost, and 270 the southernmost. In this utopian vision, a visitor to the city could visit the Temple of the Fallow Field at 90 Cedar Road (this being the northernmost property on said ring road), walk one block north, and be guaranteed to find themselves at 90 All Angels' Road, the next road out.</p>
<p>This was no issue for the smaller streets - in fact, many of the innermost ring roads would find themselves skipping house numbers to fit the pattern. The problem was with the larger ring roads that traversed the outer city: to ensure every house got a number, Theron proposed dividing the roads into one-degree &quot;arcs&quot;, assigning each arc a number, and then providing each house within the arc with a fraction such that the arc was evenly divided between its occupants.</p>
<p>For residents of the Inner City, the numbering scheme was not too much of a burden - after all, if a couple of the ring roads in the Inner City had houses labelled &quot;156½&quot; or &quot;13¾&quot;, what of it? But the districts near the edge of the city - notably, those populated by the poor and the newly-arrived - found it absurd and unworkable. These were the suburbs already marginalised by the ring roads, who'd find that their local streets would be renamed and houses torn down for widening when the Council decided it was time to add another layer to the system. Forcing entire neighbourhoods to renumber - and to add convoluted fractions to each building - was a step too far.</p>
<p>Over the first year of the scheme, the roads of the Inner City were reformed. There was some muttering and fuming as houses were renumbered, as everyone had to update their address books, but the inhabitants of the Inner City were golden at heart - that is, loyal supporters of the city Council and its ruling mandate - and kept their indignation mainly to the occasional angry letter to the editor of the local paper. But the inhabitants of the Outer City watched on nervously as the reforms worked their way further and further out.</p>
<p>The first three ring roads in the Outer City were successfully converted to the new numbering system without too much hassle, but demonstrations attended the next three. Things escalated from this point. City bureaucrats, delivering notices of the changes or attempting to remove and replace signage, were mobbed, their ceremonial hats of office stolen from their heads, their papers snatched from their hands and scattered into the mud of the street. Signs were painted over or torn down. In one notable instance a set of three city blocks was barricaded off from the rest of the city, the residents declaring the space an independent city-state, and chaos reigned until the army was called in to sort things out.</p>
<p>After a month of protests and stalled progress, Theron finally called for a change of tack – rather than push the system through, officials would meet with community leaders to discuss and collectively agree on important decisions regarding the project. Theron was clear in his rhetoric - the ring roads <em>would</em> be renumbered, construction <em>would</em> continue - but it was to be a collaborative effort. It's generally believed that this was the result of pressure from his counsellors, who threatened to depose and replace him if he didn't make some show of listening to the people.</p>
<p>While these communual meetings were widely considered something of a show, rather than a genuine attempt at consensus-building, they were effective at one thing: slowing the project down. By the time Theron was deposed fifteen months later - a common-enough fate for Guilded Ones in Istallia - three more streets had been partially renumbered, each to varying degrees. None had been completed. His successor, running on a platform of cost cuts and small city government, abandoned the project, not even bothering to try reverting Theron's changes.</p>
<p>Istallia's ring roads now follow a strange pattern as you travel out from the city centre. From the Road of All Gods, the innermost ring road in the Inner City, through to the Raven's Avenue in the Outer City, the roads obey Theron's requirements. Raven's Avenue has several buildings with fractions in their addresses, but otherwise the numbers seem very regular. After that, however, things get more complex. The Street of Sails, next out from Raven's Avenue, has no houses numbered 1 through 90 - the renumbering effort hit riots near the docks, where these houses would have been, and thus this quadrant retains its original numbers (with duplicates indicated by, for example, &quot;272 East Street of Sails&quot;, for a grain warehouse, and &quot;272 West Street of Sails&quot;, for a milliner's shop in the south of town). The Street of the Blessed Siblings, one ring further out again, has three individual arcs whose numbering follows Theron's rule, although the remaining two thirds follows the old scheme. And after that, the ring roads do what they will, their inhabitants negotiating, renumbering, and generally doing whatever is practical to continue living their lives.</p>
<p>The project to renumber Istallia's ring roads was one of many that Theron oversaw as Guilded One, but its impact on the city proper makes it loom large in the memories of Istallians. There are many ways to read this project as a lesson, but many Istallians view it as a demonstration of the hubris of the city's Council and its inability to solve the actual problems facing the city's inhabitants. Equally, you could view it as an argument for incremental change over sweeping reforms. But ultimately, historians argue, the main lesson to be learned here is what happens when one person brings a grand but simplistic vision to bear against a complex system, and refuses to adapt their plan to meet the vagaries of reality.</p>
]]></content>
  </entry>
  
  <entry>
    <title><![CDATA[📯 For what is blog]]></title>
    <link href="www.1klb.com/posts/2024/04/01/for-what-is-blog/" />
    <updated>2024-04-01T00:00:00.000Z</updated>
    <id>www.1klb.com/posts/2024/04/01/for-what-is-blog/</id>
    <content type="html"><![CDATA[<p>While the north might be pulling itself out of Winter and into Spring, down here the opposite is occurring. The mornings are getting darker, the days are starting to shorten. Plants are still fruiting, but there's a sense of urgency, like they know what's coming. Ideal time for reflection.</p>
<p>I've been reading a lot about websites recently. Specifically, I've been reading about what I'm going to call <a href="www.1klb.com/g/web_revival/" class="internal-link">Web Revival</a> -- a general reaction to the sanitised, siloed world of social media and the web as mediated commercial experience. It took me so long working out how I should call this whole movement here because it is, by its nature, fractured, decentralised, and organic.</p>
<p>Web Revival - or indie web, or small web, or what-have-you - is about the individual. It's about having one little website that you build yourself, of making a part of the web conform to you rather than the other way around. It's about hacking together some html and css and being fine with the fact that maybe thirty people will ever see the results.</p>
<p>As is the way, reading a gazillion articles about a thing will usually get you excited in some aspect or other of the thing. Reading a bunch of people's posts about how they experience the internet, and how they author their own custom blogs, makes me think about what the experience of 1klb is like, and what it should be, and how I can get from <em>here</em> to <em>there</em>.</p>
<div class="divider"></div>
<p>When I started 1klb, I was halfway through a degree in a career I didn't really want, locked in by student debt and sunk-cost fallacy. I knew I needed a way out, but I wasn't ready to just ditch progress and start again down some other path. Blogging was a way of having a web presence - of showing what I was doing in my spare time - of somehow linking skills to my name which weren't just academic papers so I could point to them when confronting a prospective employer in an unknown field and go &quot;I'm worthy of <em>something</em>, pay me money to do a thing?&quot;</p>
<p>I think this comes through in my first few years of posting to here - it's a lot of &quot;let me demonstrate how I can solve a problem&quot; coupled with my attempts to make something lasting or at least something I can put on the internet as evidence of my coding ability. It also comes through in volume: in the first three years I was averaging about 27 posts a year on here, whereas last year I posted here seven times. What can I say? I was a Ph.D. student, in a field I didn't particularly like, with a bunch of spare time on my hands and a low thrum of anxiety to drive me.</p>
<p>At the end of 2014 I <a href="www.1klb.com/posts/2024/04/01/for-what-is-blog/posts/2014/12/12/a-quick-personal-update/">got my first real job</a>, and my posting here took a hit. It turns out that having a 9-to-5 job, flat-hunting, and then settling into a new city will severely curtail your spare time for writing - plus, of course, the drive to publish had gone. I had good (well, adequate) employment, I'd switched careers - I'd made it! Technically not true in the short-term, as it'd take my failing out of that job, returning to my country of birth, and switching careers <em>once again</em> to get settled - but close enough. The patchy veracity of the facts doesn't obscure the overall truth.</p>
<p>Since then the blog has been somewhat quiet. I've had about eight posts a year, or two posts every three months. It's a good number to ensure that this place doesn't look <em>completely</em> abandoned, even as it won't win me any awards.</p>
<p>But that's only really half the story, isn't it? Even while web revival and its kin push <em>host your own website, own your content, make a place that's yours on the net</em> - we have to acknowledge the lives lived in other places. And that's what I'll deal with...whenever part two of this is ready to air.</p>
]]></content>
  </entry>
  
</feed>