<?xml version="1.0" encoding="utf-8"?><?xml-stylesheet type="text/xsl" href="rss.xsl"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Hold The Robot Blog</title>
        <link>https://holdtherobot.com/blog</link>
        <description>Hold The Robot Blog</description>
        <lastBuildDate>Tue, 24 Mar 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[Public Restroom Doors are a Nightmare]]></title>
            <link>https://holdtherobot.com/blog/public-restroom-doors-are-a-nightmare</link>
            <guid>https://holdtherobot.com/blog/public-restroom-doors-are-a-nightmare</guid>
            <pubDate>Tue, 24 Mar 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[Look at this.]]></description>
            <content:encoded><![CDATA[<p>Look at this.
This is the interior handle and lock of a single occupant public restroom door.
Someone spent a lot of time and effort creating what might be the worst lock imaginable.</p>
<div style="display:flex;justify-content:center"><video autoplay="" loop="" muted="" playsinline="" style="width:60%;height:auto"><source src="/video/restroom_lock_av1.webm" type="video/webm"><source src="/video/restroom_lock_h264.mp4" type="video/mp4"></video></div>
<br>
<br>
<p>This is the interior of an airplane restroom.
Whoever designed this door understood the assignment.</p>
<div style="display:flex;justify-content:center"><img src="https://holdtherobot.com/img/shts_airplane_lock.avif" alt="airplane door lock" style="width:60%;height:auto"></div>
<br>
<br>
<p>A restroom door needs to do three things:</p>
<ol>
<li class="">Prevent anyone from entering when there's an occupant</li>
<li class="">Make the occupant <em>feel</em> like no-one will be able to enter (otherwise it's stressful)</li>
<li class="">Prevent people from having to attempt to enter to find out if there's an occupant</li>
</ol>
<p>You'd think, considering how many of these damn doors
have been built that this would be a thoroughly solved problem,
but in my experience it's genuinely rare to find a door that
correctly and consistently does all 3.</p>
<p>It was a level 1 failure that motivated this blog post.</p>
<p>A door like this is composed of several systems. It has:</p>
<ol>
<li class="">Hinges that allow it to open and close</li>
<li class="">A latch that prevents it from opening</li>
<li class="">A knob that disengages the latch</li>
<li class="">An exterior lock that serves an authorization check (i.e. someone will size you up and decide if you deserve the code to the bathroom)</li>
<li class="">An interior lock that's operated only by the occupant</li>
</ol>
<p>Any of these systems could be omitted (except the hinges and interior lock),
and they often overlap.</p>
<div style="display:flex;justify-content:center"><img src="https://holdtherobot.com/img/asylum_lock.avif" alt="airplane door lock" style="width:60%;height:auto"></div>
<br>
<small><blockquote>
<p>Whoever put up this "look" sign knew there was a problem here,
but didn't know what it was</p>
</blockquote></small>
<br>
<p>Failures happen because human beings have to operate these systems
without necessarily knowing which ones are preset, how they work,
or what the state of each system is.</p>
<p>Sometimes the systems are bad, but more often the problem is the signals.
That handwritten "look" sign assumes people aren't looking
at the locked/unlocked indicator before trying to enter.
But the <em>are</em> looking;
they're just assuming it's an indicator for the exterior lock,
not the internal lock.
There are three locks on this door after all.
The "look" sign appeared a few weeks before the authorization lock
got taped over in what I assume is just further desperation
in trying to deal with this bad design.</p>
<p>It's not trivial to signal each of these systems correctly,
but there's really only one that matters; the interior lock.
And the state of this lock needs to be signalled clearly
<em>on both sides of the door</em>.</p>
<p>In 2026, the year of our Lord,
humanity has managed to solve half the problem
(or at least that's where the U.S. is at. This might be better elsewhere).
There is often a sign on the door exterior like this:</p>
<div style="display:flex;justify-content:center"><img src="https://holdtherobot.com/img/nossa_exterior.avif" alt="vacant lock" style="width:60%;height:auto"></div>
<br>
<p>Explicitly "there is someone in here" and you can't miss it.</p>
<p>Here's the interior of the same door:</p>
<div style="display:flex;justify-content:center"><img src="https://holdtherobot.com/img/nossa_interior.avif" alt="interior lock" style="width:60%;height:auto"></div>
<br>
<p>An employee had the sense to tape up a little sign
to at least show the direction to turn the lock,
but at what point is it locked? How will you know?
Can you at least test that it's locked?</p>
<p>I nearly walked in on a little girl using this very restroom.
The only thing that saved us both was a slight hesitation on my part
after cracking this door (with a giant VACANT sign on the front, mind you)
due to some dim memory that this lock catches early,
just like the one in the first video.
Nearly had to start my day at the coffee shop
trying to explain to some furious parent why we <em>don't</em>
need to call the police.</p>
<p>There are layers to how bad this design is.</p>
<ol>
<li class="">Any slight misalignment in the door causes the lock to catch before it's actually locked.</li>
<li class="">There's no indication at all if the lock is properly locked. It's locked at "arbitrary degrees turned", and that's knowledge you don't have.</li>
<li class="">Turning the handle unlocks the lock, so YOU CAN'T EVEN CHECK THAT IT'S LOCKED.</li>
</ol>
<p>At this point, the vacant/occupied sign is a liability,
because it tells the person on the outside
"hey there's definitely no one in here, walk right in".
If your goal was to create the most evil restroom door possible,
you could not have done better than this.</p>
<div style="display:flex;justify-content:center"><img src="https://holdtherobot.com/img/sliding_interior.avif" alt="interior lock" style="width:60%;height:auto"></div>
<br>
<small><blockquote>
<p>Cheap, low tech, and unfortunately forces the "have to try opening the door to find out if someone is in here". But it won't fail, and it's completely clear to the occupant when it's locked.</p>
</blockquote></small>
<p>I don't know what it is about public restrooms.
The doors, the stupid motion-sensing sinks and towel dispensers,
and oh god the stalls.
It's an essential part of life in public and I don't understand why we don't get it right.</p>]]></content:encoded>
            <category>design</category>
            <category>interfaces</category>
        </item>
        <item>
            <title><![CDATA[In Software, One Thing is Better Than Two Things]]></title>
            <link>https://holdtherobot.com/blog/one-thing-is-better-than-two-things</link>
            <guid>https://holdtherobot.com/blog/one-thing-is-better-than-two-things</guid>
            <pubDate>Sun, 14 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[I recently took over a software project that had, from both an engineering perspective]]></description>
            <content:encoded><![CDATA[<p>I recently took over a software project that had, from both an engineering perspective
and a usability perspective, outright failed.
The code was a teetering tower that had collapsed in on itself into a pool of leaked abstractions
and interdependent logic.</p>
<p>I've spent many hours wading through the mess,
and it's become almost a meditation on what can go wrong in software.
The issues with this specific code aren't that interesting;
what feels important here are the core values that a developer has to have
for all the little microdecisions flow out of.</p>
<p>I think it sits at the bottom of "don't repeat yourself"
or "keep it simple stupid" or all the other quippy platitudes.
Even "single source of truth" is putting it too narrowly.
Never have 2 things when you can have 1.
I think this has to be so deeply rooted
that it's more an ever present gut feeling than it is some explicitly reasoned rule.
Like if you stand on a precarious ledge
you don't need to be told to feel anxious.</p>
<p>Here's some shallow examples to try and illustrate the deeper point:</p>
<ol>
<li class="">
<p>If a SQL table needs a date, it should have <em>a</em> date.
Not a Unix timestamp <em>and</em> an ISO 8601 <em>and</em>
a colloquially-styled date string and so on.
Have one date column and make sure it's correct.
Use a functional transform if you need to display something else.</p>
</li>
<li class="">
<p>When possible, avoid cache layers.
A cache means you have data in two places
and now have the non-trivial problem of keeping it in sync.
If you can't avoid a cache layer, you at least want
to make it <em>feel</em> like one thing.
For example,
tracking changes to the underlying data and pushing them into the cache layer
is vastly better than relying on a timer to expire the cache.
If you can avoid states where the cache decouples from the data underneath, you should.</p>
</li>
<li class="">
<p>You should prefer composition over inheritance.
That's not advice; that's an observation.
If you've ever spent time working with a deep inheritance tree,
having to implement the same behaviour in multiple child classes
or seeing logic sprinkled across a 5 layer deep inheritance chain
should <em>feel wrong</em>. Composition is better because
it more tightly defines the "one thing" of a behaviour,
rather than letting that behaviour become diffused into different places
or even outright duplicated.</p>
</li>
<li class="">
<p>If data needs to be validated,
it should be validated once, at the point it enters the system.
If your code is constantly having to call <code>if is_valid(some_data) {...}</code>,
then there's no clear contract for what the application can trust.
There's "fuzziness" in the system, and that's never good.</p>
</li>
</ol>
<p>For each of these examples, it's easy to think of "hey what about scenario X"
or "but there's also consideration Y", and that's totally valid.
There <em>are</em> reasons to duplicate data or behaviors, both pragmatic and conceptual.
The point is that as you feel the fundamental tension
between differing concerns in software design,
the principle of "prefer one thing" should pull pretty hard.</p>
<p>The project failed because this principle was missing.
Multiple overlapping cache layers made data in -&gt; data out
a broken relationship.
Repeated code with small permutations meant
something was always missed when things were added or changed.
Layered, partial checks for error conditions meant
errors propagated deep into the code were only sometimes caught.
It wasn't some singular critical flaw; it was
small compounding errors that multiplied until
the whole thing fell under the event horizon.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to use Claude Code for big tasks without turning your code to shit]]></title>
            <link>https://holdtherobot.com/blog/how-to-use-claude-code-for-big-tasks-without-turning-your-code-to-shit</link>
            <guid>https://holdtherobot.com/blog/how-to-use-claude-code-for-big-tasks-without-turning-your-code-to-shit</guid>
            <pubDate>Mon, 10 Nov 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[I find myself using LLMs for coding in 4 specific ways:]]></description>
            <content:encoded><![CDATA[<p>I find myself using LLMs for coding in 4 specific ways:</p>
<ol>
<li class="">Finding information</li>
<li class="">Rubber ducking</li>
<li class="">Generating snippets of code or documentation</li>
<li class="">Having it work for hours on a big task with minimal intervention</li>
</ol>
<p>I find the first 3 to be very useful, especially now that web search is dead.
But they are similar in that the LLM in on a tight leash,
and everything still ultimately flows through my brain.
Number 4 is different though;
it gives Claude a lot of leeway to go do whatever it wants,
without my babysitting.</p>
<p>For a long time I've found the Claude-take-the-wheel style vibe coding
to be an incredible waste of my time and sanity.
For every "oh wow it worked" moment there were far to many
"oh wow I just spent an hour sifting through garbage".
Even when it accomplishes the task,
the LLM always injects entropy into the code.
Do it enough and this eventually ends in
an incoherent fever dream of code slop.
The software equivalent of
"man who is sitting on a couch but somehow also <em>is</em> the couch".
I found <a href="https://youtu.be/Uw_inKvBn6c?si=TlQDI_XNvao_iXVy" target="_blank" rel="noopener noreferrer" class="">this video</a> does a nice job demonstrating the anti-memetic nonsense you end up with.</p>
<p>Recently though, I started to worry that I had written it off too early.
Some of my friends (who's opinion I trust) seemed to be getting better results,
and I can't help but think back to the early days of Google search,
where some people seemed to "get it" and others didn't.
Clearly it <em>can</em> do impressive things; it's just a matter of</p>
<ol>
<li class="">raising the odds of success, and</li>
<li class="">lowering the risk of wasted time on my part
(I am pointedly ignoring the actually cost of token usage for now)</li>
</ol>
<p>So I committed to a full week of heavy Claude Code usage,
and set out to have it solve some major to-do items I had been putting off for months</p>
<p>The specifics don't matter too much here, but for context,
some of what I had it do:</p>
<ol>
<li class="">Research all the available on-device speech-to-text models with permissive licences</li>
<li class="">Demo the transcription speed of each one on an android device attached to the PC</li>
<li class="">Write a C wrapper for the best one (Moonshine) and build an embeddable dynamic library</li>
<li class="">Build this for iOS, Android, Linux, and macOS, and integrate it with my app code using the FFI</li>
<li class="">Build a Nim wrapper for the fdk-aac library</li>
<li class="">Integrate it with miniaudio, so I can play AAC audio and pipe the audio into Moonshine</li>
</ol>
<p>Plus many other tasks around wiring these things up and getting them running.
Collectively I would estimate that these things would have taken me a month,
and much of it would have been painful, tedious work.</p>
<p>Despite a rocky start (I nearly gave up on day 1),
I ended up very happy with the results.
I landed some solid new features, and my code is not shit (at least no more than it was).
So here are some of my findings and bits of advice for how to drive this thing.</p>
<ul>
<li class="">Every task needs a clear entry and exit point.
i.e. "Run the program with <code>./run_program.sh</code>, and look for 'module loaded successfully' in the log".
Don't let it just crawl through the code and decide when it thinks it's done.</li>
<li class="">Put your time into the setup process and the review process, but not in between.
Trying to steer Claude while it's working means you're investing time
into an ephemeral state that you will as likely as not throw out later.
If it goes wrong, just /clear, update the starting prompt, and go again.
Once it goes wrong the context is usually too polluted anyway.</li>
<li class=""><em>Always</em> protect your own work with source control.
That includes the work spent writing a prompt.
It should always be trivial to wipe everything out, make some changes, and send Claude off again.</li>
<li class="">Keep the intersection of your code and Claude's code as minimal as possible.
For example, if I want it to write a new miniaudio decoder at aac_decoder.c,
that's the <em>only</em> file it's allowed to touch.
It might generate lots of tests and docs, but those go into claude_tests and claude_docs,
never into the acutal test or docs directories.
It might seem unintuitive, since you often do want testing and documentation for a new feature,
but those things are first-order tasks that should be worked on directly.
If you want tests,
toss out all the garbage and have claude write a couple simple tests that you can actually review.</li>
<li class="">Observe a real result before you even look at the code.
If you're working on, say, an image processing feature, check the output image before reviewing anything.
Seeing something actually work means you (probably) have correct code, even if it's encased in slop.
But if there's no observable result, you're risking your time sifting through code that could be nonsense.</li>
<li class="">Constrain the context and look for references.
For example, the prompt may take the form of a document like this:
"Refer to <code>file1.c</code>, <code>file2.c</code>, and <code>project_description.md</code>. The miniaudio source is at <code>./external/miniaudio</code>.
The FDK library is at <code>./external/fdk-aac</code>. We're going to be writing an integration similar to <code>./src/opus_decoder.c</code>.  The task is to..."
The less "wander around and pull random stuff into the context" you can have it do, the better.</li>
<li class="">Set up minimal test projects. LLMs are pretty good at extracting something out of something else,
so use a prompt like "Use <code>&lt;full_project_source&gt;</code> as a reference and create a minimal project that demonstrates feature X".
Then have it extend feature X without all the extra source clouding up it's context.</li>
</ul>
<p>Metaphorically, I think about any job given to Claude as having 3 dimensions.
There's the breadth of the task (roughly how many lines of code it will touch),
the depth of the task (the complexity, the layers of abstraction needed, the decision making involved, etc.),
and the time spent working on it.
Those three axes define a cube, and the size of the cube is how much entropy I'm shoving into the project.
Something like "Update all the imports to use the new source structure"
is broad (will touch almost all the files) and potentially long-running,
but it's conceptually very simple, so the volume is low.
"Simplify the codebase and create clean lines of abstraction"
is conceptually deep, broad, and will take a long time.
Huge entropy cube.
So the idea is to shrink the cube when possible,
and to only deal with the section of the cube you need
(like <code>aac_decoder.c</code> but not <code>./aac_decoder_tests</code> and <code>./aac_decoder_project_milestones</code>).</p>
<p>Ultimately, I walked away from my week of heavy Claude usage <em>without</em> any kind of polarized opinion.
It's not a terrifying new intelligence machine on the cusp of AGI,
but it's also not useless grift.
In certain contexts it's a powerful tool that speeds up software development.
It also has the potential to be a huge time sink and can absolutely ruin code.
But after putting some time into it, my intuition has gotten much better about
when to use it, how to use it, and when to leave it alone.
My feeling is that an inexperienced developer is at risk of over using it,
but an experienced developer may be at risk of under using it.
I was the latter, and I'm very happy to have changed that.</p>]]></content:encoded>
            <category>claude-code</category>
            <category>llm</category>
            <category>ai</category>
        </item>
        <item>
            <title><![CDATA[Xcode is the Worst Piece of Professional Software I Have Ever Used]]></title>
            <link>https://holdtherobot.com/blog/xcode-is-the-worst-professional-software-i-have-ever-used</link>
            <guid>https://holdtherobot.com/blog/xcode-is-the-worst-professional-software-i-have-ever-used</guid>
            <pubDate>Mon, 22 Sep 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions]]></description>
            <content:encoded><![CDATA[<p><code>The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions</code></p>
<p>This is an error you will see often if you develop SwiftUI in Xcode.
Know what it means?
It means the compiler has given up, and you're on your own.
The error points to a file and function, but the issue could be anywhere in your codebase.
It might be a simple syntax error, or it might be code that is "too complex" for the compiler.
Hopefully you commit frequently because Xcode has turned into Notepad until you figure it out.</p>
<p>Speaking of git; consider the project file (<code>myProject.xcodeproj/project.pbxproj</code>).
This file contains all of your project settings, build configs, file references,
signing configs, and anything else you can think of.
If there are any errors in it, your project simply won't open.
It is thousands of lines long and <em>not human readable</em>.
Here's a small sample:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">7A226CEB2D722B3C001539F8 /* PBXContainerItemProxy */ = {</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	isa = PBXContainerItemProxy;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	containerPortal = 7A226C922D722973001539F8 /* Pods.xcodeproj */;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	proxyType = 2;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	remoteGlobalIDString = E826FA0DCB9AA6E7829C68391B323B78;</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">	remoteInfo = "GTMSessionFetcher-GTMSessionFetcher_Core_Privacy";</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">};</span><br></span></code></pre></div></div>
<p>I won't explain what that means, because I don't know what it means.
A merge conflict is exactly as miserable as it sounds.
The semi-sane way to deal with it is to use something like <a href="https://github.com/yonaskolb/XcodeGen" target="_blank" rel="noopener noreferrer" class="">xcodegen</a>
to rewrite the project settings in a normal-ass file type like yaml
and then use that to generate the Xcode project files.
BTW the entire UI layer for UIKit apps is stored in unreadable files like this.
Imagine.</p>
<p>Take a look at this dialog box.
<img decoding="async" loading="lazy" alt="image1" src="https://holdtherobot.com/assets/images/password_dialog-e5704b52f2c24bdf8443a1e381ad1dab.png" width="1358" height="987" class="img_ev3q">
Notice that weirdly dark drop shadow behind it?
That's not a UI glitch; that's <code>&lt;positive finite integer&gt;</code>
dialog boxes stacked on top of each other,
each waiting for your admin password.
You'll know you're close to done when the drop shadow starts to lighten.</p>
<p>Software has bugs and design flaws.
I'm not trying to say Xcode sucks because it's buggy
(although I'd like to emphasise that it is <em>very</em> buggy).
It sucks because it <em>pretends it isn't</em>.
Look back at that first error: <code>unable to type-check this expression in reasonable time; try breaking up the expression</code>
It's not a <em>bug</em>, it didn't <em>crash</em>, it just... you know. Taking a while.
Try wasting your time refactoring your code
without knowing where the problem is or having the help of a compiler.</p>
<p>Suppose you're testing out in-app purchases (God help you).
You follow Apple's docs, create a sandbox account for testing,
and then open the simulator.
Apple says the sandbox account will appear in the phone settings
after running the app, but of course it doesn't.
You attempt to manually sign in and see this:</p>
<p><img decoding="async" loading="lazy" alt="image2" src="https://holdtherobot.com/assets/images/auth_failure-48fd04a8b8e8441e21a3f546ee0ea44d.png" width="270" height="123" class="img_ev3q"></p>
<p>Okay probably a mistyped password. Try again.
Try 10 more times.
Maybe you shouldn't have skipped the 2FA setup for this test account?
You set up 2FA.
Still nothing.
You open the Xcode debugger and find
<code>Password reuse not available for account. The account state does not support password reuse</code>.
WTF is this? You're not reusing a password.
You start to wonder if maybe this doesn't work in the simulator,
even though Apple's docs make no mention of this.
You search around on the developer forums.
People confirm that this definitely does not work in the simulator.
Other people confirm that it definitely does.
There's no answers to be found, and no solid info anywhere.</p>
<p>As a developer, you learn that you simply cannot trust Apple.
There is a persistent layer of vagueness and misdirection
around every part of the experience.
All those WWDC videos showing off new features and frameworks?
You know, the ones where the presenter is seemingly
going to be shot if they don't hit the adjective quota?
Those are basically ads.
Watching a presentation on the SwiftUI preview feature
(a way to see your UI update without a full app rebuild.
Not to be confused with the actually useful hot reloading in Flutter
or a web based framework)
and then trying to actually use it was pure comedy.
There's been years of steady improvement and last I checked it was still
mostly useless for any sufficiently complex project.
So imagine how bad it was at launch.
Not a hint of this at WWDC though.
Just a seemingly complete, cutting edge new feature
that Apple is so excited to tell you about.</p>
<p>Here's the kicker. Something I cannot and will not get over.
Apple's bug tracker is private.
You can submit a bug (which has been euphemized to "radar"),
but the bug reporter is a black hole;
information goes in but it doesn't come out.
Starting to wonder if some weird behavior with
navigation is maybe not actually a problem on your end?
Expecting to search through some Github issues-like tracker to see
if anyone is experiencing something similar?
Sorry.
Better that thousands of developers waste their time rediscovering
some subtle framework bug than for Apple to publicly acknowledge a flaw.</p>
<p>It isn't just opaque issues and errors.
The <em>design</em> of Xcode and everything around it is stifling.
There are currently no real alternative editors
if you're working on an iOS project and want things like linting and code completion
(it is actually possible with neovim using <a href="https://github.com/SolaWing/xcode-build-server" target="_blank" rel="noopener noreferrer" class="">xcode-build-server</a>,
but it's pretty flaky). Jetbrains' AppCode was killed off a few years ago.
CLI tools are poorly documented and difficult to use,
which means simply trying to script basic things or do CI is painful.
Fastlane helps, but it's ridiculous that there needs to be a big Google-funded
open source project to just make scripting tolerable.
This means Claude Code will struggle to do anything useful too BTW (even more than it already does).
And of course it goes without saying that you must be doing all of this on a mac in the first place.</p>
<p>I actually learned software development in Xcode,
back before automatic reference counting was even a twinkle in Objective-C's eye.
I honestly believe that it hurt my growth as a developer
and gave me a poor set of instincts.
Rather than reacting to a problem by seeking to go deeper
and understand what is happening underneath the code I'm writing,
the solutions were mindless and ritualistic.
"Try restarting Xcode. Try clearing the derived data. Try rebooting your mac.
Try the Xcode beta branch. Try recreating the project."
Of course I could have been more mindful and deliberate,
but it's hard to know what bad thought patterns
you're picking up when the environment is working against you.</p>
<p>I wish the developer experience was better,
but Apple does not appear to want to address their technical debt,
and developers were always second class citizens anyway.
I would encourage any new developers to try and stay away from Xcode
(to the extent that you can),
and if you are using it and questioning your own sanity thinking
"am I just holding it wrong?": no, you're not. Xcode sucks.</p>]]></content:encoded>
            <category>xcode</category>
            <category>ios</category>
        </item>
        <item>
            <title><![CDATA[Heredocs Can Make Your Bash Scripts Self-Documenting]]></title>
            <link>https://holdtherobot.com/blog/heredocs-can-make-your-bash-scripts-self-documenting</link>
            <guid>https://holdtherobot.com/blog/heredocs-can-make-your-bash-scripts-self-documenting</guid>
            <pubDate>Fri, 25 Jul 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[I have long since come to appreciate the value of writing scripts]]></description>
            <content:encoded><![CDATA[<p>I have long since come to appreciate the value of writing scripts
to avoid someone else (or future me) from having to re-learn and re-solve problems,
but something about it has always bugged me.</p>
<p>I am automating a process, but I'm also <em>documenting</em> it,
and those two things struggle to coexist.</p>
<p>One option is to write a bash script for the automation
and a markdown file for the documentation,
but they inevitably end up duplicating information and/or getting out of sync.
The other is to just have a single markdown file with a bunch of inline bash
that you manually copy into a terminal.
But "running" it is clunky, tedious, and easy to mess up.</p>
<p>I tend to prefer the latter despite the annoyances,
because "keeping information in sync" is such a big problem.
But recently I've been playing with a third option.
Rather than maintaining two files or putting bash in markdown; put markdown in bash.</p>
<p>It looks like this (I'm showing you an image since Docusaurus won't syntax highlight this):</p>
<p><img decoding="async" loading="lazy" alt="image" src="https://holdtherobot.com/assets/images/inline-markdown-972100d187dd079b0b3b9ec954770eeb.png" width="1052" height="858" class="img_ev3q"></p>
<p>This is just a bash script that can be executed like normal.</p>
<p>The markdown bit is a "heredoc",
which is basically just a multiline string,
similar to a triple-quoted string in python.
The <code>&lt;&lt;'delimiter'</code> starts the string and <code>delimiter</code> ends it.
Be careful to quote the first delimiter,
otherwise you'll get parameter expansion (things like <code>$HOME</code> will expand to <code>/home/myusername</code>)
or even execution in your doc strings (intuitive as always, thanks Bash).
I chose <code>-md-</code> as a delimiter, but you can choose whatever you like,
as long as it's not a string you're going to be using otherwise.</p>
<p>If you precede there heredoc with <code>cat</code> it will print to the terminal
when you run the script, but you can also leave that out.</p>
<p>I use the vim plugin <code>preservim/vim-markdown</code> to get markdown syntax highlighting,
concealment, links, and so on.
By default, none of that is going to work inside a bash script,
but you can fix that by adding the following to <code>.config/nvim/after/syntax/sh.vim</code>
(create the file and path if needed):</p>
<div class="language-vim codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-vim codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token builtin">syntax</span><span class="token plain"> region shMarkdown </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    \ matchgroup</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">shMarkdownDelimiter </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    \ start</span><span class="token operator" style="color:#393A34">=</span><span class="token operator" style="color:#393A34">/</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token operator" style="color:#393A34">&lt;</span><span class="token string" style="color:#e3116c">'-md-'</span><span class="token plain">\s</span><span class="token operator" style="color:#393A34">*</span><span class="token plain">$</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    \ end</span><span class="token operator" style="color:#393A34">=</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">^</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">md</span><span class="token operator" style="color:#393A34">-</span><span class="token plain">\s</span><span class="token operator" style="color:#393A34">*</span><span class="token plain">$</span><span class="token operator" style="color:#393A34">/</span><span class="token plain"> </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    \ contains</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">@markdownHighlight </span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    \ containedin</span><span class="token operator" style="color:#393A34">=</span><span class="token plain">shHereDoc</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain">shHereString</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    \ keepend</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin">syntax</span><span class="token plain"> </span><span class="token builtin">include</span><span class="token plain"> @markdownHighlight </span><span class="token builtin">syntax</span><span class="token operator" style="color:#393A34">/</span><span class="token plain">markdown</span><span class="token operator" style="color:#393A34">.</span><span class="token keyword" style="color:#00009f">vim</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token comment" style="color:#999988;font-style:italic">" Link the delimiter to Comment so it's greyed out</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin">highlight</span><span class="token plain"> link shMarkdownDelimiter Comment</span><br></span></code></pre></div></div>
<p>And there you go; markdown-ified bash scripting.</p>
<p>There's still plenty of times a markdown file makes more sense,
since you're not always writing bash commands that are intended to be run top-to-bottom.
I have a file that lists various ffmpeg commands, for example,
and I'm only ever going to be copy-pasting things out of that file.
But for a runbook style script I really quite like this and I think it's
absolutely a better option than maintaining separate scripts and documentation.
There's a reason why so many modern codebases use inline documentation,
and I think bash scripts should do the same.</p>]]></content:encoded>
            <category>bash</category>
            <category>linux</category>
        </item>
        <item>
            <title><![CDATA[A CRDT-based Messenger in 12 Lines of Bash Using a Synced Folder]]></title>
            <link>https://holdtherobot.com/blog/crdt-messenger-in-12-lines-of-bash</link>
            <guid>https://holdtherobot.com/blog/crdt-messenger-in-12-lines-of-bash</guid>
            <pubDate>Wed, 25 Jun 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Put that script inside a folder, share the folder with someone via Syncthing or Dropbox or whatever,]]></description>
            <content:encoded><![CDATA[<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">mkdir</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-p</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$(</span><span class="token variable function" style="color:#d73a49">dirname</span><span class="token variable" style="color:#36acaa"> $0</span><span class="token variable" style="color:#36acaa">)</span><span class="token plain">/data</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token builtin class-name">cd</span><span class="token plain"> data</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function-name function" style="color:#d73a49">print_messages</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">clear</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">cat</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$(</span><span class="token variable function" style="color:#d73a49">ls</span><span class="token variable" style="color:#36acaa"> </span><span class="token variable parameter variable" style="color:#36acaa">-tr</span><span class="token variable" style="color:#36acaa"> </span><span class="token variable operator" style="color:#393A34">|</span><span class="token variable" style="color:#36acaa"> </span><span class="token variable function" style="color:#d73a49">tail</span><span class="token variable" style="color:#36acaa"> </span><span class="token variable parameter variable" style="color:#36acaa">-n30</span><span class="token variable" style="color:#36acaa">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token builtin class-name">printf</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string entity" style="color:#36acaa">\033</span><span class="token string" style="color:#e3116c">[31m</span><span class="token string environment constant" style="color:#36acaa">$USER</span><span class="token string" style="color:#e3116c">:</span><span class="token string entity" style="color:#36acaa">\033</span><span class="token string" style="color:#e3116c">[0m "</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token builtin class-name">export</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-f</span><span class="token plain"> print_messages</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">watchexec </span><span class="token operator file-descriptor important" style="color:#393A34">2</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> /dev/null -- </span><span class="token function" style="color:#d73a49">bash</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-c</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"print_messages"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">while</span><span class="token plain"> </span><span class="token builtin class-name">read</span><span class="token plain"> text</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token builtin class-name">printf</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string entity" style="color:#36acaa">\033</span><span class="token string" style="color:#e3116c">[31m</span><span class="token string environment constant" style="color:#36acaa">$USER</span><span class="token string" style="color:#e3116c">:</span><span class="token string entity" style="color:#36acaa">\033</span><span class="token string" style="color:#e3116c">[0m </span><span class="token string variable" style="color:#36acaa">$text</span><span class="token string entity" style="color:#36acaa">\n</span><span class="token string entity" style="color:#36acaa">\n</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$(</span><span class="token string variable" style="color:#36acaa">uuidgen</span><span class="token string variable" style="color:#36acaa">)</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">done</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain" style="display:inline-block"></span><br></span></code></pre></div></div>
<p>Put that script inside a folder, share the folder with someone via Syncthing or Dropbox or whatever,
run it, and you should get this:</p>
<div style="display:flex;justify-content:center"><video autoplay="" loop="" muted="" playsinline="" style="width:100%;height:auto"><source src="/video/crdt_demo_av1.webm" type="video/webm"><source src="/video/crdt_demo_h264.mp4" type="video/mp4"></video></div>
<br>
<p>This is hardly a Discord killer, but as far as messengers go
there are some interesting properties:</p>
<ol>
<li class="">There is no central authority or server that "owns" the messages</li>
<li class="">An offline machine can write new messages that will propagate once it's back online</li>
<li class="">All participating machines will show the same messages in the same order once they're synced,
no matter what</li>
</ol>
<p>There's nothing really novel about those three things;
that's what you get out of the box with Conflict Free Replicated Data Types (CRDTs).
So my goal with this blog post is to plant the seed in your mind
that CRDTs are just generally cool, and they are very simple.</p>
<p>And even though this little messenger is kinda toy-ish,
it's completely solid and I use it to communicate with a (equally nerdy) friend of mine.
I've used the same technique to create a time tracker that I can use
on different machines without every worrying about being online or things getting out of sync.
We're obviously relying on a file sync program to do some heavy lifting here,
but because the data is "conflict free", something as simple as rsync or scp would work (and always work) just fine.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-bash-script">The Bash Script<a href="https://holdtherobot.com/blog/crdt-messenger-in-12-lines-of-bash#the-bash-script" class="hash-link" aria-label="Direct link to The Bash Script" title="Direct link to The Bash Script" translate="no">​</a></h3>
<p>There's not much to it, so I'll run through it quick.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function" style="color:#d73a49">mkdir</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-p</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$(</span><span class="token variable function" style="color:#d73a49">dirname</span><span class="token variable" style="color:#36acaa"> $0</span><span class="token variable" style="color:#36acaa">)</span><span class="token plain">/data</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token builtin class-name">cd</span><span class="token plain"> data</span><br></span></code></pre></div></div>
<p>Create a data directory (if needed) and move into it.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token function-name function" style="color:#d73a49">print_messages</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">clear</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token function" style="color:#d73a49">cat</span><span class="token plain"> </span><span class="token variable" style="color:#36acaa">$(</span><span class="token variable function" style="color:#d73a49">ls</span><span class="token variable" style="color:#36acaa"> </span><span class="token variable parameter variable" style="color:#36acaa">-tr</span><span class="token variable" style="color:#36acaa"> </span><span class="token variable operator" style="color:#393A34">|</span><span class="token variable" style="color:#36acaa"> </span><span class="token variable function" style="color:#d73a49">tail</span><span class="token variable" style="color:#36acaa"> </span><span class="token variable parameter variable" style="color:#36acaa">-n30</span><span class="token variable" style="color:#36acaa">)</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">    </span><span class="token builtin class-name">printf</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string entity" style="color:#36acaa">\033</span><span class="token string" style="color:#e3116c">[31m</span><span class="token string environment constant" style="color:#36acaa">$USER</span><span class="token string" style="color:#e3116c">:</span><span class="token string entity" style="color:#36acaa">\033</span><span class="token string" style="color:#e3116c">[0m "</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre></div></div>
<p>Clear the screen, print the contents of the last 30 messages,
and then print the <code>mike:</code> prompt.
The gross looking <code>\033[31m</code> stuff is just ANSI escape codes to set and unset the color.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token builtin class-name">export</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-f</span><span class="token plain"> print_messages</span><br></span></code></pre></div></div>
<p>Some Bash nonsense to "export a function". Otherwise the watchexec subprocess can't see it.</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">watchexec </span><span class="token operator file-descriptor important" style="color:#393A34">2</span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> /dev/null -- </span><span class="token function" style="color:#d73a49">bash</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-c</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"print_messages"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;</span><br></span></code></pre></div></div>
<p>Start up <code>watchexec</code>. Send it's stderr output into /dev/null so it doesn't bother us.
Whenever it sees file changes, reprint the messages.
The &amp; symbol makes it run in the background so our script can do other things.</p>
<p>BTW, I used watchexec to watch for file changes because it works on Termux,
which lets me use this on Android. If you want to use <code>fswatch</code> (which seems more common) instead,
replace that line with this:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">print_messages </span><span class="token comment" style="color:#999988;font-style:italic"># fswatch doesn't fire on startup, so print messages first</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">fswatch </span><span class="token parameter variable" style="color:#36acaa">-o</span><span class="token plain"> </span><span class="token builtin class-name">.</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">|</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">while</span><span class="token plain"> </span><span class="token builtin class-name">read</span><span class="token plain"> </span><span class="token parameter variable" style="color:#36acaa">-r</span><span class="token plain"> event</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  print_messages</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">done</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&amp;</span><br></span></code></pre></div></div>
<br>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token keyword" style="color:#00009f">while</span><span class="token plain"> </span><span class="token builtin class-name">read</span><span class="token plain"> text</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"> </span><span class="token keyword" style="color:#00009f">do</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token builtin class-name">printf</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string entity" style="color:#36acaa">\033</span><span class="token string" style="color:#e3116c">[31m</span><span class="token string environment constant" style="color:#36acaa">$USER</span><span class="token string" style="color:#e3116c">:</span><span class="token string entity" style="color:#36acaa">\033</span><span class="token string" style="color:#e3116c">[0m </span><span class="token string variable" style="color:#36acaa">$text</span><span class="token string entity" style="color:#36acaa">\n</span><span class="token string entity" style="color:#36acaa">\n</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"> </span><span class="token operator" style="color:#393A34">&gt;</span><span class="token plain"> </span><span class="token string" style="color:#e3116c">"</span><span class="token string variable" style="color:#36acaa">$(</span><span class="token string variable" style="color:#36acaa">uuidgen</span><span class="token string variable" style="color:#36acaa">)</span><span class="token string" style="color:#e3116c">"</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#00009f">done</span><br></span></code></pre></div></div>
<p>Read user input. When they hit <code>Enter</code>, put whatever they wrote into a file.
Critically, use a <em>Universally Unique Identifier</em> for that file.</p>
<p>So basically, stuff messages into files that all have UUIDs.
If you look inside <code>data</code> you'll see this:</p>
<div class="language-bash codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-bash codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">$ </span><span class="token function" style="color:#d73a49">ls</span><span class="token plain"> data</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">035aa216-3e23-4921-8d14-b79bdc150232  5d07ed32-8f0c-4c88-9a93-f12606d57ea1</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">04455d8b-b58a-40da-a01a-7631e90ccbd8  6187df26-8a6e-4729-9553-ffe1acf0d45f</span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">1d621700-322e-4ba9-9d66-16a739838adf  650b8c73-9ef5-40e5-8014-a8d97d617f1f</span><br></span></code></pre></div></div>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="why-this-works">Why This Works<a href="https://holdtherobot.com/blog/crdt-messenger-in-12-lines-of-bash#why-this-works" class="hash-link" aria-label="Direct link to Why This Works" title="Direct link to Why This Works" translate="no">​</a></h3>
<p>Using UUIDs means that any machine can create files without having to worry about another
machine creating an identically-named file and causing a conflict, which would break our whole system.</p>
<p>We can't <em>delete</em> files, because if a machine deletes a file and then tries to sync,
it won't be able to tell if it deleted something or if it's just talking to a machine that has a new file
(we actually could get away with this because Syncthing keeps a local database to log file deletions,
but that's cheating, and simpler tools like scp definitely don't. Plus there's better ways anyway).</p>
<p>Lastly, after two machines exchange files, it's critical that they both can display messages in the same order.
Using <code>ls -tr</code> to order the files actually works perfectly, because <code>-tr</code> (order by time, reversed) uses the file modification date,
and that gets preserved when copying the files.
It's technically possible to create files with the same modification date on two different machines
and therefore have an arbitrary ordering,
but at least on Linux with most modern filesystems you get billionth-of-a-second granularity,
which is more than fine. On a filesystem like FAT32 with 2 second granularity this would very much be a problem.</p>
<p>So, those 3 properties mean that we have created a CRDT.
CRDTs are just data structures that:</p>
<ol>
<li class="">Can be replicated across an arbitrary number of nodes</li>
<li class="">Can be modified concurrently</li>
<li class="">Will always converge to the same thing after nodes sync with each other</li>
</ol>
<p>Specifically, we've created a grow-only set.
If we ignored the contents of each file we could still <em>count</em> them with something like <code>ls -1 | wc -l</code>,
and that would be an even simpler CRDT called a grow-only counter.</p>
<p>That's what I used in the timer-tracker thing I mentioned earlier.
Just add a file with a UUID into a directory called <code>25_minute_pomodoros</code>,
and now you have a distributed, conflict-free pomodoro counter.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="edits-and-deletions">Edits and Deletions<a href="https://holdtherobot.com/blog/crdt-messenger-in-12-lines-of-bash#edits-and-deletions" class="hash-link" aria-label="Direct link to Edits and Deletions" title="Direct link to Edits and Deletions" translate="no">​</a></h3>
<p>So an obvious problem is that you can't edit or delete a message.
And, in fact, it's fundamental to the design that once you create a new file,
you absolutely do not mess with it.</p>
<p>To get around that, you just <em>create more files</em>.
So in the Pomodoro example, there's a folder called <code>25_minute_pomodoros_deletions</code>.
If I decide that I want to decrement my Pomodoro counter,
just <code>touch 25_minute_pomodoros_deletions/$(uuidgen)</code>.
Then subtract the number of files in <code>25_minute_pomodoros_deletions</code> from <code>25_minute_pomodoros</code>.
This is called a positive-negative counter.</p>
<p>For messages, rather than just putting the plain text contents in each file,
we could do more structured data like:</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">message:Hey it's mike what's up?</span><br></span></code></pre></div></div>
<p>or</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">delete:2880dbc8-a2c6-43c0-8f88-e0fb2672755c</span><br></span></code></pre></div></div>
<p>or</p>
<div class="language-text codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_QJqH"><pre tabindex="0" class="prism-code language-text codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token plain">edit:2880dbc8-a2c6-43c0-8f88-e0fb2672755c:Hey, it's Mike what's up???</span><br></span></code></pre></div></div>
<p>We'd then have to actually inspect the contents of each messsage
and decide if it should be displayed or if it affects a previous message
(so we're well beyond 12 lines of bash at this point)
but it doesn't change anything about the properties of the system.
Any machine can make those changes freely, and messages will always be
rendered the same way.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-takeaway">The Takeaway<a href="https://holdtherobot.com/blog/crdt-messenger-in-12-lines-of-bash#the-takeaway" class="hash-link" aria-label="Direct link to The Takeaway" title="Direct link to The Takeaway" translate="no">​</a></h3>
<p>The important concept here is that data is stored in one of these very simple CRDT models,
and you can use that basic model to deterministically "render" whatever data you want.</p>
<p>Flat files and <code>uuidgen</code> is enough to implement the data structure (not saying you should, but cleary you <em>can</em>).
The sync part is what's mind blowing.
You can sync arbitrarily complex data between an arbitrary number of devices
<em>without knowing anything about it</em>. rsync or scp could easily handle this job.</p>
<p>If we were doing the same thing in a more sane way (like, say, storing these messages in a local sqlite database),
you can still pump messages between machines without any care for what's in them or what they mean.</p>
<p>Even if you want a dedicated server, the server does not need to know how to render them,
so the entirely of the server logic can be: <em>Hey, let's compare messages. Please give me the ones that I'm missing. Here's the ones that you're missing.</em>
And you have one endpoint: <code>/sync</code>.</p>
<p>I've been building things with CRDTs for a while now and have developed a real love for what they let you do.
I'd love to talk more about them soon, but for now, I hope that's at least a fun introduction for anyone who isn't familiar yet.
I really think they're being slept on and I hope more people start using them.</p>
<br>
<small><p>A Little Note:
If you actually want to play with this and you're using Syncthing, messages are kinda slow by default. There's a setting in ~/.config/syncthing/config.xml called
fsWatcherDelayS. Set it to "1" for the folder you're keeping messages in and it will be much faster.
If you're using Google Drive or Dropbox or whatever, you're on your own.</p></small>]]></content:encoded>
            <category>crdt</category>
            <category>linux</category>
        </item>
        <item>
            <title><![CDATA[Coding Without a Laptop - Two Weeks with AR Glasses and Linux on Android]]></title>
            <link>https://holdtherobot.com/blog/2025/05/11/linux-on-android-with-ar-glasses</link>
            <guid>https://holdtherobot.com/blog/2025/05/11/linux-on-android-with-ar-glasses</guid>
            <pubDate>Sun, 11 May 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[I recently learned something that blew my mind;]]></description>
            <content:encoded><![CDATA[<p>I recently learned something that blew my mind;
you can run a full desktop Linux environment on your phone.</p>
<p>Not some clunky virtual machine
and not an outright OS replacement like Ubuntu Touch or postmarketOS.
Just native arm64 binaries running inside
a little chroot container on Android. Check it out:</p>
<p><img decoding="async" loading="lazy" alt="image1" src="https://holdtherobot.com/assets/images/image1-fda7b7bad1f1031c2a9b93f312ec17f8.avif" width="3762" height="2464" class="img_ev3q"></p>
<small><blockquote>
<p>i3, picom, polybar, firefox, and htop</p>
</blockquote></small>
<p>That's a graphical environment via X11 with real window management and compositing,
Firefox comfortably playing YouTube (including working audio),
and a status bar with system stats.
It launches in less than a second and feels snappy.</p>
<p>Ignoring the details of getting this to work for the moment,
the obvious response is "okay yeah that's neat but like, <em>why</em>".
And fair enough. It's novel, but surely not useful.</p>
<p>Thing is, I had a 2 week trip coming up where I'd need to work,
and I got a little obsessed with the idea
that I could somehow leave my laptop at home and <em>just use my phone</em>.
So what if we add a folding keyboard and some AR glasses?</p>
<p><img decoding="async" loading="lazy" alt="image2" src="https://holdtherobot.com/assets/images/image2-33a8991bfbc1b166d479f61b9d3a3b53.avif" width="3668" height="2271" class="img_ev3q"></p>
<small><blockquote>
<p>Here's a CRDT-based ebook/audiobook reader I've been working on,
running a desktop Linux app and connected to the Flutter debugger.</p>
</blockquote></small>
<p>What's kind of amazing here is that both the glasses and the keyboard
fit comfortably in my pockets.
And I'm already carrying the phone, so it's not that much extra.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-hardware">The Hardware<a href="https://holdtherobot.com/blog/2025/05/11/linux-on-android-with-ar-glasses#the-hardware" class="hash-link" aria-label="Direct link to The Hardware" title="Direct link to The Hardware" translate="no">​</a></h3>
<p><strong>Keyboard:</strong> There's plenty of little folding bluetooth keyboards on the market,
and I only had to go through 5 of them before I <a href="https://amzn.to/4mnjRYq" target="_blank" rel="noopener noreferrer" class="">found one</a> that was tolerable.
I tried some with trackpads, but they were either too big
or the keys were squeezed together to make it fit.
The Termux<!-- -->:X11<!-- --> app that displays the graphical environment
is able to function as a trackpad to move a mouse pointer around,
and that turned out to be good enough for mouse input.
I'm very keyboard-centric anyway,
so I'd often go for a while without needing to touch it.</p>
<p><strong>The Glasses:</strong> Believe it or not, "augmented reality" glasses are kinda good now.
The AR part is almost entirely a misnomer; they're just tiny little OLED displays
strapped to your face attached to bird bath optics.
I was able to get a lightly used pair of <a href="https://amzn.to/4dtA4HA" target="_blank" rel="noopener noreferrer" class="">Xreal Air 2 Pros</a> off of ebay that would show me a 1080p display
with a 46° field of view.
Some of the newer ones can do large virtual displays rather than the
pinned-to-your-head image that mine have,
but I'm pretty skeptical of that setup,
at least until the resolution and field of view improve.</p>
<p><strong>The Phone:</strong> I unfortunately had to upgrade my phone,
because to drive the glasses you need to have DisplayPort Alt mode.
My very cheap, very crappy old phone did not.
The 8 series seems to be the first Pixel phone where Google decided to be marginally less evil
and not lock out the DP Alt Mode feature in software (forcing people to buy Chromecasts? IDK),
so I bought a used <a href="https://amzn.to/3F02fRD" target="_blank" rel="noopener noreferrer" class="">Pixel 8 Pro</a> on ebay.</p>
<p>So the whole setup:</p>
<ul>
<li class="">Used Pixel 8 Pro $350</li>
<li class="">Used Xreal Air 2 Pro - $260</li>
<li class="">Samers Foldable Keyboard $18</li>
</ul>
<p>Total cost: $636. Although I'm not sure the $350 for the phone should count,
because I really did need a new one.</p>
<p>After a few afternoons experimenting,
I felt like I could <em>probably</em> function with only this setup for the two weeks.
I figured the full commit would keep me from reverting back to a PC
when I hit a wall and got frustrated or bored.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-result">The Result<a href="https://holdtherobot.com/blog/2025/05/11/linux-on-android-with-ar-glasses#the-result" class="hash-link" aria-label="Direct link to The Result" title="Direct link to The Result" translate="no">​</a></h3>
<p>So after using this on an airplane, in coffee shops, at various family member's houses,
in parks, and even sitting in the car, I think I have some answers for
"why would you use this when laptops exist and are excellent".</p>
<ol>
<li class="">It really does fit into your pockets. No bag, nothing to carry.</li>
<li class="">I can use it outdoors in bright sunlight. I wrote most of this blog post sitting at a picnic table in a park.
Screen glare and brightness is not an issue.</li>
<li class="">I can fit into tight spaces. This setup was infinitely more comfortable than a laptop when on a plane.
Some coffee shops also have narrow bars that are too small for a laptop, but not for this.</li>
<li class="">The phone has a cellular connection, so I'm not tied to wifi.</li>
</ol>
<p>In other words, there's a sense of freedom that you do not get with a laptop.
And I can be <em>outdoors</em>.
One of the things I've grown tired of as software dev
is feeling like I'm stuck inside all the time in front of a screen.
With this I can walk to a coffee shop and work for an hour or two,
then get up and walk to a park for another hour of work.
It feels like a breath of fresh air, quite literally.</p>
<p>That said, there were plenty of pain points and nuances to the whole thing.
So here's my experience:</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-linux-environment">The Linux Environment<a href="https://holdtherobot.com/blog/2025/05/11/linux-on-android-with-ar-glasses#the-linux-environment" class="hash-link" aria-label="Direct link to The Linux Environment" title="Direct link to The Linux Environment" translate="no">​</a></h3>
<p>Linux-on-Android was <em>eventually</em> great,
but I don't want to gloss over the fact that it was a pain in the ass to figure out.
My definition of "sufficiently capable" was Neovim + functioning langauge servers
(Nim, Python, Dart, JS), Node, and Flutter
(compiling to both desktop and web apps that could be run and debugged).</p>
<p>The I won't go though everything line-by-line here (I can though, if anyone is interested),
but there's already some great resources out there (linked below).
Here's the high level picture, based on my learnings.</p>
<p>There's roughly 4 different approaches to Linux on Android:</p>
<ol>
<li class="">A virtual machine emulating x86_64</li>
<li class="">Termux, which is an Android app that provides a mix of terminal emulator,
lightweight Linux userland, and set of packages that are able to run in that environment.</li>
<li class="">arm64 binaries running in chroot, which is basically just a directory
where those programs will run, sealed off from the rest of the filesystem.
Notably, it requires the system to be rooted.</li>
<li class="">proot. Same idea as chroot, but doesn't use the forbidden system calls
that chroot needs root for</li>
</ol>
<p>After way too much time spent experimenting, I landed on the chroot approach.
I really didn't want to root the phone, but nothing else did what I needed.
The virtual machine was way too slow and clunky, as was proot.
Sticking to what can be run inside Termux got me surpisingly far,
but Android's C implementation is Bionic
and most programs won't run unless they're compiled with that in mind.
That, plus other differences in the environment mean you're pretty limited.
Chroot has no performance penalty as far as I can tell,
and (for the most part), anything that can be compiled for arm64 seemed to work.</p>
<p>As far as distro (I tried many), here's what matters:</p>
<ol>
<li class="">Small and light. This is a phone, after all.</li>
<li class="">Has to support aarch64, obviously.</li>
<li class="">Doesn't use systemd (I could never make it work inside chroot, and it's unclear if it's possible).</li>
<li class="">Has some amount of testing or support for running in chroot.
Arch Linux ARM, for example, had some odd issues here, like fakeroot not working.</li>
<li class="">Uses glibc. I thought Alpine was going to be the ticket,
but I really needed Flutter/Dart to work, and I couldn't get it working with musl.
This might not be a problem for everyone though.</li>
</ol>
<p>So ultimately, the aarch64 glibc rootfs tarball of Void Linux fit the bill, and it's been running beautifully.</p>
<p>I used i3 (a keyboard-centric tiling window manager),
but I tested xfce and that worked fine too.</p>
<p>Some usleful links:</p>
<ul>
<li class=""><a href="https://github.com/LinuxDroidMaster/Termux-Desktops" target="_blank" rel="noopener noreferrer" class="">https://github.com/LinuxDroidMaster/Termux-Desktops</a></li>
<li class=""><a href="https://github.com/termux/termux-x11#using-with-chroot-environment" target="_blank" rel="noopener noreferrer" class="">https://github.com/termux/termux-x11#using-with-chroot-environment</a></li>
<li class=""><a href="https://github.com/Magisk-Modules-Alt-Repo/chroot-distro" target="_blank" rel="noopener noreferrer" class="">https://github.com/Magisk-Modules-Alt-Repo/chroot-distro</a></li>
</ul>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-ar-glasses">The AR Glasses<a href="https://holdtherobot.com/blog/2025/05/11/linux-on-android-with-ar-glasses#the-ar-glasses" class="hash-link" aria-label="Direct link to The AR Glasses" title="Direct link to The AR Glasses" translate="no">​</a></h3>
<p>The quality of the image on these things is fantastic.
You're seeing bright pixels from a beatiful OLED display.
But because each pixel is bounced off the lens, a black pixel just looks clear.
So a black terminal background with white text means you're seeing white text floating in space.
This is actually pretty cool if you want "less screen, more world around you" kind of feel, but can also be distracting.
However, the model I bought has electrochromic dimming,
so you can darken the actual "sunglasses" part to block out ambient light.
Without this they'd be unuseable in bright sunlight as the image washes out,
so I highly recommend getting a pair that has this.</p>
<p><img decoding="async" loading="lazy" alt="image3" src="https://holdtherobot.com/assets/images/image3-131628ff6e93155fca11522aab8d04bf.avif" width="3012" height="3204" class="img_ev3q"></p>
<small><blockquote>
<p>It's apparently impossible to get a good through-the-lens photo,
but trust me that the image through the glasses is excellent.
This is wihout the electrochromic dimming turned on, so text just floats in front of the scenery. You can darken the glasses to the point where you can hardly see through them if you want.</p>
</blockquote></small>
<p>I do feel a little weird wearing these in public, but not <em>that</em> weird.
They more or less pass for sunglasses,
so the odd part is wearing sunglasses indoors and typing on a keyboard with nothing in front of you.
I had couple people ask me about them, but they seemed to just think they were cool.
One guy said he was going to buy a pair.
That may be selection bias though; I'm sure some people thought I was an idiot.</p>
<p>The biggest downside of the glasses is that the FOV is actually too big.
Seeing the top and bottom edges of the screen means moving your eyeballs
to angles that are just a little uncomfortable, and it's actually difficult
to get the lenses in the right spot so that both are clearly in focus at the same time.
I had the window manager add some extra padding at the top and bottom of the screen,
and that helped quite a bit.</p>
<p>Worth mentioning: I tried to get multi-display mode working on Android, and it was awful.
I ended up using <a href="https://play.google.com/store/apps/details?id=com.tribalfs.pixels&amp;hl=en-US" target="_blank" rel="noopener noreferrer" class="">this app</a>
to change the phone's resolution to 1080p, and then just mirror to the glasses.
It turned out to be great, because you can pull the glasses off and just work on the phone
whenever you want a break.</p>
<p>The focal plane of the glasses is about 10 feet.
Which means if you use readers for a laptop, you probably won't need them.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="the-keyboard">The Keyboard<a href="https://holdtherobot.com/blog/2025/05/11/linux-on-android-with-ar-glasses#the-keyboard" class="hash-link" aria-label="Direct link to The Keyboard" title="Direct link to The Keyboard" translate="no">​</a></h3>
<p><em>Sigh</em>. Can someone please make a good folding keyboard?
This little $18 piece of plastic is decent for what it is,
but this was the weakest part of the whole setup, and it feels like it should be the easiest.
It feels cheap, is bulkier than it needs to be,
doesn't lock when it's open (which means you can't really sit with it in your lap),
and there's no firmware based key remapping.</p>
<p>I might continue to play alibaba roulette and see if there's a better one out there.
But I would quite literally pay 10 times as much for something good.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="performance">Performance<a href="https://holdtherobot.com/blog/2025/05/11/linux-on-android-with-ar-glasses#performance" class="hash-link" aria-label="Direct link to Performance" title="Direct link to Performance" translate="no">​</a></h3>
<p>As a rough benchmark, I tried compiling Nim from source.</p>
<ul>
<li class="">On my Framework 13 with a Core Ultra 5 125H it took <code>4:15</code>.</li>
<li class="">On my Thinkpad T450s with an Intel Core i5-5300U it took <code>14:20</code>.</li>
<li class="">On the Pixel 8 Pro it took <code>11:20</code>.</li>
</ul>
<p>I would say qualitatively that's about how it feels to use.
Faster than the Thinkpad, but definitely not as fast as the Framework.</p>
<p>BTW I am glad I paid a little extra for the Pixel 8 Pro,
because the 12GB of RAM it has vs the 8 of the non-pro model seems worthwhile.
RAM usage often gets close to that 12GB ceiling.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="battery-life">Battery Life<a href="https://holdtherobot.com/blog/2025/05/11/linux-on-android-with-ar-glasses#battery-life" class="hash-link" aria-label="Direct link to Battery Life" title="Direct link to Battery Life" translate="no">​</a></h3>
<p>With the glasses on and the phone screen dimmed,
the phone used a little under 3 watts at idle,
and 5 to 10 when compiling or doing heavier things.
On average I'd drain about 15% battery per hour.
So 4 to 5 hours before you need to be thinking about charging,
but I'm not sure you'd want to have the glasses on longer than that anyway.</p>
<h3 class="anchor anchorTargetStickyNavbar_Vzrq" id="am-i-going-to-keep-using-this">Am I Going to Keep Using This?<a href="https://holdtherobot.com/blog/2025/05/11/linux-on-android-with-ar-glasses#am-i-going-to-keep-using-this" class="hash-link" aria-label="Direct link to Am I Going to Keep Using This?" title="Direct link to Am I Going to Keep Using This?" translate="no">​</a></h3>
<p>I'm safely out of the novelty phase at this point,
and incredibly, I think the answer is yes.
If I had my laptop with me I would never reach for the phone,
in the same way that if I'm sitting next to my desktop PC,
I'm not going to grab my laptop.
But this phone setup can go places that the laptop can't,
and that freedom is something I've been wanting for a long time,
even if I didn't quite realize it.</p>
<p>I also find it amazing that the whole thing was relatively cheap,
especially when compared to something like the Apple Vision Pro.
Which, funnily enough, can't do any of what I ended up caring about.
It can't fit in your pockets, and it's no more capable of "real" computing than an iPhone.
I guess you can use it outdoors, but your eyes are in a sealed box,
so I don't think that even counts.</p>
<p>I think there might actually be a future for ultra-mobile software development.
Especially as these AR glasses continue to improve
and Linux continues to be flexible and awesome.
Despite the rough edges, I'm able to go places and do things now that I couldn't do before,
and I'm exited about it.</p>]]></content:encoded>
            <category>linux</category>
            <category>Android</category>
            <category>augmented reality</category>
        </item>
    </channel>
</rss>