<?xml version="1.0" encoding="UTF-8"?>
<rss  xmlns:atom="http://www.w3.org/2005/Atom" 
      xmlns:media="http://search.yahoo.com/mrss/" 
      xmlns:content="http://purl.org/rss/1.0/modules/content/" 
      xmlns:dc="http://purl.org/dc/elements/1.1/" 
      version="2.0">
<channel>
<title>rostrum.blog</title>
<link>https://www.rostrum.blog/</link>
<atom:link href="https://www.rostrum.blog/index.xml" rel="self" type="application/rss+xml"/>
<description>Realised showerthoughts about R and Python mostly</description>
<generator>quarto-1.8.27</generator>
<lastBuildDate>Fri, 05 Jun 2026 23:00:00 GMT</lastBuildDate>
<item>
  <title>Get the gist of jot</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2026-06-06-jot-gist/</link>
  <description><![CDATA[ 





<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2026-06-06-jot-gist/resources/jot-gist.png" class="img-fluid quarto-figure quarto-figure-left figure-img" alt="A crudely drawn MS-Paint-style picture of a text document on the left, with an arrow pointing to the right, where there's a cloud that contains a floppy disk. In the middle is Octocat, the GitHub mascot."></p>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>My mini <a href="https://github.com/matt-dray/jot">note-taking CLI tool jot</a> can now back-up jottings to <a href="https://gist.github.com/">a GitHub gist</a>, thanks to <a href="https://cli.github.com/">GitHub’s gh CLI</a>.</p>
</section>
<section id="always-be-jotting" class="level2">
<h2 class="anchored" data-anchor-id="always-be-jotting">Always Be Jotting</h2>
<p>Last time I told you <a href="../../posts/2026-05-31-dogfood/index.html">I’ve been using jot for months</a>. It’s my opinionated CLI tool to record stuff that I can reflect on later. I just type something like <code>jot "promoted synergy"</code> in the terminal and move on.</p>
<p>And now there’s 1200 jottings.</p>
<p>But there’s only one text file.</p>
<p>A backup might be handy, eh.</p>
</section>
<section id="copy-waste" class="level2">
<h2 class="anchored" data-anchor-id="copy-waste">Copy-waste</h2>
<p>I had been copy-pasting the jot file into a private <a href="https://gist.github.com/">GitHub gist</a><sup>1</sup> at the end of each week. Gists are used mostly for throwaway scratch files, code snippets and text files.</p>
<p>Given that jot is a personal project and I want to make things easier for myself<sup>2</sup>, I think this is fine as a backup host. It’s what I’m doing already, after all.</p>
<p>But oof, <em>copy-paste</em>? We can do better. So I built in a new option into jot to support upload to a gist.</p>
<p>This is now available in the dev version (v0.3.0.dev1) available from GitHub like <code>uv tool install git+https://github.com/matt-dray/jot</code><sup>3</sup>. I’ll be testing it for a while before it goes in the stable release<sup>4</sup>.</p>
<p>With this dev version I can now run <code>jot -u</code> (short for <code>--upload</code>) every so often. No more filthy copy-pasting.</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jot</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-u</span></span>
<span id="cb1-2"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">✅</span> Success</span></code></pre></div></div>
</div>
</section>
<section id="gistjacking" class="level2">
<h2 class="anchored" data-anchor-id="gistjacking">Gistjacking</h2>
<p>Well, there’s a bit more to it than that.</p>
<p>There are some obvious checks and inputs required when <code>jot -u</code> is run. The <code>upload_jottings()</code> function has a series of hoops:</p>
<ol type="1">
<li>Is the GitHub CLI gh installed? If not, <a href="https://cli.github.com/">prompt installation</a>.</li>
<li>Is there an internet connection? You can’t upload if not, obv.</li>
<li>Is the user logged into a GitHub account? If not, prompt <code>gh auth login</code>.</li>
<li>Is there a <code>GIST_ID</code> value in the config file? If not, prompt for it (it’s the 32-character string in the gist’s URL, or found via <code>gh gist list</code>).</li>
<li>Does the <code>GIST_ID</code> match an actual gist ID in your account? The gist must already exist.</li>
<li>Is there a jot file path recorded in the config? If not, prompt the user to create one.</li>
</ol>
<p>If that’s all cleared, it runs <code>subprocess.run(["gh", "gist", "edit", gist_id, jot_path])</code> to upload the contents of the jot file. This is equivalent to running the equivalent <code>gh</code> command directly from the terminal, but with extra defensive steps.</p>
<p>In other words, I’ve hijacked gh to do all the hard work, lol. This is great, because I want jot to be minimal and require little maintenance. Thanks Michaelsoft.</p>
</section>
<section id="i-jot-u" class="level2">
<h2 class="anchored" data-anchor-id="i-jot-u">I jot u</h2>
<p>It’s a clear limitation that <code>--upload</code> only uploads to a GitHub gist. I’m a simple chap, this is fine for me for now.</p>
<p>But the interface could be tweaked in future to allow for other hosts. Maybe in the form <code>jot --upload &lt;host&gt;</code>. The CLI logic could react to <code>&lt;host&gt;</code> to decide what credentials and values are needed for the upload.</p>
<p>Oh no, what if someone <a href="https://github.com/matt-dray/jot">contributed to my poor little project</a> with their favoured host? 🥺</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>[project]
name = "2026-06-06-jot-gist"
version = "0.1.0"
requires-python = "&gt;=3.12"
dependencies = []</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>To prevent gif-style flamewars, it’s pronounced ‘gist’.↩︎</p></li>
<li id="fn2"><p>This is why people caveat their work with ‘opinionated’ by the way; it translates to ‘works for me, lol’.↩︎</p></li>
<li id="fn3"><p>You could <a href="https://pipx.pypa.io/stable/">install with pipx</a> if you prefer.↩︎</p></li>
<li id="fn4"><p>The stable version is <a href="https://pypi.org/project/jot-cli/">available from PyPi</a> like <code>uv tool install jot-cli</code>.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div></div></section></div> ]]></description>
  <category>cli</category>
  <category>github</category>
  <category>jot</category>
  <category>python</category>
  <guid>https://www.rostrum.blog/posts/2026-06-06-jot-gist/</guid>
  <pubDate>Fri, 05 Jun 2026 23:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2026-06-06-jot-gist/resources/jot-gist.png" medium="image" type="image/png" height="58" width="144"/>
