<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>tartley.com (Posts about geek)</title><link>https://www.tartley.com/</link><description>Of interest to nerds.</description><atom:link href="https://www.tartley.com/tags/geek.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 &lt;a href="mailto:tartley @ tartley dot com"&gt;Jonathan Hartley&lt;/a&gt; </copyright><lastBuildDate>Wed, 04 Feb 2026 04:18:28 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Iron Lung</title><link>https://www.tartley.com/posts/iron-lung/</link><dc:creator>Jonathan Hartley</dc:creator><description>&lt;p&gt;&lt;em&gt;Game by David Szymanski, published 2022&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Movie written, directed, and starring Mark "&lt;a href="https://www.youtube.com/channel/UC7_YxT-KID8kRbqZo7MyscQ"&gt;Markiplier&lt;/a&gt;" Fischbach, released 2026&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="float: left"&gt;
&lt;img alt="Iron Lung" src="https://www.tartley.com/files/2026/iron-lung.webp"&gt;
&lt;/span&gt;
&lt;span style="clear: both"&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;New work meeting background just dropped!&lt;/p&gt;
&lt;p&gt;Invited to view the movie at Pop's with Phil and Sarah, I crammed the game it
was based on from start to finish the night before, with Zander watching
disdainfully over my shoulder every step of the way. I gave
&lt;a href="https://scp-wiki.wikidot.com/scp-354"&gt;SCP-354&lt;/a&gt;, which inspired the game, a wide
berth.&lt;/p&gt;
&lt;p&gt;Both are tense and engaging, but clearly only for a particular kind of viewer.
The game itself has a very narrow focus, as befits a single-person indie
production. This means you'll only really enjoy it if you're dorky enough to
fixate on the mechanic of cross referencing your submarine's map co-ordinates
sufficiently hard to orient your way around a whole system of underwater
trenches. Every step of the way you negotiate your way past unseen but deadly
canyon walls by dead reckoning alone, a tense enough exercise even without the
proximity alarms. On top of that, strange noises and... other interuptions...
when they drop, are technically relatively tame and limited, but in context,
entwined in the co-ordinate grind, they are experienced as shocking and panic
inducing.&lt;/p&gt;
&lt;p&gt;Similarly, I enjoyed the movie, but I see reviews are all over the map. Clearly
some don't have patience for its limited scope - 99% of the runtime takes place
with one character in a single room. But I found it taut and thrilling. Some
patches of unclear dialog softened the high-concepts thrown around, but for me
it was always an experience of style and vibes anyway.&lt;/p&gt;</description><category>completed</category><category>geek</category><category>media</category><category>movie</category><category>pc</category><category>videogame</category><guid>https://www.tartley.com/posts/iron-lung/</guid><pubDate>Wed, 04 Feb 2026 01:55:23 GMT</pubDate></item><item><title>Tactical Breach Wizards</title><link>https://www.tartley.com/posts/tactical-breach-wizards/</link><dc:creator>Jonathan Hartley</dc:creator><description>&lt;p&gt;&lt;img alt="Tactical Breach Wizards screenshot." src="https://www.tartley.com/files/2025/tactical-breach-wizards.lossy.webp"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;In this house, we use the metric system. Released in 2024 by Suspicious Developments.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I was toying with the idea of advancing the kiddo's videogaming curriculum into
a turn-based tactics phase, maybe starting with the genius of 1994's &lt;a href="https://en.wikipedia.org/wiki/UFO%3A_Enemy_Unknown"&gt;UFO:Enemy
Unknown&lt;/a&gt;, on which I spent
endlessly fascinated evenings of my youth, or maybe one of the better of its
numerous sequels and offshoots. Somehow I was distracted from that plan by
multiple people enthusing about last year's &lt;a href="https://en.wikipedia.org/wiki/Tactical_Breach_Wizards"&gt;Tactical Breach
Wizards&lt;/a&gt;, and I'm so glad
that I was.&lt;/p&gt;
&lt;p&gt;It is &lt;em&gt;such&lt;/em&gt; a lovely, synergistic blend of gameplay mechanics, setting,
characters, story, plot-twists and whip-smart dialog, making substantial
improvements on the traditional bombastic and yet intensely thoughtful
turn-based formula.&lt;/p&gt;
&lt;p&gt;First, it defuses the self-righteous seriousness of the genre's customary tone
by replacing the gurning muscle-bound military types with a bunch of special-ops
&lt;em&gt;wizards&lt;/em&gt;. Still formidably competent, but now replete with pointy hats,
hazardous runes, and bejeweled wands protruding from their assault rifle
barrels. Further, while presenting a thrilling facade of enemies dispatched in a
dizzying flurry of rapid-fire magic, the game explicitly disavows wanton
killing. While one of our characters does sneer at the stance, your team is
revealed early-on to use only nonlethal take-downs. This is soon followed up by
a cut-scene which shows your team leaving a building after a mission, revealing
the enemies you earlier dispatched out of eighth floor windows floating gently
earthwards, each safely cocooned in a magical bubble.&lt;/p&gt;
&lt;p&gt;Second, a fundamental mechanic bestows one of your characters with the gift
of magical foresight, allowing you to see the outcome of planned actions before
you actually commit to them. It's a slick narrative integration of a mechanic
that serves multiple purposes. Preventing the anguish of losing a character due
to dumb bad luck means the player is freed up to experiment more, trying
audacious plans rather than playing it safe. Then, when it all goes wrong, you
can rewind just a smidgeon, and try out nearby alternatives, until you have it
all &lt;em&gt;just&lt;/em&gt; right, bouncing generative combos back and forth between characters,
unleashing staggering waves of action, discovering gleefully that a level you
initially thought to be an impossible slog is actually completable in a single
nimble turn. When combined with the inventive diversity of each character's
specific talents, it simultaneously presents real challenges, while allowing the
construction of surprising solutions that leave one feeling feeling incredibly
clever and creative.&lt;/p&gt;
&lt;p&gt;It's not often worthwhile dwelling on the characters in a videogame, but here
they are the stars of the show. Distinctive, flawed and intensely likeable each
in their own way, with personalities and back-stories that resonate so
pleasingly with their in-game abilities. The writing is just top notch, with
phenomenal dialog, giving the group as a whole a fresh, wholesome and real-talk
vibe.&lt;/p&gt;
&lt;p&gt;This is an all-time classic in my book, and has been fabulous to experience
alongside the 13 year-old kiddo, as we've each run parallel games through to
completion, ogling over each other's shoulders to get sneak previews of
encounters we haven't seen yet.&lt;/p&gt;</description><category>completed</category><category>geek</category><category>media</category><category>pc</category><category>videogame</category><guid>https://www.tartley.com/posts/tactical-breach-wizards/</guid><pubDate>Mon, 26 May 2025 21:43:45 GMT</pubDate></item><item><title>Integer Division With Recurring Decimals</title><link>https://www.tartley.com/posts/integer-division-with-recurring-decimals/</link><dc:creator>Jonathan Hartley</dc:creator><description>&lt;p&gt;I've been doing some programming tests and puzzles while job hunting lately. One
quick challenge was quite nice, reminding me a bit of &lt;a href="https://projecteuler.net/"&gt;Project
Euler&lt;/a&gt; questions, and I nerd sniped myself into doing
a 2nd pass at it here.&lt;/p&gt;
&lt;h3&gt;Question&lt;/h3&gt;
&lt;p&gt;Produce a Python function which takes two integers, &lt;code&gt;numerator&lt;/code&gt; and
&lt;code&gt;denominator&lt;/code&gt;, and returns the result of their division as a decimal fraction in
a string. E.g:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"0.25"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If the decimal places contain an infinite recurring pattern of digits, then
enclose the recurring digits in parentheses. E.g:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"0.(3)"&lt;/span&gt;
&lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"0.(142857)"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Wrong approaches&lt;/h3&gt;
&lt;p&gt;Evaluating the division using normal floats is going to trip you up in several
ways with the limited precision.&lt;/p&gt;
&lt;p&gt;For one, a large enough denominator might have a recurring sequence which is
longer than the number of decimal places you have available (more on this
later), which makes it impossible to detect recurring sequences by examining the
division's decimal digits.&lt;/p&gt;
&lt;p&gt;Worse, the inherent imprecision of floating point, e.g. if a simple division
like 10/3 comes back as 3.3333333333333335, then examining the trailing digits
of this looking for recurring digits is going to be problematic.&lt;/p&gt;
&lt;p&gt;Using the &lt;code&gt;decimal&lt;/code&gt; module does markedly improve precision and control. But
infinitely repeating sequences are still going to return results like
&lt;code&gt;Decimal(20) / Decimal(3) -&amp;gt; Decimal('6.666666666666666666666666667')&lt;/code&gt;, which is
going to trip us up.&lt;/p&gt;
&lt;p&gt;We can sidestep all these complexities if we see that the question is asking us
to perform this division ourselves, longhand. We are going back to elementary
school! Wheee!&lt;/p&gt;
&lt;h3&gt;Better&lt;/h3&gt;
&lt;p&gt;Let's just do basic division first, ignoring infinite or recurring digits:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numerator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;denominator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Accumulate parts of our result here&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;int_part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numerator&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;denominator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;numerator&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;denominator&lt;/span&gt;
        &lt;span class="n"&gt;numerator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int_part&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# If there is no remainder, we are done&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="c1"&gt;# Add a decimal point after our first integer part&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The only confusing parts of this are that &lt;code&gt;int_part&lt;/code&gt; might contain several
