<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Blog on Failing Productively</title>
    <link>https://michaelhoward.kiwi/blog/</link>
    <description>Recent content in Blog on Failing Productively</description>
    <generator>Hugo</generator>
    <language>en-NZ</language>
    <copyright>Copyright © 2021-2026, Michael Howard.</copyright>
    <lastBuildDate>Sun, 14 Jun 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://michaelhoward.kiwi/blog/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Get Rich or Die TTYing – Part 3: The Console Class</title>
      <link>https://michaelhoward.kiwi/get-rich-or-die-ttying-part-3-the-console-class/</link>
      <pubDate>Sun, 14 Jun 2026 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/get-rich-or-die-ttying-part-3-the-console-class/</guid>
      <description>&lt;p&gt;This is the third post in a series where I explore everything you can (and&#xA;perhaps sometimes shouldn&amp;rsquo;t) do with &lt;a href=&#34;https://github.com/Textualize/rich&#34;&gt;Rich&lt;/a&gt; – a Python library for styling&#xA;terminal output. Oh, and &lt;a href=&#34;https://en.wikipedia.org/wiki/50_Cent&#34;&gt;everyone&amp;rsquo;s favourite mid-2000s rapper&lt;/a&gt; is along for&#xA;the ride. My last two posts covered &lt;a href=&#34;https://michaelhoward.kiwi/get-rich-or-die-ttying-part-1-the-repl/&#34;&gt;blinging up the REPL&lt;/a&gt; and&#xA;&lt;a href=&#34;https://michaelhoward.kiwi/get-rich-or-die-ttying-part-2-console-markup/&#34;&gt;console markup&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Today&amp;rsquo;s post is all about the &lt;code&gt;Console&lt;/code&gt; class — the foundation for all the&#xA;really interesting things you can do with Rich.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;“People judge you by appearances, the image you project through your actions,&#xA;words, and style.&lt;/p&gt;&#xA;&lt;p&gt;— 50 Cent, &lt;em&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/The_50th_Law&#34;&gt;The 50th Law&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;overview&#34;&gt;Overview&lt;/h2&gt;&#xA;&lt;p&gt;Much like 50 Cent&amp;rsquo;s &lt;a href=&#34;https://www.slashgear.com/1280655/rarest-original-xbox-consoles/&#34;&gt;limited edition Xbox&lt;/a&gt; released alongside his&#xA;third-person shooter &lt;a href=&#34;https://en.wikipedia.org/wiki/50_Cent:_Bulletproof&#34;&gt;50 Cent: Bulletproof&lt;/a&gt;, the &lt;code&gt;Console&lt;/code&gt; isn&amp;rsquo;t the main&#xA;event, but it underpins all the fun and games you see on screen.&lt;/p&gt;&#xA;&lt;p&gt;To do this, it detects the size and capabilities of the terminal it’s running in&#xA;and generates the necessary ANSI escape sequences to style any output.&#xA;Typically, there will only be a single instance of this class.&lt;/p&gt;&#xA;&lt;p&gt;Your program&amp;rsquo;s output will be made up &lt;em&gt;renderables&lt;/em&gt; in Rich&amp;rsquo;s terminology. A&#xA;renderable is just that – something that can be rendered for display. It could&#xA;be a string, a column, a table, a markdown block, a progress bar, or any other&#xA;built-in renderable Rich provides. Moreover, renderables can be nested, meaning&#xA;you can do things like put styled text and syntax-highlighted code blocks in a&#xA;table. It&amp;rsquo;s the Console object&amp;rsquo;s job to work out how these components fit&#xA;together and tell your terminal emulator what to display.&lt;/p&gt;&#xA;&lt;h2 id=&#34;when-is-it-worth-using-console&#34;&gt;When is it worth using &lt;code&gt;Console&lt;/code&gt;?&lt;/h2&gt;&#xA;&lt;p&gt;For one-off scripts and small programs, it may not be necessary to create a&#xA;Console object – using Rich’s &lt;code&gt;print()&lt;/code&gt; function + console markup will probably&#xA;be enough. But, as soon as you need anything more complicated – drawing tables,&#xA;creating progress bars, justifying text – or you just want to modify the&#xA;behaviour of &lt;code&gt;print()&lt;/code&gt;, you’ll need a Console object.&lt;/p&gt;&#xA;&lt;p&gt;Since I&amp;rsquo;ll be taking you to the eye-candy shop in future posts, I&amp;rsquo;ll limit the&#xA;rest of this discussion to a couple of quality-of-life improvements you get once&#xA;you start using a Console object to handle your output.&lt;/p&gt;&#xA;&lt;h2 id=&#34;customising-print&#34;&gt;Customising &lt;code&gt;print()&lt;/code&gt;&lt;/h2&gt;&#xA;&lt;p&gt;As alluded to above, sometimes you’ll find overloading Python’s built-in&#xA;&lt;code&gt;print()&lt;/code&gt; function a little limiting. While it’s great that Rich provides a&#xA;drop-in replacement with the same function signature, Rich’s &lt;code&gt;print()&lt;/code&gt; doesn’t&#xA;always behave the way you want it to.&lt;/p&gt;&#xA;&lt;p&gt;For example, Rich will automatically highlight patterns in text like numbers,&#xA;booleans and collections. Normally, this is really handy. Sometimes, though,&#xA;it’s distracting. Take the following:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c4a7e7&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;rich&lt;/span&gt; &lt;span style=&#34;color:#c4a7e7&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#39;&amp;#34;In da club&amp;#34; by 50 Cent&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Which produces:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/unwanted_highlighting.png&#34; alt=&#34;A terminal window displaying the text &amp;lsquo;&amp;ldquo;In da club&amp;rdquo; by 50 Cent&amp;rsquo; – quoted text is green, the number 50 is blue&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;Both the quoted string and the number in 50&amp;rsquo;s name have been highlighted in&#xA;different colours, which is not what we want. Of course, it&amp;rsquo;s possible to&#xA;specify the output colour of this line using &lt;a href=&#34;https://rich.readthedocs.io/en/stable/markup.html&#34;&gt;console markup&lt;/a&gt;, but it&amp;rsquo;s a&#xA;hassle and you would have to repeat the process every time you invoke this rap&#xA;icon&amp;rsquo;s name.&lt;/p&gt;&#xA;&lt;p&gt;Instead, we can use the &lt;code&gt;print()&lt;/code&gt; method on our Console object, which allows for&#xA;customisation.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c4a7e7&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;rich.console&lt;/span&gt; &lt;span style=&#34;color:#c4a7e7&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;Console&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;console&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;Console&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;console&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#39;&amp;#34;In da club&amp;#34; by 50 Cent&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;highlight&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;False&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Output:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/disable_highlighting.png&#34; alt=&#34;A terminal window displaying the text &amp;lsquo;&amp;ldquo;In da club&amp;rdquo; by 50 Cent&amp;rsquo; – all text is white on a dark background&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;In fact, we can go further and disable it globally with&#xA;&lt;code&gt;console = Console(highlight=False)&lt;/code&gt;, so that it needs to be explicitly enabled&#xA;when printing.&lt;/p&gt;&#xA;&lt;h2 id=&#34;exporting-terminal-output&#34;&gt;Exporting terminal output&lt;/h2&gt;&#xA;&lt;p&gt;The Console object is also capable of exporting everything it prints/logs as&#xA;HTML and SVG, which is great for when you want to share fancy terminal output&#xA;with your crew. To do so, your Console object first needs to be initialised with&#xA;&lt;code&gt;record=True&lt;/code&gt;. Then, before your program exits, call either &lt;code&gt;save_html()&lt;/code&gt; or&#xA;&lt;code&gt;save_svg()&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;To demonstrate, I&amp;rsquo;ve put together a slightly more involved example. The script&#xA;&lt;code&gt;export_songs.py&lt;/code&gt; queries the &lt;a href=&#34;https://docs.genius.com/&#34;&gt;Genius API&lt;/a&gt; and prints a page of results&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;&#xA;as a table, before saving the output as an SVG.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 1&lt;/span&gt;&lt;span&gt;&lt;span style=&#34;color:#c4a7e7&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;json&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 2&lt;/span&gt;&lt;span&gt;&lt;span style=&#34;color:#c4a7e7&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;os&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 3&lt;/span&gt;&lt;span&gt;&lt;span style=&#34;color:#c4a7e7&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;requests&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 4&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 5&lt;/span&gt;&lt;span&gt;&lt;span style=&#34;color:#c4a7e7&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;rich.console&lt;/span&gt; &lt;span style=&#34;color:#c4a7e7&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;Console&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 6&lt;/span&gt;&lt;span&gt;&lt;span style=&#34;color:#c4a7e7&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;rich.table&lt;/span&gt; &lt;span style=&#34;color:#c4a7e7&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;Table&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 7&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 8&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt; 9&lt;/span&gt;&lt;span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;format_featured&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;featured&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;list&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;dict&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;])&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;str&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#3e8fb0&#34;&gt;None&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;10&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Return all featured artists as a comma separated string&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;11&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#3e8fb0&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;featured&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;12&lt;/span&gt;&lt;span&gt;        &lt;span style=&#34;color:#ea9a97&#34;&gt;artists&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;artist&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;get&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt; &lt;span style=&#34;color:#3e8fb0&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;artist&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;featured&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;13&lt;/span&gt;&lt;span&gt;        &lt;span style=&#34;color:#3e8fb0&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;, &amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;join&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;artists&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;14&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#3e8fb0&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#3e8fb0&#34;&gt;None&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;15&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;16&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;17&lt;/span&gt;&lt;span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;main&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;()&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#3e8fb0&#34;&gt;None&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;18&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;console&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;Console&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;record&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;True&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;highlight&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;False&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;19&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;token&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;os&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;environ&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;GENIUS_TOKEN&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;20&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;21&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#6e6a86&#34;&gt;# &amp;#39;108&amp;#39; is 50 Cent&amp;#39;s artist id on Genius&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;22&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;url&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;https://api.genius.com/artists/108/songs?page=10&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;23&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;headers&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Authorization&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Bearer &lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;token&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;24&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;console&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;log&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Retrieving song info for [i]50 Cent[/]&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;25&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;r&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;requests&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;get&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;url&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;headers&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;headers&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;26&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;27&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;songs&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;json&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;loads&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;r&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;text&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;get&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;response&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;get&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;songs&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;28&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;table&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;Table&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;title&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;$10 worth of tracks&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;29&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;column_names&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Title&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Primary artist&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Featured artist(s)&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Year&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;]&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;30&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;31&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#3e8fb0&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;name&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;column_names&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;32&lt;/span&gt;&lt;span&gt;        &lt;span style=&#34;color:#ea9a97&#34;&gt;table&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;add_column&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;33&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;34&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#3e8fb0&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;song&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;songs&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;35&lt;/span&gt;&lt;span&gt;        &lt;span style=&#34;color:#ea9a97&#34;&gt;table&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;add_row&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;36&lt;/span&gt;&lt;span&gt;            &lt;span style=&#34;color:#ea9a97&#34;&gt;song&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;get&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;37&lt;/span&gt;&lt;span&gt;            &lt;span style=&#34;color:#ea9a97&#34;&gt;song&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;get&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;primary_artist_names&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;38&lt;/span&gt;&lt;span&gt;            &lt;span style=&#34;color:#ea9a97&#34;&gt;format_featured&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;song&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;get&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;featured_artists&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;39&lt;/span&gt;&lt;span&gt;            &lt;span style=&#34;color:#ea9a97&#34;&gt;song&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;get&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;release_date_for_display&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;),&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;40&lt;/span&gt;&lt;span&gt;        &lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;41&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;42&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;console&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;table&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;43&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;console&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;save_svg&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;ten-dollar-tracks.svg&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;title&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;export_songs.py&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;44&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;45&lt;/span&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;46&lt;/span&gt;&lt;span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;__name__&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span style=&#34;white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f&#34;&gt;47&lt;/span&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;main&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here&amp;rsquo;s the result:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/ten-dollar-tracks.svg&#34; alt=&#34;Terminal output from the script export_songs.py, showing 20 songs featuring 50 Cent in a table. There are four columns: Title, Primary Artist, Featured artist(s), and Year.&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;(The same script using &lt;code&gt;save_html()&lt;/code&gt; &lt;a href=&#34;https://michaelhoward.kiwi/ten-dollar-tracks/&#34;&gt;produces this&lt;/a&gt; instead.)&lt;/p&gt;&#xA;&lt;p&gt;As you can see, the output from both &lt;code&gt;console.log()&lt;/code&gt; and &lt;code&gt;console.print()&lt;/code&gt;&#xA;(lines 24 and 42, respectively) made its way into the export. Also note that&#xA;&lt;code&gt;print()&lt;/code&gt; was passed a renderable – the table – instead of a plain old string.&lt;/p&gt;&#xA;&lt;p&gt;If writing the blog series has taught me anything, it&amp;rsquo;s that generating high&#xA;quality visuals of terminal output is surprisingly hard. Screenshots look&#xA;terrible and dedicated tools like &lt;a href=&#34;https://github.com/charmbracelet/freeze&#34;&gt;freeze&lt;/a&gt; and &lt;a href=&#34;https://github.com/charmbracelet/vhs&#34;&gt;vhs&lt;/a&gt; still require quite&#xA;a bit of tweaking. So I&amp;rsquo;m grateful Rich supports export to multiple formats out&#xA;of the box.&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;&#xA;&lt;p&gt;To sum up, a Console object acts as canvas for your application and abstracts&#xA;away the weird, low-level details of drawing stuff to the terminal. This gives&#xA;you more space to think of ways to &lt;a href=&#34;https://www.youtube.com/watch?v=Jy1D6caG8nU&amp;amp;list=RDJy1D6caG8nU&#34;&gt;P.I.M.P.&lt;/a&gt; up your output with Rich&amp;rsquo;s&#xA;renderables. It also gives you complete control of the output itself, which can&#xA;then be captured/exported if desired.&lt;/p&gt;&#xA;&lt;p&gt;For a more detailed look at everything the &lt;code&gt;Console&lt;/code&gt; class can do, refer to the&#xA;&lt;a href=&#34;https://rich.readthedocs.io/en/stable/console.html&#34;&gt;Console API&lt;/a&gt; documentation – it&amp;rsquo;s more comprehensive but unfortunately&#xA;makes no reference to 50 Cent (or even G-Unit) at all.&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;I picked the tenth page arbitrarily.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
    </item>
    <item>
      <title>Get Rich or Die TTYing – Part 2: Console markup</title>
      <link>https://michaelhoward.kiwi/get-rich-or-die-ttying-part-2-console-markup/</link>
      <pubDate>Sat, 31 Jan 2026 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/get-rich-or-die-ttying-part-2-console-markup/</guid>
      <description>&lt;p&gt;This is the second instalment in a series exploring everything you can (and&#xA;perhaps sometimes shouldn&amp;rsquo;t) do with &lt;a href=&#34;https://github.com/Textualize/rich&#34;&gt;Rich&lt;/a&gt; – a Python library for styling&#xA;terminal output. Oh, and &lt;a href=&#34;https://en.wikipedia.org/wiki/50_Cent&#34;&gt;everyone&amp;rsquo;s favourite mid-2000s rapper&lt;/a&gt; is along&#xA;for the ride. My &lt;a href=&#34;https://michaelhoward.kiwi/get-rich-or-die-ttying-part-1-the-repl/&#34;&gt;last post covered the blinging up the REPL&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Today&amp;rsquo;s topic is Rich&amp;rsquo;s &lt;a href=&#34;https://rich.readthedocs.io/en/stable/markup.html&#34;&gt;console markup&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;So, taking inspiration from the words of Fiddy himself:&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;It ain&amp;rsquo;t my fault, I just reach for style&lt;br&gt;&#xA;I&amp;rsquo;m hot, I breaks it down&lt;br&gt;&#xA;It ain&amp;rsquo;t my fault, you can&amp;rsquo;t break it down&lt;br&gt;&#xA;The way I break it down&lt;/p&gt;&#xA;&lt;p&gt;— 50 Cent, &lt;em&gt;God Gave Me Style&lt;/em&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;overview&#34;&gt;Overview&lt;/h2&gt;&#xA;&lt;p&gt;Rich uses a simple markup language for quickly annotating input passed to&#xA;&lt;code&gt;print()&lt;/code&gt;, &lt;code&gt;log()&lt;/code&gt;, or anywhere else Rich accepts strings, e.g.:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c4a7e7&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;rich&lt;/span&gt; &lt;span style=&#34;color:#c4a7e7&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;[bold]G-Unit[/bold]&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first set of square brackets contains the style (or styles) to be applied&#xA;to all text until the closing set (&lt;code&gt;[/bold]&lt;/code&gt; in this case).&lt;/p&gt;&#xA;&lt;p&gt;Styled text can also be closed without specifying the style again, i.e. just&#xA;with &lt;code&gt;[/]&lt;/code&gt;. Further, you can omit the closing syntax entirely if you&amp;rsquo;d like the&#xA;style to be applied until the end of the line. Therefore all of the following&#xA;print statements produce the same output.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;[underline]Get Rich or Die Tryin&amp;#39;[/underline]&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;[underline]Get Rich or Die Tryin&amp;#39;[/]&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;[underline]Get Rich or Die Tryin&amp;#39;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;# there&amp;#39;s even a short name for this style&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;[u]Get Rich or Die Tryin&amp;#39;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/rich_underline.png&#34; alt=&#34;Get Rich or Die Tryin&amp;rsquo;, underlined&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://knowyourmeme.com/memes/dont-care-didnt-ask-50-cent-middle-finger&#34;&gt;&amp;ldquo;Don&amp;rsquo;t care, didn&amp;rsquo;t ask,&amp;rdquo;&lt;/a&gt; I hear your atrophied attention span say.&lt;/p&gt;&#xA;&lt;p&gt;I get it. We&amp;rsquo;d all rather be in da club, bottle full of bub&amp;rsquo;. But when you&#xA;consider the alternative to the previous example relies on wrapping your string&#xA;in &lt;a href=&#34;https://en.wikipedia.org/wiki/ANSI_escape_code&#34;&gt;ANSI escape sequences&lt;/a&gt;, i.e.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;\x1b&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;[4mGet Rich Die Tryin&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;\x1b&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;[24m&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I think we can all agree Rich is a real timesaver for the modern hustler.&lt;/p&gt;&#xA;&lt;p&gt;As an aside, I was interested to see that the markup language is loosely based&#xA;on &lt;a href=&#34;https://en.wikipedia.org/wiki/BBCode&#34;&gt;BBCode&lt;/a&gt; (&amp;ldquo;Bulletin Board Code&amp;rdquo;), since a similar markup language is also&#xA;used by &lt;a href=&#34;https://spectreconsole.net/console/reference/markup-reference&#34;&gt;Spectre.Console&lt;/a&gt;, a dotnet package with close functionality to&#xA;Rich.&lt;/p&gt;&#xA;&lt;h2 id=&#34;colour&#34;&gt;Colour&lt;/h2&gt;&#xA;&lt;p&gt;Colour can be added to text in one of two ways:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Specifying one of the &lt;a href=&#34;https://rich.readthedocs.io/en/stable/appendix/colors.html&#34;&gt;256 named colours&lt;/a&gt;, either by name (&lt;code&gt;magenta&lt;/code&gt;), or&#xA;number (&lt;code&gt;color(5)&lt;/code&gt;)&lt;/li&gt;&#xA;&lt;li&gt;Supplying a hex value, e.g. &lt;code&gt;#bb2a25&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;So,&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;[magenta]Go, shawty, it&amp;#39;s your birthday[/]&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;[color(6)]We gon&amp;#39; party like it&amp;#39;s your birthday[/]&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;[#e28cf8]We gon&amp;#39; sip Bacardí like it&amp;#39;s your birthday[/]&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Produces:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/markup_colours.png&#34; alt=&#34;3 lines of &amp;lsquo;In da Club&amp;rsquo;, each a different colour&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;It&amp;rsquo;s worth noting that the first 16 named colours, e.g. &lt;code&gt;color(1)&lt;/code&gt;/&lt;code&gt;red&lt;/code&gt;, are&#xA;generally defined by a terminal&amp;rsquo;s colour scheme, rather than being an exact&#xA;colour like &lt;code&gt;#d70000&lt;/code&gt;. This is a good thing. With terminal-defined colours,&#xA;although you lose precise control of the output, you know they&amp;rsquo;ll fit with the&#xA;user&amp;rsquo;s theme and have reasonable contrast.&lt;/p&gt;&#xA;&lt;h2 id=&#34;styled-text&#34;&gt;Styled text&lt;/h2&gt;&#xA;&lt;p&gt;Beyond colour, Rich can add styles like bold, underline and italics to your&#xA;text. To illustrate, I&amp;rsquo;ve styled some heartfelt lines from &lt;a href=&#34;https://www.youtube.com/watch?v=cDMhlvbOFaM&#34;&gt;21 Questions&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;table&gt;&#xA;  &lt;thead&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;th&gt;Style&lt;/th&gt;&#xA;          &lt;th&gt;Result&lt;/th&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/thead&gt;&#xA;  &lt;tbody&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;[bold]&lt;/code&gt;/&lt;code&gt;[b]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/rich_print_bold.png&#34; alt=&#34;Monospace text printed in bold&#34;&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;[italic]&lt;/code&gt;/&lt;code&gt;[i]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/rich_print_italic.png&#34; alt=&#34;Monospace text printed in italic&#34;&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;[dim]&lt;/code&gt;/&lt;code&gt;[d]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/rich_print_dim.png&#34; alt=&#34;Monospace text printed dimly&#34;&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;[underline]&lt;/code&gt;/&lt;code&gt;[u]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/rich_print_underline.png&#34; alt=&#34;Monospace text printed with underline&#34;&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;[underline2]&lt;/code&gt;/&lt;code&gt;[uu]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/rich_print_underline2.png&#34; alt=&#34;Monospace text printed with two underlines&#34;&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;[strike]&lt;/code&gt;/&lt;code&gt;[s]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/rich_print_strike.png&#34; alt=&#34;Monospace text printed with strikethrough&#34;&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;      &lt;tr&gt;&#xA;          &lt;td&gt;&lt;code&gt;[reverse]&lt;/code&gt;/&lt;code&gt;[r]&lt;/code&gt;&lt;/td&gt;&#xA;          &lt;td&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/rich_print_reverse.png&#34; alt=&#34;Monospace text printed in inverted colours&#34;&gt;&lt;/td&gt;&#xA;      &lt;/tr&gt;&#xA;  &lt;/tbody&gt;&#xA;&lt;/table&gt;&#xA;&lt;p&gt;The table above contains common styles supported by many – but not all – modern&#xA;terminals like Ghostty, Kitty, WezTerm, Konsole and the like. On the left is&#xA;the style (and abbreviated form); on the right is its corresponding output.&lt;/p&gt;&#xA;&lt;h2 id=&#34;weird-stuff-your-terminal-probably-wont-support&#34;&gt;Weird stuff your terminal probably won&amp;rsquo;t support&lt;/h2&gt;&#xA;&lt;p&gt;Rich also allows for styles which are very seldom supported by modern&#xA;terminals. I haven&amp;rsquo;t included images of output in this section, since all&#xA;except &lt;code&gt;[overline]&lt;/code&gt; are difficult to display.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;[frame]&lt;/code&gt;: I have no idea what this is supposed to look like.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;[encircle]&lt;/code&gt;: Same here.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;[overline]&lt;/code&gt;: Like underline, but, you know, &lt;em&gt;over&lt;/em&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;[conceal]&lt;/code&gt;: Blanks out the text. Actually, this one is quite widely&#xA;supported. I just don&amp;rsquo;t know why you would use it instead of, say, not&#xA;printing.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;[blink]&lt;/code&gt;: A slow blink (under 150 times per minute).&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;[blink2]&lt;/code&gt;: A fast blink (more than 150 times per minute). Of the handful of&#xA;terminals I experimented with, only WezTerm supported both kinds of blinks.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;combining-styles&#34;&gt;Combining styles&lt;/h2&gt;&#xA;&lt;p&gt;Styles can be combined and nested within each other. To demonstrate,&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;[underline green on white]God give me style, God give me grace[/]&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;[italic magenta]God put a [yellow]smile[/] on my face[/]&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Becomes:&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/rich_combined_styles.png&#34; alt=&#34;God give me style, God give me grace, God put a smile on my face&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;strong&gt;Remember:&lt;/strong&gt; &lt;code&gt;blue&lt;/code&gt;, &lt;code&gt;white&lt;/code&gt; and &lt;code&gt;magenta&lt;/code&gt; are part of the 16 colours defined&#xA;by your terminal&amp;rsquo;s theme (here, Rosé Pine Moon), so the snippet above will&#xA;probably look different if you run it yourself.&lt;/p&gt;&#xA;&lt;h2 id=&#34;emoji&#34;&gt;Emoji&lt;/h2&gt;&#xA;&lt;p&gt;Emoji can be referred to via their name surrounded by colons:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;:heavy_dollar_sign: :heavy_division_sign: :two:&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;# is the equivalent of&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;💲 ➗ 2⃣&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For a complete list of available emoji names + examples, run &lt;code&gt;python -m rich.emoji&lt;/code&gt; in your terminal.&lt;/p&gt;&#xA;&lt;h2 id=&#34;links&#34;&gt;Links&lt;/h2&gt;&#xA;&lt;p&gt;Lastly, you can add interactive links to your output too.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;url&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;https://web.archive.org/web/20060116005724/http://www.50cent.com/&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Here&amp;#39;s 50&amp;#39;s [link=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;url&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;]website back in 2006.[/]&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/rich_links.png&#34; alt=&#34;Here&amp;rsquo;s 50&amp;rsquo;s website back in 2006, with last four words underlined&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;The caveat here is, again, as long as your terminal supports them.&lt;/p&gt;&#xA;&lt;p&gt;There&amp;rsquo;s no real standard around how they&amp;rsquo;ll be displayed either. Some terminals&#xA;will underline them. Some won&amp;rsquo;t. Others will only underline them on hover.&#xA;Typically, you will need to Ctrl+Click to follow them too.&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;&#xA;&lt;p&gt;That&amp;rsquo;s a (w)rap! I encourage you to start adding some console markup to your&#xA;scripts, new or existing. With minimal effort, you can add some swag to your&#xA;&lt;code&gt;print()&lt;/code&gt; and &lt;code&gt;log()&lt;/code&gt; statements and generally make the terminal a more&#xA;interesting place. Just don&amp;rsquo;t overdo it (or do!), and keep in mind that not all&#xA;terminals are created equal.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Get Rich or Die TTYing – Part 1: The REPL</title>
      <link>https://michaelhoward.kiwi/get-rich-or-die-ttying-part-1-the-repl/</link>
      <pubDate>Fri, 09 Jan 2026 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/get-rich-or-die-ttying-part-1-the-repl/</guid>
      <description>&lt;p&gt;This is the first post in a series where I explore everything you can (and&#xA;perhaps sometimes shouldn&amp;rsquo;t) do with &lt;a href=&#34;https://github.com/Textualize/rich&#34;&gt;Rich&lt;/a&gt; – a Python library for styling&#xA;terminal output. Oh, and &lt;a href=&#34;https://en.wikipedia.org/wiki/50_Cent&#34;&gt;everyone&amp;rsquo;s favourite mid-2000s rapper&lt;/a&gt; is along&#xA;for the ride.&lt;/p&gt;&#xA;&lt;p&gt;Today&amp;rsquo;s post focuses on the REPL.&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&amp;ldquo;Go ahead, switch the style up.&amp;rdquo;&lt;/p&gt;&#xA;&lt;p&gt;— 50 Cent, &lt;em&gt;In da Club&lt;/em&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;Rich offers a number of quality of life improvements for Python&amp;rsquo;s interactive&#xA;interpreter, or REPL. Namely, pretty printed – and stylable – output, along&#xA;with a helpful tool for inspecting objects.&lt;/p&gt;&#xA;&lt;h2 id=&#34;syntax-highlighted-data-structures&#34;&gt;Syntax highlighted data structures&lt;/h2&gt;&#xA;&lt;p&gt;While &lt;a href=&#34;https://docs.python.org/3/whatsnew/3.14.html&#34;&gt;Python 3.14&lt;/a&gt; introduced syntax highlighting in the REPL, this only&#xA;applies to input – output remains unstyled.&lt;/p&gt;&#xA;&lt;p&gt;The &lt;code&gt;rich.pretty&lt;/code&gt; class can be used prettify output by invoking the&#xA;&lt;code&gt;install()&lt;/code&gt; method. For example:&lt;/p&gt;&#xA;&lt;video class=&#34;video-shortcode&#34; preload=&#34;auto&#34; controls&gt;&#xA;    &lt;source src=&#34;https://michaelhoward.kiwi/videos/pretty.mp4&#34; type=&#34;video/mp4&#34;&gt;&#xA;    There should have been a video here but your browser does not seem&#xA;    to support it.&#xA;&lt;/video&gt;&#xA;&#xA;&lt;p&gt;IPython users can skip the &lt;code&gt;pretty&lt;/code&gt; import and &lt;code&gt;install()&lt;/code&gt; invocation and run&#xA;the following command instead.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;In [1]: %load_ext rich&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Note that the styling provided by &lt;code&gt;rich.pretty&lt;/code&gt; doesn&amp;rsquo;t apply to &lt;code&gt;print()&lt;/code&gt;&#xA;calls. For this you need &lt;code&gt;rich.print&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;pretty-printing&#34;&gt;Pretty printing&lt;/h2&gt;&#xA;&lt;p&gt;Rich can overload Python&amp;rsquo;s native &lt;code&gt;print()&lt;/code&gt; function with its own. Not only&#xA;does this allow you to pretty print syntax-aware objects, you can also use&#xA;Rich&amp;rsquo;s &lt;a href=&#34;https://rich.readthedocs.io/en/stable/markup.html&#34;&gt;console markup&lt;/a&gt; to add colour, bold, italics, emoji and more to your&#xA;print statements.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;ll be covering console markup in depth in a future post, but for now I&amp;rsquo;ll&#xA;leave you with this tasteful example.&lt;/p&gt;&#xA;&lt;video class=&#34;video-shortcode&#34; preload=&#34;auto&#34; controls&gt;&#xA;    &lt;source src=&#34;https://michaelhoward.kiwi/videos/rich-print.mp4&#34; type=&#34;video/mp4&#34;&gt;&#xA;    There should have been a video here but your browser does not seem&#xA;    to support it.&#xA;&lt;/video&gt;&#xA;&#xA;&lt;h2 id=&#34;nicely-formatted-object-inspection&#34;&gt;Nicely formatted object inspection&lt;/h2&gt;&#xA;&lt;p&gt;The &lt;code&gt;inspect()&lt;/code&gt; function prints a tidy summary of an object to the console.&#xA;It&amp;rsquo;s useful for getting a quick overview of an object&amp;rsquo;s attributes (and&#xA;methods, if needed).&lt;/p&gt;&#xA;&lt;p&gt;Say you&amp;rsquo;re debugging and want to know more about &lt;code&gt;gunit&lt;/code&gt;, an instance of the&#xA;&lt;code&gt;GUnit&lt;/code&gt; class. After importing &lt;code&gt;inspect&lt;/code&gt;, a simple &lt;code&gt;inspect(gunit, methods=True)&lt;/code&gt; should give you enough info to get started.&lt;/p&gt;&#xA;&lt;video class=&#34;video-shortcode&#34; preload=&#34;auto&#34; controls&gt;&#xA;    &lt;source src=&#34;https://michaelhoward.kiwi/videos/rich-inspect.mp4&#34; type=&#34;video/mp4&#34;&gt;&#xA;    There should have been a video here but your browser does not seem&#xA;    to support it.&#xA;&lt;/video&gt;&#xA;&#xA;&lt;h2 id=&#34;like-my-style&#34;&gt;Like My Style?&lt;/h2&gt;&#xA;&lt;p&gt;If any of this bling appeals, I&amp;rsquo;d suggest adding Rich as a dev dependency to&#xA;each of your projects.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;uv add --dev rich&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then to ensure Rich is always imported in the REPL, create a &lt;code&gt;~/.startup.py&lt;/code&gt; file,&#xA;e.g.:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c4a7e7&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;rich&lt;/span&gt; &lt;span style=&#34;color:#c4a7e7&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;pretty&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;print&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;inspect&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;pretty&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;install&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;()&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And point your &lt;code&gt;PYTHONSTARTUP&lt;/code&gt; environment variable at it.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;# in Bash&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;PYTHONSTARTUP&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;$HOME&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;/.startup.py&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fish&#34; data-lang=&#34;fish&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;# in Fish&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;set&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;PYTHONSTARTUP&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;$HOME&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;/.startup.py&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;&#xA;&lt;p&gt;Hopefully this overview of Rich in the REPL has sparked your cursiosity and got&#xA;you asking &lt;a href=&#34;https://www.youtube.com/watch?v=cDMhlvbOFaM&#34;&gt;21 Questions&lt;/a&gt; about what else the library can do. In future&#xA;posts I&amp;rsquo;ll be exploring other features of Rich like logging, tables, live&#xA;display, markdown and more. Until then, I&amp;rsquo;ll leave you &lt;a href=&#34;https://www.youtube.com/watch?v=BFPmu10joSE&#34;&gt;Patiently Waiting&lt;/a&gt;.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Miryoku layout on the Glove80</title>
      <link>https://michaelhoward.kiwi/miryoku-layout-on-the-glove80/</link>
      <pubDate>Wed, 31 Dec 2025 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/miryoku-layout-on-the-glove80/</guid>
      <description>&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/glove80.jpeg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/glove80-scaled.jpeg&#34; alt=&#34;Both halves of a white Glove80 split keyboard sitting on a grey deskmat&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;I’ve been using the Glove80 as a daily driver in my home office for the past 8 months. It’s a fantastic board and very, very comfortable. When I received it, I toyed with the idea of &lt;a href=&#34;https://github.com/sunaku/glove80-keymaps&#34;&gt;learning a new layout&lt;/a&gt; and making use of all the extra keys, but thought better of it, since I still regularly use 36-key boards when going into the office/travelling.&lt;/p&gt;&#xA;&lt;p&gt;Instead, I went with the tried and true &lt;a href=&#34;https://github.com/manna-harbour/miryoku&#34;&gt;Miryoku layout&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;h2 id=&#34;physical-layout&#34;&gt;Physical layout&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/glove80-miryoku.jpeg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/glove80-miryoku-scaled.jpeg&#34; alt=&#34;A Glove80 keyboard with the keys used by the Miryoku layout outlined in purple&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Overall, the keys used by the Miryoku layout on the Glove80 are sensible. Of course, 46 of them go to waste, but that’s unavoidable.&lt;/p&gt;&#xA;&lt;p&gt;My one complaint is that the innermost thumb key is slightly too much of a stretch.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/miryoku-thumb-keys-standard.jpeg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/miryoku-thumb-keys-standard-scaled.jpeg&#34; alt=&#34;Standard thumb key layout on the Glove80, using all three bottom-row keys in the thumb cluster&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;To get around this, I shifted the two innermost thumb cluster keys outwards and moved the outermost thumb key to the top row.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/miryoku-thumb-key-movement.jpeg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/miryoku-thumb-key-movement-scaled.jpeg&#34; alt=&#34;Standard thumb key layout on the Glove80, with arrows showing an outward/topward shift&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;The resulting triangular cluster is much more comfortable.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/miryoku-thumb-keys-modified.jpeg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/miryoku-thumb-keys-modified-scaled.jpeg&#34; alt=&#34;Modified thumb key layout on the Glove80, with Delete on the top row, Backspace beneath it and Enter to the left&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;I achieved this by making a small change to &lt;code&gt;miryoku/mapping/80/glove80.h&lt;/code&gt; (you can see the thumb clusters in the centre of the bottom two lines), which leads me on to the next section…&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;#define MIRYOKU_LAYOUTMAPPING_GLOVE80( \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;     K00, K01, K02, K03, K04,                                    K05, K06, K07, K08, K09, \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;     K10, K11, K12, K13, K14,                                    K15, K16, K17, K18, K19, \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;     K20, K21, K22, K23, K24,                                    K25, K26, K27, K28, K29, \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;     N30, N31, K32, K33, K34,                                    K35, K36, K37, N38, N39 \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;) \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;XXX  XXX  XXX  XXX  XXX                                               XXX  XXX  XXX  XXX  XXX \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;XXX  XXX  XXX  XXX  XXX  XXX                                     XXX  XXX  XXX  XXX  XXX  XXX \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;XXX  K00  K01  K02  K03  K04                                     K05  K06  K07  K08  K09  XXX \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;XXX  K10  K11  K12  K13  K14                                     K15  K16  K17  K18  K19  XXX \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;XXX  K20  K21  K22  K23  K24  K32  XXX  XXX       XXX  XXX  K37  K25  K26  K27  K28  K29  XXX \&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;XXX  XXX  XXX  XXX  XXX       K33  K34  XXX       XXX  K35  K36       XXX  XXX  XXX  XXX  XXX&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;building-the-firmware&#34;&gt;Building the firmware&lt;/h2&gt;&#xA;&lt;p&gt;Since the Glove80 is one of the Miryoku project’s supported keyboards, building the firmware is simple:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Fork the &lt;a href=&#34;https://github.com/manna-harbour/miryoku_zmk&#34;&gt;manna-harbour/miryoku_zmk&lt;/a&gt; repo.&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Make any customisations necessary, like altering the layout file above. (Optional)&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li&gt;&#xA;&lt;p&gt;Build via the ‘Build Inputs’ workflow in the GitHub Actions tab. Because the Glove80 is a split board, you need to use the name &lt;code&gt;glove80_lh,glove80_rh&lt;/code&gt; as the ‘board’ name to produce a firmware file for each half. I also specify some additional configuration in the ‘kconfig’ field (see below).&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;h3 id=&#34;important-variables-to-set&#34;&gt;Important variables to set&lt;/h3&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y&lt;/code&gt;: disables some consumer keycodes, but improves OS compatibility. Useful when connecting the board to macOS/iOS/iPadOS.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;CONFIG_ZMK_POINTING=y&lt;/code&gt;: enables mouse emulation.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;CONFIG_ZMK_SLEEP=y&lt;/code&gt;: allows the board to go into a deep sleep state during periods of inactivity. Without enabling this, battery life is extremely short (no more than a day).&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Each variable needs to be separated by a newline character, e.g. &lt;code&gt;CONFIG_ZMK_HID_CONSUMER_REPORT_USAGES_BASIC=y\nCONFIG_ZMK_POINTING=y\nCONFIG_ZMK_SLEEP=y&lt;/code&gt;.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Preventing GitHub from disabling my scheduled certificate renewal workflow</title>
      <link>https://michaelhoward.kiwi/preventing-github-from-disabling-my-scheduled-certificate-renewal-workflow/</link>
      <pubDate>Sun, 02 Mar 2025 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/preventing-github-from-disabling-my-scheduled-certificate-renewal-workflow/</guid>
      <description>&lt;h2 id=&#34;intro&#34;&gt;Intro&lt;/h2&gt;&#xA;&lt;p&gt;In January, I set up a GitHub Actions workflow to renew the certificate on &lt;em&gt;assets.michaelhoward.kiwi&lt;/em&gt;, the subdomain I use for object storage. Each month, the workflow uses David Coles&amp;rsquo; &lt;a href=&#34;https://github.com/dcoles/acme-linode-objectstorage&#34;&gt;acme-linode-objectstorage&lt;/a&gt; tool to request a new TLS certificate and handle the resulting ACME challenge.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-problem&#34;&gt;The problem&lt;/h2&gt;&#xA;&lt;p&gt;It was all working suspiciously smoothly&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; until I received an email from GitHub saying the scheduled workflow was about to be disabled.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/workflow-expiration-email.png&#34; alt=&#34;GitHub&amp;rsquo;s email warning the workflow would be disabled&#34; title=&#34;but why tho?&#34;&gt;&lt;/p&gt;&#xA;&lt;p&gt;I was confused. What&amp;rsquo;s the point of scheduling a workflow that requires routine intervention?&lt;/p&gt;&#xA;&lt;p&gt;It turns out this is intended behaviour (for public repositories, at least). As noted &lt;a href=&#34;https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-workflow-runs/disabling-and-enabling-a-workflow?tool=webui&#34;&gt;in the docs&lt;/a&gt; (emphasis mine):&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;To prevent unnecessary workflow runs, scheduled workflows may be disabled automatically. When a public repository is forked, scheduled workflows are disabled by default. &lt;strong&gt;In a public repository, scheduled workflows are automatically disabled when no repository activity has occurred in 60 days.&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;p&gt;I&amp;rsquo;m not a fan of this, since manually re-enabling the workflow every 60 days largely defeats the purpose of the automation. Ideally, I&amp;rsquo;d be able to disable this behaviour, but alas.&lt;/p&gt;&#xA;&lt;p&gt;Instead, I had two options:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Set up more automation to make regular dummy commits to the repo&lt;/li&gt;&#xA;&lt;li&gt;Make the repo private&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;the-solution&#34;&gt;The solution&lt;/h2&gt;&#xA;&lt;p&gt;For the sake of simplicity, I opted for the latter. The repository&amp;rsquo;s visibility is now set to private, which is a shame because I&amp;rsquo;m sure there&amp;rsquo;s someone out there who&amp;rsquo;d want to do something similar.&lt;/p&gt;&#xA;&lt;p&gt;While I understand GitHub doesn&amp;rsquo;t want the headache of millions of workflows in abandoned repos running indefinitely, it would be nice to set up personal automation like this and still keep it public.&lt;/p&gt;&#xA;&lt;p&gt;In future, I&amp;rsquo;ll have to make sure that all of my scheduled workflows are in private repositories.&lt;/p&gt;&#xA;&lt;h2 id=&#34;the-workflow&#34;&gt;The workflow&lt;/h2&gt;&#xA;&lt;p&gt;For posterity, I&amp;rsquo;m including the workflow file at the end of this post.&lt;/p&gt;&#xA;&lt;p&gt;For the action to run, &lt;code&gt;BUCKET_NAME&lt;/code&gt; must be set to the custom domain used for the bucket, e.g. &lt;code&gt;assets.michaelhoward.kiwi&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Additionally, two secrets need to be added to the repository:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;code&gt;ACCOUNT_KEY&lt;/code&gt;: a Let&amp;rsquo;s Encrypt account key generated locally with &lt;code&gt;openssl genrsa 4096&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;&lt;code&gt;LINODE_TOKEN&lt;/code&gt;: a Linode API key with the object storage read/write permissions.&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Note that it&amp;rsquo;s also possible to trigger the workflow manually through either GitHub&amp;rsquo;s web interface or command line client.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;Renew certificate for Linode Object Storage&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;on&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ea9a97&#34;&gt;schedule&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ea9a97&#34;&gt;cron&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#39;0 0 1 * *&amp;#39;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ea9a97&#34;&gt;workflow_dispatch&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;jobs&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#ea9a97&#34;&gt;renew_cert&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;runs-on&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;ubuntu-latest&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;steps&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#ea9a97&#34;&gt;uses&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;actions/checkout@v4&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ea9a97&#34;&gt;with&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#ea9a97&#34;&gt;repository&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;dcoles/acme-linode-objectstorage&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#ea9a97&#34;&gt;uses&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;actions/setup-python@v5&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ea9a97&#34;&gt;with&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#ea9a97&#34;&gt;python-version&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#39;3.13&amp;#39;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      - &lt;span style=&#34;color:#ea9a97&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;Install Python deps and run cert update tool&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ea9a97&#34;&gt;run&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;          echo -e &amp;#34;$ACCOUNT_KEY&amp;#34; &amp;gt; ./account_key.pem&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;          pip install -r requirements.txt&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;          python -m acme_linode_objectstorage -k ./account_key.pem --agree-to-terms-of-service &amp;#34;$BUCKET_NAME&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ea9a97&#34;&gt;env&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#ea9a97&#34;&gt;BUCKET_NAME&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;${{ vars.BUCKET_NAME }}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#ea9a97&#34;&gt;LINODE_TOKEN&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;${{ secrets.LINODE_TOKEN }}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#ea9a97&#34;&gt;ACCOUNT_KEY&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;${{ secrets.ACCOUNT_KEY }}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;Certainly much more smoothly than waiting for a periodic email from Let&amp;rsquo;s Encrypt telling me the certificate was about to expire and then manually completing the ACME challenge to renew it.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
    </item>
    <item>
      <title>Cycling through command arguments with PowerShell</title>
      <link>https://michaelhoward.kiwi/cycling-through-command-arguments-with-powershell/</link>
      <pubDate>Wed, 08 Jan 2025 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/cycling-through-command-arguments-with-powershell/</guid>
      <description>&lt;p&gt;With the holiday season behind us, I&amp;rsquo;m sure we&amp;rsquo;re all sick of navigating arguments. However, I couldn&amp;rsquo;t resist sharing this neat PowerShell trick I learnt at the end of last year.&lt;/p&gt;&#xA;&lt;p&gt;PowerShell uses &lt;a href=&#34;https://learn.microsoft.com/en-us/powershell/scripting/learn/shell/using-keyhandlers?view=powershell-7.4&#34;&gt;PSReadLine&lt;/a&gt; to handle keyboard input. One excellent feature of PSReadLine is the &lt;code&gt;SelectCommandArgument&lt;/code&gt; key handler. This selects the next argument on the command line and by default is bound to Alt+A.&lt;/p&gt;&#xA;&lt;p&gt;I&amp;rsquo;ve never seen anything like this implemented in another shell (e.g. via &lt;a href=&#34;https://en.wikipedia.org/wiki/GNU_Readline&#34;&gt;GNU Readline&lt;/a&gt; or &lt;a href=&#34;https://www.nushell.sh/book/line_editor.html&#34;&gt;Reedline&lt;/a&gt;), but it&amp;rsquo;s super useful when running several long – but similar – commands in a row.&lt;/p&gt;&#xA;&lt;p&gt;Here&amp;rsquo;s a quick example of &lt;code&gt;SelectCommandArgument&lt;/code&gt; in action.&lt;/p&gt;&#xA;&lt;video class=&#34;video-shortcode&#34; preload=&#34;auto&#34; controls&gt;&#xA;    &lt;source src=&#34;https://michaelhoward.kiwi/videos/select-command-argument.mp4&#34; type=&#34;video/mp4&#34;&gt;&#xA;    There should have been a video here but your browser does not seem&#xA;    to support it.&#xA;&lt;/video&gt;&#xA;&#xA;</description>
    </item>
    <item>
      <title>Extracting and de-minifying JavaScript from the Boox Mira AppImage</title>
      <link>https://michaelhoward.kiwi/extracting-and-de-minifying-javascript-from-the-boox-mira-appimage/</link>
      <pubDate>Sun, 05 Mar 2023 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/extracting-and-de-minifying-javascript-from-the-boox-mira-appimage/</guid>
      <description>&lt;p&gt;Boox have written an Electron &lt;a href=&#34;https://help.boox.com/hc/en-us/articles/4408324394772-Mira-Software-Download&#34;&gt;app&lt;/a&gt; for controlling their Boox Mira and Boox Mira Pro displays. As mentioned in a previous &lt;a href=&#34;https://michaelhoward.kiwi/refreshing-multiple-e-ink-displays-on-linux-using-python/&#34;&gt;post&lt;/a&gt;, I&amp;rsquo;m not a huge fan and have been working on reproducing its behaviour in a simple Python script - &lt;a href=&#34;https://git.sr.ht/~elithper/miractl&#34;&gt;miractl&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Both miractl and its inspiration &lt;a href=&#34;https://github.com/ipodnerd3019/mira-js&#34;&gt;mira-js&lt;/a&gt; are made possible because the JavaScript from the manufacturer&amp;rsquo;s app can be extracted and inspected. What follows is a quick summary of how to do the same for yourself.&lt;/p&gt;&#xA;&lt;h2 id=&#34;getting-the-boox-mira-app&#34;&gt;Getting the Boox Mira app&lt;/h2&gt;&#xA;&lt;p&gt;First, download the AppImage via Boox&amp;rsquo;s website.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;wget https://static.send2boox.com/monitor-pc/linux/Mira-latest.AppImage&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then make it executable.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;chmod +x ./Mira-latest.AppImage&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;extracting-content-from-the-appimage&#34;&gt;Extracting content from the AppImage&lt;/h2&gt;&#xA;&lt;p&gt;Before continuing, it&amp;rsquo;s probably worth creating a directory to keep all the extracted files. Once created, move the AppImage inside and use this as your working directory for the &lt;code&gt;./Mira-latest.AppImage --appimage-extract&lt;/code&gt; command.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;mkdir ~/boox-mira&#xA;mv Mira-latest.AppImage ~/boox-mira&#xA;cd ~/boox-mira &amp;amp;&amp;amp; ./Mira-latest.AppImage --appimage-extract&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This will create the folder &lt;code&gt;~/boox-mira/squashfs-root&lt;/code&gt;. Inside the &lt;code&gt;resources&lt;/code&gt; folder is an archive file called &lt;code&gt;app.asar&lt;/code&gt;, which contains the concatenated, minified source files for the app.&lt;/p&gt;&#xA;&lt;h2 id=&#34;getting-to-the-javascript&#34;&gt;Getting to the JavaScript&lt;/h2&gt;&#xA;&lt;p&gt;Install &lt;code&gt;asar&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;npm install asar -g&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Again, it&amp;rsquo;ll keep things tidier if you create a destination folder before extracting the contents of &lt;code&gt;app.asar&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;mkdir ~/boox-mira/app&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Then extract the file into your newly created directory.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;asar extract squashfs-root/resources/app.asar app/&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Almost there! All that remains is de-minifying the JS to make it readable for mere mortals.&lt;/p&gt;&#xA;&lt;h2 id=&#34;de-minifying&#34;&gt;De-minifying&lt;/h2&gt;&#xA;&lt;p&gt;Inside &lt;code&gt;app/js&lt;/code&gt; is the minified JS and its source map - &lt;code&gt;app.5a0cfb40.js.map&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I used a tool (creatively) called &lt;a href=&#34;https://github.com/pavloko/source-map-unpack&#34;&gt;source-map-unpack&lt;/a&gt;, but there are multiple ways of unpacking source maps.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;npm install source-map-unpack -g&#xA;unpack unpacked app/js/app.5a0cfb40.js.map&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;This creates a folder called &lt;code&gt;unpacked&lt;/code&gt; in your working directory and unpacks the JS there. And that&amp;rsquo;s it!&lt;/p&gt;&#xA;&lt;p&gt;Your sweet JS prize awaits in &lt;code&gt;unpacked/src&lt;/code&gt;.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Stopping Terry Pratchett sneaking into my HTTP headers</title>
      <link>https://michaelhoward.kiwi/stopping-terry-pratchett-sneaking-into-my-http-headers/</link>
      <pubDate>Sun, 26 Feb 2023 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/stopping-terry-pratchett-sneaking-into-my-http-headers/</guid>
      <description>&lt;p&gt;The other day I was testing whether &lt;code&gt;curl&lt;/code&gt; was installed on my work machine, so I made a simple request to my site.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ curl https://michaelhoward.kiwi&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;To my surprise, the late and prolific author of the Discworld series popped up in the response.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;html&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;lang&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;en-NZ&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;head&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;meta&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;generator&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Hugo 0.106.0&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;meta&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;http-equiv&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;X-Clacks-Overhead&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;GNU Terry Pratchett&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;/&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;meta&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;charset&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;meta&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;viewport&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;/&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;link&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;rel&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;shortcut icon&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;href&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;https://michaelhoward.kiwi/images/favicon.png&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;/&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;title&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;&amp;gt;&lt;/span&gt;Failing Productively&lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;/&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;title&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;meta&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Failing Productively&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;/&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#908caa&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;meta&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;name&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;content&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;Fail. Learn. Repeat. A personal blog by Michael Howard.&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;/&amp;gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;(Just to be clear, I mean Terry Pratchett. At the time of writing I am neither dead nor the author of any Discworld novels.)&lt;/p&gt;&#xA;&lt;p&gt;Not knowing what &lt;code&gt;X-Clacks-Overhead&lt;/code&gt; meant, nor what Terry Pratchett had to do with them, I began sleuthing.&lt;/p&gt;&#xA;&lt;h2 id=&#34;where-did-he-come-from&#34;&gt;Where did he come from?&lt;/h2&gt;&#xA;&lt;p&gt;This blog uses the &lt;a href=&#34;https://github.com/janraasch/hugo-bearblog&#34;&gt;Hugo Bear&lt;/a&gt; theme, so that was an obvious place to look. A quick &lt;code&gt;grep&lt;/code&gt; of the repo later and I&amp;rsquo;d found the culprit in &lt;code&gt;hugo-bearblog/layouts/_default/baseof.html&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;However, the Hugo Bear theme is actually just a port of the theme used by the minimalist blogging platform &lt;a href=&#34;https://github.com/HermanMartinus/bearblog&#34;&gt;Bear Blog&lt;/a&gt;. I guessed (rightly) that the first mention of Mr Pratchett would be &lt;a href=&#34;https://github.com/HermanMartinus/bearblog&#34;&gt;there&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Still, knowing that my theme had inherited a cryptic message about a fantasy author didn&amp;rsquo;t help explain what the message meant.&lt;/p&gt;&#xA;&lt;h2 id=&#34;why-was-he-there-in-the-first-place&#34;&gt;Why was he there in the first place?&lt;/h2&gt;&#xA;&lt;p&gt;At last, the interesting bit! It turns out that following the death of Pratchett in 2015, the idea was floated on &lt;a href=&#34;https://web.archive.org/web/20190406055628id_/https://www.reddit.com/r/discworld/comments/2yt9j6/gnu_terry_pratchett/&#34;&gt;Reddit&lt;/a&gt; of giving him a lasting internet tribute inspired by his novel &lt;em&gt;Going Postal&lt;/em&gt;.&lt;/p&gt;&#xA;&lt;p&gt;The book describes an internet-like semaphore system called the Clacks. On losing a son, one of the characters keeps his boy&amp;rsquo;s name alive by sending it back and forth along the network in perpetuity. The protocol used by the Clacks allows you to specify what should be done with a message, similar to HTTP headers. Hence the &amp;lsquo;GNU&amp;rsquo; in &amp;lsquo;GNU Terry Pratchett&amp;rsquo; represents a fictional set of instructions for infinite transmission.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;G: send the message on&lt;/li&gt;&#xA;&lt;li&gt;N: do not log the message&lt;/li&gt;&#xA;&lt;li&gt;U: turn the message around at the end of the line and send it back again&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;By getting people to insert the &lt;code&gt;X-Clacks-Overhead&lt;/code&gt; message into their headers, Pratchett&amp;rsquo;s name could be memorialised on the modern internet in a similar way.&lt;/p&gt;&#xA;&lt;p&gt;The idea caught on based the subsequent &lt;a href=&#34;http://www.gnuterrypratchett.com/&#34;&gt;GNU Terry Pratchett website&lt;/a&gt;, and there are lots of ways of sticking the author&amp;rsquo;s name in your HTTP responses besides a blog theme – modifying your Nginx/Apache config, JavaScript etc.&lt;/p&gt;&#xA;&lt;p&gt;Touching as this tribute is, I don&amp;rsquo;t think Pratchett&amp;rsquo;s name is in any danger of dying out – certainly not while his &lt;a href=&#34;https://en.m.wikipedia.org/wiki/Discworld#Bibliography&#34;&gt;41 Discworld novels&lt;/a&gt; are still in publication. And I think it&amp;rsquo;s a bit of a strange thing to keep in my blog&amp;rsquo;s theme, now that I know it&amp;rsquo;s there.&lt;/p&gt;&#xA;&lt;h2 id=&#34;a-quick-fix&#34;&gt;A quick fix&lt;/h2&gt;&#xA;&lt;p&gt;No prizes for guessing how to fix this one. Simply deleting the line in &lt;code&gt;baseof.html&lt;/code&gt; did the job. I also took this as an opportunity to fork the theme and make a couple of minor changes.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Saturn60 keyboard build log</title>
      <link>https://michaelhoward.kiwi/saturn60-keyboard-build-log/</link>
      <pubDate>Thu, 19 Jan 2023 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/saturn60-keyboard-build-log/</guid>
      <description>&lt;p&gt;I&amp;rsquo;ve been building mechanical keyboards since 2020. And even though the number of weird and wonderful boards I&amp;rsquo;ve made is now in the double digits, I haven&amp;rsquo;t documented the process very well.&lt;/p&gt;&#xA;&lt;p&gt;So, for my latest build – the 70s-inspired Saturn60 – the I decided to take some nice pictures and timelapses along the way.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/saturn60-top-down.jpg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/saturn60-top-down-scaled.jpg&#34; alt=&#34;My completed Saturn60 keyboard&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;intro&#34;&gt;Intro&lt;/h2&gt;&#xA;&lt;p&gt;I joined the group buy for the Saturn60 in October of 2020(!) and received it at the end of 2021. And if that wait wasn&amp;rsquo;t long enough, I dithered for most of 2022 working out what switches and keycaps I wanted to use. Eventually I plucked up the courage to build the thing after Christmas.&lt;/p&gt;&#xA;&lt;p&gt;While I initially thought I would opt for a standard layout, I decided that nowadays I wouldn&amp;rsquo;t get much use out of the board unless it was ortholinear. Fortunately, the Saturn60 also accommodates a &lt;a href=&#34;https://github.com/qmk/qmk_firmware/blob/master/keyboards/boardwalk/readme.md&#34;&gt;Boardwalk PCB&lt;/a&gt; and the runner of the group buy still had some matching plates in stock, well after the GB had finished.&lt;/p&gt;&#xA;&lt;h2 id=&#34;assembly&#34;&gt;Assembly&lt;/h2&gt;&#xA;&lt;p&gt;Building the Saturn60 is straightforward for the most part, but with a couple of fiddly/infuriating steps along the way. Fortunately, there is an excellent &lt;a href=&#34;https://docs.acekeyboard.co/saturn-60-assembly-guide&#34;&gt;build guide&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I started by mounting my switches&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; onto their plate. Click to see the full timelapse in non-potato quality!&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/saturn60-switch-installation.mp4&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/saturn60-switch-installation.gif&#34; alt=&#34;Mounting switches to the plate and PCB&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Following this, I soldered the switches to the PCB. As before, click for an equally boring, but much higher definition timelapse.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/saturn60-soldering.mp4&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/saturn60-soldering.gif&#34; alt=&#34;Soldering switches&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Next came the annoying part – sandwiching the plate/PCB in between the two halves of the case and screwing it all together. It sounds simple, but it&amp;rsquo;s fiendishly easy for the plate/PCB to get slightly wonky during this step. Once misaligned, not only will your keys look slightly off,&lt;sup id=&#34;fnref:2&#34;&gt;&lt;a href=&#34;#fn:2&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;2&lt;/a&gt;&lt;/sup&gt; but you risk not having enough clearance between them and the case, causing some of the keys to scrape.&lt;/p&gt;&#xA;&lt;p&gt;Still, it wasn&amp;rsquo;t too bad in the end.&lt;/p&gt;&#xA;&lt;p&gt;Until I realised I made a terrible mistake – I&amp;rsquo;d forgotten to attach the serial number plate on the back of the case. As you need access to both sides of the back case to attach this plate, I was forced to take the whole keyboard apart.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/saturn60-missing-serial-plate.jpg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/saturn60-missing-serial-plate-scaled.jpg&#34; alt=&#34;A picture of the regrettably unattached serial plate&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;(No doubt you can feel the rage emanating from this photo.)&lt;/p&gt;&#xA;&lt;p&gt;But I got done. Again.&lt;/p&gt;&#xA;&lt;h2 id=&#34;keycaps&#34;&gt;Keycaps&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/sail-keycaps-closeup.jpg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/sail-keycaps-closeup-scaled.jpg&#34; alt=&#34;A close-up of the SA SAIL keycaps&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Adorning the assembled board is a set of SA SAIL keycaps. The set is based on a keyboard used in the Stanford Artificial Intelligence Lab in the early 70s. Everything about these keycaps is &lt;em&gt;perfect&lt;/em&gt;. The colour, the mathematical symbols on the legends, the profile – everything.&lt;/p&gt;&#xA;&lt;p&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/sail-keyboard.jpg&#34; alt=&#34;A picture of the original SAIL keyboard&#34;&gt;&#xA;&lt;em&gt;The original SAIL keyboard. Image credit: &lt;a href=&#34;https://www.flickr.com/photos/mwichary/3099176571/in/photostream/&#34;&gt;Marcin Wichary&lt;/a&gt;, &lt;a href=&#34;https://creativecommons.org/licenses/by/2.0/&#34;&gt;CC BY 2.0&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;&#xA;&lt;p&gt;For the past 18 months or so I&amp;rsquo;ve been trying to hunt down a set secondhand and finally lucked out. MyKeyboard.eu had some (new!) sets available and I couldn&amp;rsquo;t resist.&lt;sup id=&#34;fnref:3&#34;&gt;&lt;a href=&#34;#fn:3&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;3&lt;/a&gt;&lt;/sup&gt; Honestly, I&amp;rsquo;m not even sure how they ended up with these in stock. Not that I&amp;rsquo;m complaining, of course.&lt;/p&gt;&#xA;&lt;h2 id=&#34;details&#34;&gt;Details&lt;/h2&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/saturn60-badge-closeup.jpg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/saturn60-badge-closeup-scaled.jpg&#34; alt=&#34;A close-up of the badge&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/saturn60-handle.jpg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/saturn60-handle-scaled.jpg&#34; alt=&#34;A view of the carry handle&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/saturn60-serial-plate.jpg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/saturn60-serial-plate-scaled.jpg&#34; alt=&#34;Saturn60 serial plate&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;closing-thoughts&#34;&gt;Closing thoughts&lt;/h2&gt;&#xA;&lt;p&gt;I had a blast building this keyboard and am really happy with the result. It may have taken a few years, but the outcome more than justifies the wait!&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;Boba U4T 68g.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:2&#34;&gt;&#xA;&lt;p&gt;Well, look slightly off to &lt;em&gt;you&lt;/em&gt;. No one else would ever notice.&amp;#160;&lt;a href=&#34;#fnref:2&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;li id=&#34;fn:3&#34;&gt;&#xA;&lt;p&gt;Even the €40 shipping fee from Belgium couldn&amp;rsquo;t dissuade me.&amp;#160;&lt;a href=&#34;#fnref:3&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
    </item>
    <item>
      <title>Logitech MX Master tweaks for an e-ink setup</title>
      <link>https://michaelhoward.kiwi/logitech-mx-master-tweaks-for-an-e-ink-setup/</link>
      <pubDate>Sun, 27 Nov 2022 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/logitech-mx-master-tweaks-for-an-e-ink-setup/</guid>
      <description>&lt;p&gt;While I prefer to use the keyboard where possible, I often find myself falling back on a mouse when browsing the web and reading articles/documentation. My mouse is an MX Master 3 which I &lt;em&gt;love&lt;/em&gt;. However, one of it big drawcards – its buttery smooth scrollwheel – isn&amp;rsquo;t as useful with e-ink displays. Sure, you can scroll quickly, and your display will do its best to keep up, but it doesn&amp;rsquo;t look smooth and creates a lot of ghosting.&lt;/p&gt;&#xA;&lt;p&gt;On the other hand, another great feature of the mouse is its reprogrammability. All of its additional buttons can be remapped&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. This makes it simple to replace scrolling with paging – Forward and Back become Page Up and Page Down.&lt;/p&gt;&#xA;&lt;p&gt;Remapping the buttons is made possible on Linux with the &lt;a href=&#34;https://github.com/PixlOne/logiops&#34;&gt;logiops&lt;/a&gt; userspace driver. With this tool, you can customise the behaviour of Logitech [devices] using a (reasonably) straightforward config file.&lt;/p&gt;&#xA;&lt;p&gt;In addition to mapping the Forward/Back buttons to Page Up/Page Down, I also set the thumb button to Alt+Shift+R – a shortcut I use to manually refresh my e-ink displays (a topic covered &lt;a href=&#34;https://michaelhoward.kiwi/refreshing-multiple-e-ink-displays-on-linux-using-python/&#34;&gt;previously&lt;/a&gt;).&lt;/p&gt;&#xA;&lt;p&gt;I did run into a small problem in writing my config though. Although the example in the repo sets the name as &amp;ldquo;Wireless Mouse MX Master&amp;rdquo;, my device didn&amp;rsquo;t like being called that, and refused to play ball until I changed the name to &amp;ldquo;Wireless Mouse MX Master 3&amp;rdquo;. Apart from that, and the squinting over braces and parentheses, setup was painless.&lt;/p&gt;&#xA;&lt;p&gt;My &lt;code&gt;/etc/logid.cfg&lt;/code&gt; can be found below.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;devices: (&#xA;{&#xA;    name: &amp;#34;Wireless Mouse MX Master 3&amp;#34;;&#xA;&#xA;    smartshift:&#xA;    {&#xA;        on: true;&#xA;        threshold: 30;&#xA;    };&#xA;&#xA;    hiresscroll:&#xA;    {&#xA;        hires: true;&#xA;        invert: false;&#xA;        target: false;&#xA;    };&#xA;&#xA;    dpi: 1500;&#xA;&#xA;    buttons: (&#xA;        // &amp;#39;Forward&amp;#39; button&#xA;        {&#xA;            cid: 0x56;&#xA;            action =&#xA;            {&#xA;                type: &amp;#34;Keypress&amp;#34;;&#xA;                keys: [&amp;#34;KEY_PAGEUP&amp;#34;];&#xA;            };&#xA;        },&#xA;        // &amp;#39;Back&amp;#39; button&#xA;        {&#xA;            cid: 0x53;&#xA;            action =&#xA;            {&#xA;                type: &amp;#34;Keypress&amp;#34;;&#xA;                keys: [&amp;#34;KEY_PAGEDOWN&amp;#34;];&#xA;            };&#xA;        },&#xA;        // Thumb button&#xA;        {&#xA;            cid: 0xc3;&#xA;            action = &#xA;            {&#xA;                type: &amp;#34;Keypress&amp;#34;;&#xA;                keys: [ &amp;#34;KEY_LEFTALT&amp;#34;, &amp;#34;KEY_LEFTSHIFT&amp;#34;, &amp;#34;KEY_R&amp;#34; ];&#xA;            };&#xA;        }&#xA;    );&#xA;}&#xA;);&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;What&amp;rsquo;s more, these can combined with mouse movements to create &amp;lsquo;gestures&amp;rsquo;.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
    </item>
    <item>
      <title>YubiKey CLI setup with ykman</title>
      <link>https://michaelhoward.kiwi/yubikey-cli-setup-with-ykman/</link>
      <pubDate>Sun, 13 Nov 2022 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/yubikey-cli-setup-with-ykman/</guid>
      <description>&lt;p&gt;I spent yesterday afternoon setting up a pair of YubiKeys – a primary key and a spare. Although I had to link them with a couple of dozen accounts, the process was surprisingly painless. Mostly thanks to their great CLI tool &lt;a href=&#34;https://docs.yubico.com/software/yubikey/tools/ykman/index.html&#34;&gt;yubikey-manager&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Below are some notes on the process, mainly for my own reference. Even though setup is relatively simple, I find the acronym salad of two-factor hardware tokens overwhelming at the best of times, so this is also to remind myself that it isn&amp;rsquo;t rocket science.&lt;/p&gt;&#xA;&lt;h2 id=&#34;installation&#34;&gt;Installation&lt;/h2&gt;&#xA;&lt;p&gt;On Debian/Ubuntu systems, the package can be found under the name &lt;code&gt;yubikey-manager&lt;/code&gt; (though the &lt;code&gt;ykman&lt;/code&gt; command is used to call the application).&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;sudo apt install yubikey-manager&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;You will also need the PC/SC Smart Card Daemon &lt;code&gt;pcscd&lt;/code&gt;. In my case, this was already installed but the service needed to be enabled.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;sudo systemctl enable pcscd.service&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;sudo systemctl start pcscd.service&lt;/code&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;setting-a-fido-pin&#34;&gt;Setting a FIDO pin&lt;/h2&gt;&#xA;&lt;p&gt;Set a pin with the following command.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;ykman fido access change-pin&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;This is seldom used, but some services require one to be set. Make sure you use the same pin on your primary key and your backup.&lt;/p&gt;&#xA;&lt;h2 id=&#34;configuring-totp-slots&#34;&gt;Configuring TOTP slots&lt;/h2&gt;&#xA;&lt;p&gt;While many sites support WebAuthn (i.e. the fancy standard that lets your browser talk directly to the key), 6–8 digit, time-based one time passwords (TOTP) are more common.&lt;/p&gt;&#xA;&lt;p&gt;To set these up, you need a secret provided by the site you&amp;rsquo;re logging into. Often, these are given in the form of a QR code, but an alphanumeric string or URI will also be available. URIs look something like this:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;otpauth://...&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;The format for adding an account via a secret is &lt;code&gt;ykman oath accounts add [OPTIONS] &amp;lt;name&amp;gt; &amp;lt;secret&amp;gt;&lt;/code&gt;. For example:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;ykman oath accounts add geocities JBSWY3DPEHPK3PXP&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;However, if using a URI, &lt;code&gt;add&lt;/code&gt; is replaced by &lt;code&gt;uri&lt;/code&gt; and the name field is omitted:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;ykman oath accounts uri otpauth://totp/Yahoo:michael@geocities.com?secret=JBSWY3DPEHPK3PXP&amp;amp;issuer=Yahoo&lt;/code&gt;&lt;/p&gt;&#xA;&lt;blockquote&gt;&#xA;&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Your backup key needs the same secret as your primary one. After adding the account to your primary key, remove the device and plug in the backup key. Then run the same command again.&lt;/p&gt;&#xA;&lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;listing-current-totp-slots&#34;&gt;Listing current TOTP slots&lt;/h2&gt;&#xA;&lt;p&gt;You can see a list of accounts you have set up TOTP authentication for with &lt;code&gt;ykman oath accounts list&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;$ ykman oath accounts list&#xA;geocities&#xA;ask-jeeves&#xA;alta-vista&#xA;napster&#xA;myspace&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id=&#34;generating-totp-codes&#34;&gt;Generating TOTP codes&lt;/h2&gt;&#xA;&lt;p&gt;Codes can be generated with &lt;code&gt;ykman oath accounts code &amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;I found this a bit clunky, so I wrote a short Bash function which runs the command and copies the resulting code to the clipboard.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;function&lt;/span&gt; yktotp &lt;span style=&#34;color:#908caa&#34;&gt;()&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ykman oath accounts code &lt;span style=&#34;color:#ea9a97&#34;&gt;$1&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt; awk &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#39;{printf $NF}&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt; xclip -selection clipboard&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The function is aliased to &lt;code&gt;2fa&lt;/code&gt; to help me remember. This lets me get TOTP codes with a simple &lt;code&gt;2fa &amp;lt;name&amp;gt;&lt;/code&gt;.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Refreshing multiple e-ink displays on Linux using Python</title>
      <link>https://michaelhoward.kiwi/refreshing-multiple-e-ink-displays-on-linux-using-python/</link>
      <pubDate>Mon, 07 Nov 2022 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/refreshing-multiple-e-ink-displays-on-linux-using-python/</guid>
      <description>&lt;p&gt;At my desk at home, I have a pair of e-ink displays – a Dasung Paperlike 253 and a Boox Mira 13.3&amp;quot;.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/my-desk-fullsize.jpg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/my-desk.jpg&#34; alt=&#34;A picture of my desk setup&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;In a &lt;a href=&#34;https://michaelhoward.kiwi/refreshing-e-ink-displays-on-windows/&#34;&gt;previous post&lt;/a&gt; I covered a how I refresh these on the Windows machine I use for work. While a bit of a hack, it works in a pinch.&lt;/p&gt;&#xA;&lt;p&gt;When using my own laptop, on the other hand, I have much greater freedom. A pair of Python scripts allow me to control either or both of the monitors.&lt;/p&gt;&#xA;&lt;h2 id=&#34;boox-mira&#34;&gt;Boox Mira&lt;/h2&gt;&#xA;&lt;p&gt;Boox have written their own Electron app for controlling Boox Mira and Boox Mira Pro displays. It&amp;rsquo;s hot garbage.&lt;/p&gt;&#xA;&lt;p&gt;Fortunately, a simpler, less bloated alternative exists. &lt;a href=&#34;https://github.com/ipodnerd3019&#34;&gt;ipodnerd3019&lt;/a&gt; had the bright idea of inspecting the Javascript used by the app to send opcodes to the device. They then turned this into a neat CLI Javascript utility – &lt;a href=&#34;https://github.com/ipodnerd3019/mira-js&#34;&gt;mira-js&lt;/a&gt;.&lt;/p&gt;&#xA;&lt;p&gt;Not being big on JS, I decided to reimplement some of this functionality in Python using &lt;code&gt;pyusb&lt;/code&gt;. For now it&amp;rsquo;s just a couple of scripts: one for refreshing the display, the other for turning off the frontlight. I&amp;rsquo;ve created &lt;a href=&#34;https://git.sr.ht/~elithper/mira-utils&#34;&gt;a repo&lt;/a&gt;, which I&amp;rsquo;ll be adding to when I get the time.&lt;/p&gt;&#xA;&lt;h2 id=&#34;dasung-paperlike-253&#34;&gt;Dasung Paperlike 253&lt;/h2&gt;&#xA;&lt;p&gt;While communicating with the Paperlike is slightly more involved, the work had already been done for me. Philip Metzler has written a great &lt;a href=&#34;https://github.com/cpmetz/dasung253&#34;&gt;Python script&lt;/a&gt;, which makes use of &lt;code&gt;pyserial&lt;/code&gt; to control the monitor.&lt;/p&gt;&#xA;&lt;p&gt;Unfortunately, getting the script to run for the first time was a learning experience, to say the least.&lt;/p&gt;&#xA;&lt;p&gt;The script needs the monitor&amp;rsquo;s device file. Something like &lt;code&gt;/dev/ttyUSB*&lt;/code&gt;, e.g.:&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;python ./dasung253.py --port /dev/ttyUSB0 --clear&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;Although I quickly identified the monitor could be found at &lt;code&gt;/dev/ttyUSB0&lt;/code&gt; when connected via USB, it would disconnect shortly after. Running &lt;code&gt;sudo dmesg | grep tty&lt;/code&gt; confirmed this.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;[ 2553.808793] usb 1-1.4.1: ch341-uart converter now attached to ttyUSB0&#xA;[ 2556.138673] ch341-uart ttyUSB0: ch341-uart converter now disconnected from ttyUSB0&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;After some very confused googling, I discovered the package &lt;code&gt;brltty&lt;/code&gt; was interfering. Although it&amp;rsquo;s a package for driving refreshable braille displays, the daemon it runs in the background was (mis)identifying the Paperlike from the CH340 serial converter chip it uses. Based on &lt;a href=&#34;https://stackoverflow.com/questions/70123431/why-would-ch341-uart-is-disconnected-from-ttyusb&#34;&gt;this thread&lt;/a&gt;, it appears the CH340 is used by a specific Braille eReader and &lt;code&gt;brltty&lt;/code&gt; therefore has conflicting udev rules for this device.&lt;/p&gt;&#xA;&lt;p&gt;To solve this I had two options:&lt;/p&gt;&#xA;&lt;ol&gt;&#xA;&lt;li&gt;Remove the package completely with a simple &lt;code&gt;sudo apt remove brltty&lt;/code&gt;.&lt;/li&gt;&#xA;&lt;li&gt;Change the udev rules.&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;p&gt;Since I&amp;rsquo;m not planning on using any braille screens anytime soon, I opted for the former.&lt;/p&gt;&#xA;&lt;p&gt;As a final hurdle, I also hit a permissions error trying to access &lt;code&gt;/dev/ttyUSB0&lt;/code&gt;. To access this device my user had to be added to the &lt;code&gt;dialout&lt;/code&gt; group: &lt;code&gt;sudo adduser cyclist dialout&lt;/code&gt;.&lt;/p&gt;&#xA;&lt;p&gt;However, once I&amp;rsquo;d done that and rebooted, the script worked like a charm!&lt;/p&gt;&#xA;&lt;h2 id=&#34;bringing-it-all-together&#34;&gt;Bringing it all together&lt;/h2&gt;&#xA;&lt;p&gt;To refresh both displays (near) simultaneously. I call them both with a Bash script.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;#!/usr/bin/env bash&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/home/cyclist/scripts/mira-refresh.py&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;python /home/cyclist/scripts/dasung253.py --port /dev/ttyUSB0 --clear&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I then bind this to a shortcut in Gnome Settings: &lt;em&gt;Settings &amp;gt; Keyboard &amp;gt; Keyboard Shortcuts &amp;gt; View and customise shortcuts&lt;/em&gt;. I&amp;rsquo;ve chosen to bind this to Shift+Alt+R (for refresh)&lt;/p&gt;&#xA;&lt;h2 id=&#34;calling-the-scripts-individually&#34;&gt;Calling the scripts individually&lt;/h2&gt;&#xA;&lt;p&gt;There are also times where I only want to refresh one of the displays. After all, I generally don&amp;rsquo;t have the same windows open on both screens and therefore I may only want to clear ghosting/sharpen the image on one of them.&lt;/p&gt;&#xA;&lt;p&gt;For this, I&amp;rsquo;ve used the same technique as above, but this time each shortcut calls its respective refresh script.&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;Paperlike 253 = Shift+Alt+P&lt;/li&gt;&#xA;&lt;li&gt;Boox Mira = Shift+Alt+B&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;Notice that I&amp;rsquo;ve used the initial letter from each monitor&amp;rsquo;s name to help me remember the shortcut.&lt;/p&gt;&#xA;&lt;p&gt;Conveniently, because I use the &lt;a href=&#34;https://github.com/manna-harbour/miryoku&#34;&gt;Miryoku layout&lt;/a&gt; on all my keyboards, P and B are right next to each other. They&amp;rsquo;re also in the same configuration as my physical monitors – Paperlike on the left, Boox Mira on the right.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/wireless-corne-fullsize.jpg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/wireless-corne.jpg&#34; alt=&#34;A picture of my Corne keyboard in the Miryoku layout&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;&#xA;&lt;p&gt;I&amp;rsquo;m pretty stoked at how simple it is to control these displays with just a few lines of Python! Having these shortcuts set up has made a world of difference in usability. Next up I&amp;rsquo;d like to expand what my scripts can do with the Boox Mira (i.e. controlling contrast, refresh rate, etc.) and making my Bash script a little smarter, so that it can handle when new monitors are plugged in/unplugged and the device files change.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Initial impressions – AirPods Max feat. a very stupid engraving</title>
      <link>https://michaelhoward.kiwi/initial-impressions-airpods-max-feat.-a-very-stupid-engraving/</link>
      <pubDate>Sun, 16 Oct 2022 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/initial-impressions-airpods-max-feat.-a-very-stupid-engraving/</guid>
      <description>&lt;p&gt;I recently made the somewhat frivolous decision to buy a pair of AirPods Max headphones. Frivolous, in that my decision was partly motivated by the fact you can customise them with an engraving.&lt;/p&gt;&#xA;&lt;p&gt;The minimalist in me wanted something understated, like my initials. The anarchic hacker in me wanted a clever, nerdy joke. In the end, I compromised.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/airpods-max-engraving-fullsize.jpg&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/airpods-max-engraving.jpg&#34; alt=&#34;My initials&amp;rsquo; Unicode code points engraved on Apple&amp;rsquo;s AirPods Max&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;As you can see, instead of engraving a boring &lt;em&gt;MH&lt;/em&gt;, I used the Unicode code points for the characters:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;LATIN CAPITAL LETTER M &lt;code&gt;U+004D&lt;/code&gt;&lt;/li&gt;&#xA;&lt;li&gt;LATIN CAPITAL LETTER H &lt;code&gt;U+0048&lt;/code&gt;&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;I&amp;rsquo;m expecting this to be extremely useful if I ever lose my headphones at a Unicode Technical Committee meeting. Or a linguistics conference.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Refreshing e-ink displays on Windows</title>
      <link>https://michaelhoward.kiwi/refreshing-e-ink-displays-on-windows/</link>
      <pubDate>Sun, 28 Aug 2022 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/refreshing-e-ink-displays-on-windows/</guid>
      <description>&lt;p&gt;One of the pain points with e-ink/e-paper displays is having to manually refresh the screen to clear ghosting. While e-ink monitors like the &lt;a href=&#34;https://onyxboox.com/boox_mira&#34;&gt;Boox Mira&lt;/a&gt; and &lt;a href=&#34;https://dasung-tech.myshopify.com/pages/more-about-paperlike-253&#34;&gt;Dasung Paperlike 253&lt;/a&gt; all have a dedicated hardware refresh button, it&amp;rsquo;s inconvenient to have to reach for your monitor(s) all day. Although this problem is a relatively simple software fix, either by using a manufacturer&amp;rsquo;s (janky) client application, or by using scripts to communicate over serial, it becomes difficult when using locked-down Windows device like my work machine.&lt;/p&gt;&#xA;&lt;p&gt;Fortunately, it&amp;rsquo;s possible to set up a pseudo-shortcut on Windows which approximates a refresh.&lt;/p&gt;&#xA;&lt;h2 id=&#34;what-does-refreshing-an-e-ink-display-actually-mean&#34;&gt;What does refreshing an e-ink display actually mean?&lt;/h2&gt;&#xA;&lt;p&gt;Since this isn&amp;rsquo;t a post on how e-ink/e-paper technology works, I&amp;rsquo;ll be brief.&lt;/p&gt;&#xA;&lt;p&gt;Whenever you use an e-ink display ghosting occurs. This is where a faint outline of previously displayed text/images sticks around. After a while, the ectoplasm builds up and you&amp;rsquo;re left with an ugly, spectral mess. To clear this, you can briefly turn the display black and then redraw the contents of the screen.&lt;/p&gt;&#xA;&lt;h2 id=&#34;blanking-your-screen-on-windows&#34;&gt;Blanking your screen on Windows&lt;/h2&gt;&#xA;&lt;p&gt;By far, the easiest way to turn your display(s) black for split second is using an AutoHotkey script like &lt;a href=&#34;https://gist.github.com/llinfeng/a1a282ec3e0d6d2510bf2c4b04a7940c&#34;&gt;this one.&lt;/a&gt; But in my case, I need a built-in Windows utility to do the same job.&lt;/p&gt;&#xA;&lt;p&gt;As it turns out, the built-in screensaver is pretty good for this! Thanks to a helpful &lt;a href=&#34;https://superuser.com/questions/1658621/on-demand-black-screen-screensaver-black-screen-without-switching-off-monitor&#34;&gt;Superuser discussion&lt;/a&gt; I learnt the following command enables the screensaver (without locking the session).&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;cmd /c scrnsave.scr /s&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;Provided the current screensaver isn&amp;rsquo;t set to an image or bright colour, running this command is an effective way of turning all connected displays black. But the gotcha here is that you have to move the mouse or press a key to return to your work.&lt;/p&gt;&#xA;&lt;p&gt;Usability wise, the real issue is not having a convenient shortcut. Having to switch to a console window every time you need to refresh the screen is about as annoying as pressing a button on the device.&lt;/p&gt;&#xA;&lt;h2 id=&#34;enter-the-pseudo-shortcut&#34;&gt;Enter the pseudo-shortcut&lt;/h2&gt;&#xA;&lt;p&gt;At this point, I realised that I could bypass the console altogether by using the Run command window. Pressing Win + R opens the Run dialog which can be used to start the &lt;code&gt;scrnsave.scr&lt;/code&gt; command.&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/scrnsave_run_dialog.png&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/scrnsave_run_dialog.png&#34; alt=&#34;Run dialog screenshot&#34; title=&#34;A screenshot of scrnsave.scr in the Run command window.&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;p&gt;Note that only the &lt;code&gt;scrnsave.scr&lt;/code&gt; part of the command is necessary. Hitting Enter will then turn on the screensaver and hitting it again will disable it.&lt;/p&gt;&#xA;&lt;p&gt;What&amp;rsquo;s more, the Run dialog remembers your last command. So if you&amp;rsquo;re like me and don&amp;rsquo;t use Run for much else, you&amp;rsquo;ll never need to retype the command after the first time. This means the shortcut Win + R Enter Enter is now always available.&lt;/p&gt;&#xA;&lt;p&gt;While far from a perfect solution, I&amp;rsquo;ve been quite happy with this hack. It works well enough most of the time and has the added benefit of working with any e-ink display, regardless of manufacturer.&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>Dipping my toes into Nushell</title>
      <link>https://michaelhoward.kiwi/dipping-my-toes-into-nushell/</link>
      <pubDate>Mon, 11 Apr 2022 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/dipping-my-toes-into-nushell/</guid>
      <description>&lt;p&gt;Nushell came to my attention a couple of weeks back, following its &lt;a href=&#34;https://www.nushell.sh/blog/2022-03-22-nushell_0_60.html&#34;&gt;0.6 release&lt;/a&gt; and immediately piqued my interest. &amp;lsquo;What&amp;rsquo;s so nu about this shell?&amp;rsquo; I thought.&lt;/p&gt;&#xA;&lt;p&gt;Simply, Nushell is a Unix-style shell that works with structured data instead of raw text. In this way, it bears a lot of similarity with PowerShell, which I use heavily at my current job. Since I&amp;rsquo;m already used to working with an object-based shell for work, I thought it would be interesting to try one out on my personal machine.&lt;/p&gt;&#xA;&lt;p&gt;To be honest, I don&amp;rsquo;t do much shell scripting for personal projects. Yes, it would make my life easier. Yes, being familiar enough with Bash/Fish/Zsh to pull out sick one-liners is cool. But I&amp;rsquo;m lazy and generally can&amp;rsquo;t convince myself it&amp;rsquo;s worth the effort.&lt;/p&gt;&#xA;&lt;p&gt;Enter Nushell! Something cool enough to get me started, but familiar enough that it doesn&amp;rsquo;t feel like too much work to learn.&lt;/p&gt;&#xA;&lt;p&gt;Starting slow, I decided to rewrite the small Fish function I mentioned in my &lt;a href=&#34;https://michaelhoward.kiwi/how-this-blog-works/&#34;&gt;last post&lt;/a&gt;. To recap, I wanted a function that takes a path to a local file, uploads that file to Linode object storage and then copies the resulting URL to the clipboard.&lt;/p&gt;&#xA;&lt;p&gt;The original:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fish&#34; data-lang=&#34;fish&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;send2obj&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;linode-cli&lt;/span&gt; obj put &lt;span style=&#34;color:#ea9a97&#34;&gt;--acl-public&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;$argv&lt;/span&gt; michaelhoward&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#3e8fb0&#34;&gt;set&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;filename&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;basename&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;$argv&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#3e8fb0&#34;&gt;echo&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;-n&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;https://michaelhoward.ap-south-1.linodeobjects.com/&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;$filename&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;tee&lt;/span&gt; /dev/tty &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;xclip&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;-selection&lt;/span&gt; clipboard&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And in Nushell:&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;# A command to send a file to Linode object storage, copying the resulting URL to the clipboard&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;def send-to-object-storage &lt;span style=&#34;color:#908caa&#34;&gt;[&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    filepath &lt;span style=&#34;color:#6e6a86&#34;&gt;# The local path of the file to upload, e.g. ~/images/screenshot.png&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;]&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#3e8fb0&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;$filepath&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt; path exists&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        linode-cli obj put --acl-public &lt;span style=&#34;color:#ea9a97&#34;&gt;$filepath&lt;/span&gt; michaelhoward&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#ea9a97&#34;&gt;echo&lt;/span&gt; $&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;https://michaelhoward.ap-south-1.linodeobjects.com/(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;$filepath&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt; | path basename)&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt; tee /dev/tty &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt; xclip -selection clipboard&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#908caa&#34;&gt;}&lt;/span&gt; &lt;span style=&#34;color:#3e8fb0&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;{&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;         &lt;span style=&#34;color:#ea9a97&#34;&gt;echo&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;File not found. Please check the path is correct.&amp;#34;&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#908caa&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;}&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#6e6a86&#34;&gt;## Aliases ##&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;alias&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;s2o&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;=&lt;/span&gt; send-to-object-storage&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As you can see, I got a little carried away. But essentially it does the same thing. Just better.&lt;/p&gt;&#xA;&lt;p&gt;On a practical level, it&amp;rsquo;s an improvement in that it checks the path to the file is valid before doing anything else. Though what I really prefer about the rewrite is the help message it displays.&lt;/p&gt;&#xA;&lt;p&gt;I can now type &lt;code&gt;send-to-object-storage --help&lt;/code&gt; or &lt;code&gt;s2o -h&lt;/code&gt; to see the following.&lt;/p&gt;&#xA;&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;A command to send a file to Linode object storage, copying the resulting URL to the clipboard&#xA;&#xA;Usage:&#xA;  &amp;gt; send-to-object-storage &amp;lt;filepath&amp;gt; &#xA;&#xA;Flags:&#xA;  -h, --help&#xA;      Display this help message&#xA;&#xA;Parameters:&#xA;  filepath: The local path of the file to upload, e.g. ~/images/screenshot.png&#xA;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;While this example doesn&amp;rsquo;t show off any of Nushell&amp;rsquo;s killer features, it was simple to put together and has me excited about experimenting further. Time to hit the docs!&lt;/p&gt;&#xA;</description>
    </item>
    <item>
      <title>How this blog works</title>
      <link>https://michaelhoward.kiwi/how-this-blog-works/</link>
      <pubDate>Sat, 19 Feb 2022 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/how-this-blog-works/</guid>
      <description>&lt;h2 id=&#34;intro&#34;&gt;Intro&lt;/h2&gt;&#xA;&lt;p&gt;As is customary for websites like this, the following post details how I run my blog.&lt;/p&gt;&#xA;&lt;p&gt;There are three main components:&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://gohugo.io/&#34;&gt;Hugo&lt;/a&gt;, a static site generator&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://srht.site/&#34;&gt;Sourcehut Pages&lt;/a&gt;, a tool for hosting static sites&lt;/li&gt;&#xA;&lt;li&gt;&lt;a href=&#34;https://www.linode.com/products/object-storage/&#34;&gt;Linode Object Storage&lt;/a&gt;, S3-compatible object storage (used here for hosting images)&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;h2 id=&#34;hugo&#34;&gt;Hugo&lt;/h2&gt;&#xA;&lt;p&gt;Hugo is my go-to static site generator. It&amp;rsquo;s fast, intuitive and there are lots of great themes out there. I&amp;rsquo;ve used it for several sites now and I&amp;rsquo;m always amazed at just how quickly I can get something up and running.&lt;/p&gt;&#xA;&lt;p&gt;For this blog, I decided to go with a barebones theme – &lt;a href=&#34;https://github.com/janraasch/hugo-bearblog/&#34;&gt;Hugo Bear Blog&lt;/a&gt;. Bearbones, if you will.&lt;/p&gt;&#xA;&lt;h2 id=&#34;sourcehut-pages&#34;&gt;Sourcehut Pages&lt;/h2&gt;&#xA;&lt;p&gt;The blog is hosted using Sourcehut Pages. Those of you who have used GitHub/GitLab Pages will be familiar with the idea – create a Git repo with all the files for your static site and it will be built and served automatically. Every time you make a commit (like adding a new post), the site will be rebuilt.&lt;/p&gt;&#xA;&lt;h2 id=&#34;linode-object-storage&#34;&gt;Linode Object Storage&lt;/h2&gt;&#xA;&lt;p&gt;While Sourcehut Pages is an excellent, straightforward way to host a blog, there is one downside – the repo can&amp;rsquo;t be larger than 1GB. Although this isn&amp;rsquo;t a huge problem, it does mean that it isn&amp;rsquo;t wise to keep lots of large images inside the repo.&lt;/p&gt;&#xA;&lt;p&gt;To get around this restriction, I&amp;rsquo;ve chosen to host my full size images using Linode&amp;rsquo;s object storage&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;. This gives me a massive 250GB for $5 USD per month. That&amp;rsquo;s overkill, of course, but does let me upload as many high quality photos of my projects as I like. No doubt I&amp;rsquo;ll find other uses for all this storage as time goes on.&lt;/p&gt;&#xA;&lt;p&gt;Object storage is ideal for a couple of reasons. Firstly, I don&amp;rsquo;t need to set up an Nginx server (at which point I may as well be hosting my own blog). And secondly, it means I can use Linode&amp;rsquo;s CLI tool to upload my images.&lt;/p&gt;&#xA;&lt;p&gt;Uploading an image is a single command.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;linode-cli obj put --acl-public /path/to/image.png mybucketname&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;Once uploaded, the image can be found at a simply structured URL.&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;https://mybucketname.serverregion.linodeobjects.com/filename.png&lt;/code&gt;&lt;/p&gt;&#xA;&lt;p&gt;So that I don&amp;rsquo;t have to remember this pattern each time, I wrote a Fish shell function to upload the image and send the resulting URL to the clipboard.&lt;/p&gt;&#xA;&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#e0def4;background-color:#232136;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-fish&#34; data-lang=&#34;fish&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#3e8fb0&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;send2obj&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#ea9a97&#34;&gt;linode-cli&lt;/span&gt; obj put &lt;span style=&#34;color:#ea9a97&#34;&gt;--acl-public&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;$argv&lt;/span&gt; michaelhoward&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#3e8fb0&#34;&gt;set&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;filename&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;basename&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;$argv&lt;/span&gt;&lt;span style=&#34;color:#908caa&#34;&gt;)&lt;/span&gt;&#xA;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#3e8fb0&#34;&gt;echo&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;-n&lt;/span&gt; &lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;https://michaelhoward.ap-south-1.linodeobjects.com/&lt;/span&gt;&lt;span style=&#34;color:#ea9a97&#34;&gt;$filename&lt;/span&gt;&lt;span style=&#34;color:#f6c177&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;tee&lt;/span&gt; /dev/tty &lt;span style=&#34;color:#908caa&#34;&gt;|&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;xclip&lt;/span&gt; &lt;span style=&#34;color:#ea9a97&#34;&gt;-selection&lt;/span&gt; clipboard&#xA;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That way I can add unnecessary images to posts in seconds!&lt;/p&gt;&#xA;&lt;p&gt;&lt;a href=&#34;https://assets.michaelhoward.kiwi/how-i-blog-screenshot.png&#34;&gt;&lt;img src=&#34;https://michaelhoward.kiwi/images/how-i-blog-screenshot-scaled.png&#34; alt=&#34;Screenshot of the blog post in Emacs.&#34; title=&#34;A scaled screenshot of this post in Emacs&#34;&gt;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;&#xA;&lt;p&gt;All in all, I&amp;rsquo;m really satisfied with the workflow for this blog. My goal was to keep things simple and completely manageable from within Emacs or a terminal. Something I think I&amp;rsquo;ve achieved tidily.&lt;/p&gt;&#xA;&lt;p&gt;Now time to write some more posts!&lt;/p&gt;&#xA;&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;&#xA;&lt;hr&gt;&#xA;&lt;ol&gt;&#xA;&lt;li id=&#34;fn:1&#34;&gt;&#xA;&lt;p&gt;In fact, if I wanted to, I could host my entire blog this way. Linode has a great &lt;a href=&#34;https://www.linode.com/docs/guides/host-static-site-object-storage/&#34;&gt;guide&lt;/a&gt; on this. However, I like managing my site through a Git repo and enjoy the convenience of automatic builds.&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#xA;&lt;/li&gt;&#xA;&lt;/ol&gt;&#xA;&lt;/div&gt;&#xA;</description>
    </item>
    <item>
      <title>First Post</title>
      <link>https://michaelhoward.kiwi/first-post/</link>
      <pubDate>Sun, 12 Sep 2021 00:00:00 +0000</pubDate>
      <guid>https://michaelhoward.kiwi/first-post/</guid>
      <description>&lt;p&gt;This blog&amp;rsquo;s first post (of many). I hope you stick around 😉&lt;/p&gt;&#xA;</description>
    </item>
  </channel>
</rss>