</item>
<item>
  <title>Eating dogfood 1000 times</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2026-05-31-dogfood/</link>
  <description><![CDATA[ 





<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2026-05-31-dogfood/resources/jot-brain.png" class="img-fluid quarto-figure quarto-figure-left figure-img" alt="A cartoon brain with a thought bubble containing another brain."></p>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>I’ve now used my CLI tool <a href="https://github.com/matt-dray/jot">jot</a> nearly 1200 times. It’s been a useful low-friction way to remember what I’ve been up to for later reflection.</p>
</section>
<section id="one-shot-jot" class="level2">
<h2 class="anchored" data-anchor-id="one-shot-jot">One-shot jot</h2>
<p>I’ve <a href="https://www.rostrum.blog/index.html#category=jot">written about jot before</a>: my very simple and opinionated Python command-line tool for jotting down little timestamped notes to myself.</p>
<p>In an open terminal I can just:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jot</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"put one of the new coversheets on the TPS reports"</span></span></code></pre></div></div>
</div>
<p>And it gets automatically prepended to a text file with the date and time.</p>
<p>Rinse and repeat throughout the day.</p>
</section>
<section id="a-jot-of-bother" class="level2">
<h2 class="anchored" data-anchor-id="a-jot-of-bother">A jot of bother</h2>
<p>Regular readers know that I make stuff and… move on.</p>
<p>Sike! I’ve actually been using this one a whole bunch.</p>
<p>In fact, I’ve been jotting things down every work day since August 2025 and have banked well over 1000 jottings.</p>
<p>But has it actually been useful?</p>
</section>
<section id="forget-me-jot" class="level2">
<h2 class="anchored" data-anchor-id="forget-me-jot">Forget me jot</h2>
<p>I have a leaky brain.</p>
<p>This causes a problem when I try to write weeknotes and timesheets. It also means I struggle to reflect on my achievements, which is pertinent given my upcoming annual appraisal.</p>
<p>So, for these past months, I’ve been taking a couple of seconds to jot down what I’m doing as I’m doing it.</p>
<p>I’ve been writing things in the style of git commit messages: they start with a verb and are short and sweet. For consistency, I’ve been referring to the name of a repository where possible and writing down colleagues’ names. For example: <code>jot "released version 1.0 of the package"</code> and <code>jot "called morag to discuss app development"</code><sup>1</sup>.</p>
<p>Typically they’re about what I <em>did</em>, rather than the outcome. I also record little human interactions and feedback, which can otherwise fall through the cracks.</p>
<p>At the most basic level, the simple act of writing things down has helped me recognise the volume and variety of my tasks. Meanwhile, jot’s options like <code>--list</code> and <code>--search</code> (with <code>--from-date</code> and <code>--to-date</code>) have helped me filter down that large volume of information when I want to get more analytical<sup>2</sup>.</p>
<p>This is not a comprehensive business tool for reminders, to-do lists, minutes and actions. It’s just a brain outside my brain.</p>
<p>Switch to the terminal; write a sentence; go back to work. And now I have 1194 reminders of what I’ve been up to. Little effort to help plug a big gap.</p>
</section>
<section id="in-the-pypiline" class="level2">
<h2 class="anchored" data-anchor-id="in-the-pypiline">In the PyPIline</h2>
<p>I started developing jot to learn ‘how to Python’ and write CLIs. I think that’s a success, but where next?</p>
<p>Well, I’ve formalised its existence by uploading it to the Python Package Index (PyPI) to make it’s more readily available.</p>
<p>You can now find it as <a href="https://pypi.org/project/jot-cli/">jot-cli on PyPI</a>, as of version 0.3.0. Yes, you have to install ‘jot-cli’ rather than ‘jot’, because the name is already taken on PyPI. But the command still remains <code>jot</code> in the terminal<sup>3</sup>.</p>
<p>You could install<sup>4</sup> with <a href="https://docs.astral.sh/uv/getting-started/installation/">uv</a><sup>5</sup>:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">uv</span> tool install jot-cli</span></code></pre></div></div>
</div>
<p>Then, to upgrade:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">uv</span> tool upgrade jot-cli</span></code></pre></div></div>
</div>
<p>The development version is <a href="https://github.com/matt-dray/jot">available from GitHub</a> like:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">uv</span> tool install git+https://github.com/matt-dray/jot</span></code></pre></div></div>
</div>
<p>Note that the GitHub repo is still called ‘jot’ despite it having to be called ‘jot-cli’ on PyPI<sup>6</sup>.</p>
</section>
<section id="thnks-fr-th-mmrs" class="level2">
<h2 class="anchored" data-anchor-id="thnks-fr-th-mmrs">Thnks fr th Mmrs</h2>
<p>Right now it works for me, which is the bottom line.</p>
<p>I know some others have had a go and found it useful too. Thanks in particular to:</p>
<ul>
<li><a href="https://github.com/oiffrig">Olivier</a> for submitting code to let the list of jottings to be filtered by date</li>
<li>my <a href="https://the-strategy-unit.github.io/data_science/about.html">work colleagues</a> (especially <a href="https://github.com/francisbarton">Fran</a> who have provided feedback in our ‘sidequests and suport’ sessions.</li>
<li><a href="https://github.com/adrianadepalma">Adriana</a> for blue-sky thinking (but mostly for listening to me ramble on about it)</li>
</ul>
<p>I want functionality to remain simple, so future updates will mostly be about improving the underlying code and user interface rather than adding entirely new features.</p>
<p>I have, however, been developing an option to upload the file to a GitHub gist as backup. But maybe that’s too radical.</p>
</section>
<section id="thats-yer-jot" class="level2">
<h2 class="anchored" data-anchor-id="thats-yer-jot">That’s yer jot</h2>
<p>So, is jot a world-beating bit of software? No, there’s plenty of similar and more powerful tools out there<sup>7</sup>.</p>
<p>And <a href="https://en.wikipedia.org/wiki/Eating_your_own_dog_food">has the dogfood been delicious</a>? Aye, gourmet, and I’ll continue to chow down.</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb5-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jot</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"wrote a blog post about jot's release to PyPI"</span></span></code></pre></div></div>
</div>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>[project]
name = "2026-05-31-dogfood-1000"
version = "0.1.0"
requires-python = "&gt;=3.12"
dependencies = []</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Not actual colleagues.↩︎</p></li>
<li id="fn2"><p>You could, for example, ask generative AI to pull out topics for you, but I quite like to eyeball it.↩︎</p></li>
<li id="fn3"><p>Awkwardly, there’s an inbuilt macOS tool called jot, so you may get a nameclash. Most likely the installed tool will take precedence over the inbuilt one when you type <code>jot</code>, but you could set an alias to avoid this. I did think about changing the name to something else entirely, but ‘jot’ works well.↩︎</p></li>
<li id="fn4"><p>Note that it has to be installed as a ‘tool’ because it’s intended for command-line use; it’s not a Python package, per se (though you could use it as one if you wanted).↩︎</p></li>
<li id="fn5"><p>Non-uv users may prefer to use <a href="https://pipx.pypa.io/">pipx</a>.↩︎</p></li>
<li id="fn6"><p>For consistency, I may eventually change the name of the repo to ‘jot-cli’ too.↩︎</p></li>
<li id="fn7"><p>The most interesting thing I’ve seen recently for note-taking (and much more) is <a href="https://github.com/xwmx/nb">nb</a>.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div></div></section></div> ]]></description>
  <category>cli</category>
  <category>jot</category>
  <category>python</category>
  <category>uv</category>
  <guid>https://www.rostrum.blog/posts/2026-05-31-dogfood/</guid>
  <pubDate>Sat, 30 May 2026 23:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2026-05-31-dogfood/resources/jot-fib.png" medium="image" type="image/png"/>
</item>
<item>
  <title>Shaka, when the walls fell</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2026-03-15-tyle-walls/</link>
  <description><![CDATA[ 





<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2026-03-15-tyle-walls/resources/tyle-walls.gif" class="img-fluid figure-img" style="width:75.0%" alt="A 10 by 20 grid of characters printed to a terminal. Tiles are mostly question-mark characters (unknown tiles). Near the middle is an 'at' symbol (the player), surrounded within two tiles of periods (floor tiles) and hash marks (wall tiles). Underneath is a prompt for the player. The player moves around and destroys walls by inputting 'b' and builds them by typing 'p'."></p>
<figcaption>Temba, his arms wide.</figcaption>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>I messed around again with <a href="https://github.com/matt-dray/tyle">tyle</a>, my Python-CLI roguelike-like. Now it has godlike destruction and creation.</p>
</section>
<section id="the-beast-at-tanagra" class="level2">
<h2 class="anchored" data-anchor-id="the-beast-at-tanagra">The beast at Tanagra</h2>
<p>Before now, you could install <a href="https://github.com/matt-dray/tyle">tyle</a><sup>1</sup> and type <code>tyle</code> into your terminal to generate a tile-grid with a player, surrounded by floor (<code>.</code>) and wall (<code>#</code>) tiles, as well as ‘fog of war’ (<code>?</code>) beyond a certain distance.</p>
<pre><code>? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . # . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . @ . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
Move (WASD+Enter): </code></pre>
<p>What next? I wanted to start thinking about enemies, but I did this with <a href="https://github.com/matt-dray/r.oguelike">r.oguelike</a> already and wanted to try something different first.</p>
<p>I’m old enough to remember when Minecraft was first cool. What is Minecraft? Break block make block.</p>
<p>We can mimic this in two dimensions pretty easily.</p>
</section>
<section id="jinza-when-the-truth-was-uncovered" class="level2">
<h2 class="anchored" data-anchor-id="jinza-when-the-truth-was-uncovered">Jinza, when the truth was uncovered</h2>
<p>In tyle, the underlying map is a <code>TileGrid</code>-class object, which is just a list of lists of <code>Tile</code>-class objects. These tiles currently take one of two values: they’re traversable floor (<code>.</code>) or untraversable wall (<code>#</code>).</p>
<p>What if you could just… remove the obstruction?</p>
<p>Well now you can. For example:</p>
<pre><code>? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . @ # . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
Facing: right
Move [wasd], [b]reak, [p]lace, [q]uit: </code></pre>
<p>We’re facing right and the adjacent tile is a wall. Inputting <kbd>b</kbd> will <u>b</u>reak the wall.</p>
<pre><code>? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . @ . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
Facing: right
Move [wasd], [b]reak, [p]lace, [q]uit: </code></pre>
<p>What’s actually happening is that the tile adjacent to the player’s direction is being replaced with a floor tile in the <code>TileGrid</code>.</p>
<p>To help the player, I’ve added to the UI a reminder of which way the player is facing, decided by the last direction of travel.</p>
<p>In <code>tile.py</code>, we can interpret an input of <kbd>b</kbd> to mean ‘update the <code>Tile</code> in the specified location’:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> move <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"b"</span>:</span>
<span id="cb4-2">    row_change, col_change <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> direction_changes[<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.direction]</span>
<span id="cb4-3">    target_row <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.player.row <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> row_change</span>
<span id="cb4-4">    target_col <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.player.col <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> col_change</span>
<span id="cb4-5">    </span>
<span id="cb4-6">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> (</span>
<span id="cb4-7">        <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> target_row <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.n_rows <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">and</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> target_col <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.n_cols</span>
<span id="cb4-8">    ):</span>
<span id="cb4-9">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb4-10"></span>
<span id="cb4-11">    target_tile <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tiles[target_row][target_col]</span>
<span id="cb4-12">    </span>
<span id="cb4-13">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> target_tile.symbol <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tileset[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"wall"</span>]:</span>
<span id="cb4-14">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tiles[target_row][target_col] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Tile(</span>
<span id="cb4-15">            <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tileset[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"floor"</span>], <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb4-16">        )</span>
<span id="cb4-17">    </span>
<span id="cb4-18">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span></code></pre></div></div>
<p>In short: if the player submits <kbd>b</kbd>, then replace the tile in the direction they’re facing with a floor tile. This gives the illusion that the wall has been destroyed.</p>
</section>
<section id="sokath-his-eyes-uncovered" class="level2">
<h2 class="anchored" data-anchor-id="sokath-his-eyes-uncovered">Sokath, his eyes uncovered</h2>
<p>Naturally, we can do this in reverse. Instead of breaking a wall block, we can build one.</p>
<p>Pressing <kbd>p</kbd> will <u>p</u>lace a wall tile in the direction we’re facing (right in this example).</p>
<pre><code>? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . @ # . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
Facing: right
Move [wasd], [b]reak, [p]lace, [q]uit: </code></pre>
<p>This requires a small change to the code, allowing for a wall <code>Tile</code> to be added to the <code>TileGrid</code> rather than removed:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> move <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> [<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"b"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"p"</span>]:</span>
<span id="cb6-2">    row_change, col_change <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> direction_changes[<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.direction]</span>
<span id="cb6-3">    target_row <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.player.row <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> row_change</span>
<span id="cb6-4">    target_col <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.player.col <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> col_change</span>
<span id="cb6-5">    </span>
<span id="cb6-6">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">not</span> (</span>
<span id="cb6-7">        <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> target_row <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.n_rows <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">and</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;=</span> target_col <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&lt;</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.n_cols</span>
<span id="cb6-8">    ):</span>
<span id="cb6-9">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb6-10"></span>
<span id="cb6-11">    target_tile <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tiles[target_row][target_col]</span>
<span id="cb6-12">    </span>
<span id="cb6-13">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> move <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"b"</span>:</span>
<span id="cb6-14">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> target_tile.symbol <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tileset[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"wall"</span>]:</span>
<span id="cb6-15">            <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tiles[target_row][target_col] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Tile(</span>
<span id="cb6-16">                <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tileset[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"floor"</span>], <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb6-17">            )</span>
<span id="cb6-18">    </span>
<span id="cb6-19">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> move <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"p"</span>:</span>
<span id="cb6-20">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> target_tile.symbol <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tileset[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"floor"</span>]:</span>
<span id="cb6-21">            <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tiles[target_row][target_col] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Tile(</span>
<span id="cb6-22">                <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tileset[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"wall"</span>], <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span></span>
<span id="cb6-23">            )</span>
<span id="cb6-24">    </span>
<span id="cb6-25">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span></code></pre></div></div>
</section>
<section id="jerna-before-the-dawn" class="level2">
<h2 class="anchored" data-anchor-id="jerna-before-the-dawn">Jerna, before the dawn</h2>
<p>At the moment, destruction and creation are infinite (within the confines of the tilegrid). You can break and build as many wall blocks as you like. Such omnipotence.</p>
<p>In future I want to add this to an inventory system. Break one wall? Now you can make one wall. No more, no less.</p>
<p>Perfectly balanced.</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered:2026-03-14 23:24:41 GMT

[project]
name = "2026-03-08-tyle-fog"
version = "0.1.0"
requires-python = "&gt;=3.12"
dependencies = []</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>You could install the development version like <code>uv tool install git+https://github.com/matt-dray/tyle.git</code>. But don’t install things from people you don’t trust.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div></div></section></div> ]]></description>
  <category>cli</category>
  <category>python</category>
  <category>tyle</category>
  <category>uv</category>
  <guid>https://www.rostrum.blog/posts/2026-03-15-tyle-walls/</guid>
  <pubDate>Sun, 15 Mar 2026 00:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2026-03-15-tyle-walls/resources/tyle-walls.gif" medium="image" type="image/gif"/>
</item>
<item>
  <title>Fog of war</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2026-03-08-tyle-fog/</link>
  <description><![CDATA[ 





<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2026-03-08-tyle-fog/resources/tyle-fog.gif" class="img-fluid figure-img" style="width:75.0%" alt="A 10 by 20 grid of characters printed to a macOS terminal. Tiles are mostly question-mark characters (unknown tiels). Near the midlle is an 'at' symbol (the player), surrounded within two tiles of the grid with periods (floor tiles) and hash marks (wall tiles). A prompt underneath reads 'Move (WASD)+Enter:', ready for user input. The player moves around and reveals the map as they go."></p>
<figcaption>Haven’t the foggiest about this terrain.</figcaption>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p><a href="https://github.com/matt-dray/tyle">tyle</a>, my Python-CLI roguelike-like, now has ‘fog of war’.</p>
</section>
<section id="grid-and-bare-it" class="level2">
<h2 class="anchored" data-anchor-id="grid-and-bare-it">Grid and bare it</h2>
<p>You may remember <a href="https://www.rostrum.blog/posts/2026-02-01-tyle/">my tyle concept</a>: the beginnings of an in-terminal tile-based <a href="https://en.wikipedia.org/wiki/Roguelike">roguelike</a> to help me improve my Python CLI skills.</p>
<p>You could install the development version like <code>uv tool install git+https://github.com/matt-dray/tyle.git</code> and initiate by typing <code>tyle</code> in a terminal.</p>
<p>Before now, you’d get something like this:</p>
<pre><code>. . . # # . # . . . . . . . # . . . . . 
. . . . . . . . . . . . . # . . . . . . 
# . . . . . . . . . . . . # . . . . . . 
. . # # . # . . . . . . . . . . . . . . 
. . . . . . . . . . @ . . . . . . . . . 
. . . . . . . . . . # . . # . . . . . . 
. . . . . . . . . # # # . . . # . . . # 
. . . . . . # . . . . . . # . . . . . # 
. # . . # . . . . . . . . . . . . . . . 
Move (WASD+Enter): </code></pre>
<p>There’s a player (<code>@</code>), floor tiles (<code>.</code>), and randomly-placed non-traversable wall tiles (<code>#</code>). You move the player by typing at the prompt.</p>
<p>It’ll be in colour if your terminal supports <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI escape codes</a>.</p>
</section>
<section id="the-big-reveal" class="level2">
<h2 class="anchored" data-anchor-id="the-big-reveal">The big reveal</h2>
<p>Obviously tyle could improve in many ways. For example, the development of enemies and pathfinding, procedural dungeons, or an inventory system.</p>
<p>A quick win is ‘fog of war’<sup>1</sup>.</p>
<p>This phrase refers to <a href="https://en.wikipedia.org/wiki/Fog_of_war">uncertainty when drawing up battle plans</a>, but is also a mechanic in many videogames, including roguelikes.</p>
<p>A classic example is in <a href="https://en.wikipedia.org/wiki/Civilization_(series)">the Civilization series</a> of games. When you start, you cannot see the content of tiles you haven’t visited. The tiles are literally <a href="https://civilization.fandom.com/wiki/Fog_of_war">shrouded in fog</a>. That keeps things interesting: will the next tile contain a barbarian hoarde or a potential city-founding location?</p>
</section>
<section id="fog-lights" class="level2">
<h2 class="anchored" data-anchor-id="fog-lights">Fog lights</h2>
<p>We can introduce fog into tyle with little effort.</p>
<p>For lore reasons, imagine the player character is carrying a lamp that illuminates a certain number of tiles in any direction<sup>2</sup>.</p>
<p>How can we achieve this practically?</p>
<p>You may remember that tyle has a <code>TileGrid</code> class. It has a <code>draw()</code> method to generate and print the current state of the tile map.</p>
<p>Previously, we drew the underlying terrain in the map by default, which is composed of the floor (symbol <code>.</code>) and wall (<code>#</code>) symbols. The only exception was to draw the player in the location given by the <code>Player</code>-class <code>row</code> and <code>column</code> properties.</p>
<p>We can intercept this logic to draw fog-of-war symbols by default, unless within a certain tile-range of the player, in which case we draw the terrain.</p>
<p>For maximum portability—and as a hat-tip to the original roguelikes—we’ve been using <a href="en.wikipedia.org/wiki/ASCII">ASCII symbols</a> as ‘graphics’. We may as well continue for the fog.</p>
<p>I couldn’t think of anything cleverer than simply putting a <code>?</code>, geddit. Also it’s coloured grey becuase that seems right.</p>
</section>
<section id="sound-the-foghorn" class="level2">
<h2 class="anchored" data-anchor-id="sound-the-foghorn">Sound the foghorn</h2>
<p>And so the relevant <code>for</code>-loop in the <code>TileGrid</code> class takes this shape:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb2-1"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> row <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.n_rows):</span>
<span id="cb2-2">    row_symbols <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> []</span>
<span id="cb2-3">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> col <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.n_cols):</span>
<span id="cb2-4">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.player.row <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> row <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">and</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.player.col <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> col:</span>
<span id="cb2-5">            symbol <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.player.symbol</span>
<span id="cb2-6">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">elif</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">max</span>(</span>
<span id="cb2-7">            <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">abs</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.player.row <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> row), <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">abs</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.player.col <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> col)</span>
<span id="cb2-8">        ) <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.fog):</span>
<span id="cb2-9">            symbol <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.tiles[row][col].symbol</span>
<span id="cb2-10">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span>:</span>
<span id="cb2-11">            symbol <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"?"</span></span></code></pre></div></div>
<p>So, for each row and column, print the symbol for the:</p>
<ul>
<li>player (<code>@</code>) if matching <code>Player</code>-class properties <code>row</code> and <code>column</code></li>
<li>terrain tile (<code>.</code> or <code>#</code>) if within <code>fog</code> number of tiles from the player</li>
<li>fog (<code>?</code>) if no other conditions are met (i.e.&nbsp;all the remaining tiles)</li>
</ul>
<p>In <code>main()</code> I’ve set <code>fog = args.fog</code> with <code>default=2</code>, so we’ll see fog beyond two tiles of the player. You can adjust the vision range in the CLI with the <code>--fog</code> (<code>-f</code>) option, like <code>tyle -f 3</code>.</p>
<p>The default gives us something like:</p>
<pre><code>? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . # . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . @ . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? . . . # . ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
Move (WASD+Enter): </code></pre>
<p>And moving left two steps gives us:</p>
<pre><code>? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? # . . . # ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? . . @ . . ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? # . . . . ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? . . . . . ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 
Move (WASD+Enter): </code></pre>
<p>Ooh, mysterious.</p>
</section>
<section id="mist-opportunity" class="level2">
<h2 class="anchored" data-anchor-id="mist-opportunity">Mist opportunity?</h2>
<p>Your ability to see <em>n</em> tiles around the player-character is pretty rudimentary. It implies that you, the player, have an omniscient top-down view of the playing area<sup>3</sup>… which you do, but that’s not very immersive<sup>4</sup>.</p>
<p>More advanced approaches might include a ‘cone of vision’. The player can only see what’s in their line of sight. In other words, they can’t see through walls.</p>
<p>Woah, woah. We’re not coding a full game engine from scratch.</p>
<p>Or are we, haha?</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered:2026-03-10 09:08:23 GMT

[project]
name = "2026-03-08-tyle-fog"
version = "0.1.0"
requires-python = "&gt;=3.12"
dependencies = []</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Coincidentally (?), <a href="https://www.rostrum.blog/posts/2026-03-01-weva/">my last post</a> was about <a href="https://github.com/matt-dray/weva">the {weva} CLI</a>, which fetches a weather report. And actually it is a bit misty here tonight.↩︎</p></li>
<li id="fn2"><p>I mean, look, the hero is carrying a little lamp, right? Squint here: <code>@</code>.↩︎</p></li>
<li id="fn3"><p><a href="https://www.gutenberg.org/ebooks/201">Read Flatland</a> by Edwin A Abbott.↩︎</p></li>
<li id="fn4"><p>How immersive can a grid of tiles in a terminal be? You tell me.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div></div></section></div> ]]></description>
  <category>cli</category>
  <category>python</category>
  <category>tyle</category>
  <category>uv</category>
  <guid>https://www.rostrum.blog/posts/2026-03-08-tyle-fog/</guid>
  <pubDate>Sun, 08 Mar 2026 00:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2026-03-08-tyle-fog/resources/tyle-fog.gif" medium="image" type="image/gif"/>
</item>
<item>
  <title>A tiny CLI weather {Rapp}ort</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2026-03-01-weva/</link>
  <description><![CDATA[ 





<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2026-03-01-weva/resources/weva.png" class="img-fluid quarto-figure quarto-figure-left figure-img" style="width:100.0%" alt="A screenshot of a terminal that's running the comand 'weva tr197aa -h 24 -e', which returns a one-line weather report with temperature and weather emojis for now and a day later, plus the temperature extreems for today. The temperature values are bold and coloured."></p>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>The <a href="https://github.com/matt-dray/weva">{weva} R package</a> contains a titchy command-line interface (CLI) to present an opinionated micro-weather report.</p>
</section>
<section id="building-some-rapport" class="level2">
<h2 class="anchored" data-anchor-id="building-some-rapport">Building some {Rapp}ort</h2>
<p>I’m spending more time in the terminal.</p>
<p>So I’ve been making little CLI concepts:</p>
<ul>
<li><a href="https://www.rostrum.blog/posts/2025-08-25-jot/">jot</a> for recording notes</li>
<li><a href="https://www.rostrum.blog/posts/2025-10-30-pet/">pet</a> for a persistent cyberpet</li>
<li><a href="https://www.rostrum.blog/posts/2026-02-01-tyle/">tyle</a> for a roguelike-like</li>
</ul>
<p>These are all written in Python, which seems a more natural choice for this purpose than R<sup>1</sup>.</p>
<p>But I was pleased to see a recent <a href="https://tidyverse.org/blog/2026/02/rapp-0-3-0/">blog post</a> about a new release of <a href="https://github.com/r-lib/Rapp">{Rapp}</a>, an R package to help create CLIs.</p>
<p>So obviously I needed to try it out.</p>
</section>
<section id="climate" class="level2">
<h2 class="anchored" data-anchor-id="climate">CLImate</h2>
<p>But a CLI for what?</p>
<p>My colleagues and I have enjoyed twee weather CLIs of late, like <a href="https://github.com/Veirt/weathr">weathr</a> and <a href="https://github.com/chubin/wttr.in">wttr.in</a>.</p>
<p>So, because I have no imagination: introducing the concept <a href="https://github.com/matt-dray/weva/">{weva}</a> (‘wevvah’<sup>2</sup>) R package.</p>
<p>It does way less than those other examples and doesn’t have fun ASCII graphics. It’s just a simple toy project to see how {Rapp} works, with no guarantees. Is that enough disclaimers?</p>
<p>Along with {Rapp}, this was made possible by the:</p>
<ul>
<li>free APIs <a href="https://open-meteo.com/">Open-Meteo</a> and <a href="https://postcodes.io/">postcodes.io</a><sup>3</sup>, which do not require a login</li>
<li>the tidyverse packages <a href="https://httr2.r-lib.org/">{httr2}</a> and <a href="https://lubridate.tidyverse.org/">{lubridate}</a></li>
</ul>
<section id="r-console" class="level3">
<h3 class="anchored" data-anchor-id="r-console">R console</h3>
<p>From the R console, you could install<sup>4</sup> {weva} from GitHub like:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1">pak<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">pak</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"matt-dray/weva"</span>)</span></code></pre></div></div>
<p>You then use a {weva} function to install the CLI tool:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1">weva<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install_cli</span>()</span></code></pre></div></div>
<p>This will print the install location of the CLI, if successful.</p>
</section>
<section id="terminal" class="level3">
<h3 class="anchored" data-anchor-id="terminal">Terminal</h3>
<p>From a terminal—not the R console—you can use <code>weva</code> to look up weather for a given UK postcode<sup>5</sup>.</p>
<p>Let’s compare Lands End (‘top’ of mainland Britain) to John o’ Groats (‘bottom’)<sup>6</sup> as I write this.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">weva</span> tr197aa</span></code></pre></div></div>
<pre><code>now 10.9°C ☁️ | +1h 11.3°C ☁️ </code></pre>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb5-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">weva</span> kw14yr</span></code></pre></div></div>
<pre><code>now 6.1°C ☁️ | +1h 6.3°C 🌧️🪶 </code></pre>
<p>The one-line display has segments for the current and future temperature and weather. There are emoji modifiers for heavy, light and freezing versions of some weather types.</p>
<p>Surprise, it’s early March everywhere in Britain.</p>
<p>Hopefully your day will be brightened by the styling with <a href="https://en.wikipedia.org/wiki/ANSI_escape_code#Colors">ANSI escape codes</a>, assuming your terminal supports them.</p>
<section id="options" class="level4">
<h4 class="anchored" data-anchor-id="options">Options</h4>
<p>You can add some flags to adjust the settings. Use <code>--hour</code> (or its shortcut <code>-h</code>) to adjust the number of hours for the forward-look. Let’s look one day ahead.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb7-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">weva</span> tr197aa <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-h</span> 24</span></code></pre></div></div>
<pre><code>now 10.9°C ☁️ | +24h 7.7°C 🌧️🪶 </code></pre>
<p>So there’s light drizzle this time tomorrow and it will be cooler (did I mention it’s early March in Britain).</p>
<p>Note that you can only look at forecasts up to the end of the day after tomorrow<sup>7</sup>.</p>
<p>Use the <code>--extremes</code> (<code>-e</code>) flag to also print a segment with today’s low and high temperatures. You can compound these options:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb9-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">weva</span> tr197aa <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-h</span> 24 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span></span></code></pre></div></div>
<pre><code>now 10.9°C ☁️ | +24h 7.7°C 🌧️🪶 | today 9.3°C to 12°C </code></pre>
<p>Well, ‘extremes’ is perhaps a bit extreme for this temperature range.</p>
</section>
<section id="time-is-hard" class="level4">
<h4 class="anchored" data-anchor-id="time-is-hard">Time is hard</h4>
<p>‘Now’ and ‘later’ in this context are actually a bit awkward.</p>
<p>The Open-Meteo API returns the ‘current’ time as a quarter-hour increment, while ‘hourly’ information is the top of the hour.</p>
<p>My low-effort solution was to round ‘now’ to the nearest hour and declare ‘+1 hour’ as the hour after that.</p>
<p>In addition, the hourly data is returned for the whole of the current day (i.e.&nbsp;some of it is from the past). That means we have to match the rounded ‘now’ time to the list of times for today to get its index, then add the user-supplied <code>--hours</code> to find the details for the correct time in the future.</p>
<p>So you can only ask for a forecast for the rest of today or the following two full days.</p>
<p>That’s a bit confusing, so you can use the <code>--datetime</code> (<code>-d</code>) option to expose the datetimes for the data in each segment of the report.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb11-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">weva</span> tr197aa <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-h</span> 24 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-e</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-d</span></span></code></pre></div></div>
<pre><code>2026-03-01 10:00 10.9°C 🌧️🪶 | 2026-03-02 10:00 7.7°C 🌧️🪶 | 2026-03-01 9.3°C to 12°C </code></pre>
<p>As I write, it’s 10:06 on 1 March 2026. That’s rounded to 10:00 for the ‘now’ time. And so 24 hours from now is 10:00 tomorrow: 2 March 2026.</p>
</section>
</section>
</section>
<section id="inside-the-crystal-ball" class="level2">
<h2 class="anchored" data-anchor-id="inside-the-crystal-ball">Inside the crystal ball</h2>
<p>What’s actually happening in the code?</p>
<p>{weva} is just a normal R package with some exported functions to <code>get_latlon()</code>, <code>get_weather()</code> and <code>write_report()</code>.</p>
<p>But there’s one difference: <a href="https://github.com/matt-dray/weva/blob/main/exec/weva.R">a script</a> under <code>exec/</code> that contains R code that {Rapp} will reinterpret (magically?) into a CLI.</p>
<p>At time of writing it looks like this:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb13" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb13-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/usr/bin/env Rapp</span></span>
<span id="cb13-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#| name: weva</span></span>
<span id="cb13-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#| description: A micro weather report with 'Open-Meteo' and 'postcodes.io' APIs</span></span>
<span id="cb13-4"></span>
<span id="cb13-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#| description: A UK postcode</span></span>
<span id="cb13-6">postcode <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">NULL</span></span>
<span id="cb13-7"></span>
<span id="cb13-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#| description: Hours from now for the 'later' segment (up to three days)</span></span>
<span id="cb13-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#| short: h</span></span>
<span id="cb13-10">hours <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>L</span>
<span id="cb13-11"></span>
<span id="cb13-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#| description: Show API and calculated datetimes in 'now' and 'later' segments?</span></span>
<span id="cb13-13"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#| short: d</span></span>
<span id="cb13-14">datetimes <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span></span>
<span id="cb13-15"></span>
<span id="cb13-16"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#| description: Show a segment for today's min and max temperatures?</span></span>
<span id="cb13-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#| short: e</span></span>
<span id="cb13-18">extremes <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span></span>
<span id="cb13-19"></span>
<span id="cb13-20"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">get_latlon</span>(postcode) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb13-21">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">get_weather</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb13-22">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">write_report</span>(</span>
<span id="cb13-23">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">hours_to_forecast =</span> hours,</span>
<span id="cb13-24">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">show_datetimes =</span> datetimes,</span>
<span id="cb13-25">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">show_extremes =</span> extremes</span>
<span id="cb13-26">  ) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb13-27">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">cat</span>()</span></code></pre></div></div>
<p>Note in this file:</p>
<ul>
<li>the <a href="https://en.wikipedia.org/wiki/Shebang_(Unix)">shebang</a> (<code>#!</code>)<sup>8</sup>, which gives the path to where {Rapp} is installed</li>
<li>the hashpipes (<code>#|</code>) for documenting the CLI and its options</li>
<li>that everything else is normal R code</li>
</ul>
<p>{Rapp} interprets <code>postcode &lt;- NULL</code> as a required positional argument for <code>weva</code>. You can just type the postcode without a flag.</p>
<p>Conversely, the remaining options must be named, though <code>short</code> names can be used to save typing. <code>--hours</code> (<code>-h</code>) is interpreted as an integer with a default of <code>1</code> <code>--datetimes</code> and <code>--extremes</code> (<code>-d</code>, <code>-e</code>) are off (<code>FALSE</code>) by default, but the user can just use the flag without an argument to turn it on.</p>
<p>What’s also nice is that {Rapp} gives us <code>--help</code> (and <code>--help-yaml</code>) for free, based on our variable documentation.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb14" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb14-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">weva</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--help</span></span></code></pre></div></div>
<pre><code>Usage: weva [OPTIONS] &lt;POSTCODE&gt;

A micro weather report with 'Open-Meteo' and 'postcodes.io' APIs

Options:
  -h, --hours &lt;HOURS&gt;             Hours from now for the 'later' segment (up to
                                  three days)
                                  [default: 1] [type: integer]
  -d, --datetimes / --no-datetimes  Show API and calculated datetimes in 'now'
                                  and 'later' segments?
                                  [default: false]
                                  Enable with `--datetimes`.
  -e, --extremes / --no-extremes  Show a segment for today's min and max
                                  temperatures?
                                  [default: false]
                                  Enable with `--extremes`.

Arguments:
  &lt;POSTCODE&gt;  A UK postcode</code></pre>
<p>In short, {Rapp} lets you write ‘normal’ looking R code that gets interpreted into a CLI with various types of options.</p>
<p>That’s quite a neat, low-effort way to create a CLI.</p>
</section>
<section id="rapping-up" class="level2">
<h2 class="anchored" data-anchor-id="rapping-up">{Rapp}ing up</h2>
<p>{weva} is [repeated disclaimers]. It would be easy to get carried away and build something even more complex. I’ve achieved the content and learning I wanted, so I stop here.</p>
<p>In fact, I already used this tool yesterday before a trip<sup>9</sup>. I discovered the day was going to be overcast with some showers. Who could have guessed?</p>
<p>But on more variable days, I’m reminded of <a href="https://github.com/francisbarton">Fran</a>’s observation when faced with differing forecasts:</p>
<blockquote class="blockquote">
<p>I had better go outside and see who’s right</p>
</blockquote>
<p>Very wise.</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered: 2026-03-07 13:30:38 GMT</code></pre>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.5.2 (2025-10-31)
Platform: aarch64-apple-darwin20
Running under: macOS Sequoia 15.6.1

Matrix products: default
BLAS:   /System/Library/Frameworks/Accelerate.framework/Versions/A/Frameworks/vecLib.framework/Versions/A/libBLAS.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.5.2    fastmap_1.2.0     cli_3.6.5        
 [5] tools_4.5.2       htmltools_0.5.8.1 yaml_2.3.12       rmarkdown_2.29   
 [9] knitr_1.50        jsonlite_2.0.0    xfun_0.52         digest_0.6.37    
[13] rlang_1.1.7       evaluate_1.0.4   </code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>I don’t want to simply run scripts from the command line, which is what <code>Rscript</code> is for.↩︎</p></li>
<li id="fn2"><p>With a respectful cockney accent.↩︎</p></li>
<li id="fn3"><p>Packages <a href="https://github.com/tpisel/openmeteo">{openmeteo}</a> and <a href="https://docs.ropensci.org/PostcodesioR/index.html">{PostcodesioR}</a> wrap their respective APIs, but I minimised the dependencies by querying myself with {httr2}.↩︎</p></li>
<li id="fn4"><p>Remember: you shouldn’t trust ‘some dude’ telling you to install random stuff from the internet.↩︎</p></li>
<li id="fn5"><p>For my purposes I don’t care about weather anywhere except in the UK. I’m more likely to know the postcode than latitude-longitude, so I’m happy to limit the tool in this way.↩︎</p></li>
<li id="fn6"><p>Actually these postcodes are for the First &amp; Last House pub and the John o’ Groats brewery.↩︎</p></li>
<li id="fn7"><p>We don’t need to forecast beyond that, because we know the weather’s going to be <a href="https://en.wikipedia.org/wiki/The_Day_After_Tomorrow">really dicey and unpredictable</a>.↩︎</p></li>
<li id="fn8"><p><a href="https://en.wikipedia.org/wiki/She_Bangs">Important disambiguation</a>.↩︎</p></li>
<li id="fn9"><p>Amusingly, given my time-related pains in {weva}, we went to <a href="https://en.wikipedia.org/wiki/Prime_meridian_(Greenwich)">Greenwich</a>.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div></div></section></div> ]]></description>
  <category>api</category>
  <category>cli</category>
  <category>r</category>
  <category>weva</category>
  <guid>https://www.rostrum.blog/posts/2026-03-01-weva/</guid>
  <pubDate>Sun, 01 Mar 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>Text hex logos with {hext}</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2026-02-15-hext/</link>
  <description><![CDATA[ 





<pre><code>  ________
 /        \
/   he     \
\     xt   /
 \________/</code></pre>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>The <a href="https://github.com/matt-dray/hext">{hext} R package</a> helps you make text-based hex logos.</p>
</section>
<section id="a-simple-mind" class="level2">
<h2 class="anchored" data-anchor-id="a-simple-mind">A simple mind</h2>
<p>Call me a normie, but I am an R-hex-logo enjoyer.</p>
<p>I took a step further last year to create my own (limited) hex-making package, <a href="https://github.com/matt-dray/gex">{gex}</a>, which I’ve <a href="https://www.rostrum.blog/posts/2025-02-25-gex/">written about</a> before<sup>1</sup>.</p>
<p>The goal of {gex} was to keep things simple: minimal dependencies, only grid graphics<sup>2</sup>.</p>
<p>But I was too short-sighted, too small-brained. I went simple, but <em>not simple enough</em>.</p>
</section>
<section id="do-the-simple-things-well" class="level2">
<h2 class="anchored" data-anchor-id="do-the-simple-things-well">Do the simple things well</h2>
<p>Yihui recently said ‘<a href="https://yihui.org/en/2026/02/bye-stickers/">bye, hex stickers</a>’.</p>
<p>In his post he discussed an entirely text-based hex logo for <a href="https://github.com/yihui/litedown/blob/main/README.md">{litedown}</a>. Charlie Gao’s <a href="https://github.com/shikokuchuo/secretbase/blob/main/README.md">{secretbase}</a><sup>3</sup> has also joined the text revolution.</p>
<p>The spirit of these packages is minimalism. Their logos match this aesthetic.</p>
<p>Simplicity is a probably a good goal in general.</p>
<p>So, naturally, I have overengineered a simple thing to make it simple for you to not think about the complication behind such a simple thing.</p>
</section>
<section id="next-text-hext" class="level2">
<h2 class="anchored" data-anchor-id="next-text-hext">Next: text hext</h2>
<p><a href="https://github.com/matt-dray/hext">{hext}</a> lets you write text-based logos.</p>
<p>You could install like:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1">pak<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">pak</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"matt-dray/hext"</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># GitHub-only</span></span></code></pre></div></div>
</div>
<p>There is only one function: <code>hext()</code>. Very simple, very demure.</p>
<p>Hex outputs have four lines<sup>4</sup>. You can add text and specify alignment for each of these.</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb3-1">hext<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">hext</span>(</span>
<span id="cb3-2">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"keep"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"it"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"simple,"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"silly"</span>,</span>
<span id="cb3-3">  <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"centre"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"right"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"left"</span></span>
<span id="cb3-4">)</span></code></pre></div></div>
<div class="cell-output cell-output-stdout">
<pre><code>  ________
 /keep    \
/    it    \
\   simple,/
 \silly___/</code></pre>
</div>
</div>
<p><code>hext()</code> handles character padding and line-length limits on your behalf. It tries its best with Unicode (like emoji)<sup>5</sup>, but rendering is hard<sup>6</sup>.</p>
<p>That’s it, really.</p>
</section>
<section id="hexagone" class="level2">
<h2 class="anchored" data-anchor-id="hexagone">Hexagone</h2>
<p>Think less, say less.</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered: 2026-02-22 21:01:02 GMT</code></pre>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.5.1 (2025-06-13)
Platform: aarch64-apple-darwin20
Running under: macOS Sequoia 15.6.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.5.1    fastmap_1.2.0     cli_3.6.5        
 [5] tools_4.5.1       htmltools_0.5.8.1 yaml_2.3.10       stringi_1.8.7    
 [9] rmarkdown_2.29    knitr_1.50        jsonlite_2.0.0    xfun_0.52        
[13] digest_0.6.37     rlang_1.1.6       hext_0.1.0        evaluate_1.0.4   </code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>You know who you are, dear one-user-who-isn’t-me↩︎</p></li>
<li id="fn2"><p>The irony being that the original iteration—called {hexbase}—was even simpler. Dependency-free, in fact.↩︎</p></li>
<li id="fn3"><p>See-also the namesake YouTube channel for sports videos. And documentaries about the history of slipping on banana peels.↩︎</p></li>
<li id="fn4"><p>The community seems divided on the number of characters per row. Like Charlie, I have opted for 8-10-10-8 top-to-bottom, which gives a little extra character space, even if makes the hexagon look a little more plump.↩︎</p></li>
<li id="fn5"><p>This works via <code>nchar(x, type = "width")</code> (or <code>stringi::stri_width()</code> if you have {stringi} installed), rather than the default <code>nchar(x, type = "chars")</code>. The smiley emoji has a width of <code>2</code>, but is <code>1</code> character long, for example. Obviously this impacts the whitespace padding that <code>hext()</code> needs to apply. If this is bothersome, you can use the argument <code>count_type</code> in <code>hext()</code> to switch between these counting modes.↩︎</p></li>
<li id="fn6"><p>‘It may look ugly, especially on Windows’, to quote Yihui’s post. If your logo is borked somewhere then… c’est la vie.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div></div></section></div> ]]></description>
  <category>hext</category>
  <category>r</category>
  <guid>https://www.rostrum.blog/posts/2026-02-15-hext/</guid>
  <pubDate>Sun, 15 Feb 2026 00:00:00 GMT</pubDate>
</item>
<item>
  <title>A roguelike-like with tyle</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2026-02-01-tyle/</link>
  <description><![CDATA[ 





<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2026-02-01-tyle/resources/tyle-grid.png" class="img-fluid quarto-figure quarto-figure-left figure-img" style="width:100.0%" alt="A 10 by 10 grid of characters printed to a macOS terminal. Tiles are mostly period characters, with some hashmark characters, and an at symbol near the middle. A prompt underneath reads 'Move (WASD)+Enter:', ready for user input."></p>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>I’m writing <a href="https://github.com/matt-dray/tyle">tyle</a> to help in my quest to slay Python.</p>
</section>
<section id="very-early-access" class="level2">
<h2 class="anchored" data-anchor-id="very-early-access">Very early access</h2>
<p>I’ve started ‘tyle’, a concept Python CLI that contains the foundations for a little in-terminal tile- and turn-based game.</p>
<p>The rudimentary tool is available to install <a href="https://github.com/matt-dray/tyle">from GitHub</a> (v0.1.0 at time of writing). I like <a href="https://docs.astral.sh/uv/">uv</a> for this job<sup>1</sup>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb1-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">uv</span> tool install git+https://github.com/matt-dray/tyle.git</span></code></pre></div></div>
<p>Then you can start a ‘game’ (heavy emphasis on those quote marks) from your terminal by typing:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode bash code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">tyle</span></span></code></pre></div></div>
<pre><code>.......#..
..........
.##..#....
....#....#
......#...
.....@....
..#.......
........#.
..........
....#.....
Move (WASD+Enter): </code></pre>
<p>The gameboard is just a tile grid printed to the terminal. You (the <code>@</code> symbol) can travel around the floor tiles (<code>.</code>), but not through obstacles (randomly-placed <code>#</code>) or off the map.</p>
<p>At the prompt you can type <kbd>W</kbd> and <kbd>Enter</kbd> to move up, for example. <kbd>Q</kbd> and <kbd>Enter</kbd> will quit.</p>
<p>Aaand that’s it for now.</p>
<div class="tip">
<p> <b>Note</b></p>
<p>A quick update on changes in v0.2.0, made immediately after this post. I’ve added CLI options so you can do something like <code>tyle -r 10 -c 20 -w 25</code> to set the starting count of <code>--rows</code>, <code>--columns</code> and <code>--walls</code> (see <code>tyle -h</code> for help). I also adjusted the column- and wall-count defaults and added a space between adjacent tiles; and introduced colours (but only if your terminal suports <a href="https://en.wikipedia.org/wiki/ANSI_escape_code">ANSI codes</a>, like zsh in the gif below).</p>
<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2026-02-01-tyle/resources/tyle.gif" class="img-fluid quarto-figure quarto-figure-left figure-img" style="width:50.0%" alt="A 10 by 20 grid of characters printed to a zsh terminal. The majority are floor tiles, which are are grey period symbols. Randomly-placed walls are red hash symbols. The player is a yellow at symbol in the middle. A prompt underneath reads 'Move (WASD)+Enter:', ready for user input. The user inputs values to make the at symbol move around the tiles."></p>
</figure>
</div>
</div>
</section>
<section id="gone-rogue" class="level2">
<h2 class="anchored" data-anchor-id="gone-rogue">Gone rogue</h2>
<p>This game style and ASCII-text ‘graphics’ may be familiar as the blueprint for <a href="https://en.wikipedia.org/wiki/Rogue_(video_game)">the classic dungeoncrawling videogame <em>Rogue</em></a> (1980).</p>
<p>Other key features are turn-based interaction, procedurally-generated environments and player permadeath. Later games aped and adapted these features, giving rise to the <a href="https://en.wikipedia.org/wiki/Roguelike">‘roguelike’ and ‘roguelite’</a> genres.</p>
<p>The scope for tyle is far tighter: I promise nothing. So let’s call this a ‘roguelike-like’ for now.</p>
</section>
<section id="a-sidequest" class="level2">
<h2 class="anchored" data-anchor-id="a-sidequest">A sidequest</h2>
<p>To explain the lore, I’ve been making some twee CLI projects to help me learn Python. First <a href="https://github.com/matt-dray/jot">jot</a> for recording little timestamped notes<sup>2</sup>, then <a href="https://github.com/matt-dray/pet">pet</a> for a mini cyberpet experience.</p>
<p>tyle is also a Python-backed CLI, but is helping me learn more about object-oriented programming and building my own <a href="https://www.w3schools.com/python/python_classes.asp">classes</a>, specifically. It’s nice to organise code into object ‘types’ that contain their own variables (properties) and functions (methods).</p>
<p>As someone with a background in R, I tend to think every nail can be hammered with a function. R supports classes, of course, most recently with <a href="https://rconsortium.github.io/S7/index.html">the {S7} package</a><sup>3</sup>. But the approach feels far more at home in Python with its greater focus on ‘programming’ than ‘doing statistics’<sup>4</sup>.</p>
</section>
<section id="a-class-act" class="level2">
<h2 class="anchored" data-anchor-id="a-class-act">A class act</h2>
<p>First, a high-level overview of what tyle’s three main classes represent:</p>
<ul>
<li><code>Tile</code> represents each square on the grid</li>
<li><code>TileGrid</code> represents the whole grid of tiles</li>
<li><code>Entity</code> represents the player character</li>
</ul>
<p>Each of these have properties and methods that allow them to store data and be queried in certain ways:</p>
<ul>
<li><code>Tile</code> objects store their representative symbol (e.g.&nbsp;<code>#</code>) and if they can be traversed</li>
<li><code>TileGrid</code> objects store and print the tile grid, while also assessing player movement</li>
<li><code>Entity</code> objects store tile coordinates and hit points for the player</li>
</ul>
<p>A tile-grid object is composed of many tile objects. Some tiles are designated as floor tiles (<code>.</code>) and others as walls (<code>#</code>). Think of the tile-grid as the ‘terrain’ layer of the game.</p>
<p>The player is not part of the terrain layer, but exists independently and carries its own coordinate properties. When printing the tilegrid with a drawing method, the player’s symbol (<code>@</code>) is printed preferentially. There’s logic to prevent the player from moving to non-traversable places (walls or off the map).</p>
<section id="grouting" class="level3">
<h3 class="anchored" data-anchor-id="grouting">Grouting</h3>
<p>What does it look like to set up one of these classes?</p>
<p>Here’s an example from <a href="https://github.com/matt-dray/tyle/tree/main/src/tyle">the <code>tiles.py</code> file</a>. The simplest possible example is <code>Tile</code>:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">class</span> Tile:</span>
<span id="cb4-2">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">__init__</span>(<span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>, symbol: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>, traversable: <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">bool</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-&gt;</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">None</span>:</span>
<span id="cb4-3">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.symbol <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> symbol</span>
<span id="cb4-4">        <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">self</span>.traversable <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> traversable</span></code></pre></div></div>
<p>I’ve shown the type hints<sup>5</sup> but hidden the docstrings for brevity.</p>
<p>To summarise, you:</p>
<ol type="1">
<li>Declare with <code>class</code>.</li>
<li>Use an UpperCamelCase name by convention.</li>
<li><code>def</code>ine a function to <code>__init__</code>ialise the class, with reference to:
<ol type="a">
<li><code>self</code>, i.e.&nbsp;a reference to the class instance itself.</li>
<li>‘normal’ function arguments (<code>symbol</code> is the text character representing the tile and <code>traversable</code> is whether the tile can be moved across).</li>
</ol></li>
<li>Set properties that may just be the arguments, or passed through other functions.</li>
<li><code>def</code>ine functions (methods) within the body of the class (not shown here because the <code>Tile</code> class doesn’t currently have any).</li>
</ol>
<p>And then you can initiate a single instance like:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1">tile <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Tile(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"."</span>, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>)</span></code></pre></div></div>
<p>You can then check parameters and run methods from the class. For example, we can use dot notation on our <code>Tile</code> instance to access the <code>symbol</code> parameter:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb6-1">tile.symbol</span></code></pre></div></div>
<pre><code>'.'</code></pre>
</section>
<section id="fire-up-the-griddle" class="level3">
<h3 class="anchored" data-anchor-id="fire-up-the-griddle">Fire up the griddle</h3>
<p>How does tyle generate a tilegrid from tiles?</p>
<p>Crucially, tile initiates distinct tile objects for every grid position. This means each one can have individual properties, like whether they’re a wall tile. Later we could give them properties for the presence of booby traps, for example.</p>
<p>We store the grid of tiles as a two-dimensional list (a list of lists). The index of:</p>
<ul>
<li>each list gives the tiles their row number</li>
<li>each tile in each list gives them their column number</li>
</ul>
<p>Here’s a simplified part of the <code>create_grid()</code> function, which uses <a href="https://www.w3schools.com/python/python_lists_comprehension.asp">list comprehension</a><sup>6</sup> to generate the grid:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1">grid <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [</span>
<span id="cb8-2">    [Tile(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"#"</span>, <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span>) <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> _ <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(n_cols)]</span>
<span id="cb8-3">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> _ <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">range</span>(n_rows)</span>
<span id="cb8-4">]</span></code></pre></div></div>
<p>Printing <code>grid</code> will reveal the structure. Suppose we had <code>n_rows = n_cols = 2</code> for a tiny 2 × 2 grid, we’d get something like this:</p>
<pre><code>[[&lt;__main__.Tile at 0x107a22120&gt;, &lt;__main__.Tile at 0x107a23170&gt;],
 [&lt;__main__.Tile at 0x107a236e0&gt;, &lt;__main__.Tile at 0x107a23020&gt;]]</code></pre>
<p>You can see the list of lists (<code>[[],[]]</code>) where each inner list contains two tile objects. We can index into it to get a tile at any given ‘coordinates’. So <code>grid[1][0]</code> would return the tile in the second row and first column.</p>
<p>You can see the default printing format for objects, which tells us where it was defined, its class and its unique memory address. Instead, <code>TileGrid</code> has a <code>draw()</code> method to print instead the <code>symbol</code> property of each <code>Tile</code> object (i.e.&nbsp;<code>.</code> or <code>#</code>).</p>
</section>
<section id="upper-class" class="level3">
<h3 class="anchored" data-anchor-id="upper-class">Upper class</h3>
<p>So that was a very simple example of the <code>Tile</code> class in action.</p>
<p>You can imagine how the <code>Entity</code> class has its own parameters like <code>hp</code> (hit points) and <code>TileGrid</code> has a <code>draw()</code> method to print the map to the screen.</p>
<p>Later we could create subclasses from the <code>Entity</code> class. <code>Player</code> and <code>Enemy</code> could inherit the properties and methods from <code>Entity</code>, but get their own unique ones too. Maybe <code>Player</code> could have a <code>name</code>, while <code>Enemy</code> could have a <code>species</code> (think <code>goblin</code>, <code>rat</code> or <code>ingrate-who-puts-their-dirty-feet-up-on-the-train-seat</code>).</p>
<p>Classes give us a much more manageable and readable way of organising and dealing with variables and functions.</p>
</section>
</section>
<section id="hack" class="level2">
<h2 class="anchored" data-anchor-id="hack">Hack</h2>
<blockquote class="blockquote">
<p>But I’ve read about <a href="https://github.com/matt-dray/r.oguelike">the {r.oguelike} R package</a> on <a href="https://www.rostrum.blog/index.html#category=r.oguelike">this blog</a> before. Have you run out of ideas?</p>
</blockquote>
<p>Uh-oh, I’ve been rumbled.</p>
<p>Listen, agitator: an R-function spaghetti was never the ‘right’ choice. Not least because that particular ‘game’ had to be played in the R console, rather than the terminal directly.</p>
<p>Python is probably more suited to this type of activity. In fact, Python can be used as a full-blown game engine, aided by tools like <a href="https://www.pygame.org/docs/">PyGame</a>. In fact, there are several roguelike-specific packages to help you build roguelikes specifically, like <a href="https://github.com/libtcod/libtcod">libtcod</a><sup>7</sup>.</p>
<p>I’m ignoring these because I want to use this little project to help me learn Python skills, not roguelike-making skills, specifically. Classes, yes, but also stuff like type hints, docstrings, writing modules, creating CLIs, etc.</p>
</section>
<section id="do-it-with-tyle" class="level2">
<h2 class="anchored" data-anchor-id="do-it-with-tyle">Do it with tyle</h2>
<p>As ever, I may stop there<sup>8</sup>. But there are some obvious improvements that could also strengthen my Python skills.</p>
<p>For example, a bunch of stuff from {r.oguelike} could be interesting to translate here, like procedural dungeons, enemy battling and pathfinding, and an inventory.</p>
<p>We should also ‘listen’ for keyboard input with something like <a href="https://pynput.readthedocs.io/en/latest/index.html">pynput</a>. A much more natural approach compared to smashing the <kbd>Enter</kbd> key.</p>
<p>Of course, learning new stuff can feel like navigating through a dungeon full of goblins. But to avoid permadeath of coding skills you’ve just got to keep going tile-by-tile, amirite?</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>[project]
name = "2026-02-01-tyle"
version = "0.1.0"
requires-python = "&gt;=3.12"
dependencies = []</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>And <code>uv tool update tyle</code> and <code>uv tool uninstall tyle</code> do what you think they do.↩︎</p></li>
<li id="fn2"><p>Actually, I’ve been using jot everyday and it helps me keep track of what I’ve been doing, which is handy for completing weeknotes and timesheets.↩︎</p></li>
<li id="fn3"><p>I wouldn’t dream of taking the cutting edge and <a href="https://www.rostrum.blog/posts/2023-02-26-nook-s7/">being frivolous with it</a>.↩︎</p></li>
<li id="fn4"><p>Wow, a take that’s hotter than the sun.↩︎</p></li>
<li id="fn5"><p><a href="https://astral.sh/">Astral</a> have brought ty out of version beta_BETA_draft_v0.0.0.0.0.900000, huzzah. You can check type hints cheaply <a href="https://docs.astral.sh/uv/guides/tools/">with uv</a> and without installing anything else, like <code>uvx ty check</code>. Or <code>uvx ruff format</code> and <code>uv ruff check</code> for linting and formatting. Real noice.↩︎</p></li>
<li id="fn6"><p>And note the use of the underscore convention as a ‘placeholder’ variable. We don’t need to name the variable because we’re not actually using it.↩︎</p></li>
<li id="fn7"><p>Check out entries from <a href="https://www.roguebasin.com/index.php?title=Seven_Day_Roguelike_Challenge">7DRL</a> (the seven-day roguelike challenge), which includes may games written this way.↩︎</p></li>
<li id="fn8"><p>Take a shot every time you read that phrase on this blog.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div></div></section></div> ]]></description>
  <category>cli</category>
  <category>python</category>
  <category>tyle</category>
  <category>uv</category>
  <guid>https://www.rostrum.blog/posts/2026-02-01-tyle/</guid>
  <pubDate>Sun, 01 Feb 2026 00:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2026-02-01-tyle/resources/tyle.png" medium="image" type="image/png"/>
</item>
<item>
  <title>Correcting a fib about jot</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2025-12-28-jot-fib/</link>
  <description><![CDATA[ 





<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-12-28-jot-fib/resources/jot-fib.png" class="img-fluid quarto-figure quarto-figure-left figure-img" alt="Screenshot of a macOS terminal window running the jot tool four times, with option 'v' to print the version number; 'w' to print where the config and jot files are; 's' and 'l' to search for the most recent jotting that contains the word 'apple'; and 'f' and 't' flags to return jottings from one date to another."></p>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p><a href="https://github.com/matt-dray/jot">jot</a> now has <code>--where</code>, <code>--version</code>, <code>--from</code> and <code>--to</code> options.</p>
</section>
<section id="whoops" class="level2">
<h2 class="anchored" data-anchor-id="whoops">Whoops</h2>
<p>Me, lying, the <a href="https://www.rostrum.blog/posts/2025-08-30-jot-options/">last time I posted about jot</a>:</p>
<blockquote class="blockquote">
<p>It’s basically feature-complete from my perspective.</p>
</blockquote>
<p>Reminder: <a href="https://github.com/matt-dray/jot">jot</a> is my simple little Python CLI for recording timestamped notes to a text file.</p>
<p>In fact, now you can:</p>
<ul>
<li>combine options</li>
<li>search between specific dates</li>
<li>quickly get tool meta-information</li>
</ul>
</section>
<section id="and" class="level2">
<h2 class="anchored" data-anchor-id="and">And?</h2>
<p>Options <code>--search</code> and <code>--list</code> (<code>-s</code> and <code>-l</code>) can now be used in combination.</p>
<p>This was achieved by adding an argument to the <code>search_jottings()</code> function that takes the value given by <code>--list</code>.</p>
<p>So, assuming this little jot file:</p>
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre><strong>jot.txt</strong></pre>
</div>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb1" data-filename="jot.txt" style="background: #f1f3f5;"><pre class="sourceCode txt code-with-copy"><code class="sourceCode default"><span id="cb1-1">[2025-08-28 11:15] ate an apple</span>
<span id="cb1-2">[2025-08-27 10:58] ate a kiwi</span>
<span id="cb1-3">[2025-08-26 11:09] ate a pineapple</span>
<span id="cb1-4">[2025-08-25 10:40] ate an apple and a pear</span></code></pre></div></div>
</div>
<p>A casual <code>--search</code> will return anything matching your search term.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jot</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"apple"</span></span></code></pre></div></div>
<pre><code>[2025-08-28 11:15] ate an apple
[2025-08-26 11:09] ate a pineapple
[2025-08-25 10:40] ate an apple and a pear</code></pre>
<p>But that could overwhelm the console if you have a lot of jottings.</p>
<p>So now you can add the <code>--list</code> flag to limit to the most recent <em>n</em> jottings that match the search term.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jot</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"apple"</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-l</span> 1</span></code></pre></div></div>
<pre><code>[2025-08-28 11:15] ate an apple</code></pre>
</section>
<section id="where-what" class="level2">
<h2 class="anchored" data-anchor-id="where-what">Where? What?</h2>
<p>I added helpers <code>--where</code> and <code>--version</code> (<code>-w</code> and <code>-v</code>) as quality-of-life improvements.</p>
<p><code>--where</code> is useful if you forget the file paths handled by jot, which are:</p>
<ul>
<li>the config file, which contains a key-value path to the jot file</li>
<li>the jot file, which is where the text of the jottings is saved</li>
</ul>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jot</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-w</span></span></code></pre></div></div>
<pre><code>📍 Default path to config file: /Users/mattdray/.jot-config.json
📍 Path to jot file: /Users/mattdray/Documents/jot.txt</code></pre>
<p>The tool has gone through a bunch of changes, so you can check what version you’re running with <code>--version</code>.</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb8-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jot</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-v</span></span></code></pre></div></div>
<pre><code>0.2.11</code></pre>
<p>The version number is also provided now when you run <code>jot --help</code> (<code>-h</code>) to print the help documentation.</p>
<p>Assuming you’re using uv, run <code>uv tool upgrade jot</code> to get the latest version.</p>
</section>
<section id="when" class="level2">
<h2 class="anchored" data-anchor-id="when">When?</h2>
<p>I mentioned using a regular expression to filter for jottings between dates. That’s awful<sup>1</sup>.</p>
<p>Helpfully, <a href="https://github.com/oiffrig">Olivier Iffrig-Petit</a> contributed a couple of new options to handle this.</p>
<p><code>--from</code> and <code>--to</code> (<code>-f</code> and <code>-t</code>) take values in the form <code>YYYYMMDD</code> to filter from (inclusive) and to (exclusive) given dates. They can be used alone or together, along with <code>--search</code> and <code>--list</code>.</p>
<p>So, to return jottings from 26 August 2025 up to (but not including) 28 August 2025:</p>
<div class="code-copy-outer-scaffold"><div class="sourceCode" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb10-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jot</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-f</span> 20250826 <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-t</span> 20250828</span></code></pre></div></div>
<pre><code>[2025-08-27 10:58] ate a kiwi
[2025-08-26 11:09] ate a pineapple</code></pre>
<p>Isolating time periods like this has helped me find information to write weeknotes and complete my timesheets.</p>
<p>Many thanks Olivier.</p>
</section>
<section id="sure" class="level2">
<h2 class="anchored" data-anchor-id="sure">Sure?</h2>
<p>Along with these features, I’ve added basic tests for the Python functions using <a href="https://docs.pytest.org/en/stable/">pytest</a>. A couple of things that were new to me were:</p>
<ul>
<li>the <code>@pytest.mark.parametrize()</code> decorator, which helps set up inputs that can be iterated over in a test</li>
<li>that <code>capsys</code> captures system outputs, as the name implies, which you can compare to expected values</li>
</ul>
<p>And naturally my patience was tested trying to get the pytest GitHub Action to run correctly.</p>
</section>
<section id="next" class="level2">
<h2 class="anchored" data-anchor-id="next">Next?</h2>
<p>Next thing might be to <a href="https://github.com/matt-dray/jot/issues/44">prepare jot for PyPi</a>. There’s probably two breaking changes to be made before then:</p>
<ol type="1">
<li>Save the config file <a href="https://github.com/matt-dray/jot/issues/14">somewhere more suitable</a> than the user’s home directory.</li>
<li><a href="https://github.com/matt-dray/jot/issues/57">Rename the package</a> because there’s <a href="https://ss64.com/mac/jot.html">already a macOS tool named ‘jot’</a>, lol<sup>2</sup>.</li>
</ol>
<p>So, perhaps, finally, maybe, for now:</p>
<blockquote class="blockquote">
<p>It’s basically feature-complete from my perspective.</p>
</blockquote>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>[project]
name = "2025-12-28-jot-fib"
version = "0.1.0"
requires-python = "&gt;=3.12"
dependencies = []</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>‘Regex? Now you have two problems.’↩︎</p></li>
<li id="fn2"><p>Or alias and document it so the user can decide. Either way, ‘jot’ is already taken on PyPi, so the package name will need changing even if the entry point doesn’t.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div></div></section></div> ]]></description>
  <category>cli</category>
  <category>jot</category>
  <category>python</category>
  <category>uv</category>
  <guid>https://www.rostrum.blog/posts/2025-12-28-jot-fib/</guid>
  <pubDate>Sun, 28 Dec 2025 00:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2025-12-28-jot-fib/resources/jot-fib.png" medium="image" type="image/png" height="51" width="144"/>
</item>
<item>
  <title>How the tables turned</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2025-11-30-turn-tables/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-11-30-turn-tables/resources/let-go.jpg" class="img-fluid figure-img" style="width:50.0%" alt="Meme of Link from The lEgend of Zelda: Windwaker. The camera is zoomed right up under his chin, his face filling the screen. There's an on-screen instruction to press the 'A' button to 'let go'."></p>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>{a11ytables} is dead, long live <a href="https://best-practice-and-impact.github.io/aftables/">{aftables}</a>.</p>
</section>
<section id="regret" class="level2">
<h2 class="anchored" data-anchor-id="regret">Regret?</h2>
<p>A while back I presented <a href="https://www.rostrum.blog/posts/2023-06-13-panic-in-the-toolshed/">‘Panic! In The Toolshed’</a> about how public-sector data scientists should share code to prevent wheel reinvention<sup>1</sup>.</p>
<p>I gave the example of my {a11ytables} R package, which has helped to publish spreadsheets of official statistics that follow <a href="https://analysisfunction.civilservice.gov.uk/policy-store/releasing-statistics-in-spreadsheets/">best practices</a>.</p>
<p>It got harder to find time to maintain the package. <a href="https://www.rostrum.blog/posts/2024-01-27-a11ytables-0.3/">I hemmed-and-hawed</a> about its existence and procrastinated two concept packages to make up for its weaknesses<sup>2</sup>.</p>
<p>But then the weight<sup>3</sup> was lifted!</p>
</section>
<section id="rehome" class="level2">
<h2 class="anchored" data-anchor-id="rehome">Rehome</h2>
<p>The Analysis Function at the Office for National Statistics (ONS) now host and maintain the package. I can’t think of a better home.</p>
<p>Transferring the package GitHub repository was a bit of a dance, passing from the co-analysis org to my personal account and finally the best-practice-and-impact org. I fretted about archiving and forking instead, but a transfer appears not to have caused issues.</p>
<p>As part of this move, the package has been renamed {aftables}. The ‘af’ stands for ‘Analysis Function’ and echoes the related <a href="https://best-practice-and-impact.github.io/afcharts/">{afcharts} package</a> that helps produce best-practice visualisations.</p>
<p>As a parting gift, I used <a href="https://github.com/matt-dray/gex">my {gex} package</a> to make a quick hex logo that echoes the {afcharts} logo:</p>
<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-11-30-turn-tables/resources/aftables.png" class="img-fluid figure-img" alt="Hex logo for aftables. Multicoloured squares on the lower half echo the cells of a spreadsheet. The name of the package is above the squares. Reminiscent of the hex logo for afcharts and uses the same colour palette." width="200"></p>
</figure>
</div>
<p>This renaming hasn’t had a major impact as far as I can tell. The team <a href="https://analysisfunction.civilservice.gov.uk/blog/best-practice-for-automated-spreadsheets-in-r-creating-accessible-spreadsheets-with-aftables/">published a launch blog</a>; I updated colleagues via the cross-government Slack; and GitHub auto-rereoutes from <a href="https://github.com/co-analysis/a11ytables">co-analysis/a11ytables</a> to <a href="https://github.com/best-practice-and-impact/aftables">best-practice-and-impact/aftables</a>.</p>
<p>The team has already made updates and <a href="https://CRAN.R-project.org/package=aftables">submitted successfully to CRAN</a> in these past few months, which I definitely didn’t have the energy to tackle.</p>
<p>So now you can just <code>install.packages("aftables")</code> to try it out. Simply lovely.</p>
</section>
<section id="relief" class="level2">
<h2 class="anchored" data-anchor-id="relief">Relief</h2>
<p>So thank you to the Analysis Function team, particularly Olivia and Zach; to Matt and Tim for their early contributions; and to everyone who has used the package and provided feedback.</p>
<p>Panic? What panic?</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered: 2025-11-30 17:03:05 GMT</code></pre>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.5.1 (2025-06-13)
Platform: aarch64-apple-darwin20
Running under: macOS Sequoia 15.6.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_GB.UTF-8/en_GB.UTF-8/en_GB.UTF-8/C/en_GB.UTF-8/en_GB.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.5.1    fastmap_1.2.0     cli_3.6.5        
 [5] tools_4.5.1       htmltools_0.5.8.1 yaml_2.3.10       rmarkdown_2.29   
 [9] knitr_1.50        jsonlite_2.0.0    xfun_0.52         digest_0.6.37    
[13] rlang_1.1.6       evaluate_1.0.4   </code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>We talk a lot about <em>reproducibility</em> of our work, as we should, but Rhian gave a good talk at the NHS RPySOC 2025 conference about <a href="https://rhian.rbind.io/talks/2024-11-22-reusability/">the power of <em>reusability</em></a>.↩︎</p></li>
<li id="fn2"><p><a href="https://github.com/matt-dray/a11ytables2">{a11ytables2}</a> aimed to use a more pipeable approach and to upgrade the package from {openxlsx} to {openxlsx2} (which the Analysis Function team have already done in {aftables}!), while <a href="https://github.com/matt-dray/yamlsheets">{yamlsheets}</a> attempted to use a text-based YAML ‘blueprint’ file to declare spreadsheet content.↩︎</p></li>
<li id="fn3"><p>The fact I was using phrases like ‘panic’, ‘kill’ and ‘rebuild’ in earlier posts about {a11ytables} tells you something about the entirely self-imposed torment I was inflicting on myself.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div id="quarto-reuse" class="quarto-appendix-contents"><div>CC BY-NC-SA 4.0</div></div></section></div> ]]></description>
  <category>a11ytables</category>
  <category>aftables</category>
  <category>r</category>
  <guid>https://www.rostrum.blog/posts/2025-11-30-turn-tables/</guid>
  <pubDate>Sun, 30 Nov 2025 00:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2025-11-30-turn-tables/resources/let-go.jpg" medium="image" type="image/jpeg"/>
</item>
<item>
  <title>={ o x o}=</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2025-10-30-pet/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-10-30-pet/resources/pet-see.png" class="img-fluid figure-img" style="width:100.0%" alt="Screenshot of a terminal window. There is text with the question 'what would you like to do' and the user has selected the option 'see'. An ASCII-art cat saying 'meow' is printed underneath. Then the same question is asked, with options for 'stats', 'see', 'feed', 'quit' and 'release'. Emoji are sprinkled throughout, like a sushi for the 'see' option."></p>
<figcaption class="figure-caption">Yes, my true calling is ASCII art.</figcaption>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>I’ve made <a href="https://github.com/matt-dray/pet">pet</a>: a (very simple) Python tool to look after a persistent cyberpet on your command line.</p>
</section>
<section id="adopt" class="level2">
<h2 class="anchored" data-anchor-id="adopt">Adopt</h2>
<p>I made this for learning purposes, but if you did want to try it:</p>
<ol start="0" type="1">
<li>Install <a href="https://docs.astral.sh/uv/">uv</a> if you somehow haven’t already.</li>
<li>From your terminal run <code>uv tool install git+https://github.com/matt-dray/pet.git</code>.</li>
<li>Run <code>pet</code>.</li>
<li>Take responsibility.</li>
</ol>
</section>
<section id="pet-the-kitty" class="level2">
<h2 class="anchored" data-anchor-id="pet-the-kitty">Pet the kitty</h2>
<p>Running <code>pet</code> takes control of the terminal and asks you to select an option with <kbd>↓</kbd>, <kbd>↑</kbd> and <kbd>Enter</kbd>:</p>
<ul>
<li>📊 <strong>Stats</strong> to show your pet’s name (supplied by the user when they first run pet), birth (creation datetime), age (days since birth) and health (out of 10)</li>
<li>👀 <strong>See</strong> to print a little picture of your pet (always the same handsome cat)</li>
<li>🍣 <strong>Feed</strong> to simulate your pet eating and gaining one unit of health</li>
<li>❌ <strong>Quit</strong> to exit the program back to the terminal</li>
<li>👋 <strong>Release</strong> to discard your pet (cry) and delete its data</li>
</ul>
<p>Here’s what a playthrough looks like:</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-10-30-pet/resources/pet-play.png" class="img-fluid figure-img" style="width:100.0%" alt="Screenshot of a terminal window. The command 'pet' has been run. The user is asked 'what would you like to do' and they respond 'stats', which returns the pet name, bith date, age in days and health status. The user chooses eom other options with different effects. Emoji are sprinkled throughout, like a battery for the 'health' status."></p>
<figcaption class="figure-caption">Obligatory <em>Monty Python</em> reference.</figcaption>
</figure>
</div>
<p>Lines starting with a question mark require input from the user. Once selected, the options disappear and the chosen one is displayed in blue. When the user can choose from several options, a blue chevron is moved between the options.</p>
<p>Crucially, if you quit and come back later, your pet will be recalled and its stats will have changed given the amount of time that’s passed. Pay attention! Your pet’s health will diminish over time<sup>1</sup>.</p>
<p>That’s it.</p>
</section>
<section id="the-soft-underbelly" class="level2">
<h2 class="anchored" data-anchor-id="the-soft-underbelly">The soft underbelly</h2>
<p>You can see the ‘game loop’ in <a href="https://github.com/matt-dray/pet/blob/main/src/pet/cli.py">the <code>cli.py</code> module</a>. Basically:</p>
<ol type="1">
<li>If it doesn’t exist, create a json file of pet stats on the user’s computer, having prompted for the pet’s name.</li>
<li>Update the stats given the time elapsed since birth and since your last interaction.</li>
<li>Deplete the pet’s health as a function of elapsed time (1 health unit lost per hour).</li>
<li>Ask the user to select an action from the menu and act accordingly, ad infinitum.</li>
<li>Break the loop on exit, and delete the stats json file if the pet was released.</li>
</ol>
<p>Not too complicated. Behind this, the Python functions in <a href="https://github.com/matt-dray/pet/blob/main/src/pet/utils.py">utils.py</a> are for wrangling and overwriting the stats file, calculating time and health loss, and for initiating the graphics engine (i.e.&nbsp;<code>print()</code>, lol).</p>
</section>
<section id="gaining-xp" class="level2">
<h2 class="anchored" data-anchor-id="gaining-xp">Gaining XP</h2>
<p>As well as getting more general experience in Python packaging, I learnt more about:</p>
<ul>
<li><a href="https://inquirerpy.readthedocs.io/en/latest/">InquirerPy</a>, which does the heavy lifting on user interaction, like <code>inquirer.select()</code> for presenting keyboard-navigable options<sup>2</sup></li>
<li><a href="https://platformdirs.readthedocs.io/en/latest/"><code>user_data_dir()</code> from platformdirs</a>, which can create a platform-independent path to a location on the user’s machine where game data can be stored</li>
</ul>
<p>As in <a href="https://www.rostrum.blog/index.html#category=uv">several recent posts</a>, uv has also been great here for the development loop, including to support use of ruff and ty<sup>3</sup> for code quality, like <code>uvx ruff format</code>.</p>
</section>
<section id="a-good-egg" class="level2">
<h2 class="anchored" data-anchor-id="a-good-egg">A good egg</h2>
<p>This pet tool has been designed for my own learning. There’s loads of missing features, but my goals have been met.</p>
<p>For more of a triple-A<sup>4</sup> virtual pet experience, you can <a href="https://www.rostrum.blog/posts/2022-11-13-tamrgo/">read earlier blogs</a> about <a href="https://github.com/matt-dray/tamRgo">my {tamRgo} package</a>, which does a much better job of this in the R console. It has lots more features, like ‘happy’, ‘hungry’ and ‘dirty’ meters; different pet species; experience points and levelling up; and ‘graphics’ dependent on the pet’s species and level. Ah, and the concept of death.</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>[project]
name = "2025-10-30-pet"
version = "0.1.0"
requires-python = "&gt;=3.12"
dependencies = []</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Do not panic: your pet will not run away or die. I was too lazy to program that.↩︎</p></li>
<li id="fn2"><p>See <a href="https://textual.textualize.io/">Textual</a> as a more advanced tool for building your own complicated terminal user interfaces.↩︎</p></li>
<li id="fn3"><p><a href="https://astral.sh/">Astral</a>: please don’t bait-and-switch me, I need this.↩︎</p></li>
<li id="fn4"><p>Amateur, Adequate, Awkward?↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div id="quarto-reuse" class="quarto-appendix-contents"><div>CC BY-NC-SA 4.0</div></div></section></div> ]]></description>
  <category>cli</category>
  <category>pet</category>
  <category>python</category>
  <category>uv</category>
  <guid>https://www.rostrum.blog/posts/2025-10-30-pet/</guid>
  <pubDate>Thu, 30 Oct 2025 00:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2025-10-30-pet/resources/pet-see.png" medium="image" type="image/png" height="64" width="144"/>
</item>
<item>
  <title>Abandon P*werpoint for the command line</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2025-09-18-rich-slides/</link>
  <description><![CDATA[ 




<p><img src="https://www.rostrum.blog/posts/2025-09-18-rich-slides/resources/slide-1.svg" class="img-fluid" style="width:100.0%" alt="A dark terminal window with ASCII art of the word 'synergy' and some text under it saying 'promote it'. There's a border line around the edge. These elements are green."></p>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>You can use Python’s <a href="https://rich.readthedocs.io/en/stable/">rich</a> library to make super minimal presentations at the command line.</p>
</section>
<section id="sliding-scale" class="level2">
<h2 class="anchored" data-anchor-id="sliding-scale">Sliding scale</h2>
<p>Recently, conference organisers forced a colleague to convert their Quarto presentation to P*werPoint. Unconscionable.</p>
<p>Proprietary software shouldn’t get in the way of good ideas, maaan.</p>
<p>Actually, if your message is good enough<sup>1</sup> then why waste time <em>rendering</em> anything? Why not just print directly to the terminal, lol?</p>
</section>
<section id="thats-rich" class="level2">
<h2 class="anchored" data-anchor-id="thats-rich">That’s rich</h2>
<p>The <a href="https://rich.readthedocs.io/en/stable/">rich</a> library is for prettifying output and making user interfaces<sup>2</sup>.</p>
<p>There’s nothing to stop you using rich to show faux ‘slides’ at the command line. So, as a thought experiment, I’ve given this a go.</p>
<p>Basic approach:</p>
<ol type="1">
<li>Clear the screen.</li>
<li>Accept keyboard input.</li>
<li>Draw the next slide.</li>
</ol>
<p>I’ve prepared a single Python demo script to do exactly this. Dependencies are declared at the top, so you can run it with <a href="https://docs.astral.sh/uv/">uv</a> like <code>uv run slides.py</code><sup>3</sup>.</p>
<p>Crucially, you’ll have to zoom in to you terminal so the words end up nice and big. I don’t know a simple way to do this automatically (or if it’s even possible).</p>
<p>Then you can use arrow keys to move through the slides and press <kbd>Q</kbd> to quit.</p>
<details>
<summary>
Click to see the full demo script
</summary>
<div class="cell">
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre><strong>slides.py</strong></pre>
</div>
<div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># /// script</span></span>
<span id="cb1-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># requires-python = "&gt;=3.12"</span></span>
<span id="cb1-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># dependencies = [</span></span>
<span id="cb1-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#     "pynput",</span></span>
<span id="cb1-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#     "rich",</span></span>
<span id="cb1-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ]</span></span>
<span id="cb1-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ///</span></span>
<span id="cb1-8"></span>
<span id="cb1-9"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> pynput <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> keyboard</span>
<span id="cb1-10"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> rich.align <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> Align</span>
<span id="cb1-11"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> rich.console <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> Console</span>
<span id="cb1-12"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> rich.panel <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> Panel</span>
<span id="cb1-13"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> os</span>
<span id="cb1-14"></span>
<span id="cb1-15">console <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Console()</span>
<span id="cb1-16"></span>
<span id="cb1-17"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> panelise(</span>
<span id="cb1-18">    <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span>args,</span>
<span id="cb1-19">    sep<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span><span class="ch" style="color: #20794D;
background-color: null;
font-style: inherit;">\n</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span>,</span>
<span id="cb1-20">    panel_title,</span>
<span id="cb1-21">    panel_subtitle<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"#ShareholderValue"</span>,</span>
<span id="cb1-22">    panel_width<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>console.size.width,</span>
<span id="cb1-23">    panel_height<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>console.size.height <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">2</span>,</span>
<span id="cb1-24">):</span>
<span id="cb1-25">    panel_text <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> sep.join(args)</span>
<span id="cb1-26">    panel_body <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Align.center(panel_text, vertical<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"middle"</span>)</span>
<span id="cb1-27">    panel <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> Panel(</span>
<span id="cb1-28">        panel_body,</span>
<span id="cb1-29">        title<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>panel_title,</span>
<span id="cb1-30">        subtitle<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>panel_subtitle,</span>
<span id="cb1-31">        width<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>panel_width,</span>
<span id="cb1-32">        height<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>panel_height,</span>
<span id="cb1-33">    )</span>
<span id="cb1-34">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span> panel</span>
<span id="cb1-35"></span>
<span id="cb1-36">slides <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [</span>
<span id="cb1-37">    panelise(</span>
<span id="cb1-38">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" ___ _   _ _ __   ___ _ __ __ _ _   _ "</span>,</span>
<span id="cb1-39">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"/ __| | | | '_ \ / _ \ '__/ _` | | | |"</span>,</span>
<span id="cb1-40">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"\__ \ |_| | | | |  __/ | | (_| | |_| |"</span>,</span>
<span id="cb1-41">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"|___/\__, |_| |_|\___|_|  \__, |\__, |"</span>,</span>
<span id="cb1-42">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"      __/ |                __/ | __/ |"</span>,</span>
<span id="cb1-43">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"     |___/                |___/ |___/ "</span>,</span>
<span id="cb1-44">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"                            promote it"</span>,</span>
<span id="cb1-45">        panel_title<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span>,</span>
<span id="cb1-46">        panel_subtitle<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">""</span>,</span>
<span id="cb1-47">    ),</span>
<span id="cb1-48">    panelise(</span>
<span id="cb1-49">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"• Push the envelope"</span>,</span>
<span id="cb1-50">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"• Double down"</span>,</span>
<span id="cb1-51">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"• Think outside the box"</span>,</span>
<span id="cb1-52">        panel_title<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Paradigm shift"</span>,</span>
<span id="cb1-53">    ),</span>
<span id="cb1-54">    panelise(</span>
<span id="cb1-55">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"• Move the needle"</span>,</span>
<span id="cb1-56">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"• Circle back"</span>,</span>
<span id="cb1-57">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"• Take this offline"</span>,</span>
<span id="cb1-58">        panel_title<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Key deliverables"</span>,</span>
<span id="cb1-59">    ),</span>
<span id="cb1-60">    panelise(</span>
<span id="cb1-61">        <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"You must clap"</span>,</span>
<span id="cb1-62">        panel_title<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"You're welcome"</span>,</span>
<span id="cb1-63">    ),</span>
<span id="cb1-64">]</span>
<span id="cb1-65"></span>
<span id="cb1-66">index <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span></span>
<span id="cb1-67">running <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">True</span></span>
<span id="cb1-68"></span>
<span id="cb1-69"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> render_slide():</span>
<span id="cb1-70">    os.system(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"clear"</span>)</span>
<span id="cb1-71"></span>
<span id="cb1-72">    width <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> console.size.width</span>
<span id="cb1-73">    filled <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">int</span>((index) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">/</span> (<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(slides) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> width)</span>
<span id="cb1-74">    bar <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"▬"</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> filled <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" "</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">*</span> (width <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> filled)</span>
<span id="cb1-75">    console.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(bar, style<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"green"</span>)</span>
<span id="cb1-76"></span>
<span id="cb1-77">    console.<span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(slides[index], style<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"green"</span>)</span>
<span id="cb1-78"></span>
<span id="cb1-79"><span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">def</span> on_press(key):</span>
<span id="cb1-80">    <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">global</span> index, running</span>
<span id="cb1-81"></span>
<span id="cb1-82">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">hasattr</span>(key, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"char"</span>) <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">and</span> key.char <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"q"</span>:</span>
<span id="cb1-83">        running <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="va" style="color: #111111;
background-color: null;
font-style: inherit;">False</span></span>
<span id="cb1-84">        listener.stop()</span>
<span id="cb1-85">        <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">return</span></span>
<span id="cb1-86"></span>
<span id="cb1-87">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> key <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> keyboard.Key.right:</span>
<span id="cb1-88">        index <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (index <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(slides)</span>
<span id="cb1-89"></span>
<span id="cb1-90">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">elif</span> key <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> keyboard.Key.left:</span>
<span id="cb1-91">        index <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> (index <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>) <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">len</span>(slides)</span>
<span id="cb1-92"></span>
<span id="cb1-93">    render_slide()</span>
<span id="cb1-94"></span>
<span id="cb1-95">listener <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> keyboard.Listener(on_press<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span>on_press)</span>
<span id="cb1-96"></span>
<span id="cb1-97">render_slide()</span>
<span id="cb1-98">listener.start()</span>
<span id="cb1-99">listener.join() </span></code></pre></div>
</div>
</div>
</details>
<p>The slides in the demo are spat out like this<sup>4</sup>:</p>
<p><img src="https://www.rostrum.blog/posts/2025-09-18-rich-slides/resources/slide-1.svg" class="img-fluid" style="width:49.0%" alt="Title slide: green ASCII art of the word 'synergy' with a green border in a dark terminal window."> <img src="https://www.rostrum.blog/posts/2025-09-18-rich-slides/resources/slide-2.svg" class="img-fluid" style="width:49.0%" alt="Slide 2, titled 'paradigm shift', with a progress bar one-third full, three bullet points including 'push the envelope' and a footer saying '#ShareholderValue'. The elements are all green."> <img src="https://www.rostrum.blog/posts/2025-09-18-rich-slides/resources/slide-3.svg" class="img-fluid" style="width:49.0%" alt="Slide 3, titled 'key deliverables', with a progress bar two-thirds full, three bullet points including 'take this offline' and a footer saying '#ShareholderValue'. The elements are all green."> <img src="https://www.rostrum.blog/posts/2025-09-18-rich-slides/resources/slide-4.svg" class="img-fluid" style="width:49.0%" alt="Slide 4, titled 'you're welcome', with a full progress bar, a line saying 'you must clap' and a footer saying '#ShareholderValue'. The elements are all green."></p>
<p>Some may call this content ‘jargon rich’ but they are losers who have simply never grinded as hard as me, bro.</p>
</section>
<section id="slidecraft" class="level2">
<h2 class="anchored" data-anchor-id="slidecraft">Slidecraft</h2>
<p>I’ll step through the code.</p>
<p>First, we initiate a console object: <code>console = Console()</code>. The <code>print()</code> method against this lets us <code>style</code> the output.</p>
<p>Next, I made a <code>panelise()</code> function, which wraps rich’s <code>Panel()</code>. This simplifies ‘slide’ setup with some defaults.</p>
<p>Incidentally, a ‘<a href="https://rich.readthedocs.io/en/stable/panel.html">panel</a>’ is just an area surrounded by a border, which I’ve ‘hacked’ to use as a bordered slide.</p>
<p>Then we create a dict of slides, passing lines of text and the slide title. In my example I created a title slide with <a href="https://patorjk.com/software/taag/">some cool ASCII art</a> and then a few slides with simple bullets.</p>
<p>Next are two important functions:</p>
<ul>
<li><code>render_slide()</code>, which clears the console, adds a progress bar at the top (oh yeah, did I mention there’s a progress bar?<sup>5</sup>) and then prints the slide content</li>
<li><code>on_press()</code>, which uses the <a href="https://pynput.readthedocs.io/en/latest/index.html">pynput</a> library to accept keypresses (left- and right-arrows to move slides and <kbd>Q</kbd> to quit)</li>
</ul>
<p>Then there are steps to run the presentation itself:</p>
<ol type="1">
<li>Start ‘listening’ to keyboard presses with <code>keyboard.Listener()</code>.</li>
<li>Render the first slide (<code>index = 0</code>) with <code>render_slide()</code>.</li>
<li>Move to the next slide with an arrow press (wrapping back to the start).</li>
<li>Exit the slides when a <kbd>q</kbd> happens (set <code>running = FALSE</code> and stop listening for keypresses).</li>
</ol>
</section>
<section id="the-slideshows-over" class="level2">
<h2 class="anchored" data-anchor-id="the-slideshows-over">The slideshow’s over</h2>
<p>So, this thing I threw together does what I wanted it to do. Improvements are absolutely possible.</p>
<p>Of course, I haven’t made full use of rich’s rich colour and style control. rich also has a module that <a href="https://rich.readthedocs.io/en/stable/markdown.html">interprets Markdown input</a>, which may be preferable to <code>Panel()</code> in some cases.</p>
<p>I know the script works on macOS but I haven’t tested it anywhere else. I suspect there may be better, platform independent approaches.</p>
<p>It’s also true that pressing the arrow keys will advance slides… even if the focus is away from the terminal. Ah well!</p>
<p>Anyway, if you want an actual fully-featured terminal-based tool, you could try something like <a href="https://maaslalani.com/slides/">Maas Lalani’s slides</a>.</p>
<p>Next slide please!</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>[project]
name = "2025-08-30-jot-options"
version = "0.1.0"
requires-python = "&gt;=3.12"
dependencies = []</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>I worked at the <a href="https://www.gov.uk/government/organisations/government-digital-service">Government Digital Service</a> (GDS) where there was <a href="https://gds.blog.gov.uk/2016/04/07/giving-clear-presentations/">a culture of good, simple slides</a>.↩︎</p></li>
<li id="fn2"><p>I used its basic features recently to add colour to <a href="https://github.com/matt-dray/jot">my minimal note-taking tool, jot</a>.↩︎</p></li>
<li id="fn3"><p>An approach I wrote about in <a href="https://www.rostrum.blog/posts/2025-08-11-uv-standalone/">a recent post</a>.↩︎</p></li>
<li id="fn4"><p>Add <code>record=TRUE</code> to the <code>Console()</code> call and <code>console.save_svg("slides.svg")</code> at the end to get an image of your slides as you progress through them.↩︎</p></li>
<li id="fn5"><p>Yes, the bar starts on the second slide, after the title slide. This is for aesthetic reasons mostly.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div id="quarto-reuse" class="quarto-appendix-contents"><div>CC BY-NC-SA 4.0</div></div></section></div> ]]></description>
  <category>cli</category>
  <category>python</category>
  <category>rich</category>
  <category>uv</category>
  <guid>https://www.rostrum.blog/posts/2025-09-18-rich-slides/</guid>
  <pubDate>Wed, 17 Sep 2025 23:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2025-09-18-rich-slides/resources/slide-1.svg" medium="image" type="image/svg+xml"/>
</item>
<item>
  <title>Keeping jot’s options open</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2025-08-30-jot-options/</link>
  <description><![CDATA[ 





<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-08-30-jot-options/resources/jot.png" class="img-fluid quarto-figure quarto-figure-left figure-img" alt="Terribly drawn image of the word 'jot' in cursive with a pencil at the end of the letter 't'.  The dot of the letter 'i' is a red love heart." width="200"></p>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>I’ve added a small set of options to <a href="https://github.com/matt-dray/jot">jot</a>, my minimal opinionated Python command-line tool that helps me remember stuff.</p>
</section>
<section id="forget-me-jot" class="level2">
<h2 class="anchored" data-anchor-id="forget-me-jot">Forget me jot</h2>
<p>You may recall <a href="https://www.rostrum.blog/posts/2025-08-25-jot/">I made a Python CLI tool called jot</a> to help me record work tasks. I go to my terminal and type <code>jot "corrected a typo"</code> and it gets added to a simple text file with a timestamp.</p>
<p>To give it a go, I recommend installing <a href="https://docs.astral.sh/uv/">uv</a> and then install <a href="https://github.com/matt-dray/jot">from GitHub</a> like <code>uv tool install git+https://github.com/matt-dray/jot</code>.</p>
</section>
<section id="opt-in" class="level2">
<h2 class="anchored" data-anchor-id="opt-in">Opt in</h2>
<p>I’ve been using jot for the past couple of weeks and it’s helping me record things I would typically forget about. As a result, I’ve found it easier to unobscure ‘<a href="https://www.youtube.com/watch?v=HiF83i1OLOM">invisible work</a>’ and reflect on what I’ve achieved.</p>
<p>But my list of jottings is getting harder to manage as it gets longer. There’s a couple of things that would make it easier for me: to be able to peek at the past few jottings and to search them for specific terms.</p>
<p>So, I’ve added two options and will demonstrate them against some imaginary data:</p>
<details>
<summary>
<code>breakfast-jot.txt</code>
</summary>
<pre><code>[2025-08-30 09:27] granola and yoghurt
[2025-08-29 07:47] pancakes, blueberries and maple syrup
[2025-08-28 08:00] fried egg on sourdough toast
[2025-08-27 08:06] yoghurt and strawberries
[2025-08-26 08:19] strawberry jam on toast
[2025-08-25 08:52] wholemeal toast and poached eggs
[2025-08-24 09:15] muesli and milk
[2025-08-23 09:23] muesli with berries and milk
[2025-08-22 08:13] hash browns and scrambled eggs
[2025-08-21 07:48] granola and yoghurt
[2025-08-20 07:54] cereal and milk</code></pre>
<p>Believe me, my actual breakfast is more boring than this. Also sometimes I have a hobbitesque second breakfast.</p>
</details>
<p>Everything below is correct as of jot v0.2.3, but I may work to improve some features. Feel free to <a href="https://github.com/matt-dray/jot/issues">make suggestions</a> too.</p>
<section id="list" class="level3">
<h3 class="anchored" data-anchor-id="list">1. List</h3>
<p>The first option is simple: print out the last <em>n</em> jottings. You can use the flag <code>--list</code>, or <code>-l</code> for short, and then provide an integer for the number of entries to show.</p>
<p>So let’s print the most recent three entries:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb2-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> jot <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-l</span> 3</span></code></pre></div></div>
</div>
<pre><code>[2025-08-30 09:27] granola and yoghurt
[2025-08-29 07:47] pancakes, blueberries and maple syrup
[2025-08-28 08:00] fried egg on sourdough toast</code></pre>
<p>This is handy for a quick peek at the last few things you’ve been doing.</p>
<p>It reminds me of returning to games like <a href="https://en.wikipedia.org/wiki/Pok%C3%A9mon_FireRed_and_LeafGreen"><em>Pokémon FireRed</em></a> after some time away and being given a quick recap.</p>
<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-08-30-jot-options/resources/previously.png" class="img-fluid figure-img" alt="A greyscale screengrab of the game Pokemon FireRed showing a recap of what the player did before their last save. At the top it says 'previously on your quest'. At the bottom, 'here in Viridian City, Red obtained the item Oak's Parcel. In the centre is an image of the player's sprite in a Pokémart, facing the shopkeeper." width="300"></p>
<figcaption><em>To collect parcels is my real test</em> (thanks <a href="https://www.reddit.com/r/pokemon/comments/f0m0ne/fire_redleaf_green_were_ahead_of_their_time/">Reddit user AIMWSTRN</a>).</figcaption>
</figure>
</div>
</section>
<section id="search" class="level3">
<h3 class="anchored" data-anchor-id="search">2. Search</h3>
<p>The second option is also simple. But much more powerful. It lets you search for words and phrases, powered by <a href="https://docs.python.org/3/library/re.html">re</a>’s regular-expression handling.</p>
<p>A simple example would be to look for a single word, this time with the <code>--search</code> or <code>-s</code> flag<sup>1</sup>:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb4-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jot</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> egg</span></code></pre></div></div>
</div>
<pre><code>[2025-08-28 08:00] fried egg on sourdough toast
[2025-08-25 08:52] wholemeal toast and poached eggs
[2025-08-22 08:13] hash browns and scrambled eggs</code></pre>
<p>But we can use a regular expression to be more specific. We can even specify a date range, thanks to the timestamps.</p>
<p>So, when did I have eggs last work-week, specifically? Let’s use a suitable <del>regeggs</del> regex:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">$</span> jot <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-s</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"2025-08-2([5-9]).*egg"</span></span></code></pre></div></div>
</div>
<pre><code>[2025-08-28 08:00] fried egg on sourdough toast
[2025-08-25 08:52] wholemeal toast and poached eggs</code></pre>
<p>With that regex I’m asking for the string ‘egg’ used in jottings between 25 and 29 August 2025’<sup>2</sup>.</p>
</section>
</section>
<section id="pain-in-the-argparse" class="level2">
<h2 class="anchored" data-anchor-id="pain-in-the-argparse">Pain in the argparse?</h2>
<p>A new learning for me has been <a href="https://docs.python.org/3/library/argparse.html">argparse package</a>, which allows your Python function to accept inputs at the command line.</p>
<p>That’s been crucial for this update to jot (v0.2.3). At the start of the <code>main()</code> function I’ve set up some argparse steps.</p>
<p>To initiate the parser:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb8" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb8-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> argparse</span>
<span id="cb8-2"></span>
<span id="cb8-3">parser <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> argparse.ArgumentParser(</span>
<span id="cb8-4">    prog<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"jot"</span>,</span>
<span id="cb8-5">    description<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Minimal opinionated Python CLI to jot timestamped thoughts."</span>,</span>
<span id="cb8-6">    epilog<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Source: https://github.com/matt-dray/jot"</span>,</span>
<span id="cb8-7">)</span></code></pre></div></div>
</div>
<p>Then we can add arguments to the parser. Here’s how we do that for the search argument, for which I’ve written a corresponding <code>search_jottings()</code> Python function:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb9" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb9-1">parser.add_argument(</span>
<span id="cb9-2">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"-s"</span>,</span>
<span id="cb9-3">    <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"--search"</span>,</span>
<span id="cb9-4">    nargs<span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"?"</span>,</span>
<span id="cb9-5">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">type</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">str</span>,</span>
<span id="cb9-6">    <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">help</span><span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"search jottings (regex supported)"</span>,</span>
<span id="cb9-7">)</span></code></pre></div></div>
</div>
<p>After these declarations, we can collect the parsed arguments:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb10" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb10-1">args <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> parser.parse_args()</span></code></pre></div></div>
</div>
<p>Later in the body of the <code>main()</code> function we can use the named elements of <code>args</code> to pass into our Python functions. For our search option, that looks like this:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb11" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb11-1">search_jottings(jot_path, args.search)</span></code></pre></div></div>
</div>
<p>As a result of this set up, we also get a nice help file for free with <code>--help</code> or <code>-h</code>, which should be helpful for users:</p>
<div class="cell">
<div class="code-copy-outer-scaffold"><div class="sourceCode cell-code" id="cb12" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb12-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">jot</span> <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">-h</span></span></code></pre></div></div>
</div>
<pre><code>usage: jot [-h] [-l [LIST]] [-s [SEARCH]] [text]

Minimal opinionated Python CLI to jot timestamped thoughts.

positional arguments:
  text                  text to write to file

options:
  -h, --help            show this help message and exit
  -l [LIST], --list [LIST]
                        show last n jottings
  -s [SEARCH], --search [SEARCH]
                        search jottings (regex supported)

Source: https://github.com/matt-dray/jot</code></pre>
</section>
<section id="jot-on" class="level2">
<h2 class="anchored" data-anchor-id="jot-on">Jot on</h2>
<p>As I’ve mentioned before, jot was built by me for me. It’s basically feature-complete from my perspective. The plan is to extend it minimally and only as a way to learn more Python. But <a href="https://github.com/matt-dray/jot/issues">I welcome suggestions</a>!</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>[project]
name = "2025-08-30-jot-options"
version = "0.1.0"
requires-python = "&gt;=3.12"
dependencies = []</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>In future I want an argument to limit the number of jottings returned, otherwise we might flood the console. With eggs. Which could get messy.↩︎</p></li>
<li id="fn2"><p>More literally, it matches the string ‘2025-08-2’, replacing the final digit of the date with 5, 6, 7, 8 or 9; then allows for any number of any characters until it finds the string ‘egg’.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div></div></section></div> ]]></description>
  <category>cli</category>
  <category>jot</category>
  <category>python</category>
  <category>uv</category>
  <guid>https://www.rostrum.blog/posts/2025-08-30-jot-options/</guid>
  <pubDate>Fri, 29 Aug 2025 23:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2025-08-30-jot-options/resources/jot.png" medium="image" type="image/png" height="120" width="144"/>
</item>
<item>
  <title>Jotting down some indifferent vibes</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2025-08-25-jot/</link>
  <description><![CDATA[ 





<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-08-25-jot/resources/install-jot.png" class="img-fluid quarto-figure quarto-figure-left figure-img" style="width:100.0%" alt="A terminal with instructions to install the jot tool from GitHub. The jot command is used to save the note 'sharpened my pencils'. The user is prompted to give a path to a text file where the note will be saved. Successive jot calls are made and the content of the text file is printed to show the notes were saved to file with a timestamp."></p>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>I’ve started a minimal opinionated Python command-line tool to help me remember stuff. It’s called <a href="https://github.com/matt-dray/jot">jot</a>.</p>
</section>
<section id="jot-or-not" class="level2">
<h2 class="anchored" data-anchor-id="jot-or-not">Jot or not</h2>
<p>This tool is for me<sup>1</sup>, but if you want to install it, I recommend you <a href="https://docs.astral.sh/uv/">install uv first</a>. Then run <code>uv tool install git+https://github.com/matt-dray/jot</code> from a terminal to install jot (currently v0.2.1) <a href="https://github.com/matt-dray/jot">from GitHub</a>.</p>
<p>Open a terminal and write your first jotting like <code>jot "sharpened my pencils"</code>. When prompted, enter a text file path where your jottings will be saved. This path will be stored in a <code>.jot-config.json</code> file in your home folder. Each <code>jot</code> call will prepend your your text file with the jotting and a timestamp.</p>
<p>Keep that terminal window open and write new jottings throughout the day, week, month, year.</p>
<p>That’s it.</p>
</section>
<section id="forget-me-jot" class="level2">
<h2 class="anchored" data-anchor-id="forget-me-jot">Forget me jot</h2>
<p>The development of jot is a story in three neurologically-themed parts:</p>
<ol type="1">
<li>Goldfish brain.</li>
<li>Robot brain.</li>
<li>Human brain.</li>
</ol>
<section id="goldfish-brain" class="level3">
<h3 class="anchored" data-anchor-id="goldfish-brain">1. Goldfish brain</h3>
<p>I forget stuff. Work stuff in particular. The fogginess stops me from properly recognising my efforts and reflecting on what I’ve learnt.</p>
<p>Recently I’ve reacquainted myself with <a href="https://gilest.org/doingweeknotes/index.html">weeknotes</a>; appreciated Jeff Huang’s <a href="https://jeffhuang.com/productivity_text_file/">neverending text file</a>; and resonated with Chris Albon’s advice to avoid ‘invisible work’ and to try a <a href="https://bsky.app/profile/chrisalbon.com/post/3ld24aoq4ik2p">lightweight logging system</a>.</p>
<p>So I figured I’d write a Python-powered platform-independent command-line tool to jot notes into an immortal text file<sup>2</sup>. I’m developing <a href="https://www.rostrum.blog/posts/2025-08-11-uv-standalone/">my Python skills</a> after all.</p>
</section>
<section id="robot-brain" class="level3">
<h3 class="anchored" data-anchor-id="robot-brain">2. Robot brain</h3>
<p>As a ‘data scientist’ on ‘the cutting edge’, I wondered if I should try ‘<a href="https://en.wikipedia.org/wiki/Vibe_coding">vibe coding</a>’ on this non-critical personal project<sup>3</sup>.</p>
<p>After some low-effort LLM prompts, I got a thing that did the thing. I figured its existence was the main goal and I could iterate later with more ‘artisanal’ approaches (i.e.&nbsp;engaging my monkey brain and paws).</p>
<p>I released this as <a href="https://github.com/matt-dray/jot/releases/tag/v0.1.0">v0.1.0</a> and started using it. It was just what I wanted (i.e.&nbsp;not very much).</p>
<p>So why then did I rewrite the whole thing from scratch?</p>
</section>
<section id="human-brain" class="level3">
<h3 class="anchored" data-anchor-id="human-brain">3. Human brain</h3>
<p>Inevitably (obviously) the vibes didn’t teach me anything<sup>4</sup>. I knew this would happen, but not how much I’d actually miss the failing and learning that comes with doing a new thing.</p>
<p>So I rewrote it myself using some old-fashioned Duck-Duck-Going, some snotty Stackoverflow comments and, shudder, some documentation.</p>
<p>I learnt things about file handling with <code>with</code>, <a href="https://docs.python.org/3.15/library/pathlib.html">pathlib</a> for easing the pain of filepaths, and <a href="https://docs.python.org/3.15/library/argparse.html">argparse</a> for interpreting inputs on the command line.</p>
<p>Is the resulting <a href="https://github.com/matt-dray/jot/releases/tag/v0.2.0">v0.2.0</a> (human-powered edition) better than v0.1.0? Well, it’s ‘as good’ in the sense it achieves the same outcome. My code is more brittle and my limits are more evident. But I’m okay with that.</p>
</section>
</section>
<section id="human-heart" class="level2">
<h2 class="anchored" data-anchor-id="human-heart">Human heart</h2>
<p>There’s not much of a lesson here, apart from the fact I like tinkering and would prefer to do things that way.</p>
<p>I’ll develop jot a bit more with the aid of ‘traditional’ techniques. The interface will remain minimal, but I’d like to add some really basic options to fetch jottings from the past day/week/month or that match a keyword. <a href="https://github.com/matt-dray/jot/issues">Add some ideas</a> if you like.</p>
<p>And actually, I think LLMs do have a place in this story, just not in the development stage. Before team standups I can consult an LLM to summarise the past week from my jottings. Before appraisals I can summarise the past year.</p>
<p>One particular LLM asked if I’d like some Python code to generate a P*werPoint slide to summarise my week. Wow!</p>
<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-08-25-jot/resources/jot-pptx.png" class="img-fluid figure-img" style="width:100.0%" alt="A PowerPoint slide. The title is 'weekly summary' with a sun emoji. There are two columns. The left is titled 'what went well' with a green checkbox emoji. The right is titled 'lessons learnt' with a book emoji. Both columns have three bullet points of text. The text doesn't wrap. It overlaps and runs off the screen."></p>
<figcaption>Word wrap is for cowards.</figcaption>
</figure>
</div>
<p>Very cool. Judgement day is inevitable.</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>[project]
name = "2025-08-25-jot"
version = "0.1.0"
requires-python = "&gt;=3.12"
dependencies = []</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Though <a href="https://www.rostrum.blog/about#adriana-de-palma">Adriana</a> has been using it too, in a slightly different way to me.↩︎</p></li>
<li id="fn2"><p>You’re thinking ‘yes, but now you have a meta problem: will you actually remember to write in it?’ I don’t think this is a problem for me, personally. A terminal window is always in front of my face and hard to miss. I seem capable of building habits for other positive things.↩︎</p></li>
<li id="fn3"><p>This blog is a non-critical personal project too, but I have not and will not write posts with anything other than my brain and typo-addled fingers. <a href="https://www.rostrum.blog/posts/2024-03-15-ai-garbage/">I’ve written before</a> about my distaste for LLMs as slop manufacturers. I have fewer qualms about using it for boilerplate, as a code helper, and for-fun personal projects. But I’m still not 100% happy with it in general from an ethical standpoint and for the existential dread it can bring.↩︎</p></li>
<li id="fn4"><p>Yes, I could have asked the LLM to walk me through the code and teach me. Would I have remembered any of it? Unlikely. More fool me.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents" id="quarto-reuse"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div class="quarto-appendix-contents"><div><a rel="license" href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></div></div></section></div> ]]></description>
  <category>cli</category>
  <category>jot</category>
  <category>python</category>
  <category>uv</category>
  <guid>https://www.rostrum.blog/posts/2025-08-25-jot/</guid>
  <pubDate>Sun, 24 Aug 2025 23:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2025-08-25-jot/resources/install-jot.png" medium="image" type="image/png" height="73" width="144"/>
</item>
<item>
  <title>A bank holiday to honour uv?</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2025-08-11-uv-standalone/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-08-11-uv-standalone/resources/bank.png" class="img-fluid figure-img" style="width:100.0%" alt="The command bank.py is run in a macOS terminal Window, yielding the phrase 'Summer bank holiday is on 25 August 2025', with a series of triangle symbols alternating with a black-and-white fill to look like bunting."></p>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>You can use <a href="https://docs.astral.sh/uv/">uv</a> to help turn a Python<sup>1</sup> script into a self-contained executable. Useful for really important stuff, like… discovering the next public holiday from your terminal?</p>
<p>This is my own attempt at something <a href="https://mathspp.com/blog/til/standalone-executable-python-scripts-with-uv">I saw Rodrigo do</a><sup>2</sup>.</p>
</section>
<section id="worth-a-bunt" class="level2">
<h2 class="anchored" data-anchor-id="worth-a-bunt">Worth a bunt</h2>
<p>The UK government maintains <a href="https://www.api.gov.uk/#uk-public-sector-apis">an API catalogue</a>. A very (very) simple API example is <a href="https://www.api.gov.uk/gds/bank-holidays/#bank-holidays">the bank-holidays API</a> run by the Government Digital Service (GDS). It’s perhaps most well-known for <a href="https://www.gov.uk/bank-holidays">the associated page on the GOV.UK website</a>.</p>
<p>All the API does is serve a JSON file with upcoming public holidays. I wrote a ramshackle little Python script that grabs the file, works out what the next holiday is, and prints it out.</p>
<p>Crucially, it also prints <a href="https://en.wikipedia.org/wiki/Bunting_(decoration)">bunting</a> if, according to the JSON, the occasion calls for it.</p>
</section>
<section id="banking-crisis" class="level2">
<h2 class="anchored" data-anchor-id="banking-crisis">Banking crisis</h2>
<p>So, here’s the quite-ordinary code for this demonstration<sup>3</sup>:</p>
<div class="cell">
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre><strong>bank.py</strong></pre>
</div>
<div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb1-1"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> httpx</span>
<span id="cb1-2"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> datetime <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> date</span>
<span id="cb1-3"></span>
<span id="cb1-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Fetch bank holidays</span></span>
<span id="cb1-5"></span>
<span id="cb1-6">resp <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> httpx.get(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://www.gov.uk/bank-holidays.json"</span>)</span>
<span id="cb1-7">events <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> resp.json()[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"england-and-wales"</span>][<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"events"</span>]</span>
<span id="cb1-8"></span>
<span id="cb1-9"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> event <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> events:</span>
<span id="cb1-10">    event[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"date"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> date.fromisoformat(event[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"date"</span>])</span>
<span id="cb1-11"></span>
<span id="cb1-12"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Find next holiday</span></span>
<span id="cb1-13">today <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> date.today()</span>
<span id="cb1-14">future <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [event <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> event <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> events <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> event[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"date"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> today]</span>
<span id="cb1-15"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> future[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb1-16"></span>
<span id="cb1-17"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Print message</span></span>
<span id="cb1-18"></span>
<span id="cb1-19">is_today <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"date"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> today</span>
<span id="cb1-20">needs_bunting <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bunting"</span>]</span>
<span id="cb1-21"></span>
<span id="cb1-22"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> is_today:</span>
<span id="cb1-23">    when <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"today!"</span></span>
<span id="cb1-24"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span>: </span>
<span id="cb1-25">    when <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"on </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'date'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>strftime(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%d</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> %B %Y'</span>)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb1-26"></span>
<span id="cb1-27">str_out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'title'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> is </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>when<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb1-28"></span>
<span id="cb1-29"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> needs_bunting:</span>
<span id="cb1-30">    str_out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> str_out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" ▼▽▼▽▼▽"</span></span>
<span id="cb1-31"></span>
<span id="cb1-32"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(str_out)</span></code></pre></div>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>Christmas Day is on 25 December 2025 ▼▽▼▽▼▽</code></pre>
</div>
</div>
<p>Very simple. Does the job<sup>4</sup>. But if we want to use the script, maybe we’ll have to clone a repo containing the script, change directory, activate the virtual environment and only then run the script. Every time we want to use it.</p>
<p>Wouldn’t it be better if I could just call it quickly from my macOS Terminal when I need it? To just type <code>bank.py</code> and get a quick response?</p>
</section>
<section id="uv-protection" class="level2">
<h2 class="anchored" data-anchor-id="uv-protection">uv protection</h2>
<p>This is where <a href="https://docs.astral.sh/uv/">uv</a> can help out<sup>5</sup>. You can <a href="https://docs.astral.sh/uv/getting-started/installation/">install</a> via Homebrew with <code>brew install uv</code>, for example.</p>
<p>You can start by adding a block at the top of your file to declare dependencies. Kind of like bundling a <code>pyproject.toml</code> inside your script<sup>6</sup>. No need to worry about a separate dependencies file, lockfile or virtual environment.</p>
<p>To add your dependencies, use <code>uv add</code> with the <code>--script</code> option in the form:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb3-1"><span class="ex" style="color: null;
background-color: null;
font-style: inherit;">uv</span> add httpx datetime <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">--script</span> bank.py</span></code></pre></div>
</div>
<p>Which will add the following section at the top of our <code>bank.py</code> script, including the Python requirement:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb4-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># /// script</span></span>
<span id="cb4-2"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># requires-python = "&gt;=3.12"</span></span>
<span id="cb4-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># dependencies = [</span></span>
<span id="cb4-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#     "datetime",</span></span>
<span id="cb4-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#     "httpx",</span></span>
<span id="cb4-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ]</span></span>
<span id="cb4-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ///</span></span></code></pre></div>
</div>
<p>And there you have it: all the information needed to run this file in an isolated manner. You’ll be able to run <code>uv run bank.py</code> from your terminal without any additional files required.</p>
<p>But wait! There’s more. How boring, how <em>painful</em> to have to write the <code>uv run</code> bit. That’s <em>six</em> characters<sup>7</sup>. And you’ll have to provide the full path to the script, yikes.</p>
<p>Instead, you can add a <a href="https://en.wikipedia.org/wiki/Shebang_%28Unix%29">shebang</a><sup>8</sup> at the top of the file to make it executable:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb5-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/usr/bin/env -S uv run</span></span></code></pre></div>
</div>
<p>Note the unusual bit on the end… aha! uv again! The <code>-S uv run</code> bit asks the interpreter to let good ol’ uv handle the whole operation.</p>
<p>Finally, you can change the mode (<code>chmod</code>) of the file by adding (<code>+</code>) executable (<code>x</code>) status. Then put the file where you terminal will look if you type the name of the file.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb6" style="background: #f1f3f5;"><pre class="sourceCode sh code-with-copy"><code class="sourceCode bash"><span id="cb6-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">chmod</span> +x bank.py</span>
<span id="cb6-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">mv</span> bank.py ~/.local/bin</span></code></pre></div>
</div>
<p>Then you can merely call <code>bank.py</code> from the terminal to know when the next bank holiday is.</p>
<div class="tip">
<p> <b>Note</b></p>
<p>In fact, I discovered later <a href="https://docs.astral.sh/uv/guides/scripts/#using-a-shebang-to-create-an-executable-file">in uv’s docs</a> that you can add the <code>--script</code> flag to the shebang and remove the <code>.py</code> extension from the filename to ensure you only need to type <code>bank</code> into the terminal. Much sleeker.</p>
</div>
</section>
<section id="one-mans-trash" class="level2">
<h2 class="anchored" data-anchor-id="one-mans-trash">One man’s trash</h2>
<p>You can find a <a href="https://gist.github.com/matt-dray/c97603f294fb80736b669141191d827b">GitHub Gist of my code</a><sup>9</sup>, or:</p>
<details>
<summary>
Click to see the complete <code>bank.py</code> script.
</summary>
<div class="cell">
<div class="code-with-filename">
<div class="code-with-filename-file">
<pre><strong>bank.py</strong></pre>
</div>
<div class="sourceCode cell-code" id="cb7" style="background: #f1f3f5;"><pre class="sourceCode python code-with-copy"><code class="sourceCode python"><span id="cb7-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#!/usr/bin/env -S uv run</span></span>
<span id="cb7-2"></span>
<span id="cb7-3"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># /// script</span></span>
<span id="cb7-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># requires-python = "&gt;=3.12"</span></span>
<span id="cb7-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># dependencies = [</span></span>
<span id="cb7-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#     "datetime",</span></span>
<span id="cb7-7"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">#     "httpx",</span></span>
<span id="cb7-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ]</span></span>
<span id="cb7-9"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># ///</span></span>
<span id="cb7-10"></span>
<span id="cb7-11"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> httpx</span>
<span id="cb7-12"><span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">from</span> datetime <span class="im" style="color: #00769E;
background-color: null;
font-style: inherit;">import</span> date</span>
<span id="cb7-13"></span>
<span id="cb7-14"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Fetch bank holidays</span></span>
<span id="cb7-15"></span>
<span id="cb7-16">resp <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> httpx.get(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"https://www.gov.uk/bank-holidays.json"</span>)</span>
<span id="cb7-17">events <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> resp.json()[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"england-and-wales"</span>][<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"events"</span>]</span>
<span id="cb7-18"></span>
<span id="cb7-19"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> event <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> events:</span>
<span id="cb7-20">    event[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"date"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> date.fromisoformat(event[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"date"</span>])</span>
<span id="cb7-21"></span>
<span id="cb7-22"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Find next holiday</span></span>
<span id="cb7-23">today <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> date.today()</span>
<span id="cb7-24">future <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> [event <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> event <span class="kw" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> events <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> event[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"date"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">&gt;=</span> today]</span>
<span id="cb7-25"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span> <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> future[<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>]</span>
<span id="cb7-26"></span>
<span id="cb7-27"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Print message</span></span>
<span id="cb7-28"></span>
<span id="cb7-29">is_today <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"date"</span>] <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> today</span>
<span id="cb7-30">needs_bunting <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bunting"</span>]</span>
<span id="cb7-31"></span>
<span id="cb7-32"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> is_today:</span>
<span id="cb7-33">    when <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"today!"</span></span>
<span id="cb7-34"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">else</span>: </span>
<span id="cb7-35">    when <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"on </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'date'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">.</span>strftime(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">%d</span><span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;"> %B %Y'</span>)<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb7-36"></span>
<span id="cb7-37">str_out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> <span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">f"</span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">next</span>[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">'title'</span>]<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;"> is </span><span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">{</span>when<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">}</span><span class="ss" style="color: #20794D;
background-color: null;
font-style: inherit;">"</span></span>
<span id="cb7-38"></span>
<span id="cb7-39"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> needs_bunting:</span>
<span id="cb7-40">    str_out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">=</span> str_out <span class="op" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">" ▼▽▼▽▼▽"</span></span>
<span id="cb7-41"></span>
<span id="cb7-42"><span class="bu" style="color: null;
background-color: null;
font-style: inherit;">print</span>(str_out)</span></code></pre></div>
</div>
</div>
</details>
<p>Next, more important goal: write something similar to help me know what bin day it is (we alternate fortnightly between refuse and recycling). I saved a neighbour from getting erroneously binfluenced recently, so this is dear to my heart.</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>[project]
name = "2025-08-11-uv-standalone"
version = "0.1.0"
description = "rostrum.blog post: uv-standalone"
requires-python = "&gt;=3.12"
dependencies = [
    "datetime&gt;=5.5",
    "httpx&gt;=0.28.1",
]</code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>And thus my 182-post streak without running Python comes to an end.↩︎</p></li>
<li id="fn2"><p>I found this <a href="https://mathspp.com/blog/til/standalone-executable-python-scripts-with-uv">thanks to Rodrigo</a>, who spotted it via <a href="https://simonwillison.net/2024/Aug/21/usrbinenv-uv-run/">Simon</a>, who spotted it <a href="https://github.com/alsuren/sixdofone/pull/8">via David</a>. And now I’m ripping it off.↩︎</p></li>
<li id="fn3"><p>Please feel free to critique my Python code. How could it possibly be worse than my R code?↩︎</p></li>
<li id="fn4"><p>As a more frequent R user: <em>0 days since last zero-indexing mistake.</em>↩︎</p></li>
<li id="fn5"><p>Perhaps you have been living under a rock where uv rays have not struck you. uv is all the rage for doing all-the-Python-setup-things-that-make-Python-setup-a-headache. And it’s super fast. Because Rust. Because carcinisation is coming for all of us.↩︎</p></li>
<li id="fn6"><p><em>The call was coming from inside the script.</em>↩︎</p></li>
<li id="fn7"><p>Likely more, given <a href="https://www.rostrum.blog/posts/2021-02-27-typos/index.html">my terrible typo history</a>.↩︎</p></li>
<li id="fn8"><p>Basically says ‘if I’m called, execute me like a little standalone program’.↩︎</p></li>
<li id="fn9"><p>I’m losing my mind becuase I’ve discovered recently that the pronunciation of ‘gist’ is another ‘how do you pronounce gif?’ situation. To be clear, it should be pronounced ‘gist’.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div id="quarto-reuse" class="quarto-appendix-contents"><div>CC BY-NC-SA 4.0</div></div></section></div> ]]></description>
  <category>api</category>
  <category>cli</category>
  <category>python</category>
  <category>uv</category>
  <guid>https://www.rostrum.blog/posts/2025-08-11-uv-standalone/</guid>
  <pubDate>Sun, 10 Aug 2025 23:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2025-08-11-uv-standalone/resources/bank.png" medium="image" type="image/png" height="35" width="144"/>
</item>
<item>
  <title>Dependency-light hex stickers with {gex}</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2025-02-25-gex/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-02-25-gex/resources/logo.png" class="img-fluid figure-img" style="width:33.0%" alt="A hexagon logo for the package 'gex'. In the centre is italicised serif text saying the package name. The background is a repeating pattern of a line-drawn frill-winged lizards heading from lower-right to top-left. It has a border. The colours are olivedrab1 and olivedrab4 from base R's named colour palette."></p>
<figcaption class="figure-caption">Would you buy this as a tasteful wallpaper?</figcaption>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>The package {hexbase} has been improved and renamed <a href="https://github.com/matt-dray/gex">{gex}</a>. It’s no longer ‘dependency free’, but has a goal of being ‘gridverse’ only. Sellout!</p>
</section>
<section id="enter-the-gecko" class="level2">
<h2 class="anchored" data-anchor-id="enter-the-gecko">Enter the gecko</h2>
<p><a href="https://www.rostrum.blog/posts/2025-01-31-hexbase/">In my last post</a> I wrote about <a href="https://github.com/matt-dray/hexbase">{hexbase}</a>: a dependency-free hex-logo builder, which was helping me learn the basics of R’s grid-graphics system.</p>
<p>Now the package goal is to stay ‘lightweight’ and import only Prof Murrell and co’s family of small ‘gridverse’ packages (I think I’ve invented this term?).<sup>1</sup></p>
<p>Hence the name change: ‘gex’ is an allusion to grid + hex. I liked ‘grex’ more, but it’s already taken on CRAN. Also, a certain flavour of 90s gamer might appreciate the new name.</p>
<p>A great example of the benefit is for the hex border. In {hexbase}, the ‘border’ was just the gap between a smaller hexagon inside a bigger one. Now, {gex} uses {gridGeometry}‘s <code>polyclipGrob()</code> with <code>op = "minus"</code> to ’cut out’ a smaller from a bigger hexagon to create a polygon that can be applied with <code>add_border()</code>.</p>
</section>
<section id="cast-a-gex-hex" class="level2">
<h2 class="anchored" data-anchor-id="cast-a-gex-hex">Cast a gex hex</h2>
<p>Building a hex with {gex} differs little from {hexbase}. The steps are:</p>
<ol type="1">
<li>Open a PNG graphics device with <code>open_hex()</code>.</li>
<li>Add the hexagon with <code>add_hex()</code>.</li>
<li>Add and arrange as many text and image elements as you like with <code>add_text()</code> and <code>add_image()</code>.</li>
<li>Choose to overlay a border with <code>add_border()</code> (or not).</li>
<li>Close the device and write the file with <code>close_device()</code>.</li>
</ol>
<p>Below is the code used to create the logo for the package itself (how meta), which is shown at the top of this post.<sup>2</sup></p>
<p>First, install the package <a href="https://github.com/matt-dray/gex">from GitHub</a>.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install.packages</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"remotes"</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># if not yet installed</span></span>
<span id="cb1-2">remotes<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install_github</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"matt-dray/gex"</span>)</span></code></pre></div>
</div>
<p>Image manipulation is at the the user’s discretion before being added to the hex. Here I’ll use {magick} to wrangle a line-drawing of a lizard that I want to use (which is bundled in the package), turning it from black to green. I’ll save it as a new PNG and re-read it.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1">img_tmp <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tempfile</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fileext =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".png"</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># temp storage for image</span></span>
<span id="cb2-2"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">system.file</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"images"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"lizard.png"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">package =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gex"</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span> </span>
<span id="cb2-3">  magick<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">image_read</span>() <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb2-4">  magick<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">image_fill</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"olivedrab1"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fuzz =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">100</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">refcolor =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"black"</span>) <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">|&gt;</span></span>
<span id="cb2-5">  magick<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">image_write</span>(img_tmp)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># write</span></span>
<span id="cb2-6">img <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> png<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">readPNG</span>(img_tmp)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># read the edited image</span></span></code></pre></div>
</div>
<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-02-25-gex/resources/lizard-olivedrab1.png" class="img-fluid figure-img" style="width:20.0%" alt="A line drawing of a lizard, mouth agape, with a winglike frill extending from its fore to hind limbs. The linework is in a bright shade of olive drab."></p>
<figcaption class="figure-caption">Charming little fellow.</figcaption>
</figure>
</div>
<p>Now I’ll start building the hex. Here I’m using loops to write multiple images and to add the text twice with an offset and different colours for a drop-shadow effect.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb3-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Set up write location and graphics device</span></span>
<span id="cb3-2">temp_path <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tempfile</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fileext =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".png"</span>)</span>
<span id="cb3-3">gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">open_device</span>(temp_path)</span>
<span id="cb3-4"></span>
<span id="cb3-5"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Add same image multiple times in a loop</span></span>
<span id="cb3-6">gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_hex</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">col =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"olivedrab4"</span>)</span>
<span id="cb3-7"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> (x <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">seq</span>(<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.04</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.2</span>)) {  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># add many images in a loop</span></span>
<span id="cb3-8">  <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> (y <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">seq</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1</span>, <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.16</span>)) {</span>
<span id="cb3-9">    gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_image</span>(img, x, y, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">width =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.3</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">angle =</span> <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span><span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">30</span>)</span>
<span id="cb3-10">  }</span>
<span id="cb3-11">}</span>
<span id="cb3-12"></span>
<span id="cb3-13"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Add text with 'drop shadow'</span></span>
<span id="cb3-14">font <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"IBM Plex Serif"</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># installed on my system</span></span>
<span id="cb3-15">x <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.52</span>; y <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.56</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># starting positions</span></span>
<span id="cb3-16">styles <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">list</span>(  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># to loop over</span></span>
<span id="cb3-17">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">list</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">coords =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> x <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">+</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.01</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> y <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">-</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.01</span>), <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">col =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"olivedrab4"</span>),</span>
<span id="cb3-18">  <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">list</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">coords =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">c</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> x, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> y), <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">col =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"olivedrab1"</span>)</span>
<span id="cb3-19">)</span>
<span id="cb3-20"><span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">for</span> (style <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">in</span> styles) {</span>
<span id="cb3-21">  gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_text</span>(</span>
<span id="cb3-22">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">string =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"gex"</span>,</span>
<span id="cb3-23">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">x =</span> style[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"coords"</span>]][[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"x"</span>]],</span>
<span id="cb3-24">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">y =</span> style[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"coords"</span>]][[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"y"</span>]],</span>
<span id="cb3-25">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">60</span>,</span>
<span id="cb3-26">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">col =</span> style[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"col"</span>]],</span>
<span id="cb3-27">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">family =</span> font,</span>
<span id="cb3-28">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">face =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"italic"</span></span>
<span id="cb3-29">  )</span>
<span id="cb3-30">}</span>
<span id="cb3-31"></span>
<span id="cb3-32"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Add border and write file</span></span>
<span id="cb3-33">gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_border</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">col =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"olivedrab1"</span>)</span>
<span id="cb3-34">gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">close_device</span>()</span></code></pre></div>
</div>
<p>As mentioned previously, you build the hex by opening a device and adding elements to it, before closing it to save the file. That may make some people uncomfortable since there’s no ‘object’ as such; you can’t pipe between functions in {gex}. But this comes as no surprise to base and grid-graphics enjoyers, which is kind of the target market. There are dozens of us!</p>
<p>Having said this, I did enjoy <a href="https://kieranhealy.org/blog/archives/2025/02/06/kerning-and-kerning-in-a-widening-gyre/">Kieran’s recent sermon</a> on this style of interaction:</p>
<blockquote class="blockquote">
<p>We have to remember to turn the device off once we’re done with it, like it’s 1997. If you forget, you won’t notice for a while but eventually it’s like your Dad is gonna yell at you because you forgot to turn the lights off downstairs before you went to bed or you left the fridge door open after you went to get a drink of milk or you opened the window while the air conditioning is running in the house what the hell kind of child have I raised.</p>
</blockquote>
<p>I’m (probably) not your dad and you (probably) weren’t raised in a barn; I trust you to <code>close_device()</code> when using {gex}. Also, the package is partly named after a videogame from pre-1997, so it’d be anachronistic to do it any other way, amirite?</p>
</section>
<section id="going-on-grid" class="level2">
<h2 class="anchored" data-anchor-id="going-on-grid">Going <em>on</em> grid</h2>
<p>Since {gex} uses the grid graphics system, other ‘gridverse’ packages can be used to help make your hex. Two others are <a href="https://cran.r-project.org/package=gridBase">{gridBase}</a> to put a base R plot in a viewport and <a href="https://cran.r-project.org/package=gridGraphics">{gridGraphics}</a> to convert that plot to a grob. Handily, Guangchuang Yu (of {hexSticker} fame!) wrapped this up in <code>ggplotify::base2grob()</code>, which does what it says on the tin.</p>
<p>Here’s a plot of multicoloured points applied to a hex. Note all the {grid} calls, which are the type of thing that {gex} hides in its functions so you don’t have to think about it (more on that later in the post).</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb4-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Start with {gex}</span></span>
<span id="cb4-2">temp_path <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tempfile</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fileext =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".png"</span>)</span>
<span id="cb4-3">gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">open_device</span>(temp_path)</span>
<span id="cb4-4">gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_hex</span>()</span>
<span id="cb4-5"></span>
<span id="cb4-6"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Add a base plot without {gex}</span></span>
<span id="cb4-7">grid<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">pushViewport</span>(grid<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">viewport</span>())</span>
<span id="cb4-8">base_plot <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> ggplotify<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">base2grob</span>(</span>
<span id="cb4-9">  \() {</span>
<span id="cb4-10">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">par</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">mar =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">rep</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">0</span>, <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">4</span>))  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># no plot margin</span></span>
<span id="cb4-11">    n <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">1000</span></span>
<span id="cb4-12">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">plot</span>(</span>
<span id="cb4-13">      <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">runif</span>(n), <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">runif</span>(n),  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 'random' points</span></span>
<span id="cb4-14">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">xaxs =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"i"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">yaxs =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"i"</span>,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 'edge-to-edge' plot</span></span>
<span id="cb4-15">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">axes =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">ann =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">FALSE</span>,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># no chart stuff</span></span>
<span id="cb4-16">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">pch =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">16</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">col =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">palette.colors</span>(n, , , <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># style points</span></span>
<span id="cb4-17">    )</span>
<span id="cb4-18">  }</span>
<span id="cb4-19">)</span>
<span id="cb4-20">grid<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grid.draw</span>(base_plot)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># it's a grob now, we can draw it</span></span>
<span id="cb4-21">grid<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">popViewport</span>()</span>
<span id="cb4-22"></span>
<span id="cb4-23"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Finish up with {gex}</span></span>
<span id="cb4-24">gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">close_device</span>()</span></code></pre></div>
</div>
<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-02-25-gex/resources/base-grob.png" class="img-fluid figure-img" style="width:25.0%" alt="A black hexagon filled with a thousand small circles of varying colours."></p>
</figure>
</div>
<p>Other grid-adjacent packages could be used as well. Trevor has put together <a href="https://trevorldavis.com/R/gridpattern/dev/">{gridpattern}</a> to allow for pattern and gradient fills. For example, you could call <code>grid.pattern_gradient()</code> to add a splendid background to your hex.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb5" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb5-1">temp_path <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tempfile</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fileext =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".png"</span>)</span>
<span id="cb5-2">gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">open_device</span>(temp_path)</span>
<span id="cb5-3">gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_hex</span>()</span>
<span id="cb5-4">gridpattern<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">grid.pattern_gradient</span>()</span>
<span id="cb5-5">gex<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">close_device</span>()</span></code></pre></div>
</div>
<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-02-25-gex/resources/pattern.png" class="img-fluid figure-img" style="width:25.0%" alt="A hexagon with a tasteful colour-gradient background."></p>
</figure>
</div>
<p>Lovely.</p>
</section>
<section id="the-sticky-back-end" class="level2">
<h2 class="anchored" data-anchor-id="the-sticky-back-end">The sticky back-end</h2>
<p>In case you’re wondering what’s going on under the hood, here’s a simplified description. This is more for the nerds. Which is what you are if you’ve read this far.</p>
<section id="grid-reference" class="level3">
<h3 class="anchored" data-anchor-id="grid-reference">Grid reference</h3>
<p>You can find lots of useful material about {grid} and the ‘gridverse’ on <a href="https://www.stat.auckland.ac.nz/~paul/grid/grid.html">the dedicated page</a> of The University of Auckland’s website and on Prof Murrell’s <a href="https://www.stat.auckland.ac.nz/~paul/">home page</a>. Read that first.</p>
<p>But here’s a massive oversimplification anyway. With grid graphics you draw ‘grobs’ (graphics objects, like polygons, lines and text) into little windows on your canvas called ‘viewports’. You create a viewport with various properties (size, position, rotation, etc), ‘push’ to activate it, draw your grobs inside it and then ‘pop’ the viewport to add it to your canvas. Importantly, you can nest viewports for finer control or to inherit characteristics. Note that the default coordinates of your canvas are on ‘npc’ (native parent coordinates) units, meaning a 1 × 1 area with origin [0,0] in the lower left.</p>
<p>{gex} probably doesn’t use {grid} optimally, but I’ve learnt enough for the package to work for my own needs at least.</p>
</section>
<section id="push-pops" class="level3">
<h3 class="anchored" data-anchor-id="push-pops">Push pops</h3>
<p>With {gex}, you start by running <code>open_device()</code> to call a <code>png()</code> graphics device pre-filled with the width and height of the stickers standard, plus a transparent background. The graphics device will be populated with grobs and eventually saved to file in <code>close_device()</code>.</p>
<p>Next, the <code>add_hex()</code> function. It first sets up a <code>polygonGrob</code> of hexagon vertices, which is used in two ways. First, a viewport is pushed with the grob as a <code>clip</code> mask, which means anything falling outside of the hexagon area will be deleted when this viewport is popped. This happens as the end in <code>close_device()</code>. Second, we <code>grid.draw()</code> the grob in its own viewport so its added to the canvas. Since a ‘point-up’ hexagon is slightly narrower than tall, we remove the ‘slivers’ either side of it on the x-axis by setting the enclosing viewport’s <code>xscale</code> to the maximum hexagon width. This ensures that our final PNG file goes ‘edge to edge’. The hex grobs are added with ‘native’ rather than ‘npc’ units so that they’re relative to that ‘sliverless’ viewport.</p>
<p>The <code>add_text()</code> and <code>add_image()</code> functions work in similar fashion: a viewport is opened to house a grob, which is drawn with <code>grid.text()</code> or <code>grid.raster()</code>. Crucially, we put that grob in another viewport in which we can control rotation. We nest the viewports this way so that the user controls the x and y positions or a rotated text or image relative to the hex. If we didn’t do this, the x and y positions would be relative to the element’s angle of rotation.</p>
<p>You would typically use <code>add_border()</code> at this stage, if you want to. The border is just a polygon with the same coordinates as in <code>add_hex()</code>, but with a smaller hexagon cut out of the middle. The <code>width</code> of the border is just the inverse ratio of the inner to the outer hexagon size. By placing this after the text and images, it will overlay them if they inhabit the same coordinates.</p>
<p>Finally, we <code>close_device()</code> to pop the ‘hanging’ viewports we pushed in <code>open_device()</code> at the start. This includes the one that clips out any content that falls outside the extent of the hexagon. The function also contains a call to <code>dev.off()</code> to close the device and save the image to the path specified by <code>file_path</code> in <code>open_device()</code>.</p>
</section>
</section>
<section id="further-gexplorations" class="level2">
<h2 class="anchored" data-anchor-id="further-gexplorations">Further gexplorations?</h2>
<p>As ever, I’m having a nice time. Also as ever, I may choose to continue this or stop working on it entirely. The most important thing is that {gex} will help me towards the utopia of Hex-Driven Development (HDD), the one true method of developing R packages by <em>starting</em> with the sticker.<sup>3</sup></p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered: 2025-02-25 22:34:12 GMT</code></pre>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.4.2 (2024-10-31)
Platform: aarch64-apple-darwin20
Running under: macOS Ventura 13.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.4.2    fastmap_1.2.0     cli_3.6.3.9000   
 [5] tools_4.4.2       htmltools_0.5.8.1 rstudioapi_0.16.0 yaml_2.3.10      
 [9] rmarkdown_2.28    knitr_1.48        jsonlite_1.8.9    xfun_0.48        
[13] digest_0.6.37     rlang_1.1.4       evaluate_1.0.1   </code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>There a few more packages listed in ‘Suggests’, but these are mostly used for testing or examples. A particular highlight is <a href="https://github.com/markvanderloo/tinytest">{tinytest}</a>, which I’m using for the first time and is very much in the ‘lightweight’ spirit.↩︎</p></li>
<li id="fn2"><p>{gex} has also been used in anger to create the hex for <a href="https://github.com/best-practice-and-impact/aftables">{aftables}</a>, which is the new name for {a11ytables} that you may recall <a href="https://www.rostrum.blog/index.html#category=a11ytables">from earlier posts</a>. It’s also now <a href="https://cran.r-project.org/package=aftables">on CRAN</a> (thanks Olivia!).↩︎</p></li>
<li id="fn3"><p>Could this explain why my packages don’t have millions of downloads? Am I so out of touch? <a href="https://frinkiac.com/caption/S05E20/291123">No, it’s the children who are wrong</a>.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div id="quarto-reuse" class="quarto-appendix-contents"><div>CC BY-NC-SA 4.0</div></div></section></div> ]]></description>
  <category>grid</category>
  <category>gex</category>
  <category>r</category>
  <guid>https://www.rostrum.blog/posts/2025-02-25-gex/</guid>
  <pubDate>Tue, 25 Feb 2025 00:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2025-02-25-gex/resources/logo.png" medium="image" type="image/png" height="167" width="144"/>
</item>
<item>
  <title>Dependency-free hex stickers with {hexbase}</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2025-01-31-hexbase/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2025-01-31-hexbase/resources/demo-hex.png" class="img-fluid figure-img" style="width:33.0%" alt="A bisque-coloured hexagon with a hotpink border. The R logo is above centre and tilted at a jaunty angle. The text 'example' writtenin Comic Sans and is copied and shifted to give the effect of a shadow, red on black. On the lower-right edge in blue papyrus font is the text 'visit rostrum.blog ftw'."></p>
<figcaption class="figure-caption">Graphic design is my passion.</figcaption>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>I’ve made <a href="https://github.com/matt-dray/hexbase">{hexbase}</a>, a simple package to create hex stickers in R. The twist: it relies only on base R.</p>
<div class="tip">
<p> <b>Note</b></p>
<p>The package has been renamed to {gex} (loosely ‘grid’ + ‘hex’) and is now, ahem, ‘dependency-light’ to take advantage of other ‘gridverse’ packages (grid-graphics expansion packages made by Prof Murrell and others). The API for {gex} has also been updated relative to the version of {hexbase} shown in this post.</p>
</div>
</section>
<section id="stick-around" class="level2">
<h2 class="anchored" data-anchor-id="stick-around">Stick around</h2>
<p>Hex stickers are obviously popular in the R community <a href="http://hexb.in/">and beyond</a> for advertising your package or other projects<sup>1</sup>. So much so that there are packages to <a href="http://hexsession.liomys.mx/">organise your stickers</a>.</p>
<p>There’s some existing tools to help you make these:</p>
<ul>
<li><a href="https://github.com/GuangchuangYu/hexSticker">{hexSticker}</a> by Guangchuang Yu</li>
<li><a href="https://github.com/dmi3kno/bunny">{bunny}</a> by Dmytro Perepolkin, used in conjunction with {magick}</li>
<li>the <a href="https://github.com/ColinFay/hexmake">hexmake Shiny app</a><sup>2</sup> by Colin Fay</li>
</ul>
<p>Surprise, I’ve made another one: <a href="https://github.com/matt-dray/hexbase">{hexbase}</a>. Why? I wanted to learn about low-level plotting with <a href="https://www.stat.auckland.ac.nz/~paul/grid/grid.html">{grid}</a>, one of R’s built-in packages<sup>3</sup>. The constraint of using only {grid} in a simple, lightweight package seemed a good test (and a good wheeze).</p>
<p>It’s not perfect, but it does what I need and I learnt a lot.</p>
</section>
<section id="do-the-griddy" class="level2">
<h2 class="anchored" data-anchor-id="do-the-griddy">Do the griddy</h2>
<p>I reckon most R users make plots with <a href="https://ggplot2.tidyverse.org/">{ggplot2}</a> otherwise R’s native <code>plot()</code>ting system. I’ll bet {grid}<sup>4</sup> is used less often. It’s lower-level and you have to build everything up yourself, but it is extremely flexible.</p>
<p>To put it extremely simply, {grid} mostly works through a system of nested ‘viewports’ that you push and pop to add and arrange various graphics objects (grobs<sup>5</sup>) onto a graphics device.</p>
<p>This post isn’t a how-to for {grid} because there’s too much to cover, though I might write a separate post to explain how {hexbase} uses {grid}. I found <a href="https://www.stat.auckland.ac.nz/~paul/grid/grid.html">Prof Paul Murrell</a>’s writings really helpful, which is no surprise as Paul is {grid}’s author. See also <a href="https://bookdown.org/rdpeng/RProgDA/the-grid-package.html">Roger’s chapter</a> for an intro.</p>
<p>However, I did learn a few things I wanted to note to myself:</p>
<ul>
<li>the <code>grDevices::png()</code> function has a handy <code>bg</code> argument that you can set to <code>"transparent"</code></li>
<li>you can clip to a grob, which means you could clip out any text or image that falls outside the hexagon boundary</li>
<li>you can rotate a viewport but the x and y coordinates will be relative to the angle, so you need to put it inside another viewport that’s relative to the sticker’s coordinates</li>
<li>the hexagon is not as wide as it is tall, so the x-axis on the default ‘normalised parent coordinate’ system (where the axes are 0 to 1) has to be narrowed by setting the viewport’s <code>xscale</code> to hexagon limits, while the hexagon grob must have <code>default.units = "native"</code> so it’s placed relative to that <code>xscale</code></li>
<li>you can make the border by stacking a smaller hexagin on a bigger one, ensuring that the maximum extent of the sticker will fit inside a unit-1 square</li>
</ul>
</section>
<section id="cast-a-hex" class="level2">
<h2 class="anchored" data-anchor-id="cast-a-hex">Cast a hex</h2>
<p>If you want to give it a go, you can install {hexbase} <a href="https://github.com/matt-dray/hexbase">from GitHub</a> like:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1"><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install.packages</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"remotes"</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># if not yet installed</span></span>
<span id="cb1-2">remotes<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">install_github</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"matt-dray/hexbase"</span>)</span></code></pre></div>
</div>
<p>At time of writing, the package is at version 0.1.0. You can install that specific version by adding <code>@v0.1.0</code> to the end of the string in <code>install_github()</code>.</p>
<p>As mentioned, the package only imports {grid} and {grDevices} from base R. Otherwise it’s BYOIAF (‘bring your own images and fonts’) by importing with packages like {png} and by installing your fonts locally. Is that cheating? Nah, the <code>Imports</code> section in <a href="https://github.com/matt-dray/hexbase/blob/main/DESCRIPTION">the <code>DESCRIPTION</code> file</a> is clean as far as I’m concerned!</p>
<p>You can then build a sticker additively with a series of {hexbase} function calls:</p>
<ol type="1">
<li><code>open_device()</code> to set up a PNG graphics device with the dimensions of <a href="https://sticker.how/#type-hexagon">the Stickers Standard</a>.</li>
<li><code>add_hex()</code> to add the hexagon and border.</li>
<li><code>add_image()</code> to place an image (run multiple times for more images).</li>
<li><code>add_text()</code> to place and style text (run multiple times for more text).</li>
<li><code>close_device()</code> to close the PNG graphics device and save to file.</li>
</ol>
<p>You can set various text and image properties like position, size, colour and angle. Note that the coordinates are relative to a unit square<sup>6</sup> with axes of 0 to 1, a lower-left origin at [0, 0] and a centre at [0.5, 0.5]. Text and images will be clipped if they exceed the boundary of the hexagon. Check the help files for all the explanation.</p>
<p>Below is an extremely basic example to demonstrate a number of features. Note how you call each function independently (i.e.&nbsp;no pipes), much like writing base plots with successive calls to <code>plot()</code>, <code>points()</code>, <code>text()</code>, etc.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Somewhere to save it</span></span>
<span id="cb2-2">temp_path <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">tempfile</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">fileext =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">".png"</span>)</span>
<span id="cb2-3"></span>
<span id="cb2-4"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># 'Bring your own image', e.g. with {png}</span></span>
<span id="cb2-5">image_path <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">system.file</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"img"</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Rlogo.png"</span>, <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">package =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"png"</span>)</span>
<span id="cb2-6">image_png <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> png<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">readPNG</span>(image_path)</span>
<span id="cb2-7"></span>
<span id="cb2-8"><span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Build up and write the sticker</span></span>
<span id="cb2-9">hexbase<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">open_device</span>(  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># open with sticker-standard dimensions</span></span>
<span id="cb2-10">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">file_path =</span> temp_path</span>
<span id="cb2-11">)</span>
<span id="cb2-12">hexbase<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_hex</span>(</span>
<span id="cb2-13">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">border_width =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.06</span>,</span>
<span id="cb2-14">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">border_col =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"hotpink3"</span>,</span>
<span id="cb2-15">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">bg_col =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bisque"</span></span>
<span id="cb2-16">)</span>
<span id="cb2-17">hexbase<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_image</span>(</span>
<span id="cb2-18">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">image_object =</span> image_png,</span>
<span id="cb2-19">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">image_y =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.6</span>,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># y = 0.5 is centre</span></span>
<span id="cb2-20">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">image_angle =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">20</span>,</span>
<span id="cb2-21">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">image_width =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.5</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># i.e. half of the npc coord system</span></span>
<span id="cb2-22">)</span>
<span id="cb2-23">hexbase<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_text</span>(</span>
<span id="cb2-24">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_string =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"example"</span>,</span>
<span id="cb2-25">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_x =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.495</span>,</span>
<span id="cb2-26">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_y =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.35</span>,</span>
<span id="cb2-27">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_col =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"#000000"</span>,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># named colour or hexadecimal</span></span>
<span id="cb2-28">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_family =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Comic Sans MS"</span>,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># must be installed locally</span></span>
<span id="cb2-29">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_face =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bold"</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># or 'plain', 'italic', 'bold.italic'</span></span>
<span id="cb2-30">)</span>
<span id="cb2-31">hexbase<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_text</span>(  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># add multiple text/images (in a loop?)</span></span>
<span id="cb2-32">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_string =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"example"</span>,</span>
<span id="cb2-33">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_x =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.505</span>,</span>
<span id="cb2-34">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_y =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.34</span>,</span>
<span id="cb2-35">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_col =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"red"</span>,</span>
<span id="cb2-36">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_family =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Comic Sans MS"</span>,</span>
<span id="cb2-37">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_face =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"bold"</span></span>
<span id="cb2-38">)</span>
<span id="cb2-39">hexbase<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">add_text</span>(</span>
<span id="cb2-40">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_string =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"visit rostrum.blog ftw"</span>,</span>
<span id="cb2-41">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_x =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.73</span>, </span>
<span id="cb2-42">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_y =</span> <span class="fl" style="color: #AD0000;
background-color: null;
font-style: inherit;">0.18</span>,</span>
<span id="cb2-43">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_angle =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">30</span>,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># rotate to match lower-right edge</span></span>
<span id="cb2-44">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_size =</span> <span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">5</span>,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># point size</span></span>
<span id="cb2-45">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_col =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"blue"</span>, </span>
<span id="cb2-46">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">text_family =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"Papyrus"</span></span>
<span id="cb2-47">)</span>
<span id="cb2-48">hexbase<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">close_device</span>()  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># close viewports, write file</span></span></code></pre></div>
</div>
<p>You can then open the image for inspection. A quick way to do this from within R would be like <code>system(paste("open", temp_path))</code>. Note that you can’t rely on plot-window previews (they lie) when you’re developing your sticker. You must inspect the generated PNG file instead.</p>
<p>The (stunning) hex produced by this code can be found at the top of this post. Well done for being able to read this far after you burnt your eyes by looking at it.</p>
</section>
<section id="dont-get-too-hexcited" class="level2">
<h2 class="anchored" data-anchor-id="dont-get-too-hexcited">Don’t get too hexcited</h2>
<p>I might play around with this a bit more if I have time and if I want to delve deeper into {grid}. The {hexbase} package seems to work for my use-case, but it can undoubtedly be improved. <a href="https://github.com/matt-dray/hexbase/issues">Let me know</a> if there’s anything you can unstick. I’d be hexstatic, etc.</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered: 2025-10-10 19:22:01 BST</code></pre>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.5.1 (2025-06-13)
Platform: aarch64-apple-darwin20
Running under: macOS Sequoia 15.6.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1