digits on the first iteration, but is only ever one decimal digit thereafter.
Plus we have to be careful to get the ordering right for our checks to exit
the loop, versus appending the decimal point to the output, to avoid weird
looking outputs like &lt;code&gt;divide(6, 2) -&amp;gt; "3."&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Trying this out:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="s1"&gt;'0.25'&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It works! But we haven't yet handled infinite decimals, they result in an
infinite loop:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Hangs!&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Recurring digits&lt;/h3&gt;
&lt;p&gt;Because we're dividing integers, we cannot get infinitely varying decimal
places. If we have an infinite number of decimal places, it must be because
of a cycle of one or more recurring digits. To detect such a cycle we have to
notice a couple of things.&lt;/p&gt;
&lt;p&gt;First, simply seeing a digit in the output which we have seen before is not
enough. Looking at the three assignments at the start of the above while-loop,
which capture our state:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;int_part&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;numerator&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;denominator&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;numerator&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;denominator&lt;/span&gt;
&lt;span class="n"&gt;numerator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here, &lt;code&gt;int_part&lt;/code&gt; gets the value of each successive decimal digit. However
if it takes on the same value as in a previous iteration, the accompanying
remainder might be different, and it is the remainder which is used to
generate the numerator for the next iteration, and hence generate the
sequence of digits after this.&lt;/p&gt;
&lt;p&gt;So, as we already knew from common sense, two iterations with the same
&lt;code&gt;int_part&lt;/code&gt; may go on to produce different sequences of subsequent digits.
However, The value of &lt;code&gt;remainder&lt;/code&gt; is the only thing which determines the inputs
to our next iteration:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;int_part&lt;/code&gt; depends on &lt;code&gt;numerator&lt;/code&gt; and on &lt;code&gt;denominator&lt;/code&gt; (which is constant)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;numerator&lt;/code&gt; depends on &lt;code&gt;remainder&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Hence, two iterations might produce different digits, but produce the same
remainder, and from that point onwards, they will be in lockstep. If we find two
such iterations, then we have detected an infinite recurring cycle of digits.&lt;/p&gt;
&lt;p&gt;So, before the loop begins, initialize a dict:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Remainders seen to date, mapped to their position in the result&lt;/span&gt;
&lt;span class="n"&gt;remainders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then inside the loop, after everything else, use our new dict to detect if we
have seen the current remainder before:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# If we have seen this remainder before, we are now in exactly the&lt;/span&gt;
&lt;span class="c1"&gt;# same state as then, so we have found a recurring digit sequence.&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;remainders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# We have found a cycle of decimal digits! Insert parens into our results,&lt;/span&gt;
    &lt;span class="c1"&gt;# from the last seen position of this remainder, up to the current digit.&lt;/span&gt;
    &lt;span class="n"&gt;last_pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;remainders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;last_pos&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"("&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;last_pos&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;")"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;
