<?xml version="1.0" encoding="utf-8"?>
<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>Simple tools for smart, attractive people like you</description>
    <lastBuildDate>Tue, 24 Mar 2026 00:00:00 GMT</lastBuildDate>
    <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. 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.]]></description>
      <category>design</category>
      <category>interfaces</category>
      <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="text-align: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>
<p><br> <br></p>
<p>This is the interior of an airplane restroom. Whoever designed this
door understood the assignment.</p>
<p><img src="/img/shts_airplane_lock.avif"
style="width:60%;display:block;margin:0 auto"
alt="airplane door lock" /></p>
<p><br> <br></p>
<p>A restroom door needs to do three things:</p>
<ol type="1">
<li>Prevent anyone from entering when there’s an occupant</li>
<li>Make the occupant <em>feel</em> like no-one will be able to enter
(otherwise it’s stressful)</li>
<li>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 type="1">
<li>Hinges that allow it to open and close</li>
<li>A latch that prevents it from opening</li>
<li>A knob that disengages the latch</li>
<li>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>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>
<p><img src="/img/asylum_lock.avif"
style="width:60%;display:block;margin:0 auto"
alt="airplane door lock" /></p>
<p><br> <small> &gt; Whoever put up this “look” sign knew there was a
problem here, &gt; but didn’t know what it was </small></p>
<p><br></p>
<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>
<p><img src="/img/nossa_exterior.avif"
style="width:60%;display:block;margin:0 auto" alt="vacant lock" /></p>
<p><br></p>
<p>Explicitly “there is someone in here” and you can’t miss it.</p>
<p>Here’s the interior of the same door:</p>
<p><img src="/img/nossa_interior.avif"
style="width:60%;display:block;margin:0 auto" alt="interior lock" /></p>
<p><br></p>
<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 type="1">
<li>Any slight misalignment in the door causes the lock to catch before
it’s actually locked.</li>
<li>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>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>
<p><img src="/img/sliding_interior.avif"
style="width:60%;display:block;margin:0 auto" alt="interior lock" /></p>
<p><br> <small> &gt; 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.
</small></p>
<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>
    </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 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.]]></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 type="1">
<li><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><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><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><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: 1. Finding information 2. Rubber ducking 3. Generating snippets of code or documentation 4. Having it work for hours on a big task with minimal intervention]]></description>
      <category>claude-code</category>
      <category>llm</category>
      <category>ai</category>
      <content:encoded><![CDATA[<p>I find myself using LLMs for coding in 4 specific ways: 1. Finding
information 2. Rubber ducking 3. Generating snippets of code or
documentation 4. Having it work for hours on a big task with minimal
intervention</p>
<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">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 1.
raising the odds of success, and 2. lowering the risk of wasted time on
my part (I am pointedly ignoring the actually cost of token usage for
now)</p>
<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: 1. Research all the available on-device speech-to-text
models with permissive licences 2. Demo the transcription speed of each
one on an android device attached to the PC 3. Write a C wrapper for the
best one (Moonshine) and build an embeddable dynamic library 4. Build
this for iOS, Android, Linux, and macOS, and integrate it with my app
code using the FFI 5. Build a Nim wrapper for the fdk-aac library 6.
Integrate it with miniaudio, so I can play AAC audio and pipe the audio
into Moonshine</p>
<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>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>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><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>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>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>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>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>
    </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 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 point]]></description>
      <category>xcode</category>
      <category>ios</category>
      <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>
<pre><code>7A226CEB2D722B3C001539F8 /* PBXContainerItemProxy */ = {
    isa = PBXContainerItemProxy;
    containerPortal = 7A226C922D722973001539F8 /* Pods.xcodeproj */;
    proxyType = 2;
    remoteGlobalIDString = E826FA0DCB9AA6E7829C68391B323B78;
    remoteInfo = &quot;GTMSessionFetcher-GTMSessionFetcher_Core_Privacy&quot;;
};</code></pre>
<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">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
src="/img/blog/password_dialog.png"
alt="Xcode password dialog boxes stacked on top of each other" /> 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 src="/img/blog/auth_failure.png"
alt="Sandbox account authentication failure" /></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">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>
    </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 to avoid someone else (or future me) from having to re-learn and re-solve problems, but something about it has always bugged me. I am automating a process, but I’m also documenting it, and those two things struggle to coexist. One o]]></description>
      <category>bash</category>
      <category>linux</category>
      <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:</p>
<p><img src="/img/blog/inline-markdown.png"
alt="Inline markdown in a bash script" /></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>
<pre class="vim"><code>syntax region shMarkdown 
    \ matchgroup=shMarkdownDelimiter 
    \ start=/&lt;&lt;&#39;-md-&#39;\s*$/ 
    \ end=/^-md-\s*$/ 
    \ contains=@markdownHighlight 
    \ containedin=shHereDoc,shHereString
    \ keepend