locale:
[1] en_GB.UTF-8/en_GB.UTF-8/en_GB.UTF-8/C/en_GB.UTF-8/en_GB.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.5.1    fastmap_1.2.0     cli_3.6.5        
 [5] tools_4.5.1       htmltools_0.5.8.1 yaml_2.3.10       rmarkdown_2.29   
 [9] knitr_1.50        jsonlite_2.0.0    xfun_0.52         digest_0.6.37    
[13] rlang_1.1.6       fontawesome_0.5.3 evaluate_1.0.4   </code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>I’ve <a href="https://github.com/matt-dray/stickers">made a few</a> myself.↩︎</p></li>
<li id="fn2"><p>There seems to be <a href="https://github.com/ColinFay/hexmake/issues/22">a problem with accessing the app</a> at time of writing.↩︎</p></li>
<li id="fn3"><p><a href="https://www.rostrum.blog/posts/2023-10-17-nhs-r-2023/">Base slaps</a>, as they say.↩︎</p></li>
<li id="fn4"><p>Though {grid} actually underpins {ggplot2}, so {ggplot2} extension-writers will be familiar.↩︎</p></li>
<li id="fn5"><p>I swear this is the name of a <em>Magic: The Gathering</em> creature or something.↩︎</p></li>
<li id="fn6"><p>Well, the initial plotting area is a unit-1 square, but the width of the hexagon is less than 1 on this scale, so two slivers are removed from the left and right of the hexagon. You can get the vertex point locations of the hexagon with the unexported <code>hexbase:::.get_hex_coords(diameter = 1)</code> function.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div id="quarto-reuse" class="quarto-appendix-contents"><div>CC BY-NC-SA 4.0</div></div></section></div> ]]></description>
  <category>grid</category>
  <category>gex</category>
  <category>r</category>
  <guid>https://www.rostrum.blog/posts/2025-01-31-hexbase/</guid>
  <pubDate>Fri, 31 Jan 2025 00:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2025-01-31-hexbase/resources/demo-hex.png" medium="image" type="image/png" height="167" width="144"/>