&lt;span class="c1"&gt;# Remember the position at which we saw this remainder&lt;/span&gt;
&lt;span class="n"&gt;remainders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;remainder&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Trying this out:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;0.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;0.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;142857&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;OMG it works!&lt;/p&gt;
&lt;h3&gt;Defensive coding&lt;/h3&gt;
&lt;p&gt;We're putatively done, but the grumpy old dev in me is uncomfortable leaving
that &lt;code&gt;while True&lt;/code&gt; in there. By deduction, we always must eventually hit the &lt;code&gt;if
&amp;lt;condition&amp;gt;: break&lt;/code&gt; to escape from it, so ostensibly it's fine. But if I have a
bug in the code or my reasoning, then it might lead to an infinite loop, in some
scenario I'm not thinking of. Can we limit the number of iterations in some
other, foolproof way? Turns out we can.&lt;/p&gt;
&lt;p&gt;We've seen already that a repeated value of &lt;code&gt;remainder&lt;/code&gt; means we can break
from the loop. Also, notice that &lt;code&gt;remainder&lt;/code&gt;, given by:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;numerator&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;denominator&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;can only take values from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;denominator - 1&lt;/code&gt;. So it can have &lt;code&gt;denominator&lt;/code&gt;
possible values, and this is the maximum number of iterations we will ever need.&lt;/p&gt;
&lt;p&gt;Hence, we can safely replace the &lt;code&gt;while(True)&lt;/code&gt; with:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;denominator&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Splendid! Much less anxiety-inducing&lt;/p&gt;
&lt;p&gt;The source is on &lt;a href="https://github.com/tartley/division"&gt;github&lt;/a&gt;.&lt;/p&gt;</description><category>geek</category><category>math</category><category>python</category><category>software</category><guid>https://www.tartley.com/posts/integer-division-with-recurring-decimals/</guid><pubDate>Mon, 03 Mar 2025 18:42:50 GMT</pubDate></item><item><title>SVG Trees using recursive Python functions</title><link>https://www.tartley.com/posts/svg-trees-using-recursive-python-functions/</link><dc:creator>Jonathan Hartley</dc:creator><description>&lt;p&gt;Inspired by a woodland hike under the first blue skies we've seen this year, I
got home and showed the kiddo how to draw an SVG tree with recursive functions
in Python.&lt;/p&gt;
&lt;p&gt;At first the generated shape looked kinda lumpy and uninspiring, but it did
demonstrate the principle. We were thinking of calling it a day, but I did a
little bit of tweaking on parameters to control how each branch differs in
length and direction from its parent. Suddenly, the generated shape really came
alive, and started to look a lot more like the trees we'd seen on our hike that
afternoon.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Silhouette of tree against a blue sky, drawn by a Python program" src="https://www.tartley.com/files/2025/tree-art.lossy.webp"&gt;&lt;/p&gt;
&lt;p&gt;This image uses a recursion depth of 18, yielding 2^18 twigs, i.e. 250,000,
which generates a 100MB SVG file. This takes about ten seconds to generate, and
another ten to display in an SVG viewer. Alternatively, I can convert the SVG to
a lossy webp, as displayed here, which is only 280kB and displays instantly.&lt;/p&gt;
&lt;p&gt;Pushing the generation to greater recursion depth makes my SVG viewer and
conversion tools start to stutter and barf. Presumably I could be smarter about
the SVG I generate -- maybe generating the outline of the tree as points on
fewer, more complex polygons, instead of a polygon for each branch segment? No
matter, the artifact is the thing here, and it's done now.&lt;/p&gt;
&lt;p&gt;Source is at &lt;a href="https://github.com/tartley/tree-art"&gt;https://github.com/tartley/tree-art&lt;/a&gt;.&lt;/p&gt;</description><category>creative</category><category>geek</category><category>genart</category><category>graphics</category><category>python</category><category>software</category><category>svg</category><guid>https://www.tartley.com/posts/svg-trees-using-recursive-python-functions/</guid><pubDate>Fri, 28 Feb 2025 17:10:27 GMT</pubDate></item><item><title>The Black Parade: Level 04: Death's Dominion</title><link>https://www.tartley.com/posts/the-black-parade-level-04-deaths-dominion/</link><dc:creator>Jonathan Hartley</dc:creator><description>&lt;p&gt;So. Looking Glass's seminal 1998 PC game &lt;em&gt;Thief: The Dark Project&lt;/em&gt; spawned an active and long-lived
modding community, who created hundreds of fan-made extra levels, many of which are extremely artful
and creative.&lt;/p&gt;
&lt;p&gt;One group of particularly obsessed loons spent seven years crafting an extraordinary set of such
levels, forming an entirely new single-player campaign for the game, named &lt;em&gt;The Black Parade&lt;/em&gt;. This
was released last year and I only just became aware of it. I'm four missions in, absolutely loving
it, and completely lost in the catacombs beneath the pseudo-medieval city.&lt;/p&gt;
&lt;p&gt;Hence, my lovingly hand-drawn map of mission 4, Death's Dominion:&lt;/p&gt;
&lt;p&gt;&lt;span style="background:#bb2200; color:white; border-radius: 1em; padding-left: 0.5em; padding-right: 0.5em; padding-top: 2px;"&gt;&lt;b&gt;spoilers&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="float: left"&gt;
&lt;img alt="Map of mission 4, Death's Dominion" src="https://www.tartley.com/files/2025/Thief-Black.Parade-04DeathsDominion.800x.q95.webp"&gt;
&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;&lt;br style="clear: left"&gt;
&lt;br&gt;&lt;/p&gt;</description><category>geek</category><category>map</category><category>media</category><category>pc</category><category>spoilers</category><category>thief</category><category>videogame</category><guid>https://www.tartley.com/posts/the-black-parade-level-04-deaths-dominion/</guid><pubDate>Fri, 07 Feb 2025 02:33:01 GMT</pubDate></item><item><title>That Which Gave Chase</title><link>https://www.tartley.com/posts/that-which-gave-chase/</link><dc:creator>Jonathan Hartley</dc:creator><description>&lt;p&gt;&lt;img alt="" src="https://www.tartley.com/files/2024/that-which-gave-chase.jpg"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Released in 2023, played on Linux in 2024.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;span style="background:#bb2200; color:white; border-radius: 1em; padding-left: 0.5em; padding-right: 0.5em; padding-top: 2px;"&gt;&lt;b&gt;spoilers&lt;/b&gt;&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Mush your dog sled across cruel Arctic wastelands, driven onwards by a brisk and intense companion, who hired you to take him back to some remote spot, where it becomes apparent he had some sort of revelation, or maybe a breakdown.&lt;/p&gt;
&lt;p&gt;The low-res, dithered presentation conveys the harsh, blinding conditions, as you struggle to make out details through the relentless wind and ice. The days and nights of the journey blur into one another, leaving you only fragmentary, disjointed memories:
sledding across the ice;
arriving at crude wooden huts for the night;
mounting the sled before dawn;
collapsing into rough bunks; 
righting the sled while your companion curses you for a fool;
silent moments alone.&lt;/p&gt;
&lt;p&gt;Smash cuts amongst snowy wastes echo &lt;a href="https://readcomic.me/comic/nemo-heart-of-ice/issue-full/31"&gt;the discontinuities in Alan Moore's "&lt;em&gt;Nemo: Heart of Ice&lt;/em&gt;"&lt;/a&gt;, albeit this is a far more understated tale. The sense is of a protracted, exhausting time spent covering the distance, through punishing conditions, and it's surprisingly evocative.&lt;/p&gt;
&lt;p&gt;The narrative leans into the disorientation, making nothing clear. Your companion becomes increasingly cryptic. He urges you onward, never pausing more than absolutely necessary. The deer behave increasingly strangely. Your companion regales you with sickening tales of the investigative mistreatment he subjected them to on his previous visit. By the time the strange mushrooms come into play it is very obvious that you are in a place to which you should never have come, very far from anywhere or anyone, with mounting dread, alone with with a madman. What happened the last time he took this route? What did he leave behind here? What awaits at your journey's end?&lt;/p&gt;
&lt;p&gt;It's hard to know whether the difficulty of interpretation, or the non-literal aspects of your journey, are intended as the result of your character's mushroom-induced fever, or the pretensions of intrusively figurative allusions. Most likely, it seems to be both. The deliberate ambiguity runs deep.&lt;/p&gt;
&lt;p&gt;Doesn't outstay its welcome, all done in an hour. But the memories remain.&lt;/p&gt;</description><category>completed</category><category>drugs</category><category>geek</category><category>media</category><category>pc</category><category>videogame</category><guid>https://www.tartley.com/posts/that-which-gave-chase/</guid><pubDate>Mon, 04 Nov 2024 21:56:38 GMT</pubDate></item><item><title>Overhauled Manual for Epomaker Galaxy80 Tri-Mode Keyboard</title><link>https://www.tartley.com/posts/overhauled-manual-for-epomaker-galaxy80-keyboard/</link><dc:creator>Jonathan Hartley</dc:creator><description>&lt;p&gt;Loving the new keyboard, an Epomaker Galaxy80 with Feker Marble White switches.&lt;/p&gt;
&lt;p&gt;I compiled &lt;a href="https://www.tartley.com/files/2025/keyboard-ecomaker-galaxy80-trimode-mine.html"&gt;an improved manual for it here&lt;/a&gt;,
which merges the English section of the official manual with other sources,
rearranging the info in a way that makes sense to me.&lt;/p&gt;
&lt;p&gt;My requirements are pretty much the same as last time I bough a keyboard:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Tenkeyless layout, or TKL as it's known, i.e. without a numpad. The kiddo and
  I fit two side-by-side gaming stations at this desk, and the extra
  mouse-swiping space is precious, as is the ergonomics of putting the mouse
  just a few inches closer.&lt;/li&gt;
