<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
  <title>Raphael Kabo</title>
  <subtitle>Raphael Kabo&#39;s blog</subtitle>
  <link href="https://raphaelkabo.com/rss.xml" rel="self" />
  <link href="https://raphaelkabo.com/" />
  <updated>2026-02-26T00:00:00Z</updated>
  <id>https://raphaelkabo.com/</id>
  <author>
    <name>Raphael Kabo</name>
  </author>
  <entry>
    <title>Navigating git diffs in VSCode with VSCodeVim</title>
    <link href="https://raphaelkabo.com/blog/navigating-git-diffs-vscode-vim/" />
    <updated>2026-02-26T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/navigating-git-diffs-vscode-vim/</id>
    <content type="html">&lt;p&gt;In Vim, &lt;code&gt;]c&lt;/code&gt; and &lt;code&gt;[c&lt;/code&gt; jump to the next and previous git diff (&lt;code&gt;c&lt;/code&gt; is for change). Unfortunately this is a keybinding I have memorized and cannot live without. The venerable &lt;a href=https://github.com/VSCodeVim/Vim&gt;Vim plugin for VSCode&lt;/a&gt; doesn&#39;t support this out of the box, but I managed to wire it up myself - it just took a bit more fiddling than I was expecting.&lt;p&gt;First I added this to &lt;code&gt;settings.json&lt;/code&gt;:&lt;pre class=language-json&gt;&lt;code class=language-json&gt;&lt;span class=&quot;token property&quot;&gt;&quot;vim.normalModeKeyBindingsNonRecursive&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;before&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;]&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;c&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;commands&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;workbench.action.editor.nextChange&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;before&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;[&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;c&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token property&quot;&gt;&quot;commands&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;workbench.action.editor.previousChange&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This works everywhere &lt;em&gt;except&lt;/em&gt; for VSCode&#39;s diff viewer, which is sort of cursed. In the diff viewer, VSCodeVim just printed the command name at the bottom of the screen instead of executing it. I spent a while confused by this until Claude suggested that I open the Command Palette and search for &amp;quot;Go to Next Change&amp;quot; while in a diff editor. From here, I can open the little cog (I had never known what the little cog does) and look at the command being executed, which was &lt;code&gt;workbench.action.compareEditor.nextChange&lt;/code&gt;, not &lt;code&gt;workbench.action.editor.nextChange&lt;/code&gt;. Because VSCode is secretly terrible, this config isn&#39;t obviously available anywhere.&lt;p&gt;Since &lt;code&gt;]c&lt;/code&gt; and &lt;code&gt;[c&lt;/code&gt; are already being captured by VSCodeVim, I couldn&#39;t bind the diff view versions in &lt;code&gt;settings.json&lt;/code&gt; the same way. Instead, these worked when added to &lt;code&gt;keybindings.json&lt;/code&gt;:&lt;pre class=language-json&gt;&lt;code class=language-json&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;] c&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;workbench.action.compareEditor.nextChange&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;when&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;textCompareEditorVisible &amp;amp;&amp;amp; vim.mode == &#39;Normal&#39;&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;[ c&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;workbench.action.compareEditor.previousChange&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token property&quot;&gt;&quot;when&quot;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;textCompareEditorVisible &amp;amp;&amp;amp; vim.mode == &#39;Normal&#39;&quot;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With these two sets of shortcuts in place, I could navigate between diffs in VSCode!</content>
  </entry>
  <entry>
    <title>Self-hosting Matrix Synapse with Docker Compose and Caddy</title>
    <link href="https://raphaelkabo.com/blog/self-hosted-matrix-synapse/" />
    <updated>2026-02-25T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/self-hosted-matrix-synapse/</id>
    <content type="html">&lt;p&gt;Discord recently &lt;a href=https://discord.com/press-releases/discord-launches-teen-by-default-settings-globally&gt;announced&lt;/a&gt; that they&#39;re planning to start requiring all their users to upload photos of their faces or government ID for age verification. Their press release was somewhat incoherent in that they are seemingly trying to prevent their investors from fleeing by assuring them that every user will now need to be age verified, but also trying to prevent their users from fleeing by assuring them that very few users will now need to be age verified. This is the same company, by the way, which was &lt;a href=https://www.howtogeek.com/discord-changes-its-age-verification-plans-following-mass-backlash/ &gt;hacked in 2025&lt;/a&gt;, resulting in approximately 70,000 government ID photos being leaked from one of their third-party age verification vendors. So now &lt;em&gt;might&lt;/em&gt; be a really great time to start considering a non-enshittified, self-hosted alternative to Discord.&lt;p&gt;The field of alternatives to Discord is... somewhat rough. There&#39;s a couple of services which have the vague stink of corporate bullshit about them - &lt;a href=https://www.rocket.chat/ &gt;Rocket.Chat&lt;/a&gt;, &lt;a href=https://zulip.com/ &gt;Zulip&lt;/a&gt;, and &lt;a href=https://mattermost.com/ &gt;Mattermost&lt;/a&gt;. I really don&#39;t want to host anything that describes itself as &amp;quot;bringing together messaging, voice, video, critical apps, and AI to keep teams connected and missions protected&amp;quot;. All these services give me the sense that they&#39;re suddenly and coincidentally going to go to a freemium model in the next twelve months.&lt;p&gt;Then there are a handful of services which seem to have sprung up quite recently, are intentionally designed to copy Discord, and have the vague stink of being written by very small teams of devs who are going to burn out and collapse in the next twelve months: &lt;a href=https://stoatchat.com/ &gt;Stoat&lt;/a&gt;, &lt;a href=https://fluxer.io/ &gt;Fluxer&lt;/a&gt;, and &lt;a href=https://spacebar.chat/ &gt;Spacebar&lt;/a&gt;. Good luck to them all.&lt;p&gt;And then there&#39;s &lt;a href=https://matrix.org/ &gt;Matrix&lt;/a&gt;, which is an encrypted messaging &lt;em&gt;protocol&lt;/em&gt; rather than a shiny all-in-one service. The company &lt;a href=https://element.io/ &gt;Element&lt;/a&gt; - founded by the same people who created the protocol - also produce user-facing clients, called &lt;a href=https://element.io/download&gt;Element and Element X&lt;/a&gt;, but there are other clients, like &lt;a href=https://fluffychat.im/ &gt;FluffyChat&lt;/a&gt;, and there are a number of protocol server implementations, among them &lt;a href=https://github.com/element-hq/synapse&gt;Synapse&lt;/a&gt;.&lt;p&gt;Matrix reminds me a lot of &lt;a href=https://www.w3.org/TR/activitypub/ &gt;ActivityPub&lt;/a&gt;: it&#39;s open source, it has a lot of devs, it&#39;s got a lot of funding (Element has raised a lot of VC investment money and contributes the vast majority of code to the protocol, but the protocol itself is governed by the independent &lt;a href=https://matrix.org/foundation/ &gt;Matrix.org Foundation&lt;/a&gt;, which means the spec can&#39;t be rug-pulled by the investors), it has a lot of users, and a lot of them are &lt;a href=https://xn--gckvb8fzb.com/giving-up-on-element-and-matrixorg/ &gt;constantly&lt;/a&gt; &lt;a href=https://github.com/element-hq/element-x-android/issues&gt;unhappy&lt;/a&gt; with various things that Element does. Development is slow and uneven. Some features get prioritised, while others - for instance a &lt;a href=https://github.com/element-hq/element-meta/issues/321&gt;GIF picker widget in Element&lt;/a&gt; - languish for years with lots of desperate user requests and little dev interest. It&#39;s all very familiar and &lt;em&gt;comforting&lt;/em&gt;. All of these, to my mind, are the tell-tale signs of a well-oiled large open source project. So I decided to self-host Matrix for my friends.&lt;h2&gt;Limitations - who is this for?&lt;/h2&gt;&lt;p&gt;Like ActivityPub, Matrix has inbuilt federation - that is, users living on one server can join chatrooms and talk to users on other servers. One of the issues with Matrix when self-hosting is that to participate in a federated room, your server has to sync the full state of that room, including all the events and media shared in it. This can be very slow and resource-hungry on lower-end hardware, particularly for large public rooms. Because I only want to host a chatroom for my friends, I&#39;ve disabled federation from my instance, meaning that all our chats are self-contained and very lightweight.&lt;p&gt;We also don&#39;t really use a lot of Discord features like granular roles, moderation, or intro channels, which Matrix doesn&#39;t have mature equivalents for. For what I wanted - a small chat server with a handful of rooms for a handful of friends - Matrix has proven easy, stable, and lean.&lt;p&gt;One tedious gotcha I&#39;ve discovered: there appears to be a &lt;a href=https://github.com/element-hq/element-web/issues/21072&gt;bug in Element&lt;/a&gt; where messages in a room created before a user joins will never appear for that user, even if the room&#39;s history visibility is set to allow new members to see old messages. This is disappointing, and there&#39;s no fix yet.&lt;h2&gt;Prerequisites&lt;/h2&gt;&lt;p&gt;I&#39;m going to assume you already have a home server running Docker Compose with Caddy as a reverse proxy, and a domain name pointed at it. If you don&#39;t, I wrote about &lt;a href=https://raphaelkabo.com/blog/wireguard-docker-caddy/ &gt;how I set up my home server&lt;/a&gt; a while back and it&#39;s still mostly the same setup. I&#39;m going to use &lt;code&gt;matrix.example.com&lt;/code&gt; as the domain throughout this guide - replace it with yours.&lt;p&gt;You&#39;ll also need to set up a DNS A record for your Matrix subdomain. If you&#39;re using Cloudflare, make sure the proxy is turned off (grey cloud, DNS only) - Cloudflare&#39;s proxy is known to cause issues with WebSocket connections, which Matrix clients rely on for real-time sync, apparently resulting in random disconnections that don&#39;t occur when connecting DNS-only.&lt;h2&gt;Step 1: Create directories&lt;/h2&gt;&lt;pre class=language-bash&gt;&lt;code class=language-bash&gt;&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; synapse/data
&lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-p&lt;/span&gt; synapse/postgres&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Step 2: Generate the Synapse config&lt;/h2&gt;&lt;p&gt;Run a one-off Docker container to generate the homeserver configuration:&lt;pre class=language-bash&gt;&lt;code class=language-bash&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; run &lt;span class=&quot;token parameter variable&quot;&gt;-it&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--rm&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token builtin class-name&quot;&gt;pwd&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;/synapse/data:/data&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;SYNAPSE_SERVER_NAME&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;matrix.example.com &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token assign-left variable&quot;&gt;SYNAPSE_REPORT_STATS&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;no &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  matrixdotorg/synapse:latest generate&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This creates &lt;code&gt;synapse/data/homeserver.yaml&lt;/code&gt; and a signing key. The &lt;code&gt;server_name&lt;/code&gt; is permanent - it can&#39;t be changed later without starting from scratch.&lt;h2&gt;Step 3: Edit homeserver.yaml&lt;/h2&gt;&lt;p&gt;Open &lt;code&gt;synapse/data/homeserver.yaml&lt;/code&gt;. There are a few things to change.&lt;h3&gt;Database&lt;/h3&gt;&lt;p&gt;Comment out the default SQLite database and replace it with PostgreSQL:&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;&lt;span class=&quot;token key atrule&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; psycopg2
  &lt;span class=&quot;token key atrule&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;user&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; synapse
    &lt;span class=&quot;token key atrule&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; YOUR_STRONG_PASSWORD
    &lt;span class=&quot;token key atrule&quot;&gt;database&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; synapse
    &lt;span class=&quot;token key atrule&quot;&gt;host&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; synapse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;postgres
    &lt;span class=&quot;token key atrule&quot;&gt;cp_min&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;cp_max&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Listener&lt;/h3&gt;&lt;p&gt;Make sure the listener has &lt;code&gt;x_forwarded: true&lt;/code&gt; so that Synapse correctly reads client IPs from behind Caddy. Don&#39;t set &lt;code&gt;bind_addresses&lt;/code&gt; to &lt;code&gt;127.0.0.1&lt;/code&gt; - that&#39;s for non-Docker installs and will prevent the reverse proxy from reaching the container.&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;&lt;span class=&quot;token key atrule&quot;&gt;listeners&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;port&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8008&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;tls&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; http
    &lt;span class=&quot;token key atrule&quot;&gt;x_forwarded&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;names&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;client&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; federation&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;compress&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Disable federation&lt;/h3&gt;&lt;p&gt;If, like me, you just want a private server for friends:&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;&lt;span class=&quot;token key atrule&quot;&gt;federation_domain_whitelist&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This whitelists no domains, effectively blocking all federation traffic.&lt;h3&gt;Registration&lt;/h3&gt;&lt;p&gt;Disable public registration. Set a shared secret you&#39;ll use to create accounts from the command line:&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;&lt;span class=&quot;token key atrule&quot;&gt;enable_registration&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;registration_shared_secret&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;GENERATE_WITH_openssl_rand_-hex_32&quot;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h3&gt;Optional tweaks&lt;/h3&gt;&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;&lt;span class=&quot;token comment&quot;&gt;# Presence broadcasts online/offline/away status to other users in real&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# time. It&#39;s nice to have but requires a surprising amount of CPU for&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# constant status updates - and on a small server, you&#39;ll notice who&#39;s&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# around anyway.&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;presence&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;false&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Limit uploads&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;max_upload_size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 50M

&lt;span class=&quot;token comment&quot;&gt;# Automatically and permanently delete media files older than these&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# thresholds. The database records are kept (so messages still show&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# that media was attached), but the files themselves are gone forever.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Set these to something you&#39;re comfortable with.&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;media_retention&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;local_media_lifetime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 90d
  &lt;span class=&quot;token key atrule&quot;&gt;remote_media_lifetime&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 14d

&lt;span class=&quot;token comment&quot;&gt;# Enable link previews. Synapse will fetch a thumbnail and summary for&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# URLs shared in chat. The IP blacklist below is required - it prevents&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Synapse from being tricked into fetching URLs on your internal network&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# (e.g. a malicious user posting a link to http://192.168.1.1/admin).&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# These ranges cover localhost, private networks, and link-local addresses.&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;url_preview_enabled&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;
&lt;span class=&quot;token key atrule&quot;&gt;url_preview_ip_range_blacklist&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;127.0.0.0/8&#39;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;10.0.0.0/8&#39;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;172.16.0.0/12&#39;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;192.168.0.0/16&#39;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;100.64.0.0/10&#39;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;169.254.0.0/16&#39;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fe80::/10&#39;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;fc00::/7&#39;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;::1/128&#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For details on all available configuration options, see the &lt;a href=https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html&gt;Synapse configuration manual&lt;/a&gt;.&lt;h2&gt;Step 4: Fix permissions&lt;/h2&gt;&lt;p&gt;The Synapse Docker image runs as UID/GID 991 by default, but the generated files are probably owned by root because root ran the setup command:&lt;pre class=language-bash&gt;&lt;code class=language-bash&gt;&lt;span class=&quot;token function&quot;&gt;sudo&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;chown&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-R&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;991&lt;/span&gt;:991 ./synapse/data/&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Step 5: Add to docker-compose.yml&lt;/h2&gt;&lt;p&gt;Add these two services to your existing &lt;code&gt;docker-compose.yml&lt;/code&gt; (I&#39;m also showing the Caddy service here for context - yours will already exist):&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;  &lt;span class=&quot;token key atrule&quot;&gt;caddy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; caddy&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;alpine
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; caddy
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./caddy/data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;rw
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./caddy/www&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/usr/share/caddy
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./caddy/Caddyfile&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/etc/caddy/Caddyfile
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token datetime number&quot;&gt;80:80&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 443&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;443&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;extra_hosts&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;host.docker.internal=host-gateway&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; caddy_network

  &lt;span class=&quot;token key atrule&quot;&gt;synapse-postgres&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; postgres&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;16&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;alpine
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; synapse&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;postgres
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;POSTGRES_DB&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; synapse
      &lt;span class=&quot;token key atrule&quot;&gt;POSTGRES_USER&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; synapse
      &lt;span class=&quot;token key atrule&quot;&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; YOUR_STRONG_PASSWORD
      &lt;span class=&quot;token key atrule&quot;&gt;POSTGRES_INITDB_ARGS&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;--encoding=UTF8 --locale=C&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./synapse/postgres&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/var/lib/postgresql/data
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; caddy_network
    &lt;span class=&quot;token key atrule&quot;&gt;healthcheck&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;test&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;CMD-SHELL&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;pg_isready -U synapse&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;interval&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 10s
      &lt;span class=&quot;token key atrule&quot;&gt;timeout&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 5s
      &lt;span class=&quot;token key atrule&quot;&gt;retries&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;

  &lt;span class=&quot;token key atrule&quot;&gt;synapse&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; matrixdotorg/synapse&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;latest
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; synapse
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token key atrule&quot;&gt;depends_on&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;synapse-postgres&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token key atrule&quot;&gt;condition&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; service_healthy
    &lt;span class=&quot;token key atrule&quot;&gt;volumes&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; ./synapse/data&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;/data
    &lt;span class=&quot;token key atrule&quot;&gt;ports&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; 8008&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;8008&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; caddy_network&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;ports: 8008:8008&lt;/code&gt; mapping on the Synapse container isn&#39;t strictly necessary for Caddy (which reaches Synapse via the Docker network), but it exposes the port to localhost, which is needed for the &lt;code&gt;register_new_matrix_user&lt;/code&gt; command we&#39;ll use later to create accounts.&lt;p&gt;Make sure the &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; matches what you put in &lt;code&gt;homeserver.yaml&lt;/code&gt;.&lt;h2&gt;Step 6: Add to Caddyfile&lt;/h2&gt;&lt;pre&gt;&lt;code&gt;matrix.example.com {
    reverse_proxy /_matrix/* synapse:8008
    reverse_proxy /_synapse/client/* synapse:8008
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Caddy handles TLS automatically.&lt;h2&gt;Step 7: Start everything&lt;/h2&gt;&lt;pre class=language-bash&gt;&lt;code class=language-bash&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; synapse-postgres
&lt;span class=&quot;token comment&quot;&gt;# Wait a few seconds for Postgres to initialise&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose up &lt;span class=&quot;token parameter variable&quot;&gt;-d&lt;/span&gt; synapse
&lt;span class=&quot;token comment&quot;&gt;# Caddy&#39;s hot-reload doesn&#39;t always pick up changes in a container:&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; compose restart caddy&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Check the logs:&lt;pre class=language-bash&gt;&lt;code class=language-bash&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; logs synapse &lt;span class=&quot;token parameter variable&quot;&gt;--tail&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You should see &lt;code&gt;Synapse now listening on TCP port 8008&lt;/code&gt; with no errors. You can verify it&#39;s working by visiting &lt;code&gt;https://matrix.example.com/_matrix/client/versions&lt;/code&gt; in a browser - it should return a JSON blob of supported spec versions.&lt;h2&gt;Step 8: Create your first user&lt;/h2&gt;&lt;pre class=language-bash&gt;&lt;code class=language-bash&gt;&lt;span class=&quot;token function&quot;&gt;docker&lt;/span&gt; &lt;span class=&quot;token builtin class-name&quot;&gt;exec&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-it&lt;/span&gt; synapse register_new_matrix_user &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  &lt;span class=&quot;token parameter variable&quot;&gt;-c&lt;/span&gt; /data/homeserver.yaml &lt;span class=&quot;token punctuation&quot;&gt;&#92;&lt;/span&gt;
  http://localhost:8008&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Follow the prompts. Make your first user an admin.&lt;h2&gt;Step 9: Sign in&lt;/h2&gt;&lt;p&gt;Download &lt;a href=https://element.io/download&gt;Element X on your phone or Element X on your desktop&lt;/a&gt;, tap Sign in, change the account provider to &lt;code&gt;https://matrix.example.com&lt;/code&gt;, and enter your username and password. You should now have access to your very own, self-hosted, encrypted chat service! Other/older Matrix clients may call this field &amp;quot;homeserver&amp;quot; rather than &amp;quot;account provider&amp;quot; - it&#39;s the same thing.&lt;h2&gt;Creating accounts for friends&lt;/h2&gt;&lt;p&gt;There&#39;s nothing lonelier and more indicative of the self-hosted lifestyle than a beautifully set up service without anybody else on it. Buck the trend. Force your friends to join the revolution. Since public registration is off, you create accounts for them with the same &lt;code&gt;register_new_matrix_user&lt;/code&gt; command, then send them their username and password - they can change their password in their account settings afterwards.&lt;h2&gt;More references&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=https://github.com/element-hq/synapse/blob/develop/docker/README.md&gt;Synapse Docker documentation&lt;/a&gt;&lt;li&gt;&lt;a href=https://element-hq.github.io/synapse/latest/reverse_proxy.html&gt;Synapse reverse proxy docs&lt;/a&gt;&lt;li&gt;&lt;a href=https://element-hq.github.io/synapse/latest/usage/configuration/config_documentation.html&gt;Synapse configuration manual&lt;/a&gt;&lt;li&gt;&lt;a href=https://willlewis.co.uk/blog/posts/run-your-own-matrix-server-guide/ &gt;Will Lewis - Run your own Matrix chat server&lt;/a&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>The Framework 12 is the Thinkpad X220 we deserve in 2025</title>
    <link href="https://raphaelkabo.com/blog/framework-12/" />
    <updated>2025-07-04T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/framework-12/</id>
    <content type="html">&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/framework-12-01.jpg alt=&quot;&quot;&gt; &lt;em&gt;Of course I covered it in stickers immediately. &#39;Trust Your Technolust&#39; is available from &lt;a href=https://www.redbubble.com/i/sticker/Trust-Your-Technolust-by-lowercasename/35966057.EJUG5&gt;my Redbubble store&lt;/a&gt;!&lt;/em&gt;&lt;p&gt;Before my Framework 12 arrived in the post today, my favourite ever laptop was a Thinkpad X220 I bought almost 10 years ago, when it was already some 5 years old. I did so many good things on that thing. I wrote a PhD thesis. I coded a utopian social network. I replaced the old barrel charging port with &lt;a href=https://www.tindie.com/products/mikepdiy/lenovo-charging-port-type-c-pd-x220-x230/ &gt;a USB-C port designed by an awesome DIY hacker&lt;/a&gt;. I performed open heart surgey on its BIOS chip to replace the boot firmware with Coreboot. I installed at least five Linux distros on it. I took it to hacker camps and festivals. And of course, I covered it with stickers. It was a computer shaped like a friend, and I would trade a hundred beautiful sleek MacBooks for one of those.&lt;p&gt;The things I loved about the X220 were what everyone loved about it: it was small, built like a cross between a tank and a bento box, had a really excellent keyboard, a great selection of ports (alas, no HDMI), was easy to customize and modify, and parts for repair were cheap and plentiful. But it had its drawbacks too. By 2025, its processor was getting long in the tooth. I could still code on it, but video games were out of the question. Its screen wasn&#39;t particularly good, even after I replaced the stock panel with an IPS panel. Its mousepad was beyond terrible - yes, the trackpoint was cool, but it was also the &lt;em&gt;only&lt;/em&gt; reasonable way to move a cursor around the screen. It had an awful webcam and I never got around to working out which Bluetooth cards it was compatible with. Its battery life was laughable, even with the extended battery pack with integrated carry handle which made it feel a bit like the world&#39;s most Matrix-y handbag. Worst of all, my particular device had a frustatingly hard-to-diagnose bug where the whole thing would just freeze up, with no warning and no pattern, and require a restart to work again. In short, I loved it, but it was time to move on.&lt;p&gt;When I saw the launch announcement for the Framework 12, I was beyond excited. I didn&#39;t own any Framework devices, but I&#39;ve been following the company for a few years, and this new device, in its product design and its marketing, felt like it could be another computer shaped like a friend. I bought one the hour it launched, ready every one of the steady drip of reviews, and waited very patiently for it to arrive. I&#39;ve now had it for twelve hours and am writing this blog post on it.&lt;p&gt;The Framework 12 incorporates the best qualities of the hackable, tactile, and versatile Thinkpad X220 while addressing and solving its drawbacks. It is destined to become my new favourite laptop.&lt;p&gt;I am impressed with Framework as a company not only because they stick by their customers, are transparent and honest in an era of corporate obfuscation, and prove by their actions their belief that the best way to do business is to let people repair and hack their devices, not lock them in. In the consumer tech space, that&#39;s a corporate philosophy every bit as radical as Patagonia&#39;s.&lt;p&gt;I&#39;m actually more impressed by the fact that Framework do all this while their products are consistently misrepresented in the press. Through determination and tenacity - and presumably some heavy investment - Framework are now enough of a contender that their new products are routinely reviewed in major consumer magazines like &lt;em&gt;Wired&lt;/em&gt; and &lt;em&gt;The Verge&lt;/em&gt;. But the yardstick by which they are reviewed is consistently that of other consumer laptops - which tend to be underpowered, not designed for Linux, barely repairable, and rarely innovative. They also tend to be cheaper. I have for this reason read a number of reviews of the Framework 12 which compare it unfavourably to the MacBook Air and Microsoft Surface, simultaneously lauding its repairability while criticising its price, comparing it to products from companies which make their profits on charging huge sums for repairs or convincing customers to replace perfectly servicable hardware with the next big thing (see, for instance, the incredible marketing boondoggle of the &#39;AI-ready chip&#39;). Patagonia seems to have sailed through this space - reviews of their products always acknowledge the cost of their products and the production choices they make as a &lt;em&gt;necessary factor&lt;/em&gt; of their commitment to helping people and the planet. Framework deserves similar treatment. Their laptops cost what they cost because that&#39;s what it takes to create a product built to last and designed for years of future innovation, not just for the current big thing.&lt;p&gt;So here are my thoughts on the Framework 12, not from the point of view of a consumer magazine, but from that of a coder, hacker, and tinkerer - from the point of someone who thinks hardware design peaked the moment the devices we could replace every part of also got wifi.&lt;p&gt;The Framework 12 comes in a range of joyful colours, although I did go with light gray for maximum sticker potential. Just as the Thinkpad is synonymous with a slightly soft plastic finish, the finish of the Framework 12 is slighty, delightfully rough and feels tough and sturdy. With its contrasting bumpers and slightly squared off design, and its lovely 12-inch form factor, it&#39;s as much of a friendly bento box as the Thinkpad X220, but a little lighter and sleeker. This is not a laptop that will slide out of your fingers - it&#39;s a laptop that will stay with you through any adventure.&lt;p&gt;Unboxing the laptop was an absolute joy, and assembling it was a breeze. I bought a stick of RAM and an SSD online - doing so was significantly cheaper than buying these same third-party items via Framework, something I see &lt;em&gt;The Verge&lt;/em&gt;, with its obsession with price, is weirdly quiet on - and both worked without a hitch. I am genuinely astounded by the number of clever, thoughtful technical decisions I&#39;ve seen inside this one laptop. A keyboard and mousepad top shell that stays attached via magnets and interfaces via pogo pins. Captive screws. Magnetized RAM covers. Modular plug-and-play ports of all varieties - USB-C, USB-A, HDMI, Ethernet, and more - which fit any way you desire into the four port slots on the sides. The freedom of deciding where my USB-C ports will go, and the knowledge that if I&#39;m ever faced with a short HDMI cable I can just switch my HDMI port to the other side of the laptop - these are rare joys that will stay with me for a long time.&lt;p&gt;I love that the webcam and microphone can both be disabled with a physical switch. I love the touchscreen, which is responsive, really bright, and crisp - very much an improvement on the Thinkpad&#39;s. I love the mousepad, which is &lt;em&gt;remarkably&lt;/em&gt; good - smooth, comfortable, and precise. I love the keyboard, which has a good deep key travel, full-size keys, and is satisfying and comfortable to hammer away on. Those are the things that make or break devices for me - their tactile and physical instruments. I hated my &#39;butterfly keyboard and touchbar&#39; MacBook Pro. I loved my Thinkpad. I will love my Framework 12.&lt;p&gt;I chose to install the Cinnamon Spin of Fedora, a process which also went so smoothly as to be barely worth mentioning. So, an hour after first booting the laptop up, I had a device with a screen which automatically rotated when I put it in tablet mode, with a dismissable onscreen keyboard, connected to my Bluetooth headphones and my local file storage, and I was watching an episode of Star Trek on it - with it propped up in tent mode next to the food processor while I was making dinner. One hour to Star Trek - that&#39;s surely the best metric of a convertible, open, flexible, joyful device like this.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/framework-12-02.jpg alt=&quot;&quot;&gt;&lt;p&gt;I do already have a list of things which could be improved - some in software, some in hardware. When the laptop is in tent mode, I can&#39;t use the media keys on the back, even to adjust the volume. This may be controllable by a BIOS setting. The keyboard isn&#39;t backlit, which isn&#39;t a concern for me because I touch-type. (Ironically, I&#39;m writing in the dark in bed and I misspelled the word &#39;touch&#39; three times in that sentence). The Cinnamon DE does not present the greatest touchscreen experience - select boxes don&#39;t stay open, the global menu isn&#39;t properly scrollable, and the onscreen keyboard is hit and miss - but these can all be fixed by contributing to open source code or switching to a different DE, probably Gnome or KDE. The speakers are quite weak - certainly I was having trouble watching Star Trek in a loud kitchen without headphones. But they were fine for background music while I was writing. The screen bezels are relatively wide for a laptop in 2025, but I&#39;m sure that an improved display will be released in a few years. These are all such minor issues and I already love this little thing so much. I&#39;ve called it Janus, partly because it flips in two directions but also because it looks forwards and backwards - to the repairable, open hardware of the past, and the innovations we haven&#39;t even imagined in the future. It&#39;s a laptop shaped like a friend, and I can&#39;t wait for the adventures we&#39;re going to have in the many years we&#39;re going to spend together.</content>
  </entry>
  <entry>
    <title>Building a Tell-Tale Heart with an Adafruit Prop-Maker FeatherWing</title>
    <link href="https://raphaelkabo.com/blog/tell-tale-heart/" />
    <updated>2025-04-13T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/tell-tale-heart/</id>
    <content type="html">&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/tell-tale-heart-table.jpg alt=&quot;&quot;&gt;&lt;p&gt;&lt;a href=https://poestories.com/read/telltaleheart&gt;&amp;quot;The Tell-Tale Heart&amp;quot;&lt;/a&gt; is a short story by Edgar Allan Poe, in which an extremely sane man very calmly proceeds to explain to you, the reader, how he murdered the old man with whom he was lodging and hid his body under the floorboards. When the police come around, he can still hear the old man&#39;s heart beating under the floorboards. It is an excellent short story and if you haven&#39;t read it, I highly recommend you go away and do so now.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/tell-tale-heart.jpg alt=&quot;&quot;&gt; &lt;em&gt;Illustration by Harry Clarke (1923)&lt;/em&gt;&lt;p&gt;This Halloween, we had friends coming over for a feast, and naturally I decided to build a Tell-Tale Heart of my very own to horrify them with. Come dinnertime, I would dramatically announce that the old man&#39;s heart was hidden somewhere in the house, and that the person who found it would get a wonderful prize (candy).&lt;p&gt;My plan for how the heart should work was as follows:&lt;ul&gt;&lt;li&gt;It would need to be battery powered, so it could beat unattended for a period of time.&lt;li&gt;It would have to make noise, so it needed a speaker and an amplifier.&lt;li&gt;It should beat softly, just above the range of normal human hearing, so I could hide it somewhere.&lt;li&gt;If it was picked up or disturbed - or even if someone moved close to it - it should begin beating much louder.&lt;li&gt;Once the disturbance ended, it should return to beating softly again.&lt;/ul&gt;&lt;p&gt;It was pretty much guaranteed that with behaviour like this, it would upset and disturb my friends thoroughly!&lt;p&gt;The first thing I did was pick up &lt;a href=https://flyingtiger.com/en-gb/products/heart-3028052&gt;a squishy heart from Flying Tiger&lt;/a&gt; for £5. I have no idea why they made this, but it&#39;s hideous and I love it.&lt;p&gt;Then I took a scalpel (craft knife) to the heart, and removed some of the innards, which were made of fluff. There was definitely enough space inside the heart to hide some electronic components, which convinced me that this project would actually work out.&lt;p&gt;Then I built a test circuit on a piece of breadboard, using a &lt;a href=https://ilabs.se/product/challenger-rp2040-wifi-ble-with-chip-antenna/ &gt;Challenger RP2040&lt;/a&gt;, a &lt;a href=https://www.adafruit.com/product/3006&gt;MAX98357A amplifier breakout board&lt;/a&gt;, and a small speaker from &lt;a href=https://shop.pimoroni.com/ &gt;Pimoroni&lt;/a&gt;. You can see that in the image at the top of this post.&lt;p&gt;The next step was to find some heartbeat sounds, for which I went to the incredible &lt;a href=https://freesound.org/ &gt;Freesound&lt;/a&gt;. This involved a lot of boring trial and error, because some heartbeat sounds looked like they might work but came out fuzzy, staticky, or warped from the speaker, and others were mysteriously quiet whatever I did. I referred to the &lt;a href=https://learn.adafruit.com/adafruit-max98357-i2s-class-d-mono-amp/circuitpython-wiring-test&gt;Adafruit docs&lt;/a&gt; for my I2S amplifier breakout a lot as I tested. It was particularly helpful that they suggested &lt;a href=https://learn.adafruit.com/circuitpython-essentials/circuitpython-audio-out#play-a-wave-file-2994862&gt;the WAV files should be converted&lt;/a&gt; to mono with 22 KHz sample rate and 16 bits per sample, which I did in Audacity. In the end I actually convered them to 16 KHz, 16 bits per sample, which worked well for the files I chose.&lt;p&gt;I ended up with two audio files: a &lt;a href=https://raphaelkabo.com/media/heartbeat_slow.wav&gt;slow heartbeat&lt;/a&gt; and a &lt;a href=https://raphaelkabo.com/media/heartbeat_fast.wav&gt;fast heartbeat&lt;/a&gt;. I popped these on the RP2040 board along with some testing code, and managed to make the speaker play one, then the other.&lt;p&gt;The next step was to add an acceleremoter into the mix, so the heart could react when it was picked up or sensed a vibration. At this point, I realised that for almost the price of an accelerometer board and the other materials I needed to create a working, battery-powered heart, I could just buy the &lt;a href=https://learn.adafruit.com/adafruit-prop-maker-featherwing/overview&gt;Adafruit Prop-Maker FeatherWing&lt;/a&gt;. This supported a battery and had an onboard accelerometer and amplifier! It&#39;s such a versatile board, and I wouldn&#39;t hesiate to use one for a similar project again.&lt;p&gt;Once I got my board, with the full moon glowering through my window and wolves baying in the woods beyond the wild darkness of the unkempt garden, I finally set to work, crafting my monstrous code.&lt;p&gt;First, we pull in our imports and do some setup. This code will require you to download the &lt;a href=https://circuitpython.org/libraries&gt;extra CircuitPython libraries&lt;/a&gt; for the accelerometer (&lt;code&gt;adafruit_lis3dh&lt;/code&gt;) and the NeoPixel (&lt;code&gt;neopixel&lt;/code&gt;).&lt;pre class=language-python&gt;&lt;code class=language-python&gt;&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; audiobusio
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; audiocore
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; board
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; array
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; time
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; math
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; audiomixer
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; audiomp3 &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; MP3Decoder
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; digitalio &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; DigitalInOut&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Direction&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Pull
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; adafruit_lis3dh
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; neopixel

&lt;span class=&quot;token comment&quot;&gt;# enable external power pin&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# provides power to the external components&lt;/span&gt;
external_power &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DigitalInOut&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;EXTERNAL_POWER&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
external_power&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;direction &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Direction&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;OUTPUT
external_power&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Disable NeoPixel&lt;/span&gt;
pixel &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; neopixel&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;NeoPixel&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;NEOPIXEL&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
pixel&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;brightness &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;

fast_heartbeat_timer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;
is_fast &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, we set up the onboard amplifier. We fetch the files from the disk, use &lt;code&gt;audiocore&lt;/code&gt; to read them into a format the processor recognises, then use &lt;code&gt;audiobusio&lt;/code&gt; and &lt;code&gt;audiomixer&lt;/code&gt; to create a mixer pointing at the I2S amplifier. Finally, on startup, we send the slow heartbeat noise to the mixer, which means it will begin playing immediately.&lt;pre class=language-python&gt;&lt;code class=language-python&gt;&lt;span class=&quot;token comment&quot;&gt;# i2s playback&lt;/span&gt;
slow_file &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;heartbeat_slow.wav&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rb&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
fast_file &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;heartbeat_fast.wav&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;rb&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
slow_wave &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; audiocore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;WaveFile&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;slow_file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
fast_wave &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; audiocore&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;WaveFile&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fast_file&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
audio &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; audiobusio&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;I2SOut&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;I2S_BIT_CLOCK&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;I2S_WORD_SELECT&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;I2S_DATA&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
mixer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; audiomixer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Mixer&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;voice_count&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; sample_rate&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;16000&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; channel_count&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
                         bits_per_sample&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; samples_signed&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
audio&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;play&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;mixer&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Play slow heartbeat&lt;/span&gt;
mixer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;voice&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;play&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;slow_wave&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; loop&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
mixer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;voice&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;level &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1.0&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With our heartbeat playing, the final step is to configure the behaviour which uses the onboard accelerometer. This setup code is taken from the Adafruit documentation.&lt;pre class=language-python&gt;&lt;code class=language-python&gt;&lt;span class=&quot;token comment&quot;&gt;# onboard LIS3DH&lt;/span&gt;
i2c &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; board&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;I2C&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
int1 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; DigitalInOut&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;board&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;ACCELEROMETER_INTERRUPT&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
lis3dh &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; adafruit_lis3dh&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LIS3DH_I2C&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;i2c&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; int1&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;int1&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
lis3dh&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;range&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; adafruit_lis3dh&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;RANGE_2_G&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then we enter an endless loop with &lt;code&gt;while True&lt;/code&gt;.&lt;pre class=language-python&gt;&lt;code class=language-python&gt;&lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; is_fast &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;and&lt;/span&gt; time&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;monotonic&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;=&lt;/span&gt; fast_heartbeat_timer&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        is_fast &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;
        mixer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;voice&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;play&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;slow_wave&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; loop&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; is_fast &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;and&lt;/span&gt; lis3dh&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;shake&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;shake_threshold&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;SHAKEN!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        mixer&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;voice&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;play&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;fast_wave&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; loop&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        is_fast &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;
        fast_heartbeat_timer &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; time&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;monotonic&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;In the logic inside this loop, which runs every tick, we check if we&#39;re beating fast. If we are, we see if the 10 seconds of &lt;code&gt;fast_heartbeat_timer&lt;/code&gt; have elapsed. If they have, we unset &lt;code&gt;is_fast&lt;/code&gt; and start playing the slow heartbeat again.&lt;p&gt;In the second part of the logic, we check, instead, if we&#39;re beating slowly and we&#39;re sensing a vibration. If so, we start beating quickly, set &lt;code&gt;is_fast&lt;/code&gt; to &lt;code&gt;True&lt;/code&gt;, and set the timer to 10 seconds.&lt;p&gt;In the final cases (we&#39;re beating fast but we haven&#39;t waited 10 seconds, or we&#39;re beating slowly but we&#39;re not sensing a vibration), we just keep looping, waiting for something to happen.&lt;p&gt;That&#39;s all the code our heart needs to do its terrible works! The last bits of the project were to attach a tiny LiPo battery and a speaker to the board, and stuff it inside the soft, protective fluff of the heart.&lt;p&gt;When my friends found the heart in its hiding place, they were &lt;em&gt;very&lt;/em&gt; upset. Some comments included:&lt;ul&gt;&lt;li&gt;&amp;quot;Why is it so &lt;em&gt;squishy&lt;/em&gt;&amp;quot;?&lt;li&gt;&amp;quot;Oh god I can feel the heartbeat coming through the speaker.&amp;quot;&lt;li&gt;&amp;quot;I don&#39;t like it, make it stop.&amp;quot;&lt;li&gt;&amp;quot;aaaaaaa&amp;quot;&lt;/ul&gt;&lt;p&gt;All in all, a huge Halloween success.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/tell-tale-heart-hands.jpg alt=&quot;&quot;&gt;</content>
  </entry>
  <entry>
    <title>Using Eleventy and S3 to build a personal photo blog</title>
    <link href="https://raphaelkabo.com/blog/photo-blog-eleventy/" />
    <updated>2025-04-13T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/photo-blog-eleventy/</id>
    <content type="html">&lt;p&gt;I wanted a nice place to display albums of photos I take (I&#39;m not a great photogapher, but it&#39;s nice to be able to share them with friends). I don&#39;t use Instagram or Flickr, and websites like 500px seem intimidating. In any case, as soon as I upload a photo to a third party business, I lose control over it. I&#39;d far rather just have a site where I put my photos up and unambiguously license them with a Creative Commons license so anyone can make use of them without a business getting in the way.&lt;p&gt;Also, I love making websites.&lt;p&gt;Here&#39;s what &lt;a href=https://photos.raphaelkabo.com/ &gt;https://photos.raphaelkabo.com/&lt;/a&gt; looks like with some albums from the last year:&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/photos-site-02.png alt=&quot;The albums page, showing three albums with cover images, titles, dates, and the number of photos in the album.&quot;&gt; &lt;em&gt;The albums list, which is the home page.&lt;/em&gt;&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/photos-site-01.png alt=&quot;An individual album page, showing the album title, description, and a grid of photos.&quot;&gt; &lt;em&gt;An individual album page. Hi &lt;a href=https://makani.dev/ &gt;Mahesh&lt;/a&gt;!&lt;/em&gt;&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/photos-site-03.png alt=&quot;A lightbox showing a single image fullscreen.&quot;&gt; &lt;em&gt;There&#39;s even a lightbox!&lt;/em&gt;&lt;p&gt;I decided to build this site with the static site generator &lt;a href=https://www.11ty.dev/ &gt;Eleventy&lt;/a&gt;, because I really like the way it combines modern JS/TS with the classic concepts (including templates, collections, and layouts) developed by veteran static site generators like &lt;a href=https://jekyllrb.com/ &gt;Jekyll&lt;/a&gt; and &lt;a href=https://gohugo.io/ &gt;Hugo&lt;/a&gt;. I could have used &lt;a href=https://astro.build/ &gt;Astro&lt;/a&gt;, which I had previously used to generate my homepage and for client projects, but I also wanted to learn some new skills - and Astro felt a bit overkill for something as simple as my photo blog concept.&lt;h2&gt;Using S3 for file storage&lt;/h2&gt;&lt;p&gt;Planning ahead a little, I didn&#39;t want to store a whole bunch of binary files (that is, high-resolution photos) in a version control system like Git. Instead, I decided to store my photos in DigitalOcean&#39;s S3-compatible &lt;a href=https://www.digitalocean.com/products/spaces&gt;Spaces&lt;/a&gt; object storage. This isn&#39;t a DigitalOcean sponsored post by any means, but the simplicity of $5/month for 250GB of storage with huge bandwidth limits and a built-in CDN appeals to me. It makes even more sense because I&#39;m using Spaces for a couple of other projects. Once the photos were uploaded to Spaces, my Eleventy site needed to somehow scoop them up, generate thumbnails from them during the build step, add some metadata, and keep track of the high-resolution URLs so I could show them in a lightbox.&lt;h2&gt;Step 1: Set up Eleventy&lt;/h2&gt;&lt;p&gt;Eleventy is refreshingly simple to set up. The file system tends to describe the output structure, so with this in my &lt;code&gt;eleventy.config.js&lt;/code&gt;, I could write the contents of my homepage, at &lt;code&gt;index.njk&lt;/code&gt;, the &lt;code&gt;albums/[album].njk&lt;/code&gt; page, and &lt;code&gt;_includes/base.njk&lt;/code&gt;:&lt;pre class=language-js&gt;&lt;code class=language-js&gt;module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;eleventyConfig&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addPassthroughCopy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;css&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  eleventyConfig&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;addPassthroughCopy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;favicon.png&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;dir&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;input&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;.&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;output&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;_site&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;includes&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;_includes&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token literal-property property&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;_data&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Step 2: Connect to S3&lt;/h2&gt;&lt;p&gt;I pull the photos in from S3 as an &lt;a href=https://www.11ty.dev/docs/collections/ &gt;Eleventy collection&lt;/a&gt;. Under &lt;code&gt;_data&lt;/code&gt;, I created a YAML database of my albums. This file holds my album metadata, because the S3 directory contains only photos:&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;// _data/albumMetadata.yaml

&lt;span class=&quot;token key atrule&quot;&gt;albums&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;folderName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2024&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;01&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;north&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;finchley
    &lt;span class=&quot;token key atrule&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; North Finchley&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; London
    &lt;span class=&quot;token key atrule&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; January 2024
    &lt;span class=&quot;token key atrule&quot;&gt;coverImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Nikon D600&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Nikon 35mm f/2.0 AF&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;D. Just a soft&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; quiet winter&#39;s day; I&#39;m trying to develop some affection for this area by working on noticing. I think the winter light adds a lovely moodiness to the colours.

  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;folderName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 2024&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;04&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;around&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;london
    &lt;span class=&quot;token key atrule&quot;&gt;displayName&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Around London
    &lt;span class=&quot;token key atrule&quot;&gt;date&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; January&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;April 2024
    &lt;span class=&quot;token key atrule&quot;&gt;coverImage&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;description&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Nikon EM&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; 50mm f/1.8&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Ilford HP5+ 400 and Kodak Gold 200

  &lt;span class=&quot;token punctuation&quot;&gt;...&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, I created an S3 client:&lt;pre class=language-js&gt;&lt;code class=language-js&gt;&lt;span class=&quot;token comment&quot;&gt;// util/s3Client.js&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token constant&quot;&gt;S3&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;@aws-sdk/client-s3&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dotenv &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;dotenv&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

dotenv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; s3Client &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;new&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;S3&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;forcePathStyle&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Configures to use subdomain/virtual calling format.&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;endpoint&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;S3_ENDPOINT&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;region&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;us-east-1&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token literal-property property&quot;&gt;credentials&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;accessKeyId&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;S3_ACCESS_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;secretAccessKey&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;S3_SECRET_KEY&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;exports &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; s3Client&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I adapted this client using the very decent &lt;a href=https://docs.digitalocean.com/reference/api/spaces/ &gt;DigitalOcean Spaces documentation&lt;/a&gt;.&lt;p&gt;Finally, we need a file which maps the photos into a format Eleventy recognises, which is at &lt;code&gt;_data/albums.js&lt;/code&gt; (this automatically makes the new collection accessible at &lt;code&gt;albums&lt;/code&gt;.)&lt;pre class=language-js&gt;&lt;code class=language-js&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; s3Client &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;../util/s3Client&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; yaml &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;js-yaml&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; fs &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;fs&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; path &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;path&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; dotenv &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;require&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;dotenv&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

dotenv&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; albumMetadata &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; yaml&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;load&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
  fs&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;readFileSync&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;join&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;__dirname&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;albumMetadata.yaml&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;utf8&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

module&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function-variable function&quot;&gt;exports&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;function&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; params &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;Bucket&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;S3_BUCKET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token literal-property property&quot;&gt;Delimiter&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; listAll &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; s3Client&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;listObjectsV2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;params&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;let&lt;/span&gt; albums &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; listAll&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;CommonPrefixes&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;folderName&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; prefix&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Prefix&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// Remove trailing slash&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; prefix&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Prefix&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;// Fetch images for each album and merge with metadata&lt;/span&gt;
    albums &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; Promise&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;all&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
      albums&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token keyword&quot;&gt;async&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;album&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; albumParams &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;Bucket&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;S3_BUCKET&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;Prefix&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; album&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;path&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; albumContents &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;await&lt;/span&gt; s3Client&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;listObjectsV2&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;albumParams&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; images &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; albumContents&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Contents&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt;
          item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Key&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;endsWith&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;.jpg&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;item&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Key&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token template-string&quot;&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;process&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;env&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token constant&quot;&gt;CDN_URL&lt;/span&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;${&lt;/span&gt;item&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;Key&lt;span class=&quot;token interpolation-punctuation punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token template-punctuation string&quot;&gt;`&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;// Find matching metadata&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; metadata &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;
          albumMetadata&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;albums&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;find&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;meta&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; meta&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;folderName &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; album&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;folderName
          &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;||&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;album&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token operator&quot;&gt;...&lt;/span&gt;metadata&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          images&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; albums&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;a&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; b&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; b&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;folderName&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;localeCompare&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;a&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;folderName&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;catch&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    console&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Error fetching albums from DigitalOcean Spaces:&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; error&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;Step 3: Use the collection in Eleventy&lt;/h2&gt;&lt;p&gt;Every time Eleventy builds the site or runs in serve mode, it fetches the photos. On the home page, &lt;code&gt;index.njk&lt;/code&gt;, I can then run through each album to build a grid:&lt;pre class=language-html&gt;&lt;code class=language-html&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;ul&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;album-list&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  {% for album in albums %}
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;a&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;href&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;./albums/{{ album.folderName | slug }}/&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
      // Here I can use album.folderName, album.metadata, and album.images
    &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;a&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;li&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  {% endfor %}
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;ul&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And on the individual album page, &lt;code&gt;[album].njk&lt;/code&gt;, I use &lt;a href=https://www.11ty.dev/docs/pagination/ &gt;Eleventy&#39;s amazing pagination feature&lt;/a&gt; to pull the collection in directly:&lt;pre class=language-html&gt;&lt;code class=language-html&gt;---
pagination:
  data: albums
  size: 1
  alias: album
permalink: &quot;albums/{{ album.folderName | slug }}/&quot;
---

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;header&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;album-header&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;{{ album.displayName or album.folderName }}&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;h1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;header&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;

&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;div&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;album-meta&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  {% if album.date %}
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;album-date&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;{{ album.date }}&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  {% endif %} {% if album.description %}
  &lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;&lt;/span&gt;p&lt;/span&gt; &lt;span class=&quot;token attr-name&quot;&gt;class&lt;/span&gt;&lt;span class=&quot;token attr-value&quot;&gt;&lt;span class=&quot;token punctuation attr-equals&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;album-description&lt;span class=&quot;token punctuation&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;{{ album.description }}&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;p&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;
  {% endif %}
&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token tag&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;&amp;lt;/&lt;/span&gt;div&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;You can see the full source code for this site, including the lightbox JavaScript, on &lt;a href=https://git.sr.ht/~lown/photos.raphaelkabo.com&gt;SourceHut&lt;/a&gt;.&lt;h2&gt;Notes&lt;/h2&gt;&lt;p&gt;Thank you to &lt;a href=https://markllobrera.com/posts/eleventy-escaping-nunjucks-statements-in-markdown-code-blocks/ &gt;Mark Llobera&lt;/a&gt; for explaining how to display Nunjucks templates in Markdown files without rendering them.</content>
  </entry>
  <entry>
    <title>OpenOrb - a curated RSS and Atom feed search engine</title>
    <link href="https://raphaelkabo.com/blog/openorb-curated-search-engine/" />
    <updated>2024-04-17T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/openorb-curated-search-engine/</id>
    <content type="html">&lt;p&gt;Two weeks ago, I built a search engine which only searches over a list of RSS feeds you expressedly give it. If you regularly read ten blogs, you can add those ten to the engine, and then search for stuff in them. I called it &lt;a href=https://openorb.idiot.sh&gt;OpenOrb&lt;/a&gt; - if you click on that link, you&#39;ll be able to use my own instance. RSS feed readers can usually do this sort of thing, but RSS readers aren&#39;t meant to be shared, so you can think of the search engine as a &#39;curated feed list as a public service&#39;. I&#39;d love to see people setting up their own OpenOrb instances to allow public access to the lists of blogs and writers they&#39;ve carefully spent years curating.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/openorb.png alt=&quot;A screenshot of the OpenOrb interface, showing the results for a search for &#39;python&#39;.&quot;&gt;&lt;p&gt;I named OpenOrb after the &#39;pondering my orb&#39; meme, which was originally an illustration by Angus McBride for a 1988 gamebook (a single-player pen-and-paper roleplaying story contained in a single book) called &lt;em&gt;A Spy in Isengard&lt;/em&gt;.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/pondering-my-orb.png alt=&quot;The &#39;pondering my orb&#39; meme - a high fantasy style wizard gazes into a magical blue orb.&quot;&gt;&lt;p&gt;Niche, purposefully small-scale, and custom search engines have been turning up in my HackerNews and Mastodon feeds a lot recently. In part, this is a bubble effect - I&#39;m interested in the handmade web and IndieWeb, in self-hosting, and in alternatives to the &lt;a href=https://pluralistic.net/2023/01/21/potemkin-ai/ &gt;enshittification of dominant web platforms&lt;/a&gt;. When I was getting rid of my Facebook account in 2018-19, I built a small-scale, communally governed social network, Sweet, which is no longer running, and a federated event sharing service, &lt;a href=https://gath.io&gt;Gathio&lt;/a&gt;, which very much is. So I&#39;m exactly the sort of audience to appreciate an alternative search engine.&lt;h2&gt;Google sucks now&lt;/h2&gt;&lt;p&gt;Of course, there&#39;s another reason why alternative search engines are becoming more popular - it&#39;s because &lt;a href=https://www.theregister.com/2024/01/17/google_search_results_spam/ &gt;Google sucks now&lt;/a&gt;. The &lt;a href=https://downloads.webis.de/publications/papers/bevendorff_2024a.pdf&gt;research paper&lt;/a&gt; which that Register article references notes that Google, Bing, and DuckDuckGo - which primarily uses Bing for its search results - all appear to be losing the game against content farms, which exploit the behaviour of search engine algorithms designed to integrate with the SEO economy, flooding search result pages with shitty, machine-generated content. In response, search engine operators have two strategies: (1) constantly screen and adjust search results by removing machine-generated drivel, which costs a lot of money; or (2) take more control of their search results at source - making them less organic, showing more ads, and platforming only the companies who pay them, which &lt;em&gt;makes&lt;/em&gt; a lot of money. Whichever strategy Google is leaning into, it&#39;s now an adage of sorts that the only way to reliably find a human-generated product review or recommendation on Google these days is by typing in whatever your search term is and adding &#39;reddit&#39; at the end. Google knows this very well, which is why it&#39;s &lt;a href=https://blog.google/inside-google/company-announcements/expanded-reddit-partnership/ &gt;signed a $60 million dollar deal&lt;/a&gt; with Reddit to license its user-generated content:&lt;blockquote&gt;&lt;p&gt;Over the years, we’ve seen that people increasingly use Google to search for helpful content on Reddit to find product recommendations, travel advice and much more. [...] This partnership will facilitate more content-forward displays of Reddit information [...].&lt;/blockquote&gt;&lt;h2&gt;The enshittification ouroboros&lt;/h2&gt;&lt;p&gt;It&#39;s easy to point the finger of blame at the evil, unscrupulous AI herders who have come down from the mountains to graze their flocks of awful, noisy, shitty goats in the pristine meadows of our search pages. It was, of course, Microsoft and Google who gave them the goats in the first place - and created the situation where in the modern Internet economy, if you want to make any money at all, you have to find a way to get your goats into the meadow, any way you can.&lt;p&gt;But the death knell for the Internet as we knew it wasn&#39;t sounded last year, or even in the early 2010s, when content farms - then human-written - became a serious issue. It sounded when the great tech powers decided that access to information was a monetisable concern. As Doctorow writes in the enshittification piece I linked above:&lt;blockquote&gt;&lt;p&gt;Searching Amazon doesn&#39;t produce a list of the products that most closely match your search, it brings up a list of products whose sellers have paid the most to be at the top of that search.&lt;/blockquote&gt;&lt;p&gt;The same goes for Google and Bing. The scale doesn&#39;t really matter - it&#39;s just particularly appalling when it happens on the titanic level of search engine indexes. On the smallest scale, if I ran a zine distro where people gave me their zines to sell, and I would sell those zines for cost, perhaps with a markup to keep my distro running, every visitor would have the same experience. But if I gave preferential treatment to people who &lt;em&gt;paid me&lt;/em&gt; to sell their zine, I&#39;d end up with tables of awful, garbage pamphlets made by people with too much money, and visitors who cared about quality zines would suffer. If I gave even more preferential treatment to producers who spruiked my own zine distro in the pages of their zines, I&#39;d end up with the Google search results page for &#39;best backpack&#39; in April 2024 - a grim, sad, deeply useless ouroboros constantly shitting its own words back into its mouth.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/ouroboros.jpg alt=&quot;A medieval depiction of an ouroboros&quot;&gt; &lt;em&gt;Everyday In 2024 11 Guide 2024 Backpacks Use To Men Backpacks Best Laptop Of Backpacks 12 Buy in 2024 For For 25 Backpacks The Best Buyer&#39;s 2024 Best Best Travel&lt;/em&gt;&lt;p&gt;A comment on a TikTok I saw recently almost hit the nail on the head: &amp;quot;The problem with the Internet is that it always thinks you&#39;re shopping, even when you&#39;re not&amp;quot;. We need to go two steps further: the problem is not with the Internet, it&#39;s with the great tech powers which control the flow of easily accessible information on the Internet - Google, Microsoft, Amazon, and Meta. And they don&#39;t always &lt;em&gt;think&lt;/em&gt; you&#39;re shopping. They&#39;re desperate for you to always &lt;em&gt;be&lt;/em&gt; shopping.&lt;h2&gt;The alternatives&lt;/h2&gt;&lt;p&gt;Google&#39;s winning formula is scale - that, and it&#39;s been around so long that the Internet has been built around its algorithms. Altrernative search engines need to find other niches to work well, and there are plenty. Blogger Seirdy has written a &lt;a href=https://seirdy.one/posts/2021/03/10/search-engines-with-own-indexes/ &gt;fantastic and comprehensive list&lt;/a&gt; of search engines, including some really specific ones. Some of my favourites are:&lt;ul&gt;&lt;li&gt;&lt;a href=https://stract.com/ &gt;Stract&lt;/a&gt; - open source, &amp;quot;made for hackers and tinkerers&amp;quot;, with optional &#39;optics&#39; written in a simple DSL used to tune search results. For instance, I can search for content only on the Fediverse; only on the IndieWeb and blogs; or only in developer documentation sites.&lt;li&gt;&lt;a href=https://search.marginalia.nu/ &gt;Marginalia&lt;/a&gt; - a precious, lovely search engine which focuses on text-only, non-commercial sites. &amp;quot;Where this search engine really shines is finding small, old and obscure websites about some given topic, perhaps old video games, a mystery, theology, the occult, knitting, computer science, or art.&amp;quot;&lt;li&gt;&lt;a href=http://teclis.com/ &gt;Teclis&lt;/a&gt; - takes a similar approach to Marginalia, and is built by the creator of &lt;a href=https://kagi.com/ &gt;Kagi&lt;/a&gt;, which is a paid-for, ad-free generalist search engine. It combines its indexer with an ad-blocker: if the ad-blocker notices more than 5 ads on the site, it doesn&#39;t get indexed.&lt;/ul&gt;&lt;h2&gt;OpenOrb&lt;/h2&gt;&lt;p&gt;It&#39;s been a roundabout journey, but we&#39;re back at OpenOrb. I mentioned ideas about alternative search engines appearing on my Mastodon feed. I was originally inspired to create OpenOrb by this post by &lt;a href=https://merveilles.town/@vesto&gt;@vesto&lt;/a&gt;:&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/openorb-vesto.png alt=&quot;A post from @vesto (posted April 6 2024) which reads: &amp;quot;on my mind today: webrings as a tool for fighting back AI crap on the internet. lists of human curated websites with search engines restricted to their contents (like we have with the merveilles webring, but perhaps less specialized). could also be cool if you could have a webring search engine protocol that would allow you to bring your own search client + easily switch it between different webrings depending on what you&#39;re looking for.&amp;quot;&quot;&gt;&lt;p&gt;That&#39;s precisely what I want OpenOrb to be - a list of human curated websites with search engines restricted to their contents. Gone is the awful, shitty ouroboros of the late capitalist hellweb. Back is the joy of curation, discovery, collection, and categorization which made the early web so delightful, at least in my memory. Who needs Google? If I need a new backpack, I&#39;ll ask my friends who have backpacks. If I want to remember which blogger I follow wrote a really excellent guide to setting up the Linux window manager &lt;a href=https://github.com/riverwm/river&gt;river&lt;/a&gt;, I&#39;ll use &lt;a href=&quot;https://openorb.idiot.sh/search?query=river&quot;&gt;OpenOrb&lt;/a&gt;. Fun fact: Google searching &#39;river window manager setup guide&#39; doesn&#39;t even bring up &lt;a href=https://leon_plickat.srht.site/writing/river-setup-guide/article.html&gt;Leon&#39;s excellent post&lt;/a&gt;.&lt;p&gt;The current incarnation of OpenOrb works well enough for two day&#39;s worth of code, but I&#39;ve got some future plans for it already:&lt;h3&gt;Better indexing&lt;/h3&gt;&lt;p&gt;Currenty, OpenOrb crawls an Atom or RSS feed. The implementation of these feeds is very loose in the wild - some feeds contain a site&#39;s entire content, while others will only have the last ten posts. Some will contain full text content for a post, while others will contain only a short excerpt, or nothing. Some will have a reliable &#39;last updated&#39; date, some will not, or the date will be configured wrong. The way OpenOrb works now, it will index all the content from an RSS feed it&#39;s given. If a feed entry doesn&#39;t have any content, it will go to the original post that entry links to, and try to read the content from there. But as @vesto suggested on Mastodon, there&#39;s another option: properly crawl the content of all the sites imported into OpenOrb (while respecting robots.txt, of course), so that all content, even content not in the RSS feed, will be indexed. This is the dream.&lt;h3&gt;An API&lt;/h3&gt;&lt;p&gt;An OpenOrb instance should have a machine-accessible API. This isn&#39;t hard to do at all.&lt;h3&gt;Federation&lt;/h3&gt;&lt;p&gt;It would be lovely if my personal instance could &#39;follow&#39; other instances I really like. If someone else has a great collection of D&amp;amp;D and tabletop roleplaying blogs which they&#39;re indexing via OpenOrb, but I don&#39;t want to import them directly into &lt;em&gt;my&lt;/em&gt; OpenOrb, I should be able to connect my instance to theirs. Then, someone searching on my instance could choose to only search over local content, or include content from federated peers.&lt;h3&gt;Better search&lt;/h3&gt;&lt;p&gt;At the moment, the actual search engine part of OpenOrb almost wholesale borrows its code from Alex Molas&#39;s &lt;a href=https://www.alexmolas.com/2024/02/05/a-search-engine-in-80-lines.html&gt;search engine in 80 lines of Python&lt;/a&gt;. This search engine is surprisingly robust! But it doesn&#39;t handle more advanced search query features like wildcards, phrases, and lemmatization/stemming (which allows a search engine to perhaps include hits for &#39;computer&#39; and &#39;compute&#39; when you search for &#39;computers&#39; and &#39;learn&#39; and &#39;learner&#39; when you search for &#39;learning&#39;). Over small sets of posts, I don&#39;t think these features are necessary, but it would certainly be nice to have them.&lt;p&gt;If you set up your own OpenOrb instance, I&#39;d love to hear about it. Also, tell me about the alternative search engines you love to use, or - looking further - your favourite alternatives to search engines in general. How do you find information, discover wonderful things, and create meaningful communities and connections in the post-shitty Internet?</content>
  </entry>
  <entry>
    <title>WS2812 fairy lights with ESPHome and the Pimoroni Plasma Stick</title>
    <link href="https://raphaelkabo.com/blog/plasma-stick-2040-w-esphome/" />
    <updated>2023-04-14T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/plasma-stick-2040-w-esphome/</id>
    <content type="html">&lt;p&gt;Last month I picked up the &lt;a href=https://shop.pimoroni.com/products/plasma-stick-2040-w&gt;Pimoroni Plasma Stick&lt;/a&gt;, which is essentially an RP2040-powered Raspberry Pi Pico bolted onto a terminal block and a Qwiic/STEMMA QT connector, to make it easier to connect WS2812/NeoPixel-style individually addressable RGB(W) LED light strips. That&#39;s a lot of terminology!&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/plasma-stick.gif alt=&quot;A GIF of me changing the colour of my fairy lights using Home Assistant&quot; style=float:right;margin-bottom:1rem;margin-left:1rem&gt;&lt;p&gt;Once the Plasma Stick arrived in the mail, I connected it to a string of WS2812 LEDs, &lt;a href=https://shop.pimoroni.com/products/rgb-led-wire&gt;also from Pimoroni&lt;/a&gt;. I like this flexible LED wire because it hangs attractively along a wall like fairy lights, although it is quite fragile - I&#39;ve had one of these short-circuit and die on me.&lt;p&gt;&lt;strong&gt;My goal&lt;/strong&gt; was to use the Plasma Stick to create a set of fairy lights I could control through &lt;a href=https://www.home-assistant.io/ &gt;Home Assistant&lt;/a&gt;, by creating an &lt;a href=https://esphome.io/ &gt;ESPHome&lt;/a&gt; device.&lt;p&gt;What are Home Assistant and ESPHome? Here&#39;s how &lt;strong&gt;ESPHome&lt;/strong&gt; describes itself:&lt;blockquote&gt;&lt;p&gt;ESPHome is a &lt;em&gt;tool&lt;/em&gt; which aims to make managing your ESP boards as simple as possible. It reads in a YAML configuration file and creates custom firmware which it installs on your ESP device. Devices or sensors added in ESPHome’s configuration will automatically show up in Home Assistant’s UI.&lt;/blockquote&gt;&lt;p&gt;In my mind, ESPHome is a toolkit which allows me to transform regular development boards into &#39;smart home&#39; devices, without locking myself into the ecosystem of any particular smart home vendor.&lt;p&gt;&lt;strong&gt;Home Assistant&lt;/strong&gt; is a web interface and mobile app which allows me to create dashboards to control my smart devices, with first-class support for ESPHome. It&#39;s much more powerful than that - it also allows me to create complex integrations and automations to make our home feel even smarter. But at the most basic level, it&#39;s a way to display a web page where I can control my fairy lights.&lt;p&gt;As the name suggests, ESPHome is originally designed to be used with ESP boards such as the ESP8266 and ESP32. The Plasma Stick, however, is an RP2040 device with a completely different architecture. But it seemed to be the very easiest way to connect RGB lights to a networked development board - so I started working out how to make ESPHome play nicely with it.&lt;h2&gt;Getting started with ESPHome&lt;/h2&gt;&lt;p&gt;This was my first foray into ESPHome, so I started by installing the toolkit (which is just a Python package). However, to make it work with our board, we need to do some Weird Hacks. So that the hacks don&#39;t interfere with the rest of my machine, I&#39;m going to perform the whole process in a separate directory structure and use a Python virtual environment:&lt;pre&gt;&lt;code&gt;mkdir -p ~/esphome/{venv,devices}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This creates a directory, &lt;code&gt;esphome/&lt;/code&gt;, and two subdirectories, &lt;code&gt;esphome/venv&lt;/code&gt; and &lt;code&gt;esphome/devices&lt;/code&gt;.&lt;p&gt;Then I create a Python virtual environment, and activate it:&lt;pre&gt;&lt;code&gt;python3 -m venv ~/esphome/venv
source ~/esphome/venv/bin/activate
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now that my virtual environment is activated, which I can confirm by checking that the left of my bash prompt reads &#39;(venv)&#39;, I can install esphome:&lt;pre&gt;&lt;code&gt;pip3 install wheel
pip3 install esphome
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;pip&lt;/code&gt; will install its files only inside my local venv folder. This is very good, because in a minute we&#39;re going to directly hack those newly installed files.&lt;h2&gt;Creating the ESPHome device&lt;/h2&gt;&lt;p&gt;The lovely thing about ESPHome is that its devices are simply YAML files of simple definitions, and the ESPHome toolkit handles the complex stuff for us. In the &lt;code&gt;devices&lt;/code&gt; subfolder, create a file called &lt;code&gt;fairy-lights.yaml&lt;/code&gt; with the following contents:&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;&lt;span class=&quot;token key atrule&quot;&gt;esphome&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fairy&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;lights

&lt;span class=&quot;token key atrule&quot;&gt;rp2040&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;board&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; rpipicow
  &lt;span class=&quot;token key atrule&quot;&gt;framework&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;platform_version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//github.com/maxgerhardt/platform&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;raspberrypi.git

&lt;span class=&quot;token key atrule&quot;&gt;wifi&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;ssid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!secret&lt;/span&gt; wifi_ssid
  &lt;span class=&quot;token key atrule&quot;&gt;password&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token tag&quot;&gt;!secret&lt;/span&gt; wifi_password
  &lt;span class=&quot;token key atrule&quot;&gt;fast_connect&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token boolean important&quot;&gt;true&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;logger&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;level&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; DEBUG

&lt;span class=&quot;token key atrule&quot;&gt;api&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;ota&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;light&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;platform&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; fastled_clockless
    &lt;span class=&quot;token key atrule&quot;&gt;rgb_order&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; GRB
    &lt;span class=&quot;token key atrule&quot;&gt;chipset&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; WS2812
    &lt;span class=&quot;token key atrule&quot;&gt;num_leds&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;50&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Fairy Lights&quot;&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;pin&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; GPIO15
    &lt;span class=&quot;token key atrule&quot;&gt;effects&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;addressable_rainbow&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;token key atrule&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Rainbow
          &lt;span class=&quot;token key atrule&quot;&gt;speed&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then ceate a file called &lt;code&gt;secrets.yaml&lt;/code&gt;:&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;&lt;span class=&quot;token key atrule&quot;&gt;wifi_ssid&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Your WiFi SSID
&lt;span class=&quot;token key atrule&quot;&gt;wifi_password&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; yourwifipassword&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;rp2040&lt;/code&gt; directive object tells ESPHome that we&#39;re targeting an RP2040 board, specifically an &lt;code&gt;rpipicow&lt;/code&gt; (i.e. Raspberry Pi Pico W) one. According to the &lt;a href=https://esphome.io/components/rp2040.html&gt;ESPHome docs&lt;/a&gt;, the &lt;code&gt;platform_version&lt;/code&gt; directive is necessary for the time being to prevent compilation errors. The &lt;code&gt;wifi&lt;/code&gt; directive pulls in its secrets from the &lt;code&gt;secrets.yml&lt;/code&gt; file during compilation - you can just hardcode those values directly if you like. The &lt;code&gt;logger&lt;/code&gt; directive provides helpful logging; the &lt;code&gt;api&lt;/code&gt; directive allows us to access this device from Home Assistant, and &lt;code&gt;ota&lt;/code&gt; allows us to update it over WiFi once ESPHome is already installed.&lt;p&gt;The longest directive is &lt;code&gt;light&lt;/code&gt;, of course, and all the documentation for it is &lt;a href=https://esphome.io/components/light/index.html&gt;here&lt;/a&gt;. We&#39;re using the &lt;code&gt;fastled_clockless&lt;/code&gt; platform for reasons which will soon become apparent, and because it allows us to implement lovely lighting effects. You may need to change the &lt;code&gt;chipset&lt;/code&gt;, &lt;code&gt;rgb_order&lt;/code&gt;, and &lt;code&gt;num_leds&lt;/code&gt; directives to suit your own lights. There&#39;s also a rainbow effect, of course, because rainbows are awesome.&lt;p&gt;In a perfect world, we would now connect our Plasma Stick to our laptop and run this command:&lt;pre&gt;&lt;code&gt;esphome run fairy-lights.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Sadly, that gives us the following output:&lt;pre&gt;&lt;code&gt;This feature is incompatible with RP2040. Please see note on documentation for FastLED.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;That&#39;s odd - while the NeoPixelBus platform &lt;a href=https://github.com/Makuna/NeoPixelBus/issues/578&gt;doesn&#39;t have support for RP2040&lt;/a&gt;, it looks like the &lt;a href=https://github.com/FastLED/FastLED/pull/1261&gt;FastLED platform does&lt;/a&gt;! Why isn&#39;t it working in ESPHome?&lt;p&gt;It turns out that the version of the FastLED library that ESPHome uses is 3.3.2, while we need to use a version with RP2040 compatibility, introduced in &lt;a href=https://github.com/FastLED/FastLED/commit/58f9c15ffb576b0ca0cd948d2aaa2aa4b9c28481&gt;this PR&lt;/a&gt;. It&#39;s been merged into main, but there hasn&#39;t been an official release since then, so it&#39;s time for Weird Hacks.&lt;h2&gt;Weird Hacks&lt;/h2&gt;&lt;p&gt;Open the file &lt;code&gt;~/esphome/venv/lib/python3.11/site-packages/esphome/components/fastled_base/__init__.py&lt;/code&gt;. If your Python version isn&#39;t 3.11, your &lt;code&gt;site-packages/&lt;/code&gt; directory will be in a different directory. This file defines ESPHome&#39;s relationship with the FastLED library. Delete the following line, replacing it with the line below:&lt;pre class=language-diff&gt;&lt;code class=language-diff&gt;&lt;span class=&quot;token unchanged&quot;&gt;&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token line&quot;&gt; await light.register_light(var, config)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token deleted-sign deleted&quot;&gt;&lt;span class=&quot;token prefix deleted&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;token line&quot;&gt; cg.add_library(&quot;fastled/FastLED&quot;, &quot;3.3.2&quot;)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token inserted-sign inserted&quot;&gt;&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token line&quot;&gt; cg.add_library(&quot;&quot;, &quot;dev&quot;, &quot;https://github.com/FastLED/FastLED#58f9c15&quot;)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token unchanged&quot;&gt;&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token line&quot;&gt; return var
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;We&#39;re telling ESPHome, instead of downloading version 3.3.2 from the &lt;code&gt;fastled/FastLED&lt;/code&gt; GitHub repo, to instead download the version of the code at commit hash &lt;code&gt;58f9c15&lt;/code&gt;, and to call it &#39;dev&#39;.&lt;p&gt;Now we need to tell ESPHome that this version of FastLED is in fact compatible with the RP2040 architecture. In the file &lt;code&gt;~/esphome/venv/lib/python3.11/site-packages/esphome/components/fastled_clockless/light.py&lt;/code&gt;, add the following line:&lt;pre class=language-diff&gt;&lt;code class=language-diff&gt;&lt;span class=&quot;token unchanged&quot;&gt;&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token line&quot;&gt; esp32_arduino=cv.Version(99, 0, 0),
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token inserted-sign inserted&quot;&gt;&lt;span class=&quot;token prefix inserted&quot;&gt;+&lt;/span&gt;&lt;span class=&quot;token line&quot;&gt; rp2040_arduino=cv.Version(99, 0, 0),
&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token unchanged&quot;&gt;&lt;span class=&quot;token prefix unchanged&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token line&quot;&gt; max_version=True,
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After these two changes have been made, you can try running &lt;code&gt;esphome run fairy-lights.yaml&lt;/code&gt; once more, with the Plasma Stick plugged into your computer of course, and this time, the toolkit should successfully compile the code for the RP2040. Give it a few minutes, and you should be able to see it appear on your WiFi network, after which you can add it into Home Assistant through the &lt;a href=https://www.home-assistant.io/integrations/esphome/ &gt;ESPHome integration&lt;/a&gt;.&lt;h2&gt;Caveats&lt;/h2&gt;&lt;p&gt;It should go without saying that directly hacking ESPHome&#39;s interface with a library may not work! In this case, I adopted the necessary changes from an &lt;a href=https://github.com/esphome/esphome/pull/4361&gt;ESPHome PR&lt;/a&gt;, which was closed and abandoned by the author with the message:&lt;blockquote&gt;&lt;p&gt;I deployed this branch on a pico-w and it resulted in undesired behavior. The pico-w was seemingly crashing at random and in general very unstable.&lt;/blockquote&gt;&lt;p&gt;In my experience running this Plasma Stick for a month, I&#39;ve seen no crashes, except for some strange behaviour after my WiFi booted the decive off the network. Nonetheless, for a better experience, it&#39;s definitely worth keeping an eye on ESPHome&#39;s releases to find out when (if ever!) FastLED compatibility goes into production, and replace the Weird Hacks edition of esphome in this venv with the new version at that point.&lt;p&gt;Happy fairy light hacking!</content>
  </entry>
  <entry>
    <title>Best-fit D&amp;D damage to dice calculator</title>
    <link href="https://raphaelkabo.com/blog/damage-to-dice-calculator/" />
    <updated>2023-02-02T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/damage-to-dice-calculator/</id>
    <content type="html">&lt;div class=alert&gt;&lt;h3&gt;Just here for the goods?&lt;/h3&gt;&lt;p&gt;Download the &lt;strong&gt;damage-to-dice&lt;/strong&gt; NPM package &lt;a href=https://www.npmjs.com/package/damage-to-dice&gt;here&lt;/a&gt;, or use the online version below!&lt;/div&gt;&lt;div id=damage-to-dice-calculator&gt;&lt;div id=inputs&gt;&lt;div&gt;&lt;label for=damage&gt;Damage&lt;/label&gt; &lt;input type=number id=damage placeholder=&quot;Damage value&quot; value=0&gt;&lt;/div&gt;&lt;div&gt;&lt;label for=modifier&gt;Modifier&lt;/label&gt; &lt;input type=number id=modifier placeholder=&quot;Optional modifier&quot; value=0&gt;&lt;/div&gt;&lt;div&gt;&lt;button id=calculate-btn&gt;Calculate&lt;/button&gt;&lt;/div&gt;&lt;/div&gt;&lt;div id=best-fit-results style=display:none&gt;&lt;h3&gt;Best fit&lt;/h3&gt;&lt;p id=best-fit-text&gt;&lt;/div&gt;&lt;div id=all-possible-rolls-results style=display:none&gt;&lt;h3&gt;All possible rolls&lt;/h3&gt;&lt;div id=all-possible-rolls-content&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;script type=module&gt;import{calculateBestFit,calculateAllPossibleRolls,numberToAsciiWithSign}from&quot;/js/damageToDice.js&quot;;class DamageToDice{constructor(){this.damage=0,this.modifier=0,this.initializeElements(),this.bindEvents()}initializeElements(){this.damageInput=document.getElementById(&quot;damage&quot;),this.modifierInput=document.getElementById(&quot;modifier&quot;),this.calculateBtn=document.getElementById(&quot;calculate-btn&quot;),this.bestFitResults=document.getElementById(&quot;best-fit-results&quot;),this.bestFitText=document.getElementById(&quot;best-fit-text&quot;),this.allPossibleRollsResults=document.getElementById(&quot;all-possible-rolls-results&quot;),this.allPossibleRollsContent=document.getElementById(&quot;all-possible-rolls-content&quot;),this.damage=parseInt(this.damageInput.value),this.modifier=parseInt(this.modifierInput.value)}bindEvents(){this.damageInput.addEventListener(&quot;input&quot;,(e=&gt;{this.damage=parseInt(e.target.value)||0})),this.modifierInput.addEventListener(&quot;input&quot;,(e=&gt;{this.modifier=parseInt(e.target.value)||0})),this.calculateBtn.addEventListener(&quot;click&quot;,(()=&gt;{this.handleCalculate()})),this.damageInput.addEventListener(&quot;focus&quot;,(e=&gt;{e.target.select()})),this.modifierInput.addEventListener(&quot;focus&quot;,(e=&gt;{e.target.select()})),this.damageInput.addEventListener(&quot;keypress&quot;,(e=&gt;{&quot;Enter&quot;===e.key&amp;&amp;this.handleCalculate()})),this.modifierInput.addEventListener(&quot;keypress&quot;,(e=&gt;{&quot;Enter&quot;===e.key&amp;&amp;this.handleCalculate()}))}bestFitToString(e){return`${e.numberOfDice}${e.die.die}${numberToAsciiWithSign(this.modifier)} = ${e.numberOfDice*e.die.averageRoll+this.modifier} ${100===e.percentage?&quot;(exact!)&quot;:&quot;&quot;}`}allPossibleRollsToString(e){return e.map((e=&gt;`${e.numberOfDice}${e.die.die}${numberToAsciiWithSign(this.modifier)} = ${e.damage} (${Math.round(e.percentage)}% of target damage); min: ${1*e.numberOfDice+this.modifier}, max: ${e.numberOfDice*e.die.sides+this.modifier}; most rolls within ${e.standardDeviation.minimum}-${e.standardDeviation.maximum}`))}handleCalculate(){if(this.damage&lt;=0)return this.bestFitResults.style.display=&quot;none&quot;,void(this.allPossibleRollsResults.style.display=&quot;none&quot;);const e=calculateBestFit(this.damage,this.modifier);if(e.numberOfDice&lt;=0)return this.bestFitText.textContent=&quot;No possible rolls.&quot;,this.bestFitResults.style.display=&quot;block&quot;,void(this.allPossibleRollsResults.style.display=&quot;none&quot;);this.bestFitText.textContent=this.bestFitToString(e),this.bestFitResults.style.display=&quot;block&quot;;const t=calculateAllPossibleRolls(this.damage,this.modifier),i=this.allPossibleRollsToString(t);this.allPossibleRollsContent.innerHTML=&quot;&quot;,i.forEach((e=&gt;{const t=document.createElement(&quot;div&quot;);t.textContent=e,this.allPossibleRollsContent.appendChild(t)})),this.allPossibleRollsResults.style.display=&quot;block&quot;}}document.addEventListener(&quot;DOMContentLoaded&quot;,(()=&gt;{new DamageToDice}))&lt;/script&gt;&lt;p&gt;I was recently creating a new homebrew monster for a 5th edition D&amp;amp;D campaign I&#39;m running, and I was horrified to discover how complex the monster creation rules in the &lt;em&gt;Dungeon Master&#39;s Guide&lt;/em&gt; were. My interesting idea for a horrific aberration to give my players weeks of nightmares was immediately bogged down in the minutae of CR balance and stat calculation.&lt;p&gt;The quick rules in the guide were a bit of an improvement on the main set, but after asking for advice on Mastodon, I was &lt;a href=https://dice.camp/@BPeylet/109794513801064497&gt;pointed&lt;/a&gt; towards Blog of Holding&#39;s superb post on &lt;a href=&quot;https://www.blogofholding.com/?p=7338&quot;&gt;simplified monster generation&lt;/a&gt;. This was a huge improvement on the original rules, but in an ideal world, I&#39;d be able to generate a monster in under 5 minutes once I have a clear idea in mind.&lt;p&gt;What slows me down is that for a given CR of monster, both rules produce damage output as flat numbers, not as die rolls. I don&#39;t want to be telling my players &#39;take 16 damage&#39; every round - I want the joy and fear that can only come with rolling dice. But I also didn&#39;t want to spend time doing the maths to find out what amount of d4s, d6s, d8s, d10s or d12s would, on average, give me a result of 16. This sounded like a perfect problem for &lt;em&gt;code&lt;/em&gt;!&lt;p&gt;First, let&#39;s clearly articulate the problem: we want a &#39;best fit&#39; calculation. Furthermore, we only want to roll one type of die for a single attack - it&#39;s traditional, and less messy. If my intended average damage output is 16, I want to find out what number of one type of die I need to roll to get an average value &lt;em&gt;as close as possible&lt;/em&gt; to 16 - whether that&#39;s a little below or a little above.&lt;p&gt;The &lt;em&gt;Dungeon Master&#39;s Guide&lt;/em&gt; gives us a formula for the average of a given die type: &lt;code&gt;sides / 2 + 0.5&lt;/code&gt;. If we divide our total damage by this average, we find out how many dice we need to roll to make the total. If we round this value to the nearest integer (as we can&#39;t roll fractions of dice, at least not very successfully), we can also find out what percentage of the total this &#39;best roll&#39; comes to:&lt;pre class=language-javascript&gt;&lt;code class=language-javascript&gt;&lt;span class=&quot;token comment&quot;&gt;// damage = 16, die average roll = 3.5&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; numberOfDice &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;damage &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; die&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;averageRoll&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 5&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; damageWithDice &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; numberOfDice &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; die&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;averageRoll&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 17.5&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; percentage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;damageWithDice &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; damage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;// 109%&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With d6s, we overshoot our average damage by 109% - not bad as a best fit!&lt;p&gt;The rest of the code will simply run through the entire set of dice from d4 to d20, comparing results until we find the best fit of all the best fits. We also add in an optional &lt;code&gt;modifier&lt;/code&gt; value, because we want to be able to calculate dice numbers for attack descriptions where we know the damage value and an attack modifier.&lt;pre class=language-javascript&gt;&lt;code class=language-javascript&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; diceMap &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;die&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;d20&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;averageRoll&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;sides&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;die&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;d12&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;averageRoll&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;sides&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;12&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;die&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;d10&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;averageRoll&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;5.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;sides&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;10&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;die&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;d8&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;averageRoll&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;sides&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;8&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;die&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;d6&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;averageRoll&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;3.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;sides&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;die&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;d4&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;averageRoll&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;2.5&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;sides&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;calculateBestFit&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;damage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; modifier &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// Remove the modifier from the damage - we can&#39;t roll the modifier damage,&lt;/span&gt;
  &lt;span class=&quot;token comment&quot;&gt;// only the unmodified part of it.&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;modifier &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    damage &lt;span class=&quot;token operator&quot;&gt;-=&lt;/span&gt; modifier&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; diceMap&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;reduce&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;lowest&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; die&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; numberOfDice &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;damage &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; die&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;averageRoll&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; damageWithDice &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; numberOfDice &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; die&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;averageRoll &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; modifier&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; percentage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;damageWithDice &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;damage &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; modifier&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// If the percentage is closer to 100% than the previous best fit,&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// use this one. Math.abs() turns a negative value positive.&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;percentage &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;abs&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;lowest&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;percentage &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;// Assign the new low value to the reducer&#39;s accumulator&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
          die&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          numberOfDice&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          &lt;span class=&quot;token literal-property property&quot;&gt;damage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; damageWithDice&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
          percentage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// Don&#39;t change the accumulator&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; lowest&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;token literal-property property&quot;&gt;percentage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;Infinity&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This function will give us a single best fit value (for 16, it&#39;s 3d10, incidentally, giving us an average value of 16.5 and a 103% overshoot). But what if we want to see the whole range of best fits, to compare them with each other? For instance, we might prefer to use 6d4s instead, because they deal 15 damage (a 94% undershoot) but have a higher minimum damage value of 6 than the 3 we get from the d10s.&lt;p&gt;We can do this by running &lt;code&gt;.map()&lt;/code&gt; instead of &lt;code&gt;.reduce()&lt;/code&gt; over the list of dice.&lt;pre class=language-javascript&gt;&lt;code class=language-javascript&gt;&lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; &lt;span class=&quot;token function-variable function&quot;&gt;calculateAllPossibleRolls&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;damage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; modifier &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;modifier &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    damage &lt;span class=&quot;token operator&quot;&gt;-=&lt;/span&gt; modifier&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; diceMap
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token parameter&quot;&gt;die&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; numberOfDice &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;damage &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; die&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;averageRoll&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;// Some die types are too small to provide the target value.&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;numberOfDice &lt;span class=&quot;token operator&quot;&gt;===&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;null&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; damageWithDice &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; numberOfDice &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; die&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;averageRoll &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; modifier&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;const&lt;/span&gt; percentage &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Math&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;round&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;
        &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;damageWithDice &lt;span class=&quot;token operator&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;damage &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; modifier&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;100&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        numberOfDice&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        die&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;token literal-property property&quot;&gt;damage&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;:&lt;/span&gt; damageWithDice&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
        percentage&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;Boolean&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This code uses the neat &lt;code&gt;filter(Boolean)&lt;/code&gt; &lt;a href=https://michaeluloth.com/filter-boolean&gt;trick&lt;/a&gt;, which removes null values from the final array before it&#39;s returned. I prefer it to &lt;code&gt;.filter(v =&amp;gt; v)&lt;/code&gt; because to my mind, filtering against a Boolean constructor makes more sense than just assuming that &lt;code&gt;.filter()&lt;/code&gt; will return a truthy value for an element.&lt;p&gt;And that&#39;s it! With this second function, we can get a full array of all possible best fit die rolls between d4 and d20. I&#39;ve created &lt;a href=https://www.npmjs.com/package/damage-to-dice&gt;an NPM package&lt;/a&gt; which wraps these functions in a command line interface. You can install it and run it from your command line, and you too will &lt;em&gt;finally&lt;/em&gt; get the full lowdown on all the different ways you can approximate an average of 16 damage with a die roll:&lt;pre class=language-bash&gt;&lt;code class=language-bash&gt;$ &lt;span class=&quot;token function&quot;&gt;npm&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;install&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--global&lt;/span&gt; damage-to-dice
$ damage-to-dice &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;
Best fit: 3d10+0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16.5&lt;/span&gt;
----------------------------------------
All possible rolls:
2d20+0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;21&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;131&lt;/span&gt;% of target damage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; min: &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;, max: &lt;span class=&quot;token number&quot;&gt;40&lt;/span&gt;
2d12+0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;13&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;81&lt;/span&gt;% of target damage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; min: &lt;span class=&quot;token number&quot;&gt;2&lt;/span&gt;, max: &lt;span class=&quot;token number&quot;&gt;24&lt;/span&gt;
3d10+0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16.5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;103&lt;/span&gt;% of target damage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; min: &lt;span class=&quot;token number&quot;&gt;3&lt;/span&gt;, max: &lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt;
4d8+0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;18&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;113&lt;/span&gt;% of target damage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; min: &lt;span class=&quot;token number&quot;&gt;4&lt;/span&gt;, max: &lt;span class=&quot;token number&quot;&gt;32&lt;/span&gt;
5d6+0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;17.5&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;109&lt;/span&gt;% of target damage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; min: &lt;span class=&quot;token number&quot;&gt;5&lt;/span&gt;, max: &lt;span class=&quot;token number&quot;&gt;30&lt;/span&gt;
6d4+0 &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;15&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;94&lt;/span&gt;% of target damage&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; min: &lt;span class=&quot;token number&quot;&gt;6&lt;/span&gt;, max: &lt;span class=&quot;token number&quot;&gt;24&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content>
  </entry>
  <entry>
    <title>Deploying an Astro site through SourceHut</title>
    <link href="https://raphaelkabo.com/blog/astro-sourcehut-deployment/" />
    <updated>2023-01-31T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/astro-sourcehut-deployment/</id>
    <content type="html">&lt;p&gt;Today I decided to finally try out &lt;a href=https://astro.build&gt;Astro&lt;/a&gt;, a Javascript framework for building websites, by trialling it on my homepage. Suffice to say I liked it so much, and it was so easy to get to grips with, that this site is now built using Astro.&lt;sup class=footnote-ref&gt;&lt;a href=https://raphaelkabo.com/blog/astro-sourcehut-deployment/#fn1 id=fnref1&gt;[1]&lt;/a&gt;&lt;/sup&gt;&lt;p&gt;I was previously less interested in Astro because I thought it was mostly a tool for building websites using an &lt;a href=https://www.patterns.dev/posts/islands-architecture/ &gt;islands architecture&lt;/a&gt;, something we&#39;re extensively trialling at work. But having read the excellent Astro docs, I realised that Astro websites are &amp;quot;Zero JS, by default&amp;quot;, and that islands are simply an optional, albeit useful feature. With Astro under the hood, this website ends up just as small and static as it was when I deployed it using my self-designed static site generator, &lt;a href=https://github.com/lowercasename/orogene&gt;Orogene&lt;/a&gt;.&lt;p&gt;With the move to a new framework and a new git repository, I also thought to make use of my &lt;a href=https://sourcehut.org/ &gt;SourceHut&lt;/a&gt; account and set up an automatic deployment script using SourceHut&#39;s superb &lt;a href=https://man.sr.ht/builds.sr.ht/ &gt;build.sr.ht&lt;/a&gt; build system. With a deployment system in place, my workflow will look like the following:&lt;ol&gt;&lt;li&gt;Write a new post, like this one, on my local machine&lt;li&gt;Commit it to this site&#39;s &lt;a href=https://git.sr.ht/~lown/homepage&gt;SourceHut repository&lt;/a&gt;&lt;li&gt;SourceHut will spin up a virtual machine, install Astro, build the static site from the latest commit in the repository, and copy the newly minted static site directly to my server - all without my lifting a finger.&lt;/ol&gt;&lt;p&gt;In my research, I found a &lt;a href=https://sidhion.com/blog/posts/simple-sourcehut-deployments/ &gt;brilliant post&lt;/a&gt; which documented a similar process, but for the static site generator Hugo. I&#39;ve borrowed extensively from that post - mostly for my own reference - the steps needed to create a secure path between SourceHut&#39;s build system and my server, which will prevent my server from being compromised if the private key is leaked. I strongly recommend you read the original post for the context.&lt;h2&gt;Create SSH keypair&lt;/h2&gt;&lt;p&gt;We&#39;ll be creating a user, &lt;code&gt;homepage_deployer&lt;/code&gt;, on the remote server, who will be allowed only to upload files to a specific directory. That user needs an SSH key to log in to the server from inside the build job. Create a keypair on your local machine:&lt;pre&gt;&lt;code&gt;ssh-keygen -t ed25519 -f ~/.ssh/homepage_deployer
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Copy the contents of the public key, &lt;code&gt;~/.ssh/homepage_deployer.pub&lt;/code&gt;, for the next step.&lt;h2&gt;Server setup&lt;/h2&gt;&lt;p&gt;This is all borrowed from &lt;a href=https://sidhion.com/blog/posts/simple-sourcehut-deployments/ &gt;sidhion.com&lt;/a&gt;:&lt;pre&gt;&lt;code&gt;# On the server:

useradd homepage_deployer
# Lock the password because we won&#39;t be using it.
passwd -l homepage_deployer
mkdir -p /home/homepage_deployer/.ssh

# Manually add the contents of homepage_deployer.pub to the authorized_keys file.
vim /home/homepage_deployer/.ssh/authorized_keys

chown -R homepage_deployer:homepage_deployer /home/homepage_deployer
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Next, we make use of a script called &lt;a href=https://download.samba.org/pub/rsync/rrsync.1&gt;rrsync&lt;/a&gt;, which is a restricted &lt;code&gt;rsync&lt;/code&gt; designed for scenarios where a user should only be permitted to rsync in a specific directory on a remote server. The script is inside rsync&#39;s directory and needs to be moved to a location on the &lt;code&gt;PATH&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;cp /usr/share/doc/rsync/scripts/rrsync /usr/local/bin/rrsync
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Reopen &lt;code&gt;homepage_deployer&lt;/code&gt;&#39;s &lt;code&gt;authorized_keys&lt;/code&gt; file and add the following text at the beginning of the line with the public key, separated from the key by a space:&lt;pre&gt;&lt;code&gt;restrict,pty,command=&amp;quot;/usr/local/bin/rrsync -wo /var/www/raphaelkabo.com&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;restrict&lt;/code&gt; prevents all forwarding for this key and restricts tty allocation; &lt;code&gt;pty&lt;/code&gt; re-enables tty allocation (since we need one for the script to run); the final option, &lt;code&gt;command=&lt;/code&gt;, will simply execute the provided command whenever this key is used for authentication, ignoring any supplied commands. All these options are documented in the &lt;a href=https://manpages.debian.org/experimental/openssh-server/authorized_keys.5.en.html&gt;sshd manpages&lt;/a&gt;.&lt;p&gt;The &lt;code&gt;rrsync&lt;/code&gt; flags tell the script that it is only allowed to write to the directory &lt;code&gt;/var/www/raphaelkabo.com&lt;/code&gt;, which is where my homepage lives on the server.&lt;p&gt;The final step is to ensure that &lt;code&gt;rsync&lt;/code&gt; has access to the directory where it needs to work and its parent directory, if need be. I ran into some issues with this: non-fatal errors caused by &lt;code&gt;rsync&lt;/code&gt; failing to save modification times caused the SourceHut build script to register a failure. Simply assigning &lt;code&gt;homepage_deployer&lt;/code&gt; to the group which owned &lt;code&gt;/var/www/raphaelkabo.com&lt;/code&gt; was not enough; I had to make &lt;code&gt;homepage_deployer&lt;/code&gt; the owner of &lt;code&gt;/var/www/raphaelkabo.com&lt;/code&gt; and all the files inside it:&lt;pre&gt;&lt;code&gt;chown -R homepage_deployer /var/www/raphaelkabo.com
chmod 755 /var/www/raphaelkabo.com
&lt;/code&gt;&lt;/pre&gt;&lt;h2&gt;SourceHut&lt;/h2&gt;&lt;p&gt;Add the contents of the &lt;em&gt;private&lt;/em&gt; key you created, &lt;code&gt;~/.ssh/homepage_deployer&lt;/code&gt;, to SourceHut&#39;s &lt;a href=https://builds.sr.ht/secrets&gt;secrets page&lt;/a&gt;. If this key is leaked, it will only allow the attacker to &lt;code&gt;rsync&lt;/code&gt; write into one directory on the server - hardly the end of the world. Make a note of the UUID which SourceHut generates for the secret.&lt;h2&gt;Build manifest&lt;/h2&gt;&lt;p&gt;SourceHut&#39;s build system is controlled by a &lt;a href=https://man.sr.ht/builds.sr.ht/ &gt;build manifest&lt;/a&gt;, such as a file called &lt;code&gt;.build.yml&lt;/code&gt; located in the root directory of the repository. This is a pleasantly simple way to orchestrate a build task and I am a big fan. When the &lt;code&gt;.build.yml&lt;/code&gt; file is commited to the repository, SourceHut immediately starts its first build, and will then trigger a build using the latest contents of &lt;code&gt;.build.yml&lt;/code&gt; on every commit.&lt;p&gt;As easy as the system might be, I did have to run my builds about twelve times before I got this right, just to save you the trouble. Here is my final build manifest to build and deploy an Astro site using &lt;code&gt;pnpm&lt;/code&gt;:&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;&lt;span class=&quot;token key atrule&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; alpine/edge
&lt;span class=&quot;token key atrule&quot;&gt;packages&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; rsync
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; nodejs
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; npm
&lt;span class=&quot;token key atrule&quot;&gt;sources&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; https&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;//git.sr.ht/~lown/homepage
&lt;span class=&quot;token key atrule&quot;&gt;secrets&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; a5e2fe34&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;6fce&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;48d3&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;be7b&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;484400a4ab04
&lt;span class=&quot;token key atrule&quot;&gt;tasks&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;install-build-deps&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      wget -qO pnpm &quot;https://github.com/pnpm/pnpm/releases/latest/download/pnpm-linuxstatic-x64&quot;
      chmod +x pnpm
      sudo mv pnpm /usr/bin&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      cd homepage
      pnpm install
      pnpm astro build&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;token key atrule&quot;&gt;deploy&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;|&lt;/span&gt;&lt;span class=&quot;token scalar string&quot;&gt;
      cd homepage
      rsync -rvz -e &quot;ssh -o StrictHostKeyChecking=no -i ~/.ssh/a5e2fe34-6fce-48d3-be7b-484400a4ab04&quot; dist/ homepage_deployer@raphaelkabo.com:/&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This file instructs SourceHut to do the following things:&lt;ol&gt;&lt;li&gt;Spin up an Alpine Linux virtual machine&lt;li&gt;Install &lt;code&gt;rsync&lt;/code&gt;, &lt;code&gt;nodejs&lt;/code&gt;, and &lt;code&gt;npm&lt;/code&gt;&lt;li&gt;Clone the latest from my repository into a folder called &lt;code&gt;homepage&lt;/code&gt; in the working directory&lt;li&gt;Fetch my secret SSH key and set it up&lt;li&gt;Download the &lt;code&gt;pnpm&lt;/code&gt; sources for Alpine Linux and move them into a location on the &lt;code&gt;PATH&lt;/code&gt;&lt;li&gt;Set up and build my website using Astro&lt;li&gt;Deploy it via &lt;code&gt;rsync&lt;/code&gt; to my server.&lt;/ol&gt;&lt;p&gt;As the original blog post notes (with my alterations):&lt;blockquote&gt;&lt;p&gt;Using &lt;code&gt;-o StrictHostKeyChecking=no&lt;/code&gt; is required, otherwise the build job will hang because it doesn’t recognize the server to connect to (and will ask for confirmation before proceeding). [...] Also important is the destination of the rsync command: &lt;code&gt;homepage_deployer@raphaelkabo.com:/&lt;/code&gt;. Notice how the path only specifies /. This is because back in the server, we told rrsync to use /var/www/raphaelkabo.com as the root path of any file synced with rsync.&lt;/blockquote&gt;&lt;p&gt;Once I got my &lt;code&gt;rsync&lt;/code&gt; permissions issues ironed out, this system worked flawlessly for me. I found the SourceHut build system very pleasant and easy to use, and I can now publish to my website with a simple commit, which is lovely. It complements Astro&#39;s design philosophy nicely: &#39;get out of the developer&#39;s way as much as possible to let them do work without fiddling with tooling for weeks&#39;.&lt;hr class=footnotes-sep&gt;&lt;section class=footnotes&gt;&lt;ol class=footnotes-list&gt;&lt;li id=fn1 class=footnote-item&gt;&lt;p&gt;I still think you should &lt;a href=https://raphaelkabo.com/blog/write-a-static-site-generator&gt;write a static site generator&lt;/a&gt;, though. &lt;a href=https://raphaelkabo.com/blog/astro-sourcehut-deployment/#fnref1 class=footnote-backref&gt;↩︎&lt;/a&gt;&lt;/ol&gt;&lt;/section&gt;</content>
  </entry>
  <entry>
    <title>Finding out when the bins need to go out using Python, Docker, and Pushover</title>
    <link href="https://raphaelkabo.com/blog/undocumented-bin-collection-api/" />
    <updated>2022-05-04T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/undocumented-bin-collection-api/</id>
    <content type="html">&lt;p&gt;When I lived in Australia, the local council would send us a little calendar a few times a year with all the bin collection dates listed on it. It had a magnet on the back, so you could stick it on the fridge. I think it also included some cheery information about how much the local MP was doing and why we should absolutely vote for them in the next election.&lt;p&gt;Now I live in Cambridge, and the local council here do not send us a little calendar, which is annoying. Instead they have an online tool where you plug in your postcode and it tells you when your collection times will be, which is neat, except I don&#39;t want to be standing in the kitchen every Tuesday Googling &#39;Cambridge bin collection times&#39;, only to find out that the collection was actually the previous day. I want to just &lt;em&gt;know&lt;/em&gt;. So I decided to code myself a solution.&lt;p&gt;I was very inspired by Julia Evans&#39;s posts on &lt;a href=https://jvns.ca/blog/2022/03/08/tiny-programs/ &gt;tiny personal programs&lt;/a&gt; and &lt;a href=https://jvns.ca/blog/2022/03/10/how-to-use-undocumented-web-apis/ &gt;how to use undocumented web APIs&lt;/a&gt;, and I felt like I had all the tools I needed to make this happen:&lt;ul&gt;&lt;li&gt;An undocumented web API&lt;li&gt;Python&lt;li&gt;Docker, which is a utility which lets me run individual scripts as standalone containers, separate from the rest of my operating system. I&#39;m not great at Docker, but there are lots of good tutorials for it. You don&#39;t need to use it - there&#39;s wasn&#39;t really any need to do so here except to avoid cluttering my Raspberry Pi with lots of little scripts. I won&#39;t remember how I started them and where to go to fix them if they break. Don&#39;t let this happen to you!&lt;li&gt;A little server (a Raspberry Pi) to run a Docker container&lt;li&gt;The delightfully simple app &lt;a href=https://pushover.net/ &gt;Pushover&lt;/a&gt;, which has a web API where I can send it a message and it sends that message to my phone as a push notification. Pushover costs $4.99, which was totally worth it to me, but you could adapt code of this sort to email you instead!&lt;/ul&gt;&lt;p&gt;The GitHub repository for this app, which I called BinBoop (because it boops you when there&#39;s bins), is &lt;a href=https://github.com/lowercasename/binboop&gt;here&lt;/a&gt;.&lt;h2&gt;Hacking an undocumented web API&lt;/h2&gt;&lt;p&gt;The first thing I needed was data on when the bins would go out. I knew that the website had the data, and I had a hunch that it received that data after the page loaded, because the URL of the page stayed general but the data appeared after I plugged my postcode in. So I felt like it was a safe bet that I could see the service that the site itself was using, and use it myself. And I was right!&lt;p&gt;In Firefox developer tools, I opened up the &#39;Network&#39; tab to spy on requests as I performed the postcode-inputting ritual. I filtered it to only show XHR requests, which are the asynchronous requests for data a page makes after it&#39;s finished loading itself. And lo and behold, I saw two GET requests. The first was to list all the street addresses for the postcode I entered, along with their location IDs in the API that the council were using. The second was to get the bin times for one location ID.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/binboop-1.png alt=&quot;A screenshot of Firefox developer tools showing two GET
requests&quot;&gt;&lt;p&gt;Looking in the &#39;Response&#39; tab for that request, I could see that it had all the data I needed: an array of collection times, and some other information. I now knew the URL I needed to use to get this data, and very luckily for me, it didn&#39;t require any kind of authentication, like a cookie. All it needed was a location ID.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/binboop-2.png alt=&quot;A screenshot of Firefox developer tools showing the JSON response for a GET request&quot;&gt;&lt;h2&gt;Writing a Python program&lt;/h2&gt;&lt;p&gt;Time to break out Python! I find Python a good language for this sort of thing because it gets out of the way to let me do things quickly, and it has a great web API handling library. You could write a similar app in any other language!&lt;p&gt;It helps me define the behaviour of a program before I write it. Here&#39;s what I came up with:&lt;ul&gt;&lt;li&gt;This program will check the bin collections API every evening&lt;li&gt;If there&#39;s a bin collection scheduled tomorrow, it&#39;ll send me a phone notification&lt;/ul&gt;&lt;p&gt;It&#39;s not the most involved program! But I already knew these things: I needed a way for my Python program to do something &lt;strong&gt;every evening&lt;/strong&gt;, to &lt;strong&gt;check an API&lt;/strong&gt;, and to &lt;strong&gt;send a phone notification&lt;/strong&gt;.&lt;p&gt;I started by finding out how I could make Python do something on a schedule. The library I found is called... &lt;a href=https://schedule.readthedocs.io/en/stable/ &gt;schedule&lt;/a&gt;! It describes itself as &#39;Python job scheduling for humans&#39;, which is good, because that&#39;s what I am.&lt;p&gt;I already had Python installed, so I just needed to add &lt;code&gt;schedule&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;pip3 install schedule
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I made a new directory and started editing a file inside it:&lt;pre&gt;&lt;code&gt;mkdir binboop
cd binboop
vim binboop.py
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here&#39;s what my file looked like after I set up &lt;code&gt;schedule&lt;/code&gt;. I also added some simple functions to print out nice logging messages.&lt;pre class=language-python&gt;&lt;code class=language-python&gt;&lt;span class=&quot;token comment&quot;&gt;# binboop.py&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; time
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; datetime
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; schedule

alert_time &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;21:00&quot;&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# Helper functions for logging&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;get_current_time&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;return&lt;/span&gt; datetime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;datetime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;now&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;strftime&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;%Y-%m-%d %H:%M:%S&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;print&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;[&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;get_current_time&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;] &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;check_bin_collections&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    log&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Checking bin collection times&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

schedule&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;every&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;day&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;at&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;alert_time&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;do&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;check_bin_collections&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;while&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    schedule&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;run_pending&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    time&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sleep&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This program uses &lt;a href=https://realpython.com/python-f-strings/ &gt;Python f-strings&lt;/a&gt;, which are one of my favourite recent Python features.&lt;p&gt;Next, it was time to actually check my API. But to do that, I needed to store a secret - the location ID of my house. I wanted to upload this program to GitHub, and I didn&#39;t want secret codes and tokens to become public! So I borrowed a technique that&#39;s used a lot in Node.js programming - an .env file. This is a file that keeps environment variables, and doesn&#39;t get uploaded to GitHub.&lt;pre&gt;&lt;code&gt;# .env

LOCATION_ID=1234567890
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To read this file, I needed a new library: &lt;a href=https://raphaelkabo.com/blog/undocumented-bin-collection-api/python-dotenv&gt;python-dotenv&lt;/a&gt;.&lt;pre&gt;&lt;code&gt;pip3 install python-dotenv
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Back in the Python file, I added the import and some more constants:&lt;pre class=language-python&gt;&lt;code class=language-python&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; dotenv &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; load_dotenv
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
location_id &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; os&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getenv&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LOCATION_ID&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
number_of_collections &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
bin_collections_url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&#39;https://api.example.com/&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;location_id&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;/?numberOfCollections=&lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;number_of_collections&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;&lt;/span&gt;&lt;/span&gt;

round_types &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&#39;ORGANIC&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;garden waste&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&#39;RECYCLE&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;recycling&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&#39;DOMESTIC&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;rubbish&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;round_types&lt;/code&gt; constant is a dictionary: a map of keys to values. Here, it will let me convert the API&#39;s descriptions of bin collection types, on the left, into something a bit friendlier, on the right.&lt;p&gt;Now I was ready to try and access the API. But wait! I needed two more libraries to do that - &lt;a href=https://pypi.org/project/requests/ &gt;requests&lt;/a&gt; to handle web API requests and responses, and &lt;a href=https://dateutil.readthedocs.io/en/stable/ &gt;python-dateutil&lt;/a&gt; to handle dates a bit better than the standard Python date library! Back to pip:&lt;pre&gt;&lt;code&gt;pip3 install requests python-dateutil
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally, I added the call to &lt;code&gt;requests&lt;/code&gt; to my function:&lt;pre class=language-python&gt;&lt;code class=language-python&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; requests
&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; dateutil &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; parser
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;check_bin_collections&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    log&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Checking bin collection times&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Fetch the API data&lt;/span&gt;
        response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; requests&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;get&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;url&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;bin_collections_url&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        data &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; response&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;json&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# Parse the data from the next collection into friendly formats&lt;/span&gt;
        next_collection &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; data&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;collections&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        log&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;next_collection&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        collection_date &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; parser&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;parse&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;next_collection&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;date&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;date&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        collection_types &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;round_types&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;x&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; x &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; next_collection&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;roundTypes&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
        collection_types_string &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39; and &#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;join&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;collection_types&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;except&lt;/span&gt; Exception &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        log&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;Error getting bin collection times: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I like wrapping my code in &lt;code&gt;try/except&lt;/code&gt; blocks, because it means my program won&#39;t crash out if something goes wrong. In this case, lots of things can go wrong. The API URL might change, or the data format might change, or I might be disconnected from the internet. The &lt;code&gt;except&lt;/code&gt; block will just output a log message in all those cases, and some information about the error so I can work out what went wrong later.&lt;p&gt;Here, I use &lt;code&gt;requests.get&lt;/code&gt; to get the response of the API URL. I parse it as JSON, and then pull the date and collection type out of the first collection in the list (the one with the index 0).&lt;p&gt;To get the collection time, I used &lt;code&gt;dateutil.parser.parse()&lt;/code&gt;, which will try and turn a string into a Python date object. In this case, the date was well formatted, so the parser had no trouble. I only needed the date section of the date, not the time, so I added &lt;code&gt;.date()&lt;/code&gt; at the end.&lt;p&gt;To get the collection types, I use a neat Python feature called &lt;a href=https://realpython.com/list-comprehension-python/#using-list-comprehensions&gt;list comprehension&lt;/a&gt;, which is a great way to turn one list into another list. Here, for every value (&lt;code&gt;x&lt;/code&gt;) in the &lt;code&gt;roundTypes&lt;/code&gt; list inside &lt;code&gt;next_collection&lt;/code&gt;, I return the matching pretty name from the &lt;code&gt;round_types&lt;/code&gt; map I created earlier. So a list that looks like this:&lt;pre&gt;&lt;code&gt;[&#39;ORGANIC&#39;, &#39;DOMESTIC&#39;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Will turn into this:&lt;pre&gt;&lt;code&gt;[&#39;garden waste&#39;, &#39;rubbish&#39;]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The next line turns that new list into this:&lt;pre&gt;&lt;code&gt;&#39;garden waste and rubbish&#39;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Much nicer!&lt;p&gt;Now that my program knew when the next collection would be and what would be in it, I needed to find out if I should be alerted or not. After the existing code, inside the &lt;code&gt;try&lt;/code&gt; block, I added the following:&lt;pre class=language-python&gt;&lt;code class=language-python&gt;&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;check_bin_collections&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Check if the collection date is tomorrow (we want to be alerted&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# the night before)&lt;/span&gt;
        tomorrow &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; datetime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;date&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;today&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt; datetime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;timedelta&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;days&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; collection_date &lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt; tomorrow&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            payload &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;token string&quot;&gt;&quot;message&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;Don&#39;t forget to put the &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;collection_types_string&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;bins&#39;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;collection_types&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;bin&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt; out tonight.&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
            send_alert&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;First, I calculate what tomorrow is: it&#39;s &lt;code&gt;datetime.date.today()&lt;/code&gt; plus a &lt;code&gt;timedelta&lt;/code&gt; of &lt;code&gt;days=1&lt;/code&gt;. Thanks, Python, that was very logical! And if the collection date is indeed tomorrow, it&#39;s time to send myself a notification. I build a new message using an f-string, and call a new function, &lt;code&gt;send_alert()&lt;/code&gt;.&lt;p&gt;To send a Pushover alert, I created a new application in Pushover and made a note of my API key and user key. These get added to the .env file:&lt;pre&gt;&lt;code&gt;# .env

...
PUSHOVER_TOKEN=1234567890
PUSHOVER_USER=0987654321
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Back in the main Python file, here&#39;s the new function, and some more setup constants. I looked at the &lt;a href=https://pushover.net/api&gt;Pushover API documentation&lt;/a&gt; to find out what I needed to do here.&lt;pre class=language-python&gt;&lt;code class=language-python&gt;pushover_url &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;https://api.pushover.net/1/messages.json&#39;&lt;/span&gt;
pushover_params &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;token&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; os&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getenv&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;PUSHOVER_TOKEN&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;token string&quot;&gt;&quot;user&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; os&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;getenv&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;PUSHOVER_USER&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Send an alert via Pushover&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;send_alert&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    log&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Sending alert&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;try&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        params &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;**&lt;/span&gt;pushover_params&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;**&lt;/span&gt;payload&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;
        response &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; requests&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;post&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pushover_url&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; params&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;params&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;except&lt;/span&gt; Exception &lt;span class=&quot;token keyword&quot;&gt;as&lt;/span&gt; e&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        log&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string-interpolation&quot;&gt;&lt;span class=&quot;token string&quot;&gt;f&quot;Error sending alert: &lt;/span&gt;&lt;span class=&quot;token interpolation&quot;&gt;&lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;e&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;&lt;/span&gt;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;params =&lt;/code&gt; line combines the default Pushover params, which authenticate my app, and the params sent as the payload to the function, which is currently just the message - but I could add &lt;a href=https://pushover.net/api&gt;lots of other things&lt;/a&gt; to those params if I wanted to! Finally, I use &lt;code&gt;requests.post&lt;/code&gt; to send Pushover my message. And with that, my whole program is done!&lt;p&gt;I tested the program a bunch, to make sure that the scheduler, the API fetching and notification sending all worked as expected. A better way to do this would be to write unit tests for the program, which I might do later!&lt;h2&gt;Creating a Docker container&lt;/h2&gt;&lt;p&gt;If I wanted to, I could just use a Linux service to run this Python program whenever my server starts, but I was developing the program on my laptop, which has Python, while the Raspberry Pi does not, so running the program there would also require setting up Python. It was instead time to set up Docker (especially because I already have lots of things running using Docker on that Raspberry Pi).&lt;p&gt;The first thing I needed to do was find a way to tell Docker which Python libraries to install using &lt;code&gt;pip&lt;/code&gt;. The easiest way I know to do this is to make a copy of all the packages I&#39;ve currently got installed into a file called &lt;code&gt;requirements.txt&lt;/code&gt;, and then remove the ones I didn&#39;t need for this program.&lt;pre&gt;&lt;code&gt;pip3 freeze &amp;gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This creates a line for each package pip has installed. I left only the ones I needed:&lt;pre&gt;&lt;code&gt;# requirements.txt

python-dateutil==2.8.2
python-dotenv==0.20.0
requests==2.27.1
schedule==1.1.0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then, I created a Dockerfile - a file that tells Docker how to create an image. To do this, I mostly followed &lt;a href=https://docs.docker.com/language/python/build-images/ &gt;these excellent instructions&lt;/a&gt;, which luckily came up when I searched for &#39;Python Docker&#39;.&lt;pre class=language-docker&gt;&lt;code class=language-docker&gt;&lt;span class=&quot;token comment&quot;&gt;# Dockerfile&lt;/span&gt;

&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;FROM&lt;/span&gt; python:alpine&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;WORKDIR&lt;/span&gt; /app&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; requirements.txt requirements.txt&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;RUN&lt;/span&gt; pip3 install -r requirements.txt&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;COPY&lt;/span&gt; . .&lt;/span&gt;
&lt;span class=&quot;token instruction&quot;&gt;&lt;span class=&quot;token keyword&quot;&gt;CMD&lt;/span&gt; [ &lt;span class=&quot;token string&quot;&gt;&quot;python3&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;-u&quot;&lt;/span&gt;, &lt;span class=&quot;token string&quot;&gt;&quot;binboop.py&quot;&lt;/span&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first line tells Docker that I want to base my image on an existing image: the Docker Python image, specifically its Alpine Linux version, because Alpine Linux is a very lightweight and resource-efficient distro. I got that &#39;tag&#39; from &lt;a href=https://hub.docker.com/_/python&gt;this page&lt;/a&gt;. Next, I switch to a new directory inside the image called &lt;code&gt;/app&lt;/code&gt;. I copy the &lt;code&gt;requirements.txt&lt;/code&gt; file I just created from my host machine into &lt;code&gt;/app&lt;/code&gt;, and run &lt;code&gt;pip3 install&lt;/code&gt; with the &lt;code&gt;-r&lt;/code&gt; flag to install everything it finds in the requirements file. Then, I copy the rest of the host directory over, and finally I run my program with the command &lt;code&gt;python3 -u binboop.py&lt;/code&gt;. The &lt;code&gt;-u&lt;/code&gt; flag tells Python to send its output unbuffered, &lt;a href=https://stackoverflow.com/questions/29663459/python-app-does-not-print-anything-when-running-detached-in-docker&gt;which I discovered I needed&lt;/a&gt;, otherwise my lovely logs wouldn&#39;t come up when I ran &lt;code&gt;docker logs&lt;/code&gt;.&lt;p&gt;Now that my Dockerfile is done, I could test my brand new image. First I build it:&lt;pre&gt;&lt;code&gt;$ docker build --tag binboop .
Sending build context to Docker daemon  66.05kB
Step 1/6 : FROM python:alpine
alpine: Pulling from library/python
df9b9388f04a: Pull complete
a1ef3e6b7a02: Pull complete
7a687728470e: Pull complete
4ecf30de1710: Pull complete
a1f99e431609: Pull complete
Digest: sha256:21c71a37aaa8b4bec1d07f373c7f32322d935accfeeb0b0860be1df6b194ecb5
Status: Downloaded newer image for python:alpine
 ---&amp;gt; 2c167788a673
Step 2/6 : WORKDIR /app
 ---&amp;gt; Running in 1d49995bd5a2
Removing intermediate container 1d49995bd5a2
 ---&amp;gt; 072a20c39822
Step 3/6 : COPY requirements.txt requirements.txt
 ---&amp;gt; 27b9839e5a13
Step 4/6 : RUN pip3 install -r requirements.txt
 ---&amp;gt; Running in ad82f1e9fcf1
[...]
Removing intermediate container ad82f1e9fcf1
 ---&amp;gt; 5980a8c5c83a
Step 5/6 : COPY . .
 ---&amp;gt; d14c4aa07381
Step 6/6 : CMD [ &amp;quot;python3&amp;quot;, &amp;quot;-u&amp;quot;, &amp;quot;binboop.py&amp;quot;]
 ---&amp;gt; Running in 2e1fc3abe01f
Removing intermediate container 2e1fc3abe01f
 ---&amp;gt; c5caa674313a
Successfully built c5caa674313a
Successfully tagged binboop:latest
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then I check that it&#39;s been successfully created:&lt;pre&gt;&lt;code&gt;$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED              SIZE
binboop      latest    c5caa674313a   About a minute ago   59.1MB
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then I run it as a container:&lt;pre&gt;&lt;code&gt;$ docker run binboop
[2022-05-04 21:42:14] BinBoop is running!
[2022-05-04 21:42:14] Sending alert
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This command then hangs, because BinBoop runs an endless &lt;code&gt;while True&lt;/code&gt; loop, but we have verified that it&#39;s working perfectly, so we can stop it and carry on with the final step of this process!&lt;h2&gt;Creating a Docker Compose file&lt;/h2&gt;&lt;p&gt;To traditionally run a Docker image as a container, you&#39;d need to write a terminal command, passing in various necessary bits of information. One of those is the environment variables that BinBoop needs to connect to its APIs. Notice how BinBoop ran above, but the environment variables weren&#39;t set, so when it came time to check the collection times, BinBoop would fail! In the future, I&#39;ll add some code to check that these environment variables are set at startup, which would be a better way to handle this issue.&lt;p&gt;I don&#39;t want to pass environment variables in a command line, so I&#39;m going to use Docker Compose - an extra Docker utility which lets me write out my containers as a YAML file, and then run them with a single simple command. Another benefit of this is that I can define a whole bunch of Docker services in one go, and run them all together, which is precisely what I do on my Raspberry Pi.Let&#39;s create &lt;code&gt;docker-compose.yml&lt;/code&gt;. I created it in the directory above the &lt;code&gt;binboop&lt;/code&gt; directory.&lt;pre class=language-yaml&gt;&lt;code class=language-yaml&gt;&lt;span class=&quot;token comment&quot;&gt;# docker-compose.yml&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;version&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;3.3&quot;&lt;/span&gt;

&lt;span class=&quot;token key atrule&quot;&gt;services&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;token key atrule&quot;&gt;binboop&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token key atrule&quot;&gt;build&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token key atrule&quot;&gt;context&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; ./binboop
      &lt;span class=&quot;token key atrule&quot;&gt;dockerfile&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; Dockerfile
    &lt;span class=&quot;token key atrule&quot;&gt;container_name&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; binboop
    &lt;span class=&quot;token key atrule&quot;&gt;restart&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; unless&lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt;stopped
    &lt;span class=&quot;token key atrule&quot;&gt;environment&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; LOCATION_ID=1234567890
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; PUSHOVER_TOKEN=1234567890
      &lt;span class=&quot;token punctuation&quot;&gt;-&lt;/span&gt; PUSHOVER_USER=0987654321&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;binboop&lt;/code&gt; service description is a bit like the Dockerfile description. There&#39;s a &lt;code&gt;context&lt;/code&gt;, which tells Compose to go into the directory &lt;code&gt;binboop&lt;/code&gt; inside the one the file is in, and a &lt;code&gt;dockerfile&lt;/code&gt; declaration, which gives it the name of the Dockerfile. Most importantly, the &lt;code&gt;environment&lt;/code&gt; array tells Compose the names and values of the environment variables that BinBoop needs to run.&lt;p&gt;With this file in place, in the same directory, I run &lt;code&gt;docker-compose&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;$ docker-compose up -d
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;-d&lt;/code&gt; flag tells Docker to &#39;daemonise&#39; my services, or run them in the background and restart them when the system restarts. With this command, Compose will do essentially all the same things as the previous individual &lt;code&gt;docker&lt;/code&gt; commands, but it will also pass my environment variables across so that BinBoop works.&lt;p&gt;Once I had my container running, I could make sure that BinBoop was working correctly by checking its logs:&lt;pre&gt;&lt;code&gt;$ docker logs binboop
[2022-05-04 19:52:41] BinBoop is running!
[2022-05-04 19:52:41] Sending alert
[2022-05-04 21:00:00] Checking bin collection times
[2022-05-04 21:00:02] {&#39;date&#39;: &#39;2022-05-04T00:00:00Z&#39;, &#39;roundTypes&#39;: [&#39;ORGANIC&#39;, &#39;RECYCLE&#39;], &#39;slippedCollection&#39;: True}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;And now I know it&#39;s working!&lt;p&gt;This was a fun way to spend an evening, messing around with APIs, Python, and Docker. I learned more about Docker, which is useful, and I enjoyed breaking down my working in this blog post. For instance, it showed me that I hadn&#39;t set up any error handling for situations where the environment variables hadn&#39;t been defined!</content>
  </entry>
  <entry>
    <title>Suspending on idle on Void Linux using Sway WM</title>
    <link href="https://raphaelkabo.com/blog/void-linux-swayidle/" />
    <updated>2022-03-16T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/void-linux-swayidle/</id>
    <content type="html">&lt;p&gt;I&#39;m setting up another PC with the delightful &lt;a href=https://voidlinux.org/ &gt;Void Linux&lt;/a&gt; (my third so far!), and I decided I wanted to set this one up with &lt;a href=https://voidlinux.org/ &gt;Sway WM&lt;/a&gt;, a Wayland-based window manager. This is my first time using Wayland (I have, if anything, been over-cautious), and it&#39;s been a real breeze so far. Despite what look like early teething problems with Sway on Void, as far as I can gather from forums, all I did to get sway up and running was follow &lt;a href=https://gist.github.com/adnan360/6cba05a3881870bf4a9e3ab2cea7709e&gt;this guide&lt;/a&gt;, the short version of which is:&lt;p&gt;sudo xbps-install sway swayidle elogind dmenu foot sudo ln -s /etc/sv/dbus /var/services mkdir -p ~/.config/sway sudo cp /etc/sway/config ~/.config/sway/config&lt;p&gt;I then restarted, changed Sway&#39;s terminal emulator of choice to &lt;code&gt;foot&lt;/code&gt; in the config file, and ran &lt;code&gt;sway&lt;/code&gt; to start it from the console.&lt;p&gt;I want my computer&#39;s display to shut down after 10 minutes of inactivity, and for the computer to go to sleep a minute thereafter. For this, I&#39;m using the excellent Sway companion tool &lt;a href=https://github.com/swaywm/swayidle&gt;swayidle&lt;/a&gt;.&lt;p&gt;In my &lt;code&gt;~/.config/sway/config&lt;/code&gt; file, I&#39;ve edited the existing configuration for swayidle to look like the following:&lt;p&gt;exec swayidle -w&lt;br&gt;timeout 600 &#39;swaymsg &amp;quot;output * dpms off&amp;quot;&#39; resume &#39;swaymsg &amp;quot;output * dpms on&amp;quot;&#39;&lt;br&gt;timeout 660 &#39;exec zzz&#39;&lt;p&gt;The &lt;code&gt;resume&lt;/code&gt; hook after &lt;code&gt;&#39;exec zzz&#39;&lt;/code&gt; in &lt;code&gt;swayidle&lt;/code&gt;&#39;s configuration doesn&#39;t seem to run on a non-systemd system, so I added a file called &lt;code&gt;01-turn-screens-on.sh&lt;/code&gt; in &lt;code&gt;/etc/zzz.d/resume&lt;/code&gt;:&lt;p&gt;#!/bin/sh swaymsg &amp;quot;output * dpms on&amp;quot;&lt;p&gt;&lt;code&gt;zzz&lt;/code&gt; runs this script after resuming from sleep, which is exactly what we want.&lt;p&gt;Finally, I allowed my own non-root user to modify &lt;code&gt;/sys/power/state&lt;/code&gt;, which is what &lt;code&gt;zzz&lt;/code&gt; does under the hood. To do so, I added the following line in &lt;code&gt;/etc/rc.local&lt;/code&gt;, which runs on system startup:&lt;p&gt;chown USERNAME: /sys/power/state&lt;p&gt;Replace &lt;code&gt;USERNAME&lt;/code&gt; with your own username, of course. Alternatively, you could create a group called, for instance, &lt;code&gt;power&lt;/code&gt;, add your user to that group, and then assign that group to &lt;code&gt;/sys/power/state&lt;/code&gt; on startup.&lt;p&gt;This was relatively easy once I&#39;d figured out how all the bits of it interacted, and I&#39;m still not sure why the &lt;code&gt;resume&lt;/code&gt; hook seems to fire a fter the first line of &lt;code&gt;swayidle&lt;/code&gt; but not after the second. Nonetheless, this is a lot of work for what seems like such a simple thing, and that&#39;s why we love Linux!</content>
  </entry>
  <entry>
    <title>Wiring up Wireguard, Caddy, and Docker on a home server</title>
    <link href="https://raphaelkabo.com/blog/wireguard-docker-caddy/" />
    <updated>2022-02-09T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/wireguard-docker-caddy/</id>
    <content type="html">&lt;p&gt;I wanted to write a little guide, mostly for my own future use, to describe how I set up Wireguard on my home sever. Hopefully this will be of use to someone else!&lt;h2&gt;Why Wireguard?&lt;/h2&gt;&lt;p&gt;I run a home server (an old, reliable, much-battered HP Proliant called Ottoline). It runs a few public-facing websites in Docker containers, like a library of plants in our house, a &lt;a href=https://gitea.io/ &gt;gitea&lt;/a&gt;, and a Nextcloud sever. It also runs a bunch of services which I&#39;d like to keep private, like a &lt;a href=https://nicolargo.github.io/glances/ &gt;Glances&lt;/a&gt; system monitor and Portainer.&lt;p&gt;I have a domain name (let&#39;s call it &lt;strong&gt;&lt;a href=http://ottoline.com&gt;ottoline.com&lt;/a&gt;&lt;/strong&gt;) which I&#39;ve linked up to my home sever via Namecheap&#39;s excellent dynamic DNS API. This means that I can access services on my home server via subdomains: &lt;a href=https://plants.ottoline.com&gt;https://plants.ottoline.com&lt;/a&gt;, &lt;a href=https://git.ottoline.com&gt;https://git.ottoline.com&lt;/a&gt;, &lt;a href=https://glances.ottoline.com&gt;https://glances.ottoline.com&lt;/a&gt;. The subdomains are wired up via a &lt;a href=https://caddyserver.com/ &gt;Caddy&lt;/a&gt; Docker container - requests come in, are received by Caddy, and are proxied to the internal networks of my Docker containers.&lt;p&gt;I want to keep these two nice features the same:&lt;ul&gt;&lt;li&gt;Accessing services via subdomain names&lt;li&gt;Accessing services from outside my home network&lt;/ul&gt;&lt;p&gt;But:&lt;ul&gt;&lt;li&gt;I want to make some services to stay private and some to stay public.&lt;/ul&gt;&lt;p&gt;Wireguard felt like the perfect solution - running Wireguard in a Docker container would allow me to set up a backdoor to my server, accessible only when I&#39;m connected to the Wireguard VPN. Public services would always work, but private services would return a 403 error if the request to them came from outside my home network (and, by extension, my VPN).&lt;p&gt;Sounds simple! But how do I do it?&lt;h2&gt;Setting up Caddy&lt;/h2&gt;&lt;p&gt;I started by reworking my Caddyfile, Caddy&#39;s main configuration file.&lt;p&gt;For publicly accessible sites, the configuration remained the same:&lt;pre&gt;&lt;code&gt;plants.ottoline.com {
  file_server
  root * /usr/share/caddy
  encode gzip
}

nextcloud.ottoline.com {
  reverse_proxy nextcloud:80
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For private sites, I used a &lt;a href=https://caddyserver.com/docs/caddyfile/matchers#named-matchers&gt;named matcher&lt;/a&gt; to match only internal IPs (including Docker&#39;s &lt;code&gt;172.x.x.x&lt;/code&gt; IPs):&lt;pre&gt;&lt;code&gt;portainer.ottoline.com {
  @internal {
    remote_ip 192.168.0.0/16 172.0.0.0/8
  }
  handle @internal {
    reverse_proxy portainer:9000
    reverse_proxy portainer:8000
  }
  respond 403
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If the IP is in the &lt;code&gt;@internal&lt;/code&gt; range, it&#39;ll be reverse proxied. Otherwise, Caddy responds with a 403. Nice!&lt;p&gt;With this setup in place, I was already half the way there - I could access public sites from the wider Internet, and private sites only from inside my home network. Now for the VPN.&lt;h2&gt;Setting up Wireguard&lt;/h2&gt;&lt;p&gt;I added the following section to my &lt;code&gt;docker-compose.yml&lt;/code&gt; and ran &lt;code&gt;docker-compose up -d&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;wireguard:
  image: linuxserver/wireguard
  container_name: wireguard
  cap_add:
    - NET_ADMIN
    - SYS_MODULE
  environment:
    - PUID=1000
    - GUID=1000
    - TZ=Europe/London
    - SERVERURL=wireguard.ottoline.com
    - PEERS=2
    - PEERDNS=auto
    - INTERNAL_SUBNET=10.10.10.0
  volumes:
    - ./wireguard:/config
    - /lib/modules:/lib/modules
  ports:
    - 51820:51820/udp
  sysctls:
    - net.ipv4.conf.all.src_valid_mark=1
  restart: unless-stopped
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I set up two peers - one for my laptop, one for my phone. When this configuration is run, it will create two peer configuration files, &lt;code&gt;peer1.conf&lt;/code&gt; and &lt;code&gt;peer2.conf&lt;/code&gt;, inside the &lt;code&gt;./wireguard&lt;/code&gt; directory on the host system. I copied the first one to &lt;code&gt;/etc/wireguard/wg0.conf&lt;/code&gt; on my laptop, but I changed the following lines:&lt;pre&gt;&lt;code&gt;[Interface]
...
DNS = 192.168.0.4 # My home server&#39;s IP address

[Peer]
...
AllowedIPs = 192.168.0.0/16
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The &lt;code&gt;AllowedIPs&lt;/code&gt; directive means that when Wireguard is enabled on my laptop, it will only send requests to the local server (which includes the subdomain names I set up) through the VPN, and pass everything else outside the VPN. The &lt;code&gt;DNS&lt;/code&gt; directive means all DNS queries will go through the home server, which is essential because Caddy needs to know which requests are internal and which are external.&lt;p&gt;For my phone, I ran the following command on the server to display a QR code and scanned it with the excellently no-nonsense Wireguard app:&lt;pre&gt;&lt;code&gt;docker exec -it wireguard /app/show-peer 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I then changed the same fields on the phone as I did on my laptop.&lt;h2&gt;Setting up DNS&lt;/h2&gt;&lt;p&gt;The final step in the process is setting up DNS on the home server to forward DNS queries being sent via Wireguard to the right Caddy routes. I used a lightweight service called dnsmasq for this. There seem to be a lot of dnsmasq settings one can set, but these worked for me.&lt;p&gt;First, I disabled the default systemd-resolved service:&lt;pre&gt;&lt;code&gt;systemctl mask systemd-resolved
systemctl stop systemd-resolved
systemctl disable systemd-resolved
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then I installed dnsmasq and edited &lt;code&gt;/etc/dnsmasq.conf&lt;/code&gt;:&lt;pre&gt;&lt;code&gt;domain-needed
bogus-priv
cache-size=1000
server=8.8.8.8
server=4.4.4.4
address=/.ottoline.com/192.168.0.4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This sets two default DNS servers (8.8.8.8 and 4.4.4.4) for unresolved queries, and catches all queries to &lt;a href=http://ottoline.com&gt;ottoline.com&lt;/a&gt; and its subdomains to the home server&#39;s local IP.&lt;p&gt;Then, turn on dnsmasq:&lt;pre&gt;&lt;code&gt;systemctl enable dnsmasq
systemctl start dnsmasq
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Finally, I needed to edit &lt;code&gt;/etc/resolv.conf&lt;/code&gt; (I&#39;m not sure why, but considering the rest of this somehow worked, I&#39;m not counting my blessings). I think that without it, DNS queries from inside the home server don&#39;t resolve). Because on Ubuntu, &lt;code&gt;resolv.conf&lt;/code&gt; is controlled by NetworkManager, it&#39;s just a symlink and is reset on reboots:&lt;pre&gt;&lt;code&gt;ls -la /etc/resolv.conf
lrwxrwxrwx 1 root root 32 Jan 11 10:47 /etc/resolv.conf -&amp;gt; /run/systemd/resolve/resolv.conf
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;I removed the symlink by renaming &lt;code&gt;/etc/resolv.conf&lt;/code&gt; to &lt;code&gt;/etc/resolv.conf.bak&lt;/code&gt; and creating a new, regular &lt;code&gt;/etc/resolv.conf&lt;/code&gt;, containing the following:&lt;pre&gt;&lt;code&gt;nameserver 8.8.8.8
nameserver 8.8.4.4
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With this final piece in place, I am able to access some of my containers only via the VPN, and others anytime.&lt;h2&gt;References&lt;/h2&gt;&lt;ul&gt;&lt;li&gt;&lt;a href=https://www.the-digital-life.com/wireguard-docker/ &gt;Create your own VPN server with WireGuard in Docker&lt;/a&gt;&lt;li&gt;&lt;a href=https://www.cloudsavvyit.com/14816/how-to-run-your-own-dns-server-on-your-local-network/ &gt;How to Run Your Own DNS Server on Your Local Network&lt;/a&gt;&lt;/ul&gt;</content>
  </entry>
  <entry>
    <title>uvcdynctrl-udev.log - a 20GB logfile</title>
    <link href="https://raphaelkabo.com/blog/20gb-logfile/" />
    <updated>2021-02-07T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/20gb-logfile/</id>
    <content type="html">&lt;p&gt;I logged on to my computer on Monday this week and ran &lt;code&gt;startx&lt;/code&gt; as usual, but Xserver crashed, complaining that it couldn&#39;t write to &lt;code&gt;/tmp&lt;/code&gt;. At first I thought there was an odd permissions issue or some problem with &lt;code&gt;.Xinitrc&lt;/code&gt;, but it turns out after running some exploratory &lt;code&gt;du -h&lt;/code&gt; commands that my root partition was completely full.&lt;p&gt;The culprit turned out to be the file &lt;code&gt;/var/log/uvcdynctrl-udev.log&lt;/code&gt;, which had ballooned to 20GB in size. The culprit is libwebcam. This is a well-attested problem which apparently still doesn&#39;t have a solution:&lt;ul&gt;&lt;li&gt;&lt;a href=https://askubuntu.com/questions/177312/filesystem-filling-up-due-to-large-uvcydnctrl-udev-log-file&gt;StackOverflow (174GB!)&lt;/a&gt;&lt;li&gt;&lt;a href=https://bugs.launchpad.net/ubuntu/+source/libwebcam/+bug/811604&gt;Ubuntu bug report (800GB!!)&lt;/a&gt;&lt;/ul&gt;&lt;p&gt;I didn&#39;t have time to work out how to solve it properly, so I set up a simple-as-it-gets crontab:&lt;pre&gt;&lt;code&gt;0 12 * * 1 rm /var/log/uvcdynctrl-udev.log
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;No dramas so far!</content>
  </entry>
  <entry>
    <title>Using a HD44780 LCD display with MicroPython on the Raspberry Pi Pico</title>
    <link href="https://raphaelkabo.com/blog/pi-pico-hd44780/" />
    <updated>2021-02-06T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/pi-pico-hd44780/</id>
    <content type="html">&lt;p&gt;I got my &lt;a href=https://www.raspberrypi.org/documentation/pico/getting-started/ &gt;Raspberry Pi Pico&lt;/a&gt; today (it&#39;s now part of our mandatory lockdown ration, right?), and immediately tried to do something vaguely undocumnented with it - namely, run one of those generic HD44780 LCD displays. Mine was &lt;a href=https://shop.pimoroni.com/products/standard-lcd-16x2-extras-white-on-blue&gt;a lovely Adafruit one via Pimoroni&lt;/a&gt;; thanks for taking all my money Pimoroni! Suckers, what they don&#39;t know is that I&#39;m going to &lt;em&gt;keep spending my money there until I&#39;m bankrupt&lt;/em&gt;.&lt;p&gt;There is what looks like a decent C library for the HD44780 and the Pi Pico, called &lt;a href=https://github.com/zadi15/picoLCD&gt;picoLCD&lt;/a&gt;, but I wanted to try out MicroPython, which was completely new to me. Luckily, &lt;strong&gt;wjdp&lt;/strong&gt; has written a perfectly functional generic MicroPython library for the HD44780 and the PyBoard, called &lt;a href=https://github.com/wjdp/micropython-lcd&gt;micropython-lcd&lt;/a&gt;. It won&#39;t work on the Pi Pico out of the box, but it took me all of fifteen minutes to adapt it to the Pi Pico. It&#39;s pretty straightforward, here&#39;s the complete code.&lt;p&gt;&lt;strong&gt;90% of this is not mine&lt;/strong&gt; - I just adjusted some function calls!&lt;pre class=language-python&gt;&lt;code class=language-python&gt;&lt;span class=&quot;token keyword&quot;&gt;from&lt;/span&gt; machine &lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; Pin&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; Timer
&lt;span class=&quot;token keyword&quot;&gt;import&lt;/span&gt; utime

&lt;span class=&quot;token comment&quot;&gt;# Almost entirely copied from micropython-lcd by wjdp&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# https://github.com/wjdp/micropython-lcd&lt;/span&gt;
&lt;span class=&quot;token keyword&quot;&gt;class&lt;/span&gt; &lt;span class=&quot;token class-name&quot;&gt;LCD&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Pinout, change within or outside class for your use case&lt;/span&gt;
    PINS &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;17&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;18&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;19&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;21&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Pin names, don&#39;t change&lt;/span&gt;
    PIN_NAMES &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;RS&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;E&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;D4&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;D5&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;D6&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;D7&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# Dict of pins&lt;/span&gt;
    pins &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# Pin mode, push-pull control&lt;/span&gt;
    PIN_MODE &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Pin&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;OUT

    &lt;span class=&quot;token comment&quot;&gt;# Define some device constants&lt;/span&gt;
    LCD_WIDTH &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;16&lt;/span&gt;    &lt;span class=&quot;token comment&quot;&gt;# Maximum characters per line&lt;/span&gt;
    &lt;span class=&quot;token comment&quot;&gt;# Designation of T/F for character and command modes&lt;/span&gt;
    LCD_CHR &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;
    LCD_CMD &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;

    LINES &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0x80&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# LCD RAM address for the 1st line&lt;/span&gt;
        &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;0xC0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# LCD RAM address for the 2nd line&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Add more if desired&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;

    &lt;span class=&quot;token comment&quot;&gt;# Timing constants&lt;/span&gt;
    E_PULSE &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;
    E_DELAY &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;init&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Initialise pins&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; pin&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pin_name &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;zip&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;PINS&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;PIN_NAMES&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pins&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_&#39;&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;+&lt;/span&gt;pin_name&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; Pin&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;pin&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;PIN_MODE&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Initialise display&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lcd_byte&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x33&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_CMD&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lcd_byte&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x32&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_CMD&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lcd_byte&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x28&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_CMD&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lcd_byte&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x0C&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_CMD&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lcd_byte&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x06&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_CMD&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lcd_byte&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x01&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_CMD&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;clear&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Clear the display&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lcd_byte&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x01&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_CMD&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;set_line&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; line&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Set the line that we&#39;re going to print to&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lcd_byte&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LINES&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;line&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_CMD&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;set_string&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Pad string out to LCD_WIDTH&lt;/span&gt;
        m_length &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; m_length &lt;span class=&quot;token operator&quot;&gt;&amp;lt;&lt;/span&gt; self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_WIDTH&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            short &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_WIDTH &lt;span class=&quot;token operator&quot;&gt;-&lt;/span&gt; m_length
            blanks&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; i &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;short&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
                blanks&lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39; &#39;&lt;/span&gt;
            message&lt;span class=&quot;token operator&quot;&gt;+=&lt;/span&gt;blanks
        &lt;span class=&quot;token keyword&quot;&gt;for&lt;/span&gt; i &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;token builtin&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_WIDTH&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;lcd_byte&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token builtin&quot;&gt;ord&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;message&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;i&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;LCD_CHR&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;lcd_byte&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; bits&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; mode&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Send byte to data pins&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# bits = data&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# mode = True  for character&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;#        False for command&lt;/span&gt;

        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_RS&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; mode&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;token comment&quot;&gt;# RS&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# High bits&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D4&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D5&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D6&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D7&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; bits&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x10&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x10&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D4&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; bits&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x20&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x20&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D5&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; bits&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x40&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x40&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D6&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; bits&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x80&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x80&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D7&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# Toggle &#39;Enable&#39; pin&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;udelay&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;E_DELAY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_E&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;udelay&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;E_PULSE&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_E&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;udelay&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;E_DELAY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# Low bits&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D4&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D5&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D6&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D7&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; bits&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x01&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x01&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D4&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; bits&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x02&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x02&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D5&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; bits&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x04&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x04&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D6&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; bits&lt;span class=&quot;token operator&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x08&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0x08&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_D7&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

        &lt;span class=&quot;token comment&quot;&gt;# Toggle &#39;Enable&#39; pin&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;udelay&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;E_DELAY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_E&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;udelay&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;E_PULSE&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pin_action&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&#39;LCD_E&#39;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;token boolean&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;udelay&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;E_DELAY&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;udelay&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; us&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Delay by us microseconds, set as function for portability&lt;/span&gt;
        utime&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;sleep_ms&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;us&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

    &lt;span class=&quot;token keyword&quot;&gt;def&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;pin_action&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;self&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; pin&lt;span class=&quot;token punctuation&quot;&gt;,&lt;/span&gt; high&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;token comment&quot;&gt;# Pin high/low functions, set as function for portability&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; high&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pins&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;pin&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt;
            self&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;pins&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;pin&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;value&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

display &lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt; LCD&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
display&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;init&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;

display&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;set_line&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
display&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;set_string&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;Hello world!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
display&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;set_line&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token number&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
display&lt;span class=&quot;token punctuation&quot;&gt;.&lt;/span&gt;set_string&lt;span class=&quot;token punctuation&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;token string&quot;&gt;&quot;I am Pi Pico!&quot;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Here&#39;s a photo of the finished monstrosity (wire as I say, not as I do, etc.):&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/pi-pico-hd44780.jpg alt=&quot;A photograph of the HD44780 LCD wired up to a Raspberry Pi Pico on a small breadboard with a mess of jumper wires&quot;&gt;&lt;p&gt;For reference, my wiring is as follows:&lt;div class=table-container&gt;&lt;table&gt;&lt;tr&gt;&lt;th&gt;HD44780&lt;th&gt;Destination&lt;th&gt;Function&lt;tr&gt;&lt;td&gt;1 (Vss)&lt;td&gt;Ground (0V)&lt;td&gt;Powers the HD44780&#39;s processor&lt;tr&gt;&lt;td&gt;2 (Vdd)&lt;td&gt;5V&lt;td&gt;Powers the HD44780&#39;s processor&lt;tr&gt;&lt;td&gt;3 (V0)&lt;td&gt;Ground via a 1K resistor*&lt;td&gt;LCD contrast control: high for nothing, low for aggressive white squares that hate you&lt;tr&gt;&lt;td&gt;4 (RS)&lt;td&gt;Pi Pico GP16 (pin 21)&lt;td&gt;Register Select. High to send data, low to send instructions (like &#39;clear screen&#39;).&lt;tr&gt;&lt;td&gt;5 (R/W)&lt;td&gt;Ground&lt;td&gt;Read/write toggle. I&#39;ve kept it permanently tied low since we only ever output to the display.&lt;tr&gt;&lt;td&gt;6 (E)&lt;td&gt;Pi Pico GP17 (pin 22)&lt;td&gt;Enable signal. Makes the thing do the stuff.&lt;tr&gt;&lt;td&gt;11, 12, 13, 14 (DB4-DB7)&lt;td&gt;Pi Pico GP18, GP19, GP20, GP21 (pins 24-27)&lt;td&gt;Data pins. We send data through them!&lt;tr&gt;&lt;td&gt;15 (BLA)&lt;td&gt;5V&lt;td&gt;Backlight LED anode.&lt;tr&gt;&lt;td&gt;16 (BLK)&lt;td&gt;Ground (0V)&lt;td&gt;Backlight LED cathode. I stuck a 220 ohm resistor in here to chill it out a bit.&lt;/table&gt;&lt;/div&gt;&lt;p&gt;* My LCD came with a rotary potentiometer to adjust the contrast. The resistor works great too!&lt;p&gt;If I expand on this, it will be to add custom character functionality, which is something the HD44780 supports. I just need to read the datasheet, on which note, &lt;a href=https://www.openhacks.com/uploadsproductos/eone-1602a1.pdf&gt;this looks like the one&lt;/a&gt; for the specific 16x2 1602A model I&#39;m using. But you might as well just read &lt;a href=https://en.wikipedia.org/wiki/Hitachi_HD44780_LCD_controller&gt;the Wiki page for the HD44780&lt;/a&gt; - the functionality (like war) doesn&#39;t change.&lt;p&gt;This was a fun thing to mess around with of an evening - now that I&#39;m more familiar with the display, I&#39;m hoping to get it wired up to a Z80 processor I&#39;m currently playing with.</content>
  </entry>
  <entry>
    <title>A simple gocryptfs mounting/unmounting script</title>
    <link href="https://raphaelkabo.com/blog/gocryptfs-mounting-script/" />
    <updated>2020-07-23T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/gocryptfs-mounting-script/</id>
    <content type="html">&lt;p&gt;Nothing fancy, but I got bored of typing out both directories when I was mounting or unmounting my gocryptfs filesystem. This script creates a mount folder, mounts to it, and opens it in Thunar. When you unmount, it also deletes that directory if it&#39;s empty. Neat, and makes me happy.&lt;pre class=language-bash&gt;&lt;code class=language-bash&gt;&lt;span class=&quot;token shebang important&quot;&gt;#! /bin/bash&lt;/span&gt;

&lt;span class=&quot;token comment&quot;&gt;# cryptmount v1.0.0&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# mv this script to /usr/local/bin (or similar)&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# Change the directories to match yours&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# To mount: cryptmount m&lt;/span&gt;
&lt;span class=&quot;token comment&quot;&gt;# To unmount: cryptmount u&lt;/span&gt;

&lt;span class=&quot;token assign-left variable&quot;&gt;ACTION&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$1&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;ENCDIR&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;/path/to/encrypted/directory
&lt;span class=&quot;token assign-left variable&quot;&gt;MOUNTDIR&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;/path/to/decrypted/directory
&lt;span class=&quot;token assign-left variable&quot;&gt;FILEMANAGER&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;thunar

&lt;span class=&quot;token keyword&quot;&gt;case&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$ACTION&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;in&lt;/span&gt;

  &lt;span class=&quot;token function&quot;&gt;mount&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; m&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;findmnt &lt;span class=&quot;token parameter variable&quot;&gt;-M&lt;/span&gt; $MOUNTDIR&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
      &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;The encrypted directory is already mounted.&quot;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
      gocryptfs &lt;span class=&quot;token parameter variable&quot;&gt;--version&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;cut&lt;/span&gt; -d&lt;span class=&quot;token string&quot;&gt;&#39;;&#39;&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-f1&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;mkdir&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$MOUNTDIR&lt;/span&gt;
      gocryptfs &lt;span class=&quot;token variable&quot;&gt;$ENCDIR&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$MOUNTDIR&lt;/span&gt;
      &lt;span class=&quot;token variable&quot;&gt;$FILEMANAGER&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$MOUNTDIR&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  unmount &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;umount&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;|&lt;/span&gt; u&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;findmnt &lt;span class=&quot;token parameter variable&quot;&gt;-M&lt;/span&gt; $MOUNTDIR&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
      &lt;span class=&quot;token function&quot;&gt;umount&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$MOUNTDIR&lt;/span&gt;
      &lt;span class=&quot;token comment&quot;&gt;# Make sure the directory is empty before deleting it&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;[&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-z&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-A&lt;/span&gt; $MOUNTDIR&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;&quot;&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;token keyword&quot;&gt;then&lt;/span&gt;
        &lt;span class=&quot;token function&quot;&gt;rm&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-rf&lt;/span&gt; &lt;span class=&quot;token variable&quot;&gt;$MOUNTDIR&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
        &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;Mount directory not empty, exiting.&quot;&lt;/span&gt;
      &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;else&lt;/span&gt;
      &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;The encrypted directory is not mounted.&quot;&lt;/span&gt;
    &lt;span class=&quot;token keyword&quot;&gt;fi&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

  *&lt;span class=&quot;token punctuation&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&#39;Please specify an action: mount (m) or unmount (u).&#39;&lt;/span&gt;
    &lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;

&lt;span class=&quot;token keyword&quot;&gt;esac&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content>
  </entry>
  <entry>
    <title>Announcing two new online utopian games</title>
    <link href="https://raphaelkabo.com/blog/two-online-utopian-games/" />
    <updated>2020-06-28T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/two-online-utopian-games/</id>
    <content type="html">&lt;p&gt;Over the last year, I&#39;ve been really enjoying learning React, and I&#39;ve recently used all the fun new skills I&#39;ve learned to code two online card games, both about imagining and exploring utopian spaces and communities, but in very different ways. Both started life as physical games, but have become virtual because of the Covid-19 lockdown.&lt;p&gt;&lt;a href=https://loving-allness.mimir.computer/ &gt;&lt;strong&gt;Loving Allness&lt;/strong&gt;&lt;/a&gt; is a game designed by &lt;a href=https://sinjinli.com/ &gt;Sinjin Li&lt;/a&gt; which exists as a beautiful physical game with various permutations of rules, and now can also be played online. It is a game about travelling between worlds, creating meanings, and rearranging tiles. It helps tell stories about what we want to take with us, and what we want to leave behind, when we change where we are and what things mean. I refined and simplified some of the rules to make it easier to code, but this was still a serious coding challenge, requiring me to push &lt;a href=https://github.com/atlassian/react-beautiful-dnd&gt;react-beautiful-dnd&lt;/a&gt; to the limits of its capabilities to permit me to rearrange tiles between multiple lists while changing their ownership between different players.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/loving-allness.png alt=&quot;A screenshot of Loving Allness&quot;&gt;&lt;hr&gt;&lt;p&gt;&lt;a href=https://wehavethesquare.utopia.ac/ &gt;&lt;strong&gt;We Have the Square&lt;/strong&gt;&lt;/a&gt; is a game co-designed by me and my utopian collaborator Katie Stone in our combined existence as &lt;a href=https://utopia.ac&gt;Utopian Acts&lt;/a&gt;. It is a collaborative storytelling game inspired by occupation protest movements, asking players to think through the victories and challenges of creating an oppositional utopian community. While Loving Allness is poetic and evocative, We Have the Square is more open-ended, playing less like a game and more like a storytelling exercise.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/we-have-the-square.png alt=&quot;A screenshot of We Have the Square&quot;&gt;&lt;hr&gt;&lt;p&gt;Get some friends together and play both of them!</content>
  </entry>
  <entry>
    <title>You should write a static site generator</title>
    <link href="https://raphaelkabo.com/blog/write-a-static-site-generator/" />
    <updated>2020-06-18T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/write-a-static-site-generator/</id>
    <content type="html">&lt;p&gt;Recently, I decided to learn &lt;a href=https://www.rust-lang.org/ &gt;everyone&#39;s new favourite programming language, Rust&lt;/a&gt;. I really, really enjoy Rust, mostly because learning it has made me realise that our human brains must do a vast amount of hiding the compile-time errors of our thoughts from us.&lt;p&gt;My problem was that I am by no means a patient or an exacting coder. I had a brief look at the Rust book, found some source code which looked well-written, hacked together a command-line utility which sort of worked, and then decided that the best way to learn more would be to move this website over from Jekyll to a handmade static site generator. This meant that I spent hours slowly and meticulously adding &lt;code&gt;&amp;amp;&lt;/code&gt; to variable names and seeing whether the angry wiggly red lines would move somewhere different instead of, y&#39;know, reading the manual. But hey, many ways to mutate and/or borrow a cat, right?&lt;p&gt;I don&#39;t actually think that using a handmade site generator is particularly brilliant or clever or should be encouraged when amazing open source tools to do this already exist and could probably use the energy of keen programmers to improve their own codebases. &lt;em&gt;However&lt;/em&gt;, I think that building a static site generator is a fun learning activity in any language because it taught me about:&lt;ul&gt;&lt;li&gt;Writing, reading, deleting, and copying files&lt;li&gt;Parsing text and manipulating strings&lt;li&gt;Parsing command line arguments (I could have also built the generator around a config file, with the same general result)&lt;li&gt;Writing more efficient code&lt;li&gt;Error handling (sort of); I mean I ignored most of what I learned but it&#39;s now in my brain somewhere&lt;/ul&gt;&lt;p&gt;My result is called &lt;a href=https://github.com/lowercasename/orogene&gt;Orogene&lt;/a&gt;. It&#39;s named after the characters in N.K Jemisin&#39;s astonishing, magisterial &lt;em&gt;Broken Earth&lt;/em&gt; trilogy, because I just finished the last book and I feel very emotional. It&#39;s messy, panic-prone, and feature-limited, but I&#39;m still really proud of it, and it generated this website. Consider writing your own!</content>
  </entry>
  <entry>
    <title>HelloEdit: The demon child of a Markdown editor, Microsoft Word 5.1, and Macintosh System 7</title>
    <link href="https://raphaelkabo.com/blog/helloedit/" />
    <updated>2019-05-29T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/helloedit/</id>
    <content type="html">&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/HelloEdit.png alt=&quot;A screenshot of HelloEdit&quot;&gt;&lt;p&gt;Recently, I booted up Mini vMac, an emulator for classic Macintosh computers from the 1980s, and set up System 7 with Microsoft Word 5.1a, running on a Macintosh Plus. This replicates the first computer I ever used, a retired Macintosh Plus from mum&#39;s office. My parents inexplicably set it up in the laundry, and I spent a lot of time writing &#39;novels&#39; in Word and drawing weird abstract art in MacPaint.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/word51.png alt=&quot;&quot;&gt; &lt;em&gt;Hello, old friend.&lt;/em&gt;&lt;p&gt;For &lt;a href=https://www.wired.com/2004/06/word-refuseniks-never-upgrade/ &gt;a certain&lt;/a&gt; &lt;a href=https://www.macprices.net/2014/11/12/word-for-ios-restoration-of-classic-microsoft-word-on-the-mac-the-book-mystique/ &gt;section of&lt;/a&gt; &lt;a href=https://512pixels.net/2011/05/markdown-new-word51/ &gt;the internet&lt;/a&gt;, Word 5.1a is as good as word processors ever got. Indeed, as Kevin Lipe writes in the last article linked there, Word 5.1a embodied many features of Jon Gruber&#39;s Markdown - it kept out of the way while typing, it had only the bare minimum of formatting features, and it opens almost instantly. Sadly, these days, you would also need to have set up an emulator or, god forbid, an actual old Macintosh to run it (and getting your files off there is a huge pain).&lt;p&gt;&lt;em&gt;Or would you&lt;/em&gt;?&lt;p&gt;You may be surprised to discover that I &lt;a href=https://raphaelkabo.com/blog/posts/markdown-to-word/ &gt;spend a lot of time&lt;/a&gt; in &lt;a href=https://raphaelkabo.com/blog/posts/introducing-docdown/ &gt;the world of Markdown&lt;/a&gt;. So I decided to take Kevin Lipe at his very literal word, and create a Markdown editor which looked like Microsoft Word 5.1 running on the glorious 8-bit monochrome display of System 7.&lt;p&gt;&lt;a href=https://helloedit.raphaelkabo.com&gt;This is the result&lt;/a&gt;. It runs on my server, but for personal use I highly recommend you download and set it up locally from &lt;a href=https://github.com/lowercasename/helloedit/ &gt;the GitHub repository&lt;/a&gt;. Files are saved into a very simple database, and the backend itself is an Express app. Overkill, perhaps, but also it took me ten minutes to set up.&lt;p&gt;HelloEdit is a fully functional wrapper for the lovely Markdown editor &lt;a href=https://github.com/Ionaru/easy-markdown-editor&gt;EasyMDE&lt;/a&gt; by Jeroen Akkerman. I&#39;ve taken a lot of opinionated liberty with the styling of the Markdown writing area, opting for a starker, less WYSISWYG interface which appeals to my Markdown-writing sensibilities. I drew all the sprites for the buttons and so forth myself - they&#39;re mostly direct copies from the original interface, so please don&#39;t sue me, retired 1980s Microsoft designers! It was a real joy to re-implement some of these lovely design decisions - I absolutely love the little spark/star sprite that appears when you click the minimise and maximise buttons, and the way the big buttons push down when you click them. Looking at the transition from monochrome to color displays on the old Macintosh computers, I really feel like a lot of the charm of the design was lost when colors were brought in. And that weird purple is kind of unforgivable.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/macos70.png alt=&quot;&quot;&gt; &lt;em&gt;No.&lt;/em&gt;&lt;p&gt;One of the most important part of any UI is the fonts, and I relied entirely on the stellar work of many other people to implement the fonts in HelloEdit. Of course, the person we all have to thank is &lt;a href=https://en.wikipedia.org/wiki/Susan_Kare&gt;Susan Kare&lt;/a&gt;, one of the original Apple designers, who created iconic fonts like Chicago, Geneva, and Monaco; the infamous Dogcow (Moof!); the Happy Mac and Sad Mac icons; and the Command key symbol. The two main fonts HelloEdit uses, &lt;a href=http://www.suppertime.co.uk/blogmywiki/2017/04/chicago/ &gt;ChiKareGo2&lt;/a&gt; and &lt;a href=http://www.suppertime.co.uk/blogmywiki/2017/04/finderskeepers/ &gt;FindersKeepers&lt;/a&gt;, are homages to her work by Giles Booth. For the actual editor, I find it difficult to read the jagged bitmap fonts used in the original Macintosh interface for long stretches of time - and in any case, it&#39;s impossible to anti-anti-alias fonts in modern browsers, for good reason. The default font is therefore one of my very favourite sans-serif fonts, &lt;a href=https://fonts.google.com/specimen/Fira+Sans&gt;Fira Sans&lt;/a&gt;, but I also included Fira Mono and three bitmap-ish fonts: &lt;a href=&quot;http://www.pentacom.jp/pentacom/bitfontmaker2/gallery/?id=57&quot;&gt;TimesNewPixel&lt;/a&gt;, &lt;a href=https://github.com/sunaku/tamzen-font&gt;Tamzen&lt;/a&gt;, and a pixel font I made myself, modelled on New Transport by the amazing Margaret Calvert (appropriately called Calvert). In terms of other design, I lazily relied on &lt;a href=https://fontawesome.com/ &gt;FontAwesome&lt;/a&gt; to display the interface icons (I think they look great, honestly!).&lt;p&gt;I hope you enjoy it! I wrote this blog post in it, and I might try to write the remainder of my PhD in it, too...</content>
  </entry>
  <entry>
    <title>A script to create print-ready tiny zine layouts on macOS</title>
    <link href="https://raphaelkabo.com/blog/tiny-zines-script/" />
    <updated>2019-05-25T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/tiny-zines-script/</id>
    <content type="html">&lt;p&gt;I recently experienced a headache when I was trying to make a very, very small zine.&lt;p&gt;Zines are great, and small zines are even better - especially for sharing academic research. People love small things; they&#39;re memorable; and the small format forces you to really think hard and critically about the most important things you want to say to an audience. You can make zines and give them out before a panel or conference paper!&lt;p&gt;By far the easiest way to make a small zine is the &lt;a href=http://experimentwithnature.com/03-found/experiment-with-paper-how-to-make-a-one-page-zine/ &gt;fabled one-page zine format&lt;/a&gt;. All you need for this is a sheet of paper and a pair of scissors, and it transforms a single A4 page into an adorable 8-page booklet, with space for a secret poster on the back.&lt;p&gt;But I wasn&#39;t content with the 8-page format because it loses the entire second side of the page - I wanted to work out the easiest way to create a zine with regular double-sided pages. My solution requires a stapler and probably a guillotine or paper trimmer, because it involves dividing one sheet of paper, printed on both sides, into eight tiny double-sided sheets. Each group of four sheets is then folded in half, for a total of &lt;em&gt;two&lt;/em&gt; tiny adorable 16-page zines from one piece of A4 paper.&lt;p&gt;It&#39;s easy enough to mock a zine like this up in Word - just set your paper size to 74mm wide by 52.5mm tall, with 3mm margins. As far as I know, you unfortunately can&#39;t set manual paper sizes in Google Docs.&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; you need to create exactly 16 pages of zine. The first page is the front cover; the second page is the inside of the front cover, which you can leave blank if you want; the second-last page is therefore the inside back cover; and the last, sixteenth page is the back cover. This gives you 12 inside pages, or 14 if you write on the inside covers.&lt;p&gt;When you&#39;re done writing - 10pt or 9pt font is surprisingly legible if you pick a good sans-serif - save the file as a PDF.&lt;p&gt;Now comes the difficult part. One of the great benefits of a zine like this is that you can fit two copies of the zine onto one piece of A4 paper, but this requires some truly painful playing around with PDF reordering and n-up layouts. Luckily, the whole point of this blog post is saving you the trouble by automating it for you.&lt;p&gt;The first thing you need to do is install MacTeX. If you don&#39;t use LaTeX and don&#39;t fancy downloading a 3GB file, you only actually need the 70MB BasicTeX package, which is available from &lt;a href=http://www.tug.org/mactex/morepackages.html&gt;this page&lt;/a&gt;.&lt;p&gt;Once you&#39;ve installed MacTeX, if your Terminal is open, close and open it again. Double-check that the necessary components have been installed by running the following two commands in Terminal - both should give you some information rather than an error or a blank line:&lt;pre&gt;&lt;code&gt;pdftex -v
kpsewhich pdfpages.sty
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now you need to install the PDFjam tools - these provide an easy wrapper for the PDF tools that come with pdfTeX. The easiest way to do this is to use a small helper script I wrote - open your Terminal and enter this line:&lt;pre&gt;&lt;code&gt;curl https://raphaelkabo.com/scripts/pdfjaminstall.sh | bash
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This script downloads the latest PDFjam source files, extracts them, and installs them in your /usr/bin directory. As ever with strange scripts you find on the internet, you should open the file and read through it to make sure it won&#39;t destroy your computer before running the above command.&lt;p&gt;If you&#39;d rather do it manually, head to &lt;a href=https://warwick.ac.uk/fac/sci/statistics/staff/academic-research/firth/software/pdfjam/ &gt;the PDFjam page&lt;/a&gt; and download the pdfjam_latest.tgz file. Extract it, then copy all the files in the bin/ directory to a directory on your path (like /usr/bin).&lt;p&gt;Verify PDFjam works by running the following command:&lt;pre&gt;&lt;code&gt;pdfjam --version
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now you&#39;re ready to rock! In Terminal, navigate to the directory containing your beautiful 16-page PDF. Enter the following command, substituting the filename of your PDF file:&lt;pre&gt;&lt;code&gt;pdfjam --nup 4x4 --a4paper --landscape INPUTFILE.pdf &#39;8,9,6,11,4,13,2,15,8,9,6,11,4,13,2,15,12,5,10,7,16,1,14,3,12,5,10,7,16,1,14,3&#39; -o OUTPUTFILE.pdf
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The file you get out of this looks completely bonkers, but I assure you that once the file has been printed off, cut, and assembled, you will end up with two perfectly logically ordered zines.&lt;p&gt;Alternatively, you can download a bash script I wrote, &lt;a href=https://gist.github.com/lowercasename/31342fadfc2a5cf608275d1097e23764&gt;available here as a Gist&lt;/a&gt; which does this for you. Open the link, save the raw file to your computer, navigate to where you downloaded it, and run it like so (you only need to &lt;code&gt;chmod&lt;/code&gt; it once, to make it executable):&lt;pre&gt;&lt;code&gt;chmod +x tinyzine
./tinyzine /path/to/input.pdf
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To make it easier to use from anywhere, once you&#39;ve downloaded it, navigate to the directory it&#39;s in and execute the following commands:&lt;pre&gt;&lt;code&gt;chmod +x tinyzine
sudo mv tinyzine /usr/bin/tinyzine
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Now you can simply run this command from any directory (like the directory where your PDF file is):&lt;pre&gt;&lt;code&gt;tinyzine input.pdf
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;It will create a file in the same directory with the appended name &#39;-16-up&#39;.&lt;p&gt;Now all that&#39;s left is to open your file, print it, cut it into four lengthways strips, cut each strip in half, and assemble your tiny zines! Happy zineing!</content>
  </entry>
  <entry>
    <title>Introducing DocDown, the easy way to turn academic Markdown into Word documents</title>
    <link href="https://raphaelkabo.com/blog/introducing-docdown/" />
    <updated>2019-01-12T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/introducing-docdown/</id>
    <content type="html">&lt;p&gt;In a &lt;a href=https://raphaelkabo.com/blog/markdown-to-word/ &gt;previous post&lt;/a&gt;, I elaborated a complex solution to a problem I was encountering while writing my PhD (which, by the way, I&#39;m still writing - isn&#39;t procrastination swell?). I wanted to write in Markdown in the amazing MacOS text editor &lt;a href=https://ulysses.app/ &gt;Ulysses&lt;/a&gt;, using citation keys automatically generated by &lt;a href=https://www.zotero.org/ &gt;Zotero&lt;/a&gt; for my footnotes and bibliography. But I (and my supervisor, and eventually my viva panel) also wanted to see my work in easily readable Word documents, with the footnotes embedded, the citations properly formatted, and any necessary styles integrated.&lt;p&gt;My process involved creating a little application using Automator to filter Markdown documents through the incredible command line utility &lt;a href=https://pandoc.org/ &gt;Pandoc&lt;/a&gt;, a file-converting powerhouse. This application was quite difficult to edit and quite fiddly to set up - and recent changes to the way MacOS handles application accessibility permissions may make it unusable.&lt;p&gt;So I turned it into a native app (my first ever), called DocDown! I&#39;ve used Electron, but I haven&#39;t yet tried using it on platforms other than macOS. Chances are it won&#39;t work on Windows without some modifications. You can download the &lt;a href=https://github.com/lowercasename/docdown/releases&gt;DMG file here&lt;/a&gt;, and visit its &lt;a href=https://github.com/lowercasename/docdown&gt;GitHub repository here&lt;/a&gt;.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/DocDown-menu-bar.png alt=&quot;DocDown in the menu bar&quot;&gt; &lt;em&gt;DocDown lives in the menu bar, out of your way&lt;/em&gt;&lt;p&gt;DocDown still requires you to provide it with Pandoc, a Zotero .bib file, a CSL file, and a reference docx. Instructions for obtaining all of these are on the original blog post (follow steps 1-10 of the &lt;strong&gt;Referencing&lt;/strong&gt; section and steps 1-4 of the &lt;strong&gt;Exporting&lt;/strong&gt; section - DocDown will handle the rest.)&lt;p&gt;To make your life easier, the app comes bundled with CSL files for a couple of commonly used citation formats and a couple of basic Word templates I put together - all you need if you use these is your own .bib file.&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; I don&#39;t have an Apple Developer certificate (£79 a year!), so when you first launch DocDown, it will warn you that it&#39;s from an unidentified developer. I am the developer, I have identified myself, do not fear. Follow &lt;a href=&quot;https://support.apple.com/kb/ph25088?locale=en_US&quot;&gt;these instructions&lt;/a&gt; to open the app - basically, right-click on the app icon and click &#39;Open&#39; instead of double-clicking.&lt;p&gt;Enjoy, and let me know on GitHub or by email when you inevitably encounter problems! If anyone would like to help out with the source code on GitHub, your assistance will be warmly welcomed.</content>
  </entry>
  <entry>
    <title>Wordpress in a subdirectory with Caddy</title>
    <link href="https://raphaelkabo.com/blog/caddy-wordpress-subdirectory/" />
    <updated>2019-01-10T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/caddy-wordpress-subdirectory/</id>
    <content type="html">&lt;p&gt;I&#39;ve recently moved my webserver to &lt;a href=https://caddyserver.com/ &gt;Caddy&lt;/a&gt;, a fantastic modern webserver which is very user-friendly, with a helpful support community, and most importantly, effortless HTTPS on every site through the good folks at &lt;a href=https://letsencrypt.org/ &gt;Let&#39;s Encrypt&lt;/a&gt;.&lt;p&gt;The one issue I had with Caddy was running a Wordpress blog which was located on a subdirectory (&lt;code&gt;https://domain.com/blog&lt;/code&gt;) of the domain &lt;code&gt;https://domain.com&lt;/code&gt;. Whatever redirect rules I tried, either JS and CSS files wouldn&#39;t load, or pretty permalinks (&lt;code&gt;https://domain.com/blog/category-name/&lt;/code&gt;) wouldn&#39;t work at all. Thanks to the people at the Caddy forums, I finally worked out how to combine redirect rules in my Caddyfile to get everything working.&lt;p&gt;The important thing to note is that there is no need to create two directives in the Caddyfile, one for the main domain, one for the blog - instead, combine the &lt;code&gt;/blog/&lt;/code&gt; redirect directive directly into the main directive:&lt;pre&gt;&lt;code&gt;domain.com {
	root /var/www/domain.com # Directory on server
	fastcgi / /run/php/php7.2-fpm.sock php # PHP-FPM enabled
	errors /var/log/caddy/domain.com/error.log # Error logging
	# Attempt to rewrite every file under / first to its path, then to a directory, then as a URL query appended to index.php.
	rewrite / {
		to {path} {path}/ /index.php?{query}
	}
	# The same rewrite for Wordpress, with the addition of the /blog/ subdirectory, works flawlessly. Because of the way Caddy&#39;s rewrite rules work, any URLs featuring /blog will use this redirect before they attempt the more general rule.
	rewrite /blog {
		to {path} {path}/ /blog/index.php?{query}
	}
	gzip # Compress all files for faster loading.
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This worked great for me!</content>
  </entry>
  <entry>
    <title>Simple image captions in Jekyll</title>
    <link href="https://raphaelkabo.com/blog/jekyll-image-captions/" />
    <updated>2018-10-08T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/jekyll-image-captions/</id>
    <content type="html">&lt;p&gt;Images, and image captions, are always a problem in Markdown. Not only is there no real way to semantically signify images using a text-only format, but it&#39;s never really obvious how to tie image captions to the images to which they belong.&lt;p&gt;&lt;a href=https://stackoverflow.com/a/30366422/1057716&gt;This Stack Overflow post&lt;/a&gt; demonstrates the simplest way to display image captions on a Jekyll site or blog while keeping the markup and resulting HTML reasonably semantic. The same process will work for any other site which transforms Markdown into HTML.&lt;p&gt;Just wrap your caption in any tag, such as an emphasis tag, and put it directly below the image, without a new line. To anyone reading raw Markdown, this will hopefully signify the image and caption are linked. I like using the single asterisk italics tags here and the underscore tags elsewhere, to distinguish actual italicised text from caption text.&lt;pre class=language-markdown&gt;&lt;code class=language-markdown&gt;![](path_to_image)
_image_caption_&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;For those seeing the HTML, I style the caption using the following CSS. The &lt;a href=https://www.w3schools.com/cssref/sel_element_pluss.asp&gt;&lt;code&gt;+&lt;/code&gt; selector&lt;/a&gt; will only target an element directly after another, so it doesn&#39;t affect any other &lt;code&gt;&amp;lt;em&amp;gt;&lt;/code&gt; tags on the page.&lt;pre class=language-css&gt;&lt;code class=language-css&gt;&lt;span class=&quot;token selector&quot;&gt;img + em&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; smaller&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The CSS stops the caption from being italicised, but if you need to be able to italicise text inside the caption, you could wrap it in bold Markdown tags (&lt;code&gt;**bold text**&lt;/code&gt;) and add the following CSS:&lt;pre class=language-css&gt;&lt;code class=language-css&gt;&lt;span class=&quot;token selector&quot;&gt;img + em &gt; strong&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-weight&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; 400&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; italic&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;If you need to make text in a caption both italic and bold, rethink your design choices.&lt;p&gt;To centre-align captions under images, try this:&lt;pre class=language-css&gt;&lt;code class=language-css&gt;&lt;span class=&quot;token selector&quot;&gt;img + em&lt;/span&gt; &lt;span class=&quot;token punctuation&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;display&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; block&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;text-align&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; center&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-style&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; normal&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
  &lt;span class=&quot;token property&quot;&gt;font-size&lt;/span&gt;&lt;span class=&quot;token punctuation&quot;&gt;:&lt;/span&gt; smaller&lt;span class=&quot;token punctuation&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;token punctuation&quot;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;</content>
  </entry>
  <entry>
    <title>A simple Jekyll rsync script</title>
    <link href="https://raphaelkabo.com/blog/jekyll-rsync-script/" />
    <updated>2018-08-03T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/jekyll-rsync-script/</id>
    <content type="html">&lt;p&gt;I&#39;ve just moved this site to &lt;a href=http://jekyllrb.com&gt;Jekyll&lt;/a&gt;, which is the first time I&#39;ve played around with it properly, and I&#39;m very happy with how easy and logical it has been to use. Thanks, Jekyll.&lt;p&gt;I keep my Jekyll sites in a folder on my Macbook, and have written this very simple bash script to build and upload any updates to my server. It&#39;s a little different to the rsync script available on the Jekyll site because I need it to not delete files and directories I already have on my server when it updates those Jekyll is responsible for. Just change up the bits in all caps to suit you (except the $TARGET variable).&lt;p&gt;With my configuration, I name my development directory the same as my site URL (&lt;a href=http://raphaelkabo.com&gt;raphaelkabo.com&lt;/a&gt;, for instance), and then I type &lt;code&gt;ju.sh raphaelkabo.com&lt;/code&gt; to invoke it. Remember to pop this script in &lt;code&gt;/usr/local/bin/&lt;/code&gt; or another directory in your path and &lt;code&gt;chmod +x&lt;/code&gt; it. For a bit more magic, you can also keep the script in a local directory, like &lt;code&gt;~/scripts/&lt;/code&gt;, then run &lt;code&gt;ln -s ~/scripts/ju.sh /usr/local/bin/ju&lt;/code&gt; to be able to invoke the script without typing &lt;code&gt;.sh&lt;/code&gt; at the end. Makes you feel like a badass.&lt;pre class=language-bash&gt;&lt;code class=language-bash&gt;&lt;span class=&quot;token shebang important&quot;&gt;#!/bin/sh&lt;/span&gt;
&lt;span class=&quot;token assign-left variable&quot;&gt;TARGET&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;$1&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;cd&lt;/span&gt; /Users/HOMEDIRECTORY/jekyll-dev-sites/&lt;span class=&quot;token variable&quot;&gt;$TARGET&lt;/span&gt;/ &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; jekyll build &lt;span class=&quot;token parameter variable&quot;&gt;--incremental&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&quot;token function&quot;&gt;rsync&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-avh&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--update&lt;/span&gt; /Users/HOMEDIRECTORY/jekyll-dev-sites/&lt;span class=&quot;token variable&quot;&gt;$TARGET&lt;/span&gt;/_site/ username@REMOTESERVER.COM:/&lt;span class=&quot;token environment constant&quot;&gt;PATH&lt;/span&gt;/TO/WWW/DIRECTORY/&lt;span class=&quot;token variable&quot;&gt;$TARGET&lt;/span&gt;/public_html/&lt;/code&gt;&lt;/pre&gt;</content>
  </entry>
  <entry>
    <title>A command line die roller for D&amp;D</title>
    <link href="https://raphaelkabo.com/blog/command-line-die-roller/" />
    <updated>2018-08-03T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/command-line-die-roller/</id>
    <content type="html">&lt;p&gt;Real nerds roll dice from the command line, so I made a command line die roller. It&#39;s very basic and doesn&#39;t have good error checking, but it&#39;s the first proper thing I&#39;ve ever written in Python. It&#39;s heavily adapted from &lt;a href=https://github.com/shadsbot/roll&gt;this script&lt;/a&gt;, but I&#39;ve added the ability to repeat a roll multiple times (while losing the ability to roll multiple sets of dice in the same command, which I will bring back eventually).&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/roll-sh.png alt=&quot;Command line die roller example&quot;&gt;&lt;p&gt;It&#39;s &lt;a href=https://github.com/lowercasename/roll&gt;available on Github&lt;/a&gt; for all your rolling needs.</content>
  </entry>
  <entry>
    <title>My workflow for transforming academic Markdown into beautiful Word documents</title>
    <link href="https://raphaelkabo.com/blog/markdown-to-word/" />
    <updated>2017-05-11T00:00:00Z</updated>
    <id>https://raphaelkabo.com/blog/markdown-to-word/</id>
    <content type="html">&lt;div class=alert&gt;&lt;h3&gt;12/01/2019&lt;/h3&gt;&lt;p&gt;I&#39;ve bundled a significant part of this setup into a simple menu bar application, &lt;a href=https://raphaelkabo.com/blog/introducing-docdown/ &gt;available here&lt;/a&gt;.&lt;/div&gt;&lt;div class=alert&gt;&lt;h3&gt;01/03/2018&lt;/h3&gt;&lt;p&gt;Updated for compatibility with Pandoc version 2.1.1.&lt;/div&gt;&lt;p&gt;Over the last few months, as I&#39;ve been working on my PhD, I&#39;ve set up a workflow to convert my writing (in everyone&#39;s favourite simple markup language, Markdown), &lt;em&gt;as if by magic&lt;/em&gt;, into Word documents which I can email to my supervisor, without having to explain to her that Markdown is great, really, if you only tried it you&#39;d see. She is busy and does not have time for my shit. As a bonus, this workflow turns all your citations into automatic references according to whatever style you happen to be using (Chicago, MLA, APA, MHRA, &lt;em&gt;anything&lt;/em&gt;), with all of the appropriate &lt;em&gt;ibids.&lt;/em&gt; and page numbers and everything. Seriously.&lt;p&gt;To that end, this guide - as much for my own future benefit as anyone else&#39;s - lists everything that is required to allow me to transform what is on the left into what is on the right &lt;em&gt;with a click of a goddamn button&lt;/em&gt;. We do live in the future.&lt;p&gt;&lt;img src=https://raphaelkabo.com/media/ulysses-to-word.png alt=&quot;Ulysses to Word&quot;&gt;&lt;h2&gt;Writing&lt;/h2&gt;&lt;p&gt;First, I write my work in &lt;a href=https://ulyssesapp.com&gt;Ulysses&lt;/a&gt;. Ulysses is a beautiful Markdown word processor and project manager which has the inner workings of a really powerful writing tool like Scrivener, but stays well out of the way when I write. Ulysses is great and I love it a lot. I&#39;ve only made minor aesthetic changes to Ulysses&#39; default preferences, as well as one &lt;em&gt;very important&lt;/em&gt; non-aesthetic change which I will get to in the next section.&lt;h2&gt;Referencing&lt;/h2&gt;&lt;p&gt;When it comes to academic references, I use a reasonably complex system which is, however, &lt;em&gt;extremely&lt;/em&gt; flexible, sits in the background, and doesn&#39;t disturb anyone. It&#39;s also so portable that when I reinstalled macOS recently, it took me fifteen minutes to set it up again. I have named this system &lt;strong&gt;Zotero/BetterBibTeX/zotpick-applescript/Service&lt;/strong&gt;, because that is what it is made of and I&#39;m sorry, I can&#39;t be called upon to be imaginative on demand. This is how to set up the same system for yourself:&lt;ol&gt;&lt;li&gt;Use &lt;a href=https://www.zotero.org&gt;Zotero&lt;/a&gt;. If you don&#39;t use Zotero, you&#39;re bang out of luck, although I know a similar (and slightly more swish) system is already inbuilt for Papers (&lt;a href=https://ulyssesapp.com/blog/2015/12/academic-writing-on-the-mac-papers-and-ulysses/ &gt;look, the Ulysses blog even have a tutorial for it - &lt;em&gt;traitors&lt;/em&gt;&lt;/a&gt;), but I don&#39;t want anything to do with Papers because Zotero is beautifully, charmingly ugly, and that&#39;s the way I like it. I have tried all of the referencing managers, Zotero is the one for me, I will not apologise. If people want a guide to how to get a lot out use and goodness out of Zotero, please email me and I will comply.&lt;li&gt;Install &lt;a href=https://github.com/retorquere/zotero-better-bibtex&gt;Zotero Better BibTeX&lt;/a&gt;. Follow the &lt;a href=https://github.com/retorquere/zotero-better-bibtex/wiki/Installation&gt;installation instructions here&lt;/a&gt;. This is a plugin for Zotero which sets up better (hence the name) citation keys for every item in your database. A citation key is a unique reference to an item - for instance, Judith Butler&#39;s 2009 book &lt;em&gt;Frames of War&lt;/em&gt; will be given the key &lt;strong&gt;butler2009&lt;/strong&gt;, and when I add Butler&#39;s 2009 article &#39;Performativity, Precarity, and Sexual Politics&#39; to my Zotero database, it will automatically be named &lt;strong&gt;butler2009a&lt;/strong&gt;, to distinguish it from the book. You can also change these citation keys manually to whatever you want!&lt;li&gt;Once you&#39;ve installed Better BibTeX, go into Zotero. You need to change a few things. Open &lt;strong&gt;Preferences&lt;/strong&gt;, click the &lt;strong&gt;Better BibTeX&lt;/strong&gt; tab, and then under &lt;strong&gt;Citation keys&lt;/strong&gt; change the citation key format to &lt;strong&gt;[auth:lower][year]&lt;/strong&gt;, like this:&lt;br&gt;&lt;img src=https://raphaelkabo.com/media/zotero-preferences.png alt=&quot;Zotero preferences&quot;&gt;&lt;br&gt;Now the citation keys for all your papers are going to look something like this:&lt;br&gt;&lt;img src=https://raphaelkabo.com/media/citation-keys.png alt=&quot;Citation keys&quot;&gt;&lt;li&gt;Now go to the &lt;strong&gt;File&lt;/strong&gt; menu in Zotero and click &lt;strong&gt;Export Library&lt;/strong&gt;. You are about to create an auto-updating list of all the citation keys in your Zotero library! For Format, select &#39;Better BibLaTeX&#39;. Don&#39;t tick any of the other boxes apart from &#39;Keep updated&#39;. Select where to save this file - I keep mine in my Dropbox.&lt;li&gt;Now go back to &lt;strong&gt;Preferences - Better BibTeX&lt;/strong&gt;. Go to the &lt;strong&gt;Automatic Export&lt;/strong&gt; tab. You&#39;ll see your Better BibLaTeX library. I changed the &lt;strong&gt;Automatic export&lt;/strong&gt; option here to &lt;strong&gt;On change&lt;/strong&gt;, which means it&#39;ll update my library as soon as I change or add anything to Zotero, but you can keep yours however you want. This finishes the Zotero section of the referencing system.&lt;li&gt;Now download &lt;a href=https://github.com/davepwsmith/zotpick-applescript&gt;zotpick-applescript&lt;/a&gt;. This is a magical piece of code which will allow you to drop your newly minted citation keys into any text processor you happen to be using. You actually only need the file named &#39;zotpick-pandoc.applescript&#39; - open it up and copy the contents.&lt;li&gt;Now open Automator (in the Applications folder). Click &lt;strong&gt;New Document&lt;/strong&gt; and select &lt;strong&gt;Service&lt;/strong&gt;. At the top, select &lt;strong&gt;no input&lt;/strong&gt; in &lt;strong&gt;any application&lt;/strong&gt;. Then drag the &#39;Run AppleScript&#39; module from the left sidebar into the main window. Replace everything in the text box with the contents of the &#39;zotpick-pandoc.applescript&#39; you copied. Save your new service!&lt;li&gt;Open &lt;strong&gt;System Preferences&lt;/strong&gt;. Go to &lt;strong&gt;Keyboard&lt;/strong&gt;, then &lt;strong&gt;Shortcuts&lt;/strong&gt;, then &lt;strong&gt;Services&lt;/strong&gt; in the left sidebar, scroll down to the bottom, click on your newly named service, click the &#39;Add Shortcut&#39; button, and press your chosen shortcut for quickly activating Zotpick. I use ⌘-` (tilde key, on the top left corner of my keyboard).&lt;li&gt;Now go to your text editor of choice. When you need to add a citation, first type two square brackets and place the cursor between them. Then press your chosen shortcut. The Zotpick window will appear - search for the item you need, select it, add prefixes, suffixes, and page numbers. When you press Enter, the full citation key will be copied into your text editor.&lt;br&gt;&lt;img src=https://raphaelkabo.com/media/zotpick.png alt=Zotpick&gt;&lt;br&gt;&lt;img src=https://raphaelkabo.com/media/ulysses-citation.png alt=&quot;Ulysses citation&quot;&gt;&lt;li&gt;This is where the non-aesthetic change to Ulysses comes in. Ulysses&#39; standard Markdown parser will render &lt;code&gt;[]&lt;/code&gt; as the beginning of a link, as would any standard Markdown parser. This doesn&#39;t matter one bit for the export stage, but Ulysses will try to dress up your citations as links, which is bothersome. To avoid this, you need to change the link symbols in Ulysses to something else, like angle brackets - to do so, follow the Preparations for Ulysses section in &lt;a href=https://ulyssesapp.com/blog/2015/12/academic-writing-on-the-mac-papers-and-ulysses/ &gt;this guide&lt;/a&gt;.&lt;/ol&gt;&lt;h2&gt;Exporting&lt;/h2&gt;&lt;p&gt;Now for the fun bit. Before you proceed, make sure you have the following things set up:&lt;ul&gt;&lt;li&gt;A Zotero BibTeX library which stores all your citation keys.&lt;li&gt;A Markdown-formatted document which incorporates your citation keys in square brackets in the places where your references are going to go (whether you are using inline references or footnotes - I use footnotes, my partner loathes them, but then there&#39;s no accounting for classicists - place your citation keys &lt;em&gt;before&lt;/em&gt; your full stops. The export engine will sort it all out for you.&lt;/ul&gt;&lt;p&gt;Now you are going to install &lt;a href=http://pandoc.org&gt;Pandoc&lt;/a&gt;. Pandoc is an obscenely powerful text converter which can transform almost any popular file format into almost any other popular file format. The downside - if you don&#39;t use the Terminal much - is that it&#39;s a command line utility, but we&#39;re going to turn that command line utility into a simple app.&lt;ol&gt;&lt;li&gt;&lt;p&gt;To begin, download the file named &#39;pandoc-2.1.1-macOS.pkg&#39; from &lt;a href=https://github.com/jgm/pandoc/releases/tag/2.1.1&gt;this page&lt;/a&gt;. Install the package. (Alternatively, if you are comfortable using Homebrew, install Pandoc through Homebrew: &lt;code&gt;brew install pandoc&lt;/code&gt;. This will make future updating easier.)&lt;li&gt;&lt;p&gt;Test that Pandoc has installed correctly. Open Terminal and type the following: &lt;code&gt;pandoc -v&lt;/code&gt;. If you get some information about Pandoc&#39;s version, it&#39;s working. Close the Terminal.&lt;li&gt;&lt;p&gt;Now you need to set up two things for Pandoc to be able to create a beautifully formatted and referenced Word document. The first of these is your CSL file of choice. There are &lt;a href=http://citationstyles.org&gt;CSL, or Citation Style Language&lt;/a&gt; files for almost any referencing system under the sun, which contain all the necessary rules to automate referencing. Go to the &lt;a href=https://github.com/citation-style-language/styles&gt;CSL repository&lt;/a&gt; and find your favourite style (I use Chicago note and bibliography with no ibid. - yes, you can be &lt;em&gt;that&lt;/em&gt; specific with some of the more popular styles). Download this file and save it somewhere you&#39;ll want to keep it (I created a &#39;Scripting&#39; folder in my PhD folder in my Dropbox).&lt;li&gt;&lt;p&gt;The second thing you need to do is create a reference Word document file - a template with preset styles so Pandoc knows what sort of file to output. This is a finicky process, described in the &lt;a href=http://pandoc.org/MANUAL.html&gt;Pandoc manual&lt;/a&gt; (search for &#39;reference-doc&#39;). Alternatively, &lt;a href=https://raphaelkabo.com/media/reference.docx&gt;download the one I made&lt;/a&gt; - it&#39;s Quite Nice. You can consult the manual to see which styles you can tweak yourselves. Save it somewhere sensible too.&lt;li&gt;&lt;p&gt;Now open up your trusty Automator again. This time you want to create an &lt;strong&gt;Application&lt;/strong&gt;.&lt;li&gt;&lt;p&gt;Drag the module called &#39;Run Shell Script&#39; (in the Utilities section) into the main window. Make sure that your shell is &#39;/bin/bash&#39; and you are passing input &lt;strong&gt;as arguments&lt;/strong&gt;. Now copy the following piece of code into the text box:&lt;pre class=language-bash&gt;&lt;code class=language-bash&gt;&lt;span class=&quot;token assign-left variable&quot;&gt;title&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;token variable&quot;&gt;&lt;span class=&quot;token variable&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;token function&quot;&gt;basename&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$@&lt;/span&gt;&quot;&lt;/span&gt; .md&lt;span class=&quot;token variable&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;token builtin class-name&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;-e&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token entity&quot; title=&#92;n&gt;&#92;n&lt;/span&gt;&lt;span class=&quot;token entity&quot; title=&#92;n&gt;&#92;n&lt;/span&gt;# Bibliography&quot;&lt;/span&gt; &lt;span class=&quot;token operator&quot;&gt;&gt;&gt;&lt;/span&gt; &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$@&lt;/span&gt;&quot;&lt;/span&gt;
/usr/local/bin/pandoc &lt;span class=&quot;token parameter variable&quot;&gt;-s&lt;/span&gt; &lt;span class=&quot;token parameter variable&quot;&gt;--filter&lt;/span&gt;&lt;span class=&quot;token operator&quot;&gt;=&lt;/span&gt;/usr/local/bin/pandoc-citeproc &lt;span class=&quot;token parameter variable&quot;&gt;--bibliography&lt;/span&gt; /&lt;span class=&quot;token environment constant&quot;&gt;PATH&lt;/span&gt;/TO/BIBTEX/LIBRARY/Zotero.bib &lt;span class=&quot;token parameter variable&quot;&gt;--csl&lt;/span&gt; /&lt;span class=&quot;token environment constant&quot;&gt;PATH&lt;/span&gt;/TO/CSL/FILE/FILENAME.csl --reference-doc /&lt;span class=&quot;token environment constant&quot;&gt;PATH&lt;/span&gt;/TO/REFERENCE/DOC/reference.docx &lt;span class=&quot;token parameter variable&quot;&gt;-f&lt;/span&gt; markdown+smart &lt;span class=&quot;token parameter variable&quot;&gt;-t&lt;/span&gt; docx &lt;span class=&quot;token parameter variable&quot;&gt;-o&lt;/span&gt; /&lt;span class=&quot;token environment constant&quot;&gt;PATH&lt;/span&gt;/WHERE/YOU/WANT/TO/EXPORT/&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$title&lt;/span&gt;&quot;&lt;/span&gt;.docx &lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$@&lt;/span&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;token function&quot;&gt;open&lt;/span&gt; /&lt;span class=&quot;token environment constant&quot;&gt;PATH&lt;/span&gt;/WHERE/YOU/WANT/TO/EXPORT/&lt;span class=&quot;token string&quot;&gt;&quot;&lt;span class=&quot;token variable&quot;&gt;$title&lt;/span&gt;&quot;&lt;/span&gt;.docx&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;The first line sets the filename of your new file to be identical to the filename of whatever file you sent into it. The second line puts the heading &#39;Bibliography&#39; at the end of the document - that way the bibliography Pandoc will automatically create will have a heading. You can delete this line if you don&#39;t need it. The third line is the Pandoc command. You need to edit all the paths in ALL CAPS in this code. Make sure you&#39;ve got the right location for your exported BibTeX Zotero library, your CSL file, your reference.docx, and the place you want your converted file to end up. If you&#39;re not sure what the path of a folder is, use this one weird trick: in Finder, grab the blue folder icon at the very top of the window, next to the name of the window, and &lt;em&gt;drag and drop&lt;/em&gt; it into the text box. Cool, right? If your path has spaces in it, put it in double quotes, like this: &lt;code&gt;&amp;quot;/Users/bobross/Happy Squirrels/reference.docx&amp;quot;&lt;/code&gt;.&lt;li&gt;&lt;p&gt;I then also dragged the &#39;Display Notification&#39; module into the main Automator window below the script module, so that I&#39;d know when the conversion was done. In practice, it happens pretty much instantaneously, but it&#39;s cute. Here&#39;s what mine looks like: &lt;img src=https://raphaelkabo.com/media/automator.png alt=Automator&gt;&lt;li&gt;&lt;p&gt;Save your Automator application in your Applications folder, because that&#39;s where it deserves to go - I&#39;ve named mine DocDown.&lt;li&gt;&lt;p&gt;Now for the cool bit. You can simply drag any text file with the extension &lt;code&gt;.md&lt;/code&gt; onto the DocDown application and it will do its magic conversion business. But in Ulysses, you can make the magic happen without even leaving the editor. Click on the Share button in the top right, select &lt;strong&gt;Text&lt;/strong&gt;, &lt;strong&gt;Markdown&lt;/strong&gt;, then click the A button and select &lt;strong&gt;Other&lt;/strong&gt;. Find DocDown. &lt;img src=https://raphaelkabo.com/media/ulysses-export.png alt=&quot;Ulysses export&quot;&gt; Your current Ulysses document will be converted instantly into a Word document in your chosen location, with references flown in from your Zotero library and edited by your CSL file. Well done. You are academia god. We all bow down.&lt;/ol&gt;</content>
  </entry>
</feed>