</item>
<item>
  <title>A roguelike sprite randomiser with Shiny</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2024-12-15-sprite-builder/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2024-12-15-sprite-builder/resources/sprites.gif" class="img-fluid figure-img" style="width:50.0%" alt="A web app titled 'roguelike sprite builder'. A 'reroll' button is pressed several times, which causes a 16-by-16 pixel character sprite to be regenerated with random hair, shirt, weapons, etc. There are additional buttons to download the image. The art is attributed to Kenney."></p>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>I made <a href="https://matt-dray.github.io/roguelike-sprite-builder/">a quick Shiny app</a> that generates randomised characters for your next roguelike game, made with parts designed by <a href="https://www.kenney.nl/">Kenney</a>.</p>
</section>
<section id="standing-on-the-shoulders-of-kenney" class="level2">
<h2 class="anchored" data-anchor-id="standing-on-the-shoulders-of-kenney">Standing on the shoulders of Kenney</h2>
<p><a href="https://www.kenney.nl/">Kenney</a> makes free (<a href="http://creativecommons.org/publicdomain/zero/1.0/">CC0</a>) assets for videogames, like <a href="https://www.kenney.nl/assets?search=tiny&amp;sort=update">the 16×16-pixel sprites</a> I used in <a href="https://www.rostrum.blog/posts/2024-07-14-tilebased/">a previous post</a>, where you could move a little character move around a procedural forest glade while being chased by a mouse.</p>
<p>Kenney also has <a href="https://kenney.nl/assets/roguelike-characters">a roguelike characters pack</a> where you can create your own characters by mixing and matching bodies, clothes and weapons. I’ve had a stab at making <a href="https://github.com/matt-dray/r.oguelike">a toy roguelike in R</a> before, but the ‘art’ and interface were purely composed of text.</p>
<p>With a long-term goal of creating a ‘real’ <a href="https://en.wikipedia.org/wiki/Roguelike">roguelike</a> game (<a href="https://www.rostrum.blog/posts/2023-04-02-splendid-r-games/">maybe in R?</a>), I made a small app to combine sprite parts randomly to create new characters. It’s a pretty simple implementation for now, but I’m recording its current state in case I never come back to it.</p>
</section>
<section id="an-applike" class="level2">
<h2 class="anchored" data-anchor-id="an-applike">An ‘applike’</h2>
<p>The app is made with Shiny, deployed for the browser with <a href="https://posit-dev.github.io/r-shinylive/">{shinylive}</a> and served with GitHub Pages. The source for the app is <a href="https://github.com/matt-dray/roguelike-sprite-builder/">on GitHub</a>. I’ve embedded it below, but you can also visit it in <a href="https://matt-dray.github.io/roguelike-sprite-builder/">a standalone window</a>. It might take a moment to load.</p>
<iframe width="800" height="550" src="https://matt-dray.github.io/roguelike-sprite-builder/" title="roguelike-sprite-builder">
</iframe>
<p>Hit the reroll button to randomise the sprite parts. You can also download as PNG at 16×16 and 1024×1024 pixels or as an R-specific RDS file containing a nativeRaster representation of the sprite.</p>
</section>
<section id="going-rogue" class="level2">
<h2 class="anchored" data-anchor-id="going-rogue">Going rogue</h2>
<p>The basic approach was to:</p>
<ol type="1">
<li><a href="https://github.com/matt-dray/roguelike-sprite-builder/blob/main/data-raw/extract.R">‘Cut out’ and save each individual sprite part</a> from a single ‘spritesheet’.</li>
<li>Select sprite parts at random given some weighting (bodies are always selected, but weapons aren’t).</li>
<li>Store the part names in a <code>reactiveValues()</code> object so they can be called by the draw method (to display the sprite onscreen) and via the download options.</li>
<li>Use <code>image_mosaic()</code> from <a href="https://docs.ropensci.org/magick/articles/intro.html#layers">the {magick package}</a> to print sprite parts on top of each other and store this as a nativeRaster object.</li>
<li>Read the nativeRaster and use {grid} functions to draw it.</li>
</ol>
<p>Why not just draw the <code>image_mosaic()</code> object? Two reasons: I specifically want to download the nativeRaster version of the sprite (for later use in R) and because nativeRaster can then be used to produce an image at any size. The nativeRaster object is just a matrix of colour information, where each cell is a pixel of the image. That means you can re-plot it any size you want.</p>
<p>Of course, I could use <a href="https://coolbutuseless.github.io/package/nara/index.html">{nara} by Mike</a> (coolbutuseless) to ‘blit’ sprite parts to a single object, but I had some trouble using the package in a {shinylive} context as its not on CRAN and the GitHub repo <a href="https://github.com/matt-dray/roguelike-sprite-builder/issues/15">doesn’t have any releases with the necessary WebAssembly binaries</a> (yet?).</p>
<div class="tip">
<p> <b>Note</b></p>
<p>Mike points out that <a href="https://coolbutuseless.r-universe.dev/nara">{nara} is on R-universe</a>, which <a href="https://ropensci.org/blog/2023/11/17/runiverse-wasm/">builds WASM binaries for use by WebR applications</a> including {shinylive}. Totally forgot about this! Will give it a go.</p>
</div>
</section>
<section id="expanding-the-inventory" class="level2">
<h2 class="anchored" data-anchor-id="expanding-the-inventory">Expanding the inventory</h2>
<p>As ever, there were some learning points, like how:</p>
<ul>
<li>I used <code>ignoreNULL = FALSE</code> in <code>shiny::bindEvent()</code> on the step that chooses sprite parts, so that a sprite is created on startup without the need for the user to click the reroll button</li>
<li>there’s <a href="https://github.com/posit-dev/r-shinylive/tree/actions-v1/examples">a {shinylive} deployment GitHub Action</a> that can save the step of having to <code>shinylive::export()</code> your app whenever you make changes</li>
<li>the Chrome browser appears to have a problem downloading the files when the buttons are clicked, so there is a workaround at time of writing that’s used in <a href="https://shinylive.io/r/examples/#r-file-download">one of the examples</a> on the {shinylive} website</li>
</ul>
<p>Even something this simple can be a learning experience.</p>
</section>
<section id="permadeath" class="level2">
<h2 class="anchored" data-anchor-id="permadeath">Permadeath</h2>
<p>Obviously this app is more of a proof-of-concept (as ever on this blog). It’s not particularly special or all that useful outside of my needs.</p>
<p>Next steps might involve letting the user <a href="https://github.com/matt-dray/roguelike-sprite-builder/issues/1">select parts manually</a>, <a href="https://github.com/matt-dray/roguelike-sprite-builder/issues/5">styling and theming the app</a> and giving the sprites <a href="https://github.com/matt-dray/roguelike-sprite-builder/issues/9">a transparent background</a>.</p>
<p>You can add a pull request <a href="https://github.com/matt-dray/roguelike-sprite-builder">to the GitHub repo</a> if this interests you. But beware, adventurer, I can’t promise my code is less labyrinthine than a classic Rogue dungeon.</p>