&lt;li&gt;Standard ANSI layout, to match the other keyboards I commonly use.&lt;/li&gt;
&lt;li&gt;Mechanical, although I'm not experienced enough to know a good one from a bad
  one.&lt;/li&gt;
&lt;li&gt;At least two connections which are easy to switch between, for work and
  personal computers. This one has five, three of which are Bluetooth.&lt;/li&gt;
&lt;li&gt;At least one of those connections should be reasonably low latency, i.e.
  &amp;lt;5ms, which means wired or a dedicated 2.4GHz dongle, not Bluetooth. The
  Galaxy80 has both. I'm a long way away from being a pro gamer, but even down
  here in the GamerDad leagues, I seem to be more aware of annoying latency than
  most people are.&lt;/li&gt;
&lt;li&gt;Backlit. I don't especially care about per-key RGB, but that seems to be
  extremely common. Shine-through keycaps would be nice, but these seem to be
  increasingly rare outside of garish gamer-boi cyber-monstrosities, so not a
  big deal.&lt;/li&gt;
&lt;li&gt;Hot-swappable switches. This is the requirement I compromised on last time I
  bought a keyboard, settling for the Logitech G915, which was great, but got
  old after switches started failing. I'm tired of desoldering them and am
  noping out to buy something else, a mere 16 months later.&lt;/li&gt;