syntax include @markdownHighlight syntax/markdown.vim

&quot; Link the delimiter to Comment so it&#39;s greyed out
highlight link shMarkdownDelimiter Comment</code></pre>
<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>
    </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[<pre class=sourceCode bash>mkdir -p $(dirname $0)/data; cd data print_messages() { clear cat $(ls -tr | tail -n30) printf &quot;033[31m$USER:033[0m &quot; } export -f print_messages watchexec 2&gt; /dev/null -- bash -c &quot;print_messages&quot; &amp; while read text; do printf]]></description>
      <category>crdt</category>
      <category>linux</category>
      <content:encoded><![CDATA[<div class="sourceCode" id="cb1"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="fu">mkdir</span> <span class="at">-p</span> <span class="va">$(</span><span class="fu">dirname</span> <span class="va">$0)</span>/data<span class="kw">;</span> <span class="bu">cd</span> data</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="fu">print_messages()</span> <span class="kw">{</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>    <span class="fu">clear</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>    <span class="fu">cat</span> <span class="va">$(</span><span class="fu">ls</span> <span class="at">-tr</span> <span class="kw">|</span> <span class="fu">tail</span> <span class="at">-n30</span><span class="va">)</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>    <span class="bu">printf</span> <span class="st">&quot;\033[31m</span><span class="va">$USER</span><span class="st">:\033[0m &quot;</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="at">-f</span> <span class="va">print_messages</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="ex">watchexec</span> <span class="dv">2</span><span class="op">&gt;</span> /dev/null <span class="at">--</span> bash <span class="at">-c</span> <span class="st">&quot;print_messages&quot;</span> <span class="kw">&amp;</span></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="cf">while</span> <span class="bu">read</span> <span class="va">text</span><span class="kw">;</span> <span class="cf">do</span></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>  <span class="bu">printf</span> <span class="st">&quot;\033[31m</span><span class="va">$USER</span><span class="st">:\033[0m </span><span class="va">$text</span><span class="st">\n\n&quot;</span> <span class="op">&gt;</span> <span class="st">&quot;</span><span class="va">$(</span><span class="fu">uuidgen</span><span class="va">)</span><span class="st">&quot;</span></span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="cf">done</span></span></code></pre></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">
<p><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></p>
</div>
<p>This is hardly a Discord killer, but as far as messengers go there
are some interesting properties:</p>
<ol type="1">
<li>There is no central authority or server that “owns” the
messages</li>
<li>An offline machine can write new messages that will propagate once
it’s back online</li>
<li>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 id="the-bash-script">The Bash Script</h3>
<p>There’s not much to it, so I’ll run through it quick.</p>
<div class="sourceCode" id="cb2"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="fu">mkdir</span> <span class="at">-p</span> <span class="va">$(</span><span class="fu">dirname</span> <span class="va">$0)</span>/data<span class="kw">;</span> <span class="bu">cd</span> data</span></code></pre></div>
<p>Create a data directory (if needed) and move into it.</p>
<div class="sourceCode" id="cb3"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="fu">print_messages()</span> <span class="kw">{</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a>    <span class="fu">clear</span></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a>    <span class="fu">cat</span> <span class="va">$(</span><span class="fu">ls</span> <span class="at">-tr</span> <span class="kw">|</span> <span class="fu">tail</span> <span class="at">-n30</span><span class="va">)</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a>    <span class="bu">printf</span> <span class="st">&quot;\033[31m</span><span class="va">$USER</span><span class="st">:\033[0m &quot;</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="kw">}</span></span></code></pre></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="sourceCode" id="cb4"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="bu">export</span> <span class="at">-f</span> <span class="va">print_messages</span></span></code></pre></div>
<p>Some Bash nonsense to “export a function”. Otherwise the watchexec
subprocess can’t see it.</p>
<div class="sourceCode" id="cb5"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="ex">watchexec</span> <span class="dv">2</span><span class="op">&gt;</span> /dev/null <span class="at">--</span> bash <span class="at">-c</span> <span class="st">&quot;print_messages&quot;</span> <span class="kw">&amp;</span></span></code></pre></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="sourceCode" id="cb6"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="ex">print_messages</span> <span class="co"># fswatch doesn&#39;t fire on startup, so print messages first</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="ex">fswatch</span> <span class="at">-o</span> . <span class="kw">|</span> <span class="cf">while</span> <span class="bu">read</span> <span class="at">-r</span> <span class="va">event</span><span class="kw">;</span> <span class="cf">do</span></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a>  <span class="ex">print_messages</span></span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="cf">done</span> <span class="kw">&amp;</span></span></code></pre></div>
<div class="sourceCode" id="cb7"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="cf">while</span> <span class="bu">read</span> <span class="va">text</span><span class="kw">;</span> <span class="cf">do</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a>  <span class="bu">printf</span> <span class="st">&quot;\033[31m</span><span class="va">$USER</span><span class="st">:\033[0m </span><span class="va">$text</span><span class="st">\n\n&quot;</span> <span class="op">&gt;</span> <span class="st">&quot;</span><span class="va">$(</span><span class="fu">uuidgen</span><span class="va">)</span><span class="st">&quot;</span></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="cf">done</span></span></code></pre></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="sourceCode" id="cb8"><pre
class="sourceCode bash"><code class="sourceCode bash"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="ex">$</span> ls data</span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a><span class="ex">035aa216-3e23-4921-8d14-b79bdc150232</span>  5d07ed32-8f0c-4c88-9a93-f12606d57ea1</span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="ex">04455d8b-b58a-40da-a01a-7631e90ccbd8</span>  6187df26-8a6e-4729-9553-ffe1acf0d45f</span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="ex">1d621700-322e-4ba9-9d66-16a739838adf</span>  650b8c73-9ef5-40e5-8014-a8d97d617f1f</span></code></pre></div>
<h3 id="why-this-works">Why This Works</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 type="1">
<li>Can be replicated across an arbitrary number of nodes</li>
<li>Can be modified concurrently</li>
<li>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 id="edits-and-deletions">Edits and Deletions</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>
<pre><code>message:Hey it&#39;s mike what&#39;s up?</code></pre>
<p>or</p>
<pre><code>delete:2880dbc8-a2c6-43c0-8f88-e0fb2672755c</code></pre>
<p>or</p>
<pre><code>edit:2880dbc8-a2c6-43c0-8f88-e0fb2672755c:Hey, it&#39;s Mike what&#39;s up???</code></pre>
<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 id="the-takeaway">The Takeaway</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>
<p><small> 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. </small></p>]]></content:encoded>
    </item>
    <item>
      <title><![CDATA[Coding Without a Laptop - Two Weeks with AR Glasses and Linux on Android]]></title>
      <link>https://holdtherobot.com/blog/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; you can run a full desktop Linux environment on your phone.]]></description>
      <category>linux</category>
      <category>android</category>
      <category>augmented reality</category>
      <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 src="/img/blog/image1.avif"
alt="i3, picom, polybar, firefox, and htop" /></p>
<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 src="/img/blog/image2.avif"
alt="A CRDT-based ebook/audiobook reader running a desktop Linux app and connected to the Flutter debugger" /></p>
<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 id="the-hardware">The Hardware</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">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">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">Pixel 8 Pro</a> on ebay.</p>
<p>So the whole setup:</p>
<ul>
<li>Used Pixel 8 Pro $350</li>
<li>Used Xreal Air 2 Pro - $260</li>
<li>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 id="the-result">The Result</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 type="1">
<li>It really does fit into your pockets. No bag, nothing to carry.</li>
<li>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>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>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 id="the-linux-environment">The Linux Environment</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 type="1">
<li>A virtual machine emulating x86_64</li>
<li>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>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>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 type="1">
<li>Small and light. This is a phone, after all.</li>
<li>Has to support aarch64, obviously.</li>
<li>Doesn’t use systemd (I could never make it work inside chroot, and
it’s unclear if it’s possible).</li>
<li>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>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 useful links:</p>
<ul>
<li><a href="https://github.com/LinuxDroidMaster/Termux-Desktops"
class="uri">https://github.com/LinuxDroidMaster/Termux-Desktops</a></li>
<li><a
href="https://github.com/termux/termux-x11#using-with-chroot-environment"
class="uri">https://github.com/termux/termux-x11#using-with-chroot-environment</a></li>
<li><a href="https://github.com/Magisk-Modules-Alt-Repo/chroot-distro"
class="uri">https://github.com/Magisk-Modules-Alt-Repo/chroot-distro</a></li>
</ul>
<h3 id="the-ar-glasses">The AR Glasses</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 src="/img/blog/image3.avif"
alt="Through-the-lens photo of the AR glasses without electrochromic dimming" /></p>
<p><small> 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 without 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. </small></p>
<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">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 id="the-keyboard">The Keyboard</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 id="performance">Performance</h3>
<p>As a rough benchmark, I tried compiling Nim from source.</p>
<ul>
<li>On my Framework 13 with a Core Ultra 5 125H it took
<code>4:15</code>.</li>
<li>On my Thinkpad T450s with an Intel Core i5-5300U it took
<code>14:20</code>.</li>
<li>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 id="battery-life">Battery Life</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 id="am-i-going-to-keep-using-this">Am I Going to Keep Using
This?</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>
    </item>
  </channel>
</rss>