</section>

<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered: 2024-12-24 08:57:34 GMT</code></pre>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.4.2 (2024-10-31)
Platform: aarch64-apple-darwin20
Running under: macOS Ventura 13.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.4.2    fastmap_1.2.0     cli_3.6.3.9000   
 [5] tools_4.4.2       htmltools_0.5.8.1 rstudioapi_0.16.0 yaml_2.3.10      
 [9] rmarkdown_2.28    knitr_1.48        jsonlite_1.8.9    xfun_0.48        
[13] digest_0.6.37     rlang_1.1.4       fontawesome_0.5.2 evaluate_1.0.1   </code></pre>
</div>
</div>
</details>


</div></section><section class="quarto-appendix-contents"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div id="quarto-reuse" class="quarto-appendix-contents"><div>CC BY-NC-SA 4.0</div></div></section></div> ]]></description>
  <category>kenney</category>
  <category>r</category>
  <category>shiny</category>
  <category>shinylive</category>
  <guid>https://www.rostrum.blog/posts/2024-12-15-sprite-builder/</guid>
  <pubDate>Sun, 15 Dec 2024 00:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2024-12-15-sprite-builder/resources/sprites.gif" medium="image" type="image/gif"/>
</item>
<item>
  <title>Deploy a Shiny app to itch.io</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2024-11-16-itch/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2024-11-16-itch/resources/itch.png" class="img-fluid figure-img" style="width:100.0%" alt="The indie game distribution platform, itch.io, showing a pixel-editor tool made with R Shiny."></p>
<figcaption class="figure-caption">Graphic design is my passion.</figcaption>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>As a test, I uploaded an R Shiny app to <a href="https://itch.io/">itch.io</a>, which is a platform for sharing indie games. <a href="https://mattdray.itch.io/little-pixel-fun-zone">It worked</a>.</p>
</section>
<section id="scratching-an-itch.io" class="level2">
<h2 class="anchored" data-anchor-id="scratching-an-itch.io">Scratching an itch.io</h2>
<p>Where can you host your Shiny app for free? Typical options are services like <a href="https://www.shinyapps.io/">shinyapps.io</a> or <a href="https://connect.posit.cloud/">Connect Cloud</a> by Posit.</p>
<p>I have another idea: <a href="https://itch.io/">itch.io</a> is a web-based platform known for hosting indie videogames, assets and other miscellaneous game-adjacent tools<sup>1</sup>. Developers can upload file bundles for users to download, or they can serve their HTML apps directly on the site.</p>
<p>Since <a href="http://localhost:6375/posts/2023-04-02-splendid-r-games/">R is a game engine</a><sup>2</sup>, why aren’t R users uploading their apps to itch.io?</p>
<p>Well, part of the problem is that you didn’t know this was possible. And also because it’s only recently that you can use <a href="https://shiny.posit.co/">{shinylive}</a> to convert your app to be served entirely in the browser with no need for a server<sup>3</sup>.</p>
<p><code>shinylive::export()</code> generates a folder containing your app and all the assets needed for deployment. You just need to zip it up and upload it to itch.io.</p>
<p>Which is <a href="https://mattdray.itch.io/little-pixel-fun-zone">exactly what I did</a> with <a href="https://github.com/matt-dray/little-pixel-fun-zone/">a toy pixel editor</a> that I made and <a href="https://www.rostrum.blog/posts/2024-09-15-shiny-pixel/">wrote about</a> recently.</p>
<p>This requires an itch.io account. When you upload the app you fill in a short pro forma, making sure to set the ‘kind of project’ to HTML and to tick a checkbox to say ‘this game will be played in the browser’. For my example, I also set the ‘classification’ to ‘tools’.</p>
<p>I can’t really think of a catch, but there are limitations around size and complexity: the upload must be under 1 GB and I think there’s a limit of 1000 files (this tripped me up when I tried once before).</p>
</section>
<section id="scratch-my-back" class="level2">
<h2 class="anchored" data-anchor-id="scratch-my-back">Scratch my back</h2>
<p>I did this for the lols, of course, but I do think itch.io is a viable option for certain types of app. It’s a platform for people to share their creativity and no-one cares what language was used if the outcome is fun or useful.</p>
<p>And if that wasn’t enough, you can also use itch.io as a payment platform. Users can leave you a little tip for your efforts.</p>
<p>So, what’s stopping you from becoming the first professional indie R game developer?</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered: 2024-11-16 20:28:05 GMT</code></pre>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.4.0 (2024-04-24)
Platform: aarch64-apple-darwin20
Running under: macOS Ventura 13.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.4.0    fastmap_1.2.0     cli_3.6.3.9000   
 [5] tools_4.4.0       htmltools_0.5.8.1 rstudioapi_0.16.0 yaml_2.3.10      
 [9] rmarkdown_2.28    knitr_1.48        jsonlite_1.8.9    xfun_0.48        