&lt;li&gt;Not egregiously incompatible with Linux. It would be hard to find a keyboard
  which doesn't actually work with Linux, but maybe some manufacturer has buried
  some vital configuration detail in badly written Windows-only configuration
  software that doesn't play nice with Wine, etc.&lt;/li&gt;
&lt;li&gt;Without expensive features I don't need, like configurable activation height,
  or OLED screens.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The switches are described as sounding "like marbles clacking", which worried me
that they might be too loud and piercing. But now it's arrived, they are
actually quieter than any other mechanical switch I've had. The sound is deeper
than I expected. Recognizably like marbles, but merged with the sound of
pebbles, and a hint of a wooden xylophone.&lt;/p&gt;
&lt;p&gt;I really like it! Although since pulling the trigger I've seen that Reddit
doesn't like Epomaker. I'm just not going to read those posts for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: The incantations needed to get function keys working the way you
want them to on Ubuntu :eyeroll: etc. Despite the mention of 'apple' in here,
this still assumes you have the keyboard switched into 'Win' mode. (&lt;a href="https://www.reddit.com/r/Epomaker/comments/1bte204/galaxy80_cant_use_functionkeys_in_linux/"&gt;via
Reddit&lt;/a&gt;):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"options hid_apple fnmode=2"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;tee&lt;span class="w"&gt; &lt;/span&gt;/etc/modprobe.d/hid_apple.conf
sudo&lt;span class="w"&gt; &lt;/span&gt;update-initramfs&lt;span class="w"&gt; &lt;/span&gt;-u
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and reboot.&lt;/p&gt;
&lt;hr&gt;</description><category>geek</category><category>hardware</category><category>keyboard</category><category>manual</category><guid>https://www.tartley.com/posts/overhauled-manual-for-epomaker-galaxy80-keyboard/</guid><pubDate>Mon, 04 Nov 2024 00:22:45 GMT</pubDate></item><item><title>TIL: Constructing a PDF from .jpg image files</title><link>https://www.tartley.com/posts/til-constructing-a-pdf-from-jpg-image-files/</link><dc:creator>Jonathan Hartley</dc:creator><description>&lt;p&gt;I have some folders of .jpg images that make up a comic. I want to convert them into a PDF to read
on my tab and other devices, and import into my Calibre bookshelf.&lt;/p&gt;
&lt;h3&gt;1. Install some prerequisites&lt;/h3&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;sudo&lt;span class="w"&gt; &lt;/span&gt;apt&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;imagemagick&lt;span class="w"&gt; &lt;/span&gt;pdftk
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;2. Do the conversion&lt;/h3&gt;
&lt;p&gt;The versatile ImageMagick has a 'convert' command that seems to handle it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;convert&lt;span class="w"&gt; &lt;/span&gt;*.jpg&lt;span class="w"&gt; &lt;/span&gt;output.pdf
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But this has some issues:&lt;/p&gt;
&lt;h4&gt;2.1. Failure due to security policy&lt;/h4&gt;
&lt;p&gt;'convert' currently refuses to generate PDFs: 'attempt to perform an operation not allowed by the security policy'. Apply the fix described on &lt;a href="https://stackoverflow.com/questions/52998331/imagemagick-security-policy-pdf-blocking-conversion"&gt;StackOverflow&lt;/a&gt;. :eyeroll:&lt;/p&gt;
&lt;h4&gt;2.2. Failure due to cache space&lt;/h4&gt;
&lt;p&gt;You might not need this fix if you generate smaller documents, or generate chapter-by-chapter as
described below, but here it is in case.&lt;/p&gt;
&lt;p&gt;Don't close that editor! In the same policy.xml you were just editing are resource size declarations for memory and disk. If 'convert' barfs with an error about running out of cache space, then bump
up the disk resource size. I set mine to 8GB. &lt;a href="https://unix.stackexchange.com/questions/329530/increasing-imagemagick-memory-disk-limits"&gt;StackOverflow again&lt;/a&gt; for details. :eyeroll: again.&lt;/p&gt;
&lt;h3&gt;3. Include a table of contents&lt;/h3&gt;
&lt;p&gt;I want to add bookmarks to the generated PDF marking each chapter.&lt;/p&gt;
&lt;p&gt;Put the .jpgs into subdirectories by chapter, eg:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;src/
|--chapter01/
|  |--0001.jpg
|  |--0002.jpg
|  |  ...
|--chapter02/
|  |--0001.jpg
|  |--0002.jpg
|  |  ...
|
...
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Pad the chapter numbers with preceding zeros so that they sort into the correct order. I added
an artificial 'chapter00' containing the front cover, separate from individual chapters.&lt;/p&gt;
&lt;p&gt;Now we need to generate individual PDFs for each chapter. We can then use 'pdftk' to
count the number of pages in each chapter, and use those counts to place bookmarks on
the correct pages when pfdtk combines the chapters into one final output PDF.&lt;/p&gt;
&lt;p&gt;I ended up regenerating each chapter a bunch while I tweaked the content, such as deleting adverts
from the images. So I put these commands into a Makefile:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nf"&gt;help&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;## Show this help.&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;@grep&lt;span class="w"&gt; &lt;/span&gt;-E&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'^[^_][a-zA-Z_\/\.%-]+:.*?## .*$$'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;MAKEFILE_LIST&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;awk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-12s\033[0m %s\n", $$1, $$2}'&lt;/span&gt;
&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;help&lt;/span&gt;

&lt;span class="nv"&gt;chapter_dirs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;wildcard&lt;span class="w"&gt; &lt;/span&gt;src/*&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;chapters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;chapter_dirs:src/%&lt;span class="o"&gt;=&lt;/span&gt;%&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;chapter_pdfs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;chapters:%&lt;span class="o"&gt;=&lt;/span&gt;%.pdf&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;bookmarks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;bookmarks.txt
&lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;output.pdf

&lt;span class="nf"&gt;clean&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;## Delete all generated PDFs&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;chapter_pdfs&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;output&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;

&lt;span class="nf"&gt;chapter%.pdf&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;/&lt;span class="n"&gt;chapter&lt;/span&gt;%/*.&lt;span class="n"&gt;jpg&lt;/span&gt; &lt;span class="c"&gt;## Each individual chapter, use 2 digits&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;convert&lt;span class="w"&gt; &lt;/span&gt;src/chapter&lt;span class="nv"&gt;$*&lt;/span&gt;/*.jpg&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;

&lt;span class="nf"&gt;$(bookmarks)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;chapter_pdfs&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;./make-bookmarks&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="k"&gt;$(&lt;/span&gt;bookmarks&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;$(output)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;chapter_pdfs&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;bookmarks&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;pdftk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;chapter_pdfs&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;output&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;pdftk&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;update_info&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;bookmarks&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;output&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;output&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;output&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt; &lt;span class="c"&gt;## Build final output PDF&lt;/span&gt;
&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Where 'make-bookmarks' is a bash script that generates the intermediate 'bookmarks.txt' file:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="ch"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-e&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# exit on error&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-u&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;# treat unset vars as errors&lt;/span&gt;
&lt;span class="c1"&gt;# set -x # debugging output&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;pipefail

&lt;span class="c1"&gt;# Generate a bookmarks file for all the matching PDF files&lt;/span&gt;

&lt;span class="nv"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"BookmarkBegin&lt;/span&gt;
&lt;span class="s2"&gt;BookmarkTitle: %s&lt;/span&gt;
&lt;span class="s2"&gt;BookmarkLevel: 1&lt;/span&gt;
&lt;span class="s2"&gt;BookmarkPageNumber: %d&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;declare&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-a&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;chapter*.pdf&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;file&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;files&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;file&lt;/span&gt;&lt;span class="p"&gt;%.*&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$fmt&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$title&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;num_pages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;pdftk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;dump_data&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;NumberOfPages&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;awk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{print $2}'&lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nv"&gt;page&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;&lt;span class="nv"&gt;page&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;num_pages&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now &lt;code&gt;make all&lt;/code&gt; will produce the final output.pdf. You might want to open up the generated
bookmarks.txt and edit the placeholder "chapter01" names. Then run &lt;code&gt;make all&lt;/code&gt; again to
regenerate the final output PDF with your fixed chapter names.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Rorschach II meets Adrian" src="https://www.tartley.com/files/2024/doomsday-clock-r2-meets-adrian.webp"&gt;&lt;/p&gt;</description><category>bash</category><category>comic</category><category>geek</category><category>linux</category><category>terminal</category><category>til</category><guid>https://www.tartley.com/posts/til-constructing-a-pdf-from-jpg-image-files/</guid><pubDate>Mon, 21 Oct 2024 14:55:18 GMT</pubDate></item><item><title>TIL: Shell environment variable tricks</title><link>https://www.tartley.com/posts/til-shell-environment-variable-tricks/</link><dc:creator>Jonathan Hartley</dc:creator><description>&lt;p&gt;&lt;code&gt;envsubst&lt;/code&gt; is an executable you likely already have on your PATH (part of the gettext package, often
installed with dev packages), which is a convenient way to replace &lt;code&gt;$VAR&lt;/code&gt; or &lt;code&gt;${VAR}&lt;/code&gt; style
environment variables with their values. This allows rendering templates without heavyweight
tools like Ansible, Jinja, or embedding with heredocs. Usage is:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;envsubst &amp;lt;template &amp;gt;rendered
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For example:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;$&lt;span class="w"&gt; &lt;/span&gt;envsubst&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s1"&gt;'Hello $USER'&lt;/span&gt;
Hello&lt;span class="w"&gt; &lt;/span&gt;jonathan
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(Note the use of single quotes so that &lt;code&gt;$USER&lt;/code&gt; isn't expanded by our shell, as
it wouldn't be in the file which &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt; is emulating for us.)&lt;/p&gt;
&lt;p&gt;If you'd like to use KEY=value declarations from a dotenv-style &lt;code&gt;.env&lt;/code&gt; file, you can auto-export
them by setting the &lt;code&gt;-a&lt;/code&gt; Bash option:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;set -a; source .env; set +a
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Something I've managed to avoid ever realizing for 30 years, but now that I've seen it I can't
imagine a week going by without using it. The kind of thing that should be part of everyone's "Week
1 in a terminal" training that formal education courses never include.&lt;/p&gt;</description><category>bash</category><category>geek</category><category>linux</category><category>software</category><category>terminal</category><category>til</category><guid>https://www.tartley.com/posts/til-shell-environment-variable-tricks/</guid><pubDate>Thu, 03 Oct 2024 20:37:27 GMT</pubDate></item><item><title>Fully Operational</title><link>https://www.tartley.com/posts/fully-operational/</link><dc:creator>Jonathan Hartley</dc:creator><description>&lt;p&gt;Now witness the power of this fully armed and operational battle station.&lt;/p&gt;
&lt;p&gt;&lt;img alt="My desk featuring too many computers" src="https://www.tartley.com/files/2023/desk-with-computers.webp"&gt;&lt;/p&gt;
&lt;p&gt;New job means new laptop means it's time to clean and re-org the desk.&lt;/p&gt;
&lt;dl&gt;
&lt;dt&gt;Leftmost blue skies&lt;/dt&gt;
&lt;dd&gt;Linux laptop (a free hand-me-down from a job ten years ago). Acting as the
house Plex / streaming media server, usually tucked away more discreetly than
this.&lt;/dd&gt;
&lt;dt&gt;Left top green forest&lt;/dt&gt;
&lt;dd&gt;Heavy duty work / gaming Linux laptop ("hardware bonus" from my last
employer). Has been my primary work machine, but sounds like it's getting
replaced by...&lt;/dd&gt;
&lt;dt&gt;Left bottom spaceship drawing&lt;/dt&gt;
&lt;dd&gt;Macbook Pro (Brand new! Just unwrapped yesterday. Thank you new employer
&lt;a href="https://lambdalabs.com"&gt;Lambda&lt;/a&gt;!) Looks like this means I'm returning to
developing on a Mac and VMs, after a full decade on Ubuntu &amp;amp; derivatives.
I'm told Docker for Desktop now behaves better than it used to.&lt;/dd&gt;
&lt;dt&gt;Left bottom, under the Mac&lt;/dt&gt;
&lt;dd&gt;You can sort of see the 10" whiteboard I use to combat ADHD by writing a
sentence about what I'm &lt;em&gt;supposed&lt;/em&gt; to be working on, then I can spot it every
few minutes and drag my mind back to the task in hand. (a technique described
in the fabulous
&lt;a href="https://gamkedo.gumroad.com/l/self-command/"&gt;Self Command by Chris DeLeon&lt;/a&gt;.&lt;/dd&gt;
&lt;dt&gt;Center&lt;/dt&gt;
&lt;dd&gt;Main monitor and wireless tenkeyless mechanical keyboard &amp;amp; mouse combo, all
switchable to any of the laptops. Under the keyboard you can sort-of see the
&lt;a href="https://ultrapro.com/collections/gaming-accessories-magic-the-gathering/type_playmat"&gt;Magic the Gathering 13x24" gaming mat&lt;/a&gt;
(free from local gaming store's MtG lessons) pressed into duty as the world's
most gigantic, beautiful, and luxurious mouse mat.&lt;/dd&gt;
&lt;dt&gt;Right monitor, keyboard and mouse&lt;/dt&gt;
&lt;dd&gt;are wired to the Windows gaming PC under the desk (not visible). The kiddo's
current &lt;a href="https://store.steampowered.com/app/361420/ASTRONEER/"&gt;Astroneer&lt;/a&gt;
session is visible. The monitor is switchable to any of the laptops.&lt;/dd&gt;
&lt;dt&gt;Right tab&lt;/dt&gt;
&lt;dd&gt;Absolute workhorse 12.6" Android tablet on which I do most of my reading,
laid in the picture here just to be gratuitous.&lt;/dd&gt;
&lt;/dl&gt;</description><category>geek</category><category>hardware</category><category>journal</category><category>keyboard</category><category>osx-dev</category><guid>https://www.tartley.com/posts/fully-operational/</guid><pubDate>Thu, 21 Sep 2023 15:51:12 GMT</pubDate></item></channel></rss>