[13] digest_0.6.37     rlang_1.1.4       evaluate_1.0.1   </code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Kinda like <a href="https://store.steampowered.com/">Steam</a>, but with a greater focus on independent developers and the rest of us.↩︎</p></li>
<li id="fn2"><p>Are you sick of me saying this yet?↩︎</p></li>
<li id="fn3"><p>Which makes it possible to deploy apps for free via static-site services like <a href="https://pages.github.com/">GitHub Pages</a> or <a href="https://www.netlify.com/">Netlify</a>.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div id="quarto-reuse" class="quarto-appendix-contents"><div>CC BY-NC-SA 4.0</div></div></section></div> ]]></description>
  <category>ich.io</category>
  <category>r</category>
  <category>shiny</category>
  <category>shinylive</category>
  <guid>https://www.rostrum.blog/posts/2024-11-16-itch/</guid>
  <pubDate>Sat, 16 Nov 2024 00:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2024-11-16-itch/resources/itch.png" medium="image" type="image/png" height="90" width="144"/>
</item>
<item>
  <title>{sortable} card games in {shiny}</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2024-10-25-not-balatro/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2024-10-25-not-balatro/resources/cards.gif" class="img-fluid figure-img" style="width:100.0%" alt="Two rows of playing cards labelled 'pool' and 'hand'. Buttons are pressed to order the cards by suit and then by rank. Cards are dragged from the pool to the hand. An ace is dragged and the text updates from 'no hand' to 'high card'. Another ace and it changes to 'a pair'. A 'draw' button is pressed and two new cards are added to the pool. The pool is ordered again by rank. Then two queens are added to the hand and the text changes to 'two pair'."></p>
<figcaption class="figure-caption">You sunk my battleship!</figcaption>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>Use <a href="https://rstudio.github.io/sortable/">{sortable}</a> in a <a href="https://shiny.posit.co/">{shiny}</a> app to build card games, maybe? I mocked up a demo and <a href="https://matt-dray.github.io/not-balatro/">made it available on the web</a> with <a href="https://posit-dev.github.io/r-shinylive/">{shinylive}</a>.</p>
</section>
<section id="balatro" class="level2">
<h2 class="anchored" data-anchor-id="balatro">Balatro</h2>
<p>LocalThunk<sup>1</sup>—the anonymous creator of <a href="https://en.wikipedia.org/wiki/Balatro_(video_game)"><em>Balatro</em></a>, the hit poker-inspired roguelike videogame<sup>2</sup>—revealed recently that 11,000 <em>years</em> of gameplay have been sunk into the game.</p>
<p>The average productivity of the universe has continued to drop now that the game is available on mobile. I am one of the suckers that made the purchase.</p>
<p>As a result, I wondered how viable a card game in R might be. Not just a simple blackjack simulator in the console, but a drag-and-droppable interface in the browser.</p>
<p>It must be possible, since <a href="https://www.rostrum.blog/posts/2023-04-02-splendid-r-games/">R is a game engine (fight me)</a>.</p>
</section>
<section id="badlatro" class="level2">
<h2 class="anchored" data-anchor-id="badlatro">Badlatro</h2>
<p>And so I began experimenting.</p>
<p>I wanted to use {shiny} and existing R packages and to avoid writing much JavaScript and CSS. After all, it’s hard to fit in this frivolity after I’ve finished grinding all day at work and then grinding all night on <em>Balatro</em>.</p>
<p>Luckily, the <a href="https://rstudio.github.io/sortable/">{sortable}</a> package does most of the hard work. The package from Andrie, Barrett and Kent wraps the <a href="https://sortablejs.github.io/Sortable/">SortableJS</a> library and lets you drag list elements around, including into other lists.</p>
<p>Typically, you would use {sortable} to drag little boxes of text. The order can then be read to record preference or perhaps as part of a quiz<sup>3</sup>. But now I’ve hijacked it to show little images of cards that you can drag between a ‘pool’ (randomly drawn cards) and a ‘hand’ (cards selected by the user).</p>
</section>
<section id="app" class="level2">
<h2 class="anchored" data-anchor-id="app">App</h2>
<p>You can check out <a href="https://github.com/matt-dray/not-balatro">the source on GitHub</a> and <a href="https://matt-dray.github.io/not-balatro/">find the app deployed online</a>, thanks to {shinylive} and GitHub Pages. I’ve embedded it below as well<sup>4</sup> (it’ll take a moment to load). I recommend using this on desktop for now, rather than mobile.</p>
<iframe width="800" height="650" src="https://matt-dray.github.io/not-balatro/" title="not-balatro">
</iframe>
</section>
<section id="features" class="level2">
<h2 class="anchored" data-anchor-id="features">Features</h2>
<p>So far it doesn’t do much, but it does enough to prove the concept. Here’s some notes on a few of the technicals.</p>
<section id="counting-cards" class="level3">
<h3 class="anchored" data-anchor-id="counting-cards">Counting cards</h3>
<p>I iterated over all suits and ranks with <a href="https://docs.ropensci.org/magick/">{magick}</a> to apply text and symbols to a blank PNG image. I then read the 52 cards into a <code>shiny::tagList()</code> and passed that to the <code>label</code> argument of <code>sortable::list()</code>. From there, the images could be matched to sampled card names and displayed in the app. This is slightly off-label (ha) compared to ‘normal’ use of the package, which generally involves providing text rather than images.</p>
</section>
<section id="drag-til-you-drop" class="level3">
<h3 class="anchored" data-anchor-id="drag-til-you-drop">Drag ’til you drop</h3>
<p>{sortable} has a nice feature where you can drag list elements between lists within ‘buckets’. I opted instead to use two <code>rank_list()</code>s that shared a <code>group</code> name in their <code>options</code> argument. This meant I could restrict the list size (8 cards in the pool, 5 in hand), thanks to some JavaScript from Barrett in a response to <a href="https://forum.posit.co/t/shiny-sortable-how-to-limit-number-of-items-that-can-be-dropped/69233/2">a Posit community post</a>.</p>
</section>
<section id="a-bit-flushed" class="level3">
<h3 class="anchored" data-anchor-id="a-bit-flushed">A bit flushed</h3>
<p>Detecting poker hands is tricky because you want to recognise that two jacks and three kings is a full house, not a pair of jacks or a three-of-a-kind, for example. I also made things harder by wanting to evaluate poker hands on the fly rather than when the user submits the hand.</p>
<p>I read about lots of very clever algorithms to do this. But I basically just brute-forced it, lol. It’s basically <code>if</code> statements that evaluate and return strongest hands first. So the function will assess a royal flush (i.e.&nbsp;ace, king, queen, jack and 10 of the same suit) and confirm it before it checks for a more basic straight (consecutive ranks of any suit) or a flush (any suit of non-consecutive rank).</p>
</section>
<section id="hit-the-deck" class="level3">
<h3 class="anchored" data-anchor-id="hit-the-deck">Hit the deck</h3>
<p>The deck is stored in ‘dynamic memory’ as a <code>reactieValues()</code> element. When cards are drawn, they’re removed from the deck and can’t be redrawn. This meant I could add a ‘draw’ button that adds previously unseen cards into the pool’s empty slots. Of course, we can take the length of the deck and present this back to the user as well.</p>
</section>
</section>
<section id="improvements" class="level2">
<h2 class="anchored" data-anchor-id="improvements">Improvements</h2>
<p>There’s many features that would improve the demo app. Below are some examples: one seems like it’s not possible, one won’t add that much to what I’ve learnt so far, one is obvious and one is just… bad programming.</p>
<section id="stop-n-swop" class="level3">
<h3 class="anchored" data-anchor-id="stop-n-swop">Stop ‘n’ swop</h3>
<p>It’s satisfying to pick up a card, drag it to a new position and drop it between the cards at that location. This action feels how it would when sorting real cards in your real hand. I’d like the option to be able to directly <em>swap</em> two cards between the pool and hand, though. This would be useful if you change your mind about the hand you’re building as new cards are drawn. As far as I can tell, SortableJS allows <code>swap = TRUE</code> within each rank list and if they are part of the same <code>group</code> then you can swap between them. I haven’t yet found a way to allow swapping to happen <em>only</em> when moving cards between lists, however.</p>
</section>
<section id="diss-card" class="level3">
<h3 class="anchored" data-anchor-id="diss-card">Diss card</h3>
<p>The awkward thing about having a ‘pool’ and a ‘hand’ is that there’s no natural way to discard. I haven’t properly explored solutions for earmarking cards to toss; as it stands, you have to drag the card somewhere for an action to be performed on it. The answer might be to drag these cards to a third area, where they’re binned. Compare this to a game like <em>Balatro</em>, where you first select cards in your hand and then click a button to either play or discard them.</p>
</section>
<section id="u-and-i" class="level3">
<h3 class="anchored" data-anchor-id="u-and-i">U and I</h3>
<p>Of course, as a proof of concept, I haven’t paid much attention to the user interface and experience. You can imagine making prettier cards, a nice green baise to mimic a poker table and even some animations to show cards being dealt. For now, I think the janky plainness is a good indicator that it’s just a demo.</p>
</section>
<section id="card-trick" class="level3">
<h3 class="anchored" data-anchor-id="card-trick">Card trick</h3>
<p>Oh yeah, haha, sometimes the cards disappear. It can happen when you drag from the pool to the hand, then back again and hit the ‘rank’ or ‘suit’ sorting buttons. We know that the card returns correctly to the pool by checking <code>input$pool_list</code> in the app, but there’s some issue with displaying the image, maybe? This is the only true bug that needs fixing, but I’d rather just write this blog post and deal with it later<sup>5</sup>.</p>
</section>
</section>
<section id="a-gamble" class="level2">
<h2 class="anchored" data-anchor-id="a-gamble">A gamble</h2>
<p>Obviously this isn’t yet a ‘game’. There’s a few ways this could go:</p>
<ul>
<li>remake <em>Balatro</em> in R (absolutely no chance)</li>
<li>make a small simulator of something like <a href="https://en.wikipedia.org/wiki/Video_poker">video poker</a> (too basic?)</li>
<li>create a new, simple game to chase high scores with combos of poker hands, discards and chip ‘bets’, perhaps incorporating some <em>Balatro</em>-inspired joker-activated bonuses or wild cards <a href="https://en.wikipedia.org/wiki/Dungeons_%26_Degenerate_Gamblers">like in <em>Dungeons and Degenerate Gamblers</em></a> (might actually be fun)</li>
<li>do nothing (appealing option)</li>
</ul>
<p>Feel free to chip (HA HA HA) in <a href="https://github.com/matt-dray/not-balatro/issues">your ideas</a>.</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered: 2024-10-26 21:53:18 BST</code></pre>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.4.0 (2024-04-24)
Platform: aarch64-apple-darwin20
Running under: macOS Ventura 13.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.4.0    fastmap_1.2.0     cli_3.6.3.9000   
 [5] tools_4.4.0       htmltools_0.5.8.1 rstudioapi_0.16.0 yaml_2.3.10      
 [9] rmarkdown_2.28    knitr_1.48        jsonlite_1.8.9    xfun_0.48        
[13] digest_0.6.37     rlang_1.1.4       evaluate_1.0.1   </code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>Apparently LocalThunk’s name <a href="https://youtu.be/iaIu0PT8n_o?t=47">was inspired partly by R</a>.↩︎</p></li>
<li id="fn2"><p>If you’ve been here before, you know I’ve toyed around with roguelikes in R with the concept packages <a href="https://github.com/matt-dray/r.oguelike/">{r.oguelike}</a> and <a href="https://github.com/matt-dray/tilebased">{tilebased}</a>.↩︎</p></li>
<li id="fn3"><p>For example, Rasmus has done this recently in the <a href="https://www.sumsar.net/climate-impact-sorting-challenge/">climate impact sorting challenge</a>. I scored 1 on my first go, ha.↩︎</p></li>
<li id="fn4"><p>Psst, click the joker card at the bottom of the app to see ‘dev mode’ (you’ll have to scroll down in the embedded app to see it).↩︎</p></li>
<li id="fn5"><p>This is basically the entire modus operandi of this blog, lol.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div id="quarto-reuse" class="quarto-appendix-contents"><div>CC BY-NC-SA 4.0</div></div></section></div> ]]></description>
  <category>games</category>
  <category>r</category>
  <category>shiny</category>
  <category>shinylive</category>
  <category>sortable</category>
  <guid>https://www.rostrum.blog/posts/2024-10-25-not-balatro/</guid>
  <pubDate>Thu, 24 Oct 2024 23:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2024-10-25-not-balatro/resources/cards.gif" medium="image" type="image/gif"/>
</item>
<item>
  <title>Little pixel fun zone</title>
  <dc:creator>Matt Dray</dc:creator>
  <link>https://www.rostrum.blog/posts/2024-09-15-shiny-pixel/</link>
  <description><![CDATA[ 




<div class="quarto-figure quarto-figure-left">
<figure class="figure">
<p><img src="https://www.rostrum.blog/posts/2024-09-15-shiny-pixel/resources/SANIC.png" class="img-fluid figure-img" style="width:50.0%" data-fig.alt="Screenshot of a web app called 'little pixel fun zone'. in the centre is a child's drawing of the fast blue hedgehog rendered in blocky square pixels. Under the image are some buttons to select a colour, undo, fill and downloade outputs."></p>
<figcaption class="figure-caption">Fast blue hedgehog man in Little Pixel Fun Zone, Act 1 (original art by the author).</figcaption>
</figure>
</div>
<section id="tldr" class="level2">
<h2 class="anchored" data-anchor-id="tldr">tl;dr</h2>
<p>I made a <a href="https://matt-dray.github.io/little-pixel-fun-zone/">simple toy pixel-editor for the browser</a>, which helped me learn about reacting to user clicks and implementing undo/redo in {shiny}.</p>
</section>
<section id="just-browsing" class="level2">
<h2 class="anchored" data-anchor-id="just-browsing">Just browsing</h2>
<p>My <a href="https://github.com/matt-dray/pixeltrix">{pixeltrix} package</a> is a local pixel editor you can run from the R console<sup>1</sup>. It’s intentionally simple. You start a graphics device and click the squares (‘pixels’), cycling through your provided colour palette. You’re returned a little pixeltrix-class matrix as a memento, which you can re-edit later.</p>
<p>It might be nice to have a browser-based version, but I’ve never really had the time and figured it would be too fiddly. Well guess what, I had some time and it wasn’t that fiddly.</p>
<p>I’ve been learning a bit more <a href="https://shiny.posit.co/">{shiny}</a> of late. This mini project was a good way to learn a few things that might come in handy later. In particular, how to:</p>
<ol type="1">
<li>Read a clicked point on a plot and react to it.</li>
<li>Implement a basic undo/redo feature.</li>
</ol>
<p>This is a reminder to myself of how to do these things.</p>
</section>
<section id="app" class="level2">
<h2 class="anchored" data-anchor-id="app">App</h2>
<p>The app is available <a href="https://matt-dray.github.io/little-pixel-fun-zone/">via {shinylive} deployed via GitHub Pages</a> and <a href="https://github.com/matt-dray/little-pixel-fun-zone">the source is on GitHub</a> (v0.3.0 at time of writing).</p>
<p>There’s (intentionally) only a few features. You can:</p>
<ul>
<li>click a pixel to toggle it on/off</li>
<li>change the colour, thanks to <a href="https://daattali.com/shiny/colourInput/">Dean Attali’s {colourpicker}</a></li>
<li>undo/redo (with a very short ‘memory’)</li>
<li>flood fill</li>
<li>have an assistant ✨AI✨ draw a picture for you<sup>2</sup></li>
<li>download a matrix representation of your treasured art (pixeltrix-class, for my own needs) as an RDS file</li>
<li>download a png copy of your treasured art</li>
</ul>
<p>Wow!</p>
<p>Here’s an embedded version of the app (may take a moment to load):</p>
<iframe width="800" height="585" src="https://matt-dray.github.io/little-pixel-fun-zone/" title="roguelike-sprite-builder">
</iframe>
<div class="tip">
<p> <b>Note</b></p>
<p>To test the limits of {shinylive}, I also managed to <a href="https://mattdray.itch.io/little-pixel-fun-zone">deploy the app to itch.io</a>, the indie-game dispensation platform! You can read <a href="https://www.rostrum.blog/posts/2024-11-16-itch/">a separate blog post</a> about that.</p>
</div>
</section>
<section id="tricks" class="level2">
<h2 class="anchored" data-anchor-id="tricks">Tricks</h2>
<p>I mentioned there were two things I learnt in particular: handling click-reacts and undo/redo.</p>
<section id="click-react" class="level3">
<h3 class="anchored" data-anchor-id="click-react">Click-react</h3>
<p>Thanks to the <code>locator()</code> function (witchcraft), {pixeltrix} reads the coordinates of a user’s click on the plotting device. The returned values can be used to identify the nearest ‘pixel’ clicked.</p>
<p>With {shiny} we can generate a plot output and then capture an input from it when clicked<sup>3</sup> using the handy <code>click</code> argument to <code>plotOutput()</code>.</p>
<p>In the example but of UI code below, we output a plot with ID <code>pixel_grid</code> that was generated in the server. Then, a <code>click</code> on that plot in the app would be registered as the ID <code>clicked_point</code> for retrieval in the server. In my case, I took the x and y elements of that object and matched them to the nearest pixel (using self-plagiarised {pixeltrix} code).</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb1" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb1-1">shiny<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">plotOutput</span>(</span>
<span id="cb1-2">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">outputId =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"pixel_grid"</span>,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the plot of the pixel grid</span></span>
<span id="cb1-3">  <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">click =</span> shiny<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">clickOpts</span>(</span>
<span id="cb1-4">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">id =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"clicked_point"</span>,  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># to retrieve as input$clicked_point in server</span></span>
<span id="cb1-5">    <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">clip =</span> <span class="cn" style="color: #8f5902;
background-color: null;
font-style: inherit;">TRUE</span>  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># restrict to plot bounds?</span></span>
<span id="cb1-6">  )</span>
<span id="cb1-7">)</span></code></pre></div>
</div>
<p>I also made the click trigger <code>observeEvent()</code> to refresh the plot given the user’s selection. So, the user clicks the plot and the nearest pixel is toggled.</p>
</section>
<section id="undoredo" class="level3">
<h3 class="anchored" data-anchor-id="undoredo">Undo/redo</h3>
<p>If you misclick a pixel in your picture you could correct it by changing the colour in the colourpicker and re-clicking the errant point. But that takes, ooh, perhaps a couple of seconds. Instead, it seems conceptually more fun to have an undo button. And imagine how exhilarating it would be if that button toggled to redo after it was clicked? I know, calm down.</p>
<p>So, there’s probably lots of ways to do this, but I settled on bookkeeping with ‘memory slots’ in a sort-of cyber-goldfish brain, using <code>reactiveValues()</code>.</p>
<p>The image is stored as a matrix object, with each cell representing the current state of the corresponding pixel when drawn. The current state of the matrix is stored in the first (of two) memory slots, while the second slot is the previous matrix state. Each time you update the drawing, slot 1 overwrites slot 2 and the new matrix takes its place in slot 1.</p>
<p>So, for example, the app starts up with a ‘blank’ grid:</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb2" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb2-1">pixel_matrices <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> shiny<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">reactiveValues</span>(<span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">slot1 =</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">.gen_grid</span>(<span class="dv" style="color: #AD0000;
background-color: null;
font-style: inherit;">16</span>, <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"#E5E5E5"</span>))</span></code></pre></div>
</div>
<p>And then <code>observeEvent()</code> handles things when a click occurs. The click coordinates and currently-selected colour are used to toggle or adjust a pixel in the underlying matrix. The resulting, new, version of the matrix is added to slot 1 and the prior version slips into slot 2.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb3" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb3-1">shiny<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">observeEvent</span>(input<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>clicked_point, {  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># trigger on click</span></span>
<span id="cb3-2">  </span>
<span id="cb3-3">  matrix_updated <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">.gen_updated_pixel_matrix</span>(  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># update current drawing</span></span>
<span id="cb3-4">    shiny<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">isolate</span>(pixel_matrices[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"slot1"</span>]]),  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># the current drawing</span></span>
<span id="cb3-5">    <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">pixel_coords</span>(),  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># location of clicked point to update</span></span>
<span id="cb3-6">    input<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>selected_colour  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># current user-selected colour</span></span>
<span id="cb3-7">  )</span>
<span id="cb3-8">  </span>
<span id="cb3-9">  pixel_matrices[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"slot2"</span>]] <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> pixel_matrices[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"slot1"</span>]]  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># current is now old</span></span>
<span id="cb3-10">  pixel_matrices[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"slot1"</span>]] <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> matrix_updated  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># new is now current</span></span>
<span id="cb3-11">  </span>
<span id="cb3-12">})</span></code></pre></div>
</div>
<p>Could the so-called cyber-goldfish brain be larger, with more brain slots? No, because then it would be a cyber-elephant and I think that mental image is less funny. But yes, you could probably create slots on the fly and have the ability to undo much further back in history.</p>
<p>So, to perform a do-over, we just need to access the older slot with a button click. The icon for the button is stored in a <code>reactiveVal()</code> that starts as ‘undo’ arrow. When clicked, we flip the memory slots to put the older version in slot 1 and also update the icon to be a ‘redo’ arrow. Another click of the button and the situation will revert again<sup>4</sup>. Voila: time travel.</p>
<div class="cell">
<div class="sourceCode cell-code" id="cb4" style="background: #f1f3f5;"><pre class="sourceCode r code-with-copy"><code class="sourceCode r"><span id="cb4-1">button_icon <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> shiny<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">reactiveVal</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rotate-left"</span>)  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># starting condition</span></span>
<span id="cb4-2"></span>
<span id="cb4-3">shiny<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">observeEvent</span>(input<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">$</span>button_undo, {  <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># trigger on click</span></span>
<span id="cb4-4">    </span>
<span id="cb4-5">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Switch 'memory' slots</span></span>
<span id="cb4-6">    slot1 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> pixel_matrices[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"slot1"</span>]]</span>
<span id="cb4-7">    slot2 <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> pixel_matrices[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"slot2"</span>]]</span>
<span id="cb4-8">    pixel_matrices[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"slot2"</span>]] <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> slot1</span>
<span id="cb4-9">    pixel_matrices[[<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"slot1"</span>]] <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> slot2</span>
<span id="cb4-10">    </span>
<span id="cb4-11">    <span class="co" style="color: #5E5E5E;
background-color: null;
font-style: inherit;"># Invert undo/redo icon</span></span>
<span id="cb4-12">    </span>
<span id="cb4-13">    current_icon <span class="ot" style="color: #003B4F;
background-color: null;
font-style: inherit;">&lt;-</span> <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">undo_button_icon</span>()</span>
<span id="cb4-14">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> (current_icon <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rotate-left"</span>) <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">undo_button_icon</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rotate-right"</span>)</span>
<span id="cb4-15">    <span class="cf" style="color: #003B4F;
background-color: null;
font-weight: bold;
font-style: inherit;">if</span> (current_icon <span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">==</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rotate-right"</span>) <span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">undo_button_icon</span>(<span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"rotate-left"</span>)</span>
<span id="cb4-16">    </span>
<span id="cb4-17">    shiny<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">updateActionButton</span>(</span>
<span id="cb4-18">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">inputId =</span> <span class="st" style="color: #20794D;
background-color: null;
font-style: inherit;">"button_undo"</span>,</span>
<span id="cb4-19">      <span class="at" style="color: #657422;
background-color: null;
font-style: inherit;">icon =</span> shiny<span class="sc" style="color: #5E5E5E;
background-color: null;
font-style: inherit;">::</span><span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">icon</span>(<span class="fu" style="color: #4758AB;
background-color: null;
font-style: inherit;">button_icon</span>())</span>
<span id="cb4-20">    )</span>
<span id="cb4-21">    </span>
<span id="cb4-22">  })</span></code></pre></div>
</div>
<p>Inelegant, perhaps, but manageable<sup>5</sup>.</p>
</section>
</section>
<section id="fiddly-is-money" class="level2">
<h2 class="anchored" data-anchor-id="fiddly-is-money">Fiddly is money</h2>
<p>There’s a number of features I could implement, like arbitrary pixel-grid sizes, uploading a matrix to continue editing it, or maybe even multi-select with click-and-drag (is this even possible?).</p>
<p>If only I could perceive that I have more time and that the fiddlyometer gauge was lower. Yes, this is current me goading future me.</p>
<p>Until then, drop bug reports and how to fix them in <a href="https://github.com/matt-dray/little-pixel-fun-zone/issues">the issues</a>.</p>
<p>Also use the app to draw a classic videogame action scene and send it to me, obviously (see inspiration at top of post).</p>

</section>


<div id="quarto-appendix" class="default"><section id="environment" class="level3 appendix"><h2 class="anchored quarto-appendix-heading">Environment</h2><div class="quarto-appendix-contents">

<details>
<summary>
Session info
</summary>
<div class="cell">
<div class="cell-output cell-output-stdout">
<pre><code>Last rendered: 2025-01-15 21:05:27 GMT</code></pre>
</div>
<div class="cell-output cell-output-stdout">
<pre><code>R version 4.4.2 (2024-10-31)
Platform: aarch64-apple-darwin20
Running under: macOS Ventura 13.2.1

Matrix products: default
BLAS:   /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRblas.0.dylib 
LAPACK: /Library/Frameworks/R.framework/Versions/4.4-arm64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.0

locale:
[1] en_US.UTF-8/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8

time zone: Europe/London
tzcode source: internal

attached base packages:
[1] stats     graphics  grDevices utils     datasets  methods   base     

loaded via a namespace (and not attached):
 [1] htmlwidgets_1.6.4 compiler_4.4.2    fastmap_1.2.0     cli_3.6.3.9000   
 [5] tools_4.4.2       htmltools_0.5.8.1 rstudioapi_0.16.0 yaml_2.3.10      
 [9] rmarkdown_2.28    knitr_1.48        jsonlite_1.8.9    xfun_0.48        
[13] digest_0.6.37     rlang_1.1.4       fontawesome_0.5.2 evaluate_1.0.1   </code></pre>
</div>
</div>
</details>


</div></section><section id="footnotes" class="footnotes footnotes-end-of-document"><h2 class="anchored quarto-appendix-heading">Footnotes</h2>

<ol>
<li id="fn1"><p>You can read about {pixeltrix} in <a href="https://www.rostrum.blog/index.html#category=pixeltrix">other posts</a>.↩︎</p></li>
<li id="fn2"><p>This most often <em>looks</em> like random noise, but I’m sure that’s because of the training set or something. Maybe squint? I’ve been told that robot self-portraits sometimes appear, but I’m sure that’s just a hallucination.↩︎</p></li>
<li id="fn3"><p>Note also that you can perform actions on <code>dblclick</code>, <code>hover</code> and <code>brush</code>, not just a single click.↩︎</p></li>
<li id="fn4"><p>If you undo and then make a new click, then the undo/redo button will continue to show the ‘redo’ icon. Subtle, but to avoid this, you must reset the <code>reactiveVal()</code> to show the ‘undo’ icon if you perform any other actions after undoing.↩︎</p></li>
<li id="fn5"><p>If we messed with time travel and things didn’t go a bit <a href="https://en.wikipedia.org/wiki/Primer_(film)"><em>Primer</em></a>, then I’d say we’ve done well.↩︎</p></li>
</ol>
</section><section class="quarto-appendix-contents"><h2 class="anchored quarto-appendix-heading">Reuse</h2><div id="quarto-reuse" class="quarto-appendix-contents"><div>CC BY-NC-SA 4.0</div></div></section></div> ]]></description>
  <category>pixeltrix</category>
  <category>r</category>
  <category>shiny</category>
  <guid>https://www.rostrum.blog/posts/2024-09-15-shiny-pixel/</guid>
  <pubDate>Sat, 14 Sep 2024 23:00:00 GMT</pubDate>
  <media:content url="https://www.rostrum.blog/posts/2024-09-15-shiny-pixel/resources/SANIC.png" medium="image" type="image/png" height="202" width="144"/>
</item>
</channel>
</rss